在当今的网络安全领域,Windows操作系统的漏洞挖掘一直是研究者们关注的焦点之一。其中,RPC(Remote Procedure Call,远程方法调用)服务作为Windows系统的核心组件,因其广泛的使用和潜在的安全风险,成为了漏洞挖掘的重要目标,历史上出了非常多的漏洞。

一、Windows RPC介绍

1.1 RPC是什么?

RPC是Windows系统里最基础的组件之一,很多服务都是基于此开发。RPC是Remote Procedure Call的缩写,即远程方法调用。简单来说,客户端可以通过RPC远程调用服务端已注册的接口内的方法并取得执行结果,而无需关心具体实现。这种机制使得不同进程之间的通信变得高效且透明。

1.2 RPC工作原理

RPC的工作过程可以分为以下几个步骤,参考微软官方文档

  1. 客户端将参数和要调用的方法按约定序列化成NDR(Network Data Representation)格式。
  2. 通过网络或管道将数据发送给服务端。
  3. 服务端接收数据后将数据反序列化,并调用对应的接口中的方法,反序列化的数据按约定作为各参数。
  4. 服务端方法执行结束后,将返回结果序列化。
  5. 再次通过网络或管道将数据发送给客户端。
  6. 客户端接收数据后将数据反序列化,从而获得服务端执行结果。

1.3 为什么关注RPC?

RPC作为Windows系统的核心组件,具有以下特点使其成为漏洞挖掘的重要目标,并且历史上也出现了非常多的漏洞:

  1. 丰富的攻击面:由于RPC是许多服务的基础,因此它提供了大量的潜在攻击点。
  2. 高权限运行:RPC服务通常以高权限(如SYSTEM权限)运行,或者至少具备SeImpersonatePrivilege权限。这意味着一旦被利用,攻击者可以利用RPC服务进行提权操作。

二、Windows RPC Demo

为了更好地理解RPC的工作机制,我们可以通过一个简单的Demo来展示RPC的基本操作。以下是Windows官方教程的Windows RPC服务端和客户端代码示例:

2.1 服务端代码

//file hello.idl
[
    uuid(7a98c250-6808-11cf-b73b-00aa00b677a7),
    version(1.0)
]
interface hello
{
    void HelloProc([in, string] unsigned char * pszString);
    void Shutdown(void);
}
/* file: hellos.c */
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include "hello.h"
#include <windows.h>

void HelloProc(char * pszString)
{
    printf("%s\n", pszString);
}

void Shutdown(void)
{
    RPC_STATUS status;
 
    status = RpcMgmtStopServerListening(NULL);
 
    if (status) 
    {
       exit(status);
    }
 
    status = RpcServerUnregisterIf(NULL, NULL, FALSE);
 
    if (status) 
    {
       exit(status);
    }
}

void main()
{
    RPC_STATUS status;
    unsigned char * pszProtocolSequence = "ncacn_np";
    unsigned char * pszSecurity         = NULL; 
    unsigned char * pszEndpoint         = "\\pipe\\hello";
    unsigned int    cMinCalls = 1;
    unsigned int    fDontWait = FALSE;
 
    status = RpcServerUseProtseqEp(pszProtocolSequence,
                                   RPC_C_LISTEN_MAX_CALLS_DEFAULT,
                                   pszEndpoint,
                                   pszSecurity); 
 
    if (status) exit(status);
 
    status = RpcServerRegisterIf(hello_ServerIfHandle,  
                                 NULL,   
                                 NULL); 
 
    if (status) exit(status);
 
    status = RpcServerListen(cMinCalls,
                             RPC_C_LISTEN_MAX_CALLS_DEFAULT,
                             fDontWait);
 
    if (status) exit(status);
 }
  1. 初始化 RPC 服务
    • 通过 RpcServerUseProtseqEp 指定通信协议和端点。
    • 通过 RpcServerRegisterIf 注册 RPC 接口(hello_ServerIfHandle 是由 MIDL 编译器对interface hello生成的接口句柄,表示服务端的 RPC 接口)。
  2. 进入监听状态
    • 调用 RpcServerListen,服务端开始监听客户端的 RPC 调用请求。
  3. 处理客户端请求
    • 当客户端通过 RPC 调用服务器端的远程过程时,RPC 运行时会调用相应的服务器存根代码,进而执行实际的远程方法(HelloProcShutdown)。

2.2 客户端代码

/* file: helloc.c */
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include "hello.h" 
#include <windows.h>

void main()
{
    RPC_STATUS status;
    unsigned char * pszUuid             = NULL;
    unsigned char * pszProtocolSequence = "ncacn_np";
    unsigned char * pszNetworkAddress   = NULL;
    unsigned char * pszEndpoint         = "\\pipe\\hello";
    unsigned char * pszOptions          = NULL;
    unsigned char * pszStringBinding    = NULL;
    unsigned char * pszString           = "hello, world";
    unsigned long ulCode;
 
    status = RpcStringBindingCompose(pszUuid,
                                     pszProtocolSequence,
                                     pszNetworkAddress,
                                     pszEndpoint,
                                     pszOptions,
                                     &pszStringBinding);
    if (status) exit(status);

    status = RpcBindingFromStringBinding(pszStringBinding, &hello_ClientIfHandle);
 
    if (status) exit(status);
 
    RpcTryExcept  
    {
        HelloProc(pszString);
        Shutdown();
    }
    RpcExcept(1) 
    {
        ulCode = RpcExceptionCode();
        printf("Runtime reported exception 0x%lx = %ld\n", ulCode, ulCode);
    }
    RpcEndExcept
 
    status = RpcStringFree(&pszStringBinding); 
 
    if (status) exit(status);
 
    status = RpcBindingFree(&hello_IfHandle);
 
    if (status) exit(status);

    exit(0);
}
  1. 构造字符串信息
    • 通过RpcStringBindingCompose将协议序列、端点等信息合并为一个字符串信息。
      • pszEndpoint:端点名称,指定为 \pipe\hello,与服务端的端点一致。
  2. 创建绑定句柄
    • 通过RpcBindingFromStringBinding从上述字符串信息创建一个 RPC 绑定句柄,用于与服务端进行通信。
  3. 调用远程方法
    • RpcTryExcept 异常处理块中,客户端调用服务端的远程方法(HelloProcShutdown)。

三、FAX服务

Windows Fax ServerWindows Server中的一个服务器角色,可被用来远程发送和接收传真。

微软为传真服务定义了一套基于RPC的交互协议,提供多种传真相关功能,并提供了详细的说明文档

3.1 Fax Server Interface

Fax服务端接口存在105个方法可被远程调用

CVE-2023-21694 Windows 传真服务远程代码执行漏洞

通过查看微软文档,FAX_CreateAccount似乎是个有趣的方法,该方法会验证客户端用户是否有权限创建账户,若验证成功,则会根据传入的参数Buffer创建对应账户。如下是该方法声明:

 error_status_t FAX_CreateAccount(
   [in] handle_t hBinding,
   [in] DWORD level,
   [in, ref, size_is(BufferSize)] const LPBYTE Buffer,
   [in, range(0,FAX_MAX_RPC_BUFFER)]
     DWORD BufferSize
 );

可以看到BufferLPBYTE类型,大小由BufferSize决定,而文档上说Buffer应该是FAX_ACCOUNT_INFO_0指针类型,那么意味着在FAX_CreateAccount内部肯定会有类型转换。

Buffer: A pointer to a FAX_ACCOUNT_INFO_0 that contains fax account information. The lpcwstrAccountName member of the FAX_ACCOUNT_INFO_0 MUST be set to the name of the operating system user account for which the new fax user account is to be created, using the same account name. The format of the user account name string is described in section 2.2.24 (FAX_ACCOUNT_INFO_0).

BufferSize: A DWORD value that indicates the return size, in bytes, of the buffer that is pointed to by the Buffer parameter. The maximum size is FAX_MAX_RPC_BUFFER(section 2.2.82).

FAX_ACCOUNT_INFO_0类型由Fixed_PortionVariable_Data两部分组成。

Fixed_PortiondwSizeOfStruct字段(当前Fixed_Portion结构大小,固定为8)和lpcwstrAccountNameOffset字段(Variable_Data相对FAX_ACCOUNT_INFO_0的偏移)组成。这里需要注意的是lpcwstrAccountNameOffset字段后续会被转换为存放指向Variable_Data指针,64位下指针类型占用8字节,加上dwSizeOfStruct字段的4字节,一共是12字节,对齐后Fixed_Portion的大小为16字节,后续也是按16字节进行处理。

Variable_Data只有lpcwstrAccountName字段,以0结尾的字符串。

分析FAX_CreateAccount里将Buffer转换为FAX_ACCOUNT_INFO_0的逻辑,根据BufferSize分配一段内存,并拷贝Buffer内容,接着调用MarshallUpStructure。此时只检查BufferSize不为0。

MarshallUpStructure调用IsBufferSizeEnough来判断原有的Buffer大小是否足够,值得注意的是,整个过程并没有对BufferSize进行检查,调用IsBufferSizeEnough也不带BufferSize

分析IsBufferSizeEnough,发现只要lpcwstrAccountNameOffset等于0x10即可满足要求,使IsBufferSizeEnough返回True

那漏洞就很明显了,当BufferSize为8,lpcwstrAccountNameOffset为0x10时,即可在后续AjustPointersInStructuresArray中将lpcwstrAccountNameOffset转换成指针时触发越界写。

type_strict_context_handle

通过查看数据类型idl文件faxdatatypes.idl,发现定义了多种context_handle类型:

 typedef [context_handle] HANDLE RPC_FAX_HANDLE;
 typedef [ref] RPC_FAX_HANDLE* PRPC_FAX_HANDLE;
  
 typedef [context_handle] HANDLE RPC_FAX_PORT_HANDLE;
 typedef RPC_FAX_PORT_HANDLE* PRPC_FAX_PORT_HANDLE;
  
 typedef [context_handle] HANDLE RPC_FAX_SVC_HANDLE;
 typedef RPC_FAX_SVC_HANDLE* PRPC_FAX_SVC_HANDLE;
  
 typedef [context_handle] HANDLE RPC_FAX_MSG_ENUM_HANDLE;
 typedef RPC_FAX_MSG_ENUM_HANDLE* PRPC_FAX_MSG_ENUM_HANDLE;
  
 typedef [context_handle] HANDLE RPC_FAX_COPY_HANDLE;
 typedef RPC_FAX_COPY_HANDLE* PRPC_FAX_COPY_HANDLE;
  
 typedef [context_handle] HANDLE RPC_FAX_EVENT_HANDLE;
 typedef RPC_FAX_EVENT_HANDLE* PRPC_FAX_EVENT_HANDLE;
  
 typedef [context_handle] HANDLE RPC_FAX_EVENT_EX_HANDLE;
 typedef RPC_FAX_EVENT_EX_HANDLE* PRPC_FAX_EVENT_EX_HANDLE;

不同方法间混用不一样的context_handle,会不会存在类型混淆问题呢?实际上微软早就考虑到了这种问题,并在一开始就规定了必须使用type_strict_context_handle

This protocol MUST specify to the RPC runtime via the type_strict_context_handle attribute, which rejects the use of context handles created by a method that creates a different type of context handle  ([MS-RPCE] section 3).

但类型混淆问题在Fax服务协议中就真的不存在吗?

3.2 Fax Client Interface

Fax客户端接口存在4个方法可被远程调用

客户端连接服务端后,为了获取服务端主动发送的事件消息,也会注册对应的RPC接口,等待服务端回连传送消息。

客户端在调用FAX_StartServerNotification之后会监听1024端口等待服务端调用FAX_OpenConnection回连。值得注意的是此时1024端口是监听在0.0.0.0上,所有人都可以访问该端口。

CVE-2024-38104 Windows 传真客户端远程代码执行漏洞

根据文档说明,Context 参数是一个 ULONG64 类型的句柄,其值应与通过 FAX_StartServerNotification 发送的值保持一致。

 error_status_t FAX_OpenConnection(
   [in] handle_t hBinding,
   [in] unsigned __int64 Context,
   [out] PRPC_FAX_HANDLE FaxHandle
 );

Context: A ULONG64 ([MS-DTYP] section 2.2.51) containing a context information handle. This handle SHOULD match the one supplied to the server when using the FAX_StartServerNotification family of calls. For more information, see the following topics:

§ FAX_StartServerNotification

  • FAX_StartServerNotificationEx
  • FAX_StartServerNotificationEx2

但逆向分析FAX_OpenConnection发现并没有对Context做任何检查,就直接作为指针使用。

并且在注册RPC接口时虽然调用RpcServerRegisterAuthInfoW提供了认证选项,但实际上RpcServerRegisterAuthInfoW并不拒绝未认证的连接。

所以任何人通过1024端口连接,发送FAX_OpenConnection,都可以触发任意地址解引用。

四、StorSvc服务

4.1 CVE-2024-38248 Windows 存储特权提升漏洞

StorSvc服务并不像传真服务一样具有完整而详细的文档说明。此时可以通过借助RpcView来反编译其接口方法说明。

逆向其RPC接口注册过程,可以看到通过RpcServerRegisterIf3注册了两个接口。

分别具有如下方法:

RPC接口方法具有高度的规范化特点,那么是否可以使用静态工具自动化扫描这些接口方法呢?

考虑最简单的条件竞争漏洞模式:在未加锁的情况下,存在一条路径对全局变量执行释放操作。

首先通过破壳平台查找会被free释放的全局变量

增加约束条件,仅考虑那些可通过RPC接口方法调用的路径。

当两个线程同时进入StorageService::GetLastFailedSaveLocationPath会导致条件竞争,全局变量Block会被释放两次。

五、总结

通过本文,我们简单探讨了RPC服务的工作原理、漏洞挖掘方法以及具体案例分析。RPC服务作为Windows系统的核心组件,其安全性和稳定性对于整个系统的安全至关重要。在实际的漏洞挖掘过程中,我们需要关注RPC服务的以下特点:

  1. 丰富的攻击面:RPC服务提供了大量的潜在攻击点,需要仔细分析每个接口方法的实现和调用过程。
  2. 高权限运行:RPC服务通常以高权限运行,一旦被利用,攻击者可以实现提权等恶意操作。
  3. 复杂的通信机制:RPC的通信机制涉及参数的序列化与反序列化、网络传输等多个环节,容易出现安全漏洞。