windows的rpc是一个很重要的接口, 以前我对它一直不算了解, 今天以一个安全研究的角度去介绍一下它.

注册rpc服务

server 通过 RpcServerUseProtseqEp 注册服务, 可以有的类型有: ncalrpc (ALPC), ncacn_np (named pipe) or ncacn_ip_tcp (TCP socket) 参考
pipe类型:

1
2
3
4
5
RpcServerUseProtseqEp(
L"ncacn_np",
RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
L"\\pipe\\DEMO",
nullptr);

socket类型:

1
2
3
4
5
status = RpcServerUseProtseqEp(
reinterpret_cast<unsigned char*>("ncacn_ip_tcp"), // Use TCP/IP protocol.
RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // Backlog queue length for TCP/IP.
reinterpret_cast<unsigned char*>("4747"), // TCP/IP port to use.
NULL); // No security.

alpc类型:

1
RpcServerUseProtseqEpA("ncalrpc", 10, "spoolss", SecurityDescriptor);

之后是注册rpc函数接口:

以后面示例代码Example1Server.cpp为例:

1
2
3
4
5
6
7
8
status = RpcServerRegisterIf2(
Example1_v1_0_s_ifspec, // Interface to register.
NULL, // Use the MIDL generated entry-point vector.
NULL, // Use the MIDL generated entry-point vector.
RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, // Forces use of security callback.
RPC_C_LISTEN_MAX_CALLS_DEFAULT, // Use default number of concurrent calls.
(unsigned)-1, // Infinite max size of incoming data blocks.
SecurityCallback); // Naive security callback.

第四个参数flag, 值如下:

1
2
3
4
5
6
7
8
9
10
#define RPC_IF_AUTOLISTEN                   0x0001
#define RPC_IF_OLE 0x0002
#define RPC_IF_ALLOW_UNKNOWN_AUTHORITY 0x0004
#define RPC_IF_ALLOW_SECURE_ONLY 0x0008
#define RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH 0x0010
#define RPC_IF_ALLOW_LOCAL_ONLY 0x0020
#define RPC_IF_SEC_NO_CACHE 0x0040
#if (NTDDI_VERSION >= NTDDI_VISTA)
#define RPC_IF_SEC_CACHE_PER_PROC 0x0080
#define RPC_IF_ASYNC_CALLBACK 0x0100

RPC_IF_ALLOW_SECURE_ONLY代表接口需要认证.

示例项目里, 它会自动生成Example1_s.c文件, 里面有Example1_v1_0_s_ifspec的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
RPC_IF_HANDLE Example1_v1_0_s_ifspec = (RPC_IF_HANDLE)& Example1___RpcServerInterface;
static const RPC_SERVER_INTERFACE Example1___RpcServerInterface =
{
sizeof(RPC_SERVER_INTERFACE),
{{0x00000001,0xEAF3,0x4A7A,{0xA0,0xF2,0xBC,0xE4,0xC3,0x0D,0xA7,0x7E}},{1,0}},
{{0x8A885D04,0x1CEB,0x11C9,{0x9F,0xE8,0x08,0x00,0x2B,0x10,0x48,0x60}},{2,0}},
(RPC_DISPATCH_TABLE*)&Example1_v1_0_DispatchTable,
0,
0,
0,
&Example1_ServerInfo,
0x06000000
};
static const RPC_DISPATCH_FUNCTION Example1_table[] =
{
NdrServerCall2,
0
};
static const RPC_DISPATCH_TABLE Example1_v1_0_DispatchTable =
{
1,// 指定了有几个回调函数.
(RPC_DISPATCH_FUNCTION*)Example1_table
};

static const SERVER_ROUTINE Example1_ServerRoutineTable[] =
{
(SERVER_ROUTINE)Output// 导出函数
};

static const MIDL_SERVER_INFO Example1_ServerInfo =
{
&Example1_StubDesc,
Example1_ServerRoutineTable,// 导出函数表
Example1__MIDL_ProcFormatString.Format,
(unsigned short *) Example1_FormatStringOffsetTable,
0,
(RPC_SYNTAX_IDENTIFIER*)&_NDR64_RpcTransferSyntax_1_0,
2,
(MIDL_SYNTAX_INFO*)Example1_SyntaxInfo
};

在IDA中看Example1___RpcServerInterface:

1694054196198

其中+4位置是rpc服务对应的UUID.

再看[60h]位置的Example1_ServerInfo:

1694054233133

再看[8h]位置的SERVER_ROUTINE表:

1694054247738

client调用

client要调用服务, 必须通过RpcStringBindingCompose函数绑定, 再通过RpcBindingFromStringBinding获得RPC_BINDING_HANDLE, 下面是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
status = RpcStringBindingCompose(
NULL, // UUID to bind to.
reinterpret_cast<unsigned char*>("ncacn_ip_tcp"), // Use TCP/IP
// protocol.
reinterpret_cast<unsigned char*>("localhost"), // TCP/IP network
// address to use.
reinterpret_cast<unsigned char*>("4747"), // TCP/IP port to use.
NULL, // Protocol dependent network options to use.
&szStringBinding); // String binding output.

status = RpcBindingFromStringBinding(
szStringBinding, // The string binding to validate.
&hExample1Binding); // Put the result in the implicit binding
// handle defined in the IDL file.

bind后, 直接调用接口代码即可Output("Hello Implicit RPC World!");, 实际上, 它真实的调用是如下:

1694054894682

通过调用NdrClientCall3函数来实现调用. 第二个参数就是目标server的函数编号, 此处就是第0号函数(即Example1_ServerRoutineTable[0]), 从第四个参数开始, 就是目标函数所需要的参数.

观察一下它的 Example1_ProxyInfo

1694055232546

Example1_StubDesc:

1694055253629

从这个结构体可以看到, +18h位置是&hExample1Binding, 指向RPC_BINDING_HANDLE.

再看Example1___RpcClientInterface对象(_RPC_CLIENT_INTERFACE结构体), +4位置是server对应的uuid

1694083472060

下面从调试角度看看它的关系:

1694056212795

继续handle对应的结构体:

1694056354014

如图, 在**[f0h]**偏移位置的地址, 指向的结构体存在三个指针, 这三个指针分别是RpcStringBindingCompose的参数.

NdrClientCall3找rpc接口

一个方法就是通过上述poi(poi(poi(poi(@rcx)+18))+f0)偏移去找字符串

某些情况下, 可能RPC_BINDING_HANDLE指针不存在(即poi(poi(@rcx)+18)为空), 这种情况下, 一般第四个参数(即函数调用的第一个参数), 是一个和binding_handle 有关的结构体. 在同一个dll里, 通过查找它的引用, 大概率找得到声明位置.

以下以sspi接口中sspicli.dll!SspipProcessSecurityContext调用的rpc来示例如何寻找:

方法1: 通过逆向代码查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  LODWORD(v19.Pointer) = IsOkayToExec(&v58);
if ( SLODWORD(v19.Simple) >= 0 )
{
v79 = v58[3];
......
v19.Pointer = NdrClientCall3((MIDL_STUBLESS_PROXY_INFO *)&sspirpc_ProxyInfo, 6u, 0i64, v79, &v90, v61, v59).Pointer;// 1. 关注第四个参数, 来自行1的v58

NTSTATUS __fastcall IsOkayToExec(_QWORD *a1)// 2. 深入函数实现
{
if ( (DllState & 0x40000000) != 0 )
{
if ( a1 )
{
if ( !SecDllClient )
return -1073741502;
*a1 = SecDllClient;// 3. 找到赋值
}
return 0;

通过查找SecDllClient的引用, 来到函数InitState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
__int64 InitState(void)
{
......
RpcConnection = CreateRpcConnection(0, 2, (unsigned int)&v4, (unsigned int)&SecLsaPackageCount, (__int64)&v3);
......
SecDllClient = LocalAlloc(0x40u, 0x30ui64);
if ( SecDllClient )
{
*((_QWORD *)SecDllClient + 3) = v4;// 4. +8位置赋值了v4

__int64 __fastcall CreateRpcConnection(__int64 a1, int a2, __int64 a3, __int64 a4, __int64 a5)
{
result = SecpGetRpcBinding(&Binding);
if ( (int)result >= 0 )
{
v14.Simple = 0i64;
v12 = a2;
v10.Pointer = NdrClientCall3((MIDL_STUBLESS_PROXY_INFO *)&sspirpc_ProxyInfo, 0, 0i64, Binding, a1, v12, a5, a4, a3).Pointer;// 5. v4即a3, a3来自于rpc调用的赋值. 关注到第四个参数binding

int __fastcall SecpGetRpcBinding(RPC_BINDING_HANDLE *a1)
{
Binding = 0i64;
v2 = RpcStringBindingComposeW(
0i64,
(RPC_WSTR)L"ncalrpc",
0i64,
(RPC_WSTR)L"lsasspirpc",
word_180030B10,
&StringBinding);
v2 = RpcBindingFromStringBindingW(StringBinding, &Binding);// 6. 可以看到, binding就是我们要找的rpc_binding_handle

因为名字有isass, 估计是进程isass.exe, 通过rpcview查看:

1694059325724

rpcview不会自动识别来自哪个dll, 因此需要我们猜一下.

因为是sspi组件, 所以查看左下方所有的dll, 猜测应该是sspisrv.dll对应的服务是目标lsasspirpc, 右下方就是rpc的调用表.

如果想有符号, 通过下列方式添加:

1694059416499

1694059427500

类似于windbg. 添加后重启rpcview.

它不会自动下载符号, 需要把符号先下载到对应目录才行, 下面是下载方法

1
2
cmd> cd "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\"
cmd> .\symchk /s srv*c:\SYMBOLS*https://msdl.microsoft.com/download/symbols C:\Windows\System32\*.dll

在sspisvc.dll里, 我们查找RpcServerRegisterIf3引用, 找到:

1694059702812

跟踪dword_180006380:

1694059793138

查看[60h]:

1694060073435

查看[8h]:

1694060911071

自此我们找到了它的调用接口.

一般rpc server的调用栈回溯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SspiSrv!SspirProcessSecurityContext
RPCRT4!Invoke+0x73
RPCRT4!Ndr64StubWorker+0xb98
RPCRT4!NdrServerCallAll+0x3c
RPCRT4!DispatchToStubInCNoAvrf+0x17
RPCRT4!RPC_INTERFACE::DispatchToStubWorker+0x1a8
RPCRT4!RPC_INTERFACE::DispatchToStub+0xf1
RPCRT4!LRPC_SCALL::DispatchRequest+0x14d
RPCRT4!LRPC_SCALL::HandleRequest+0xd5a
RPCRT4!LRPC_SASSOCIATION::HandleRequest+0x2c3
RPCRT4!LRPC_ADDRESS::HandleRequest+0x183
RPCRT4!LRPC_ADDRESS::ProcessIO+0x939
RPCRT4!LrpcIoComplete+0xfe
ntdll!TppAlpcpExecuteCallback+0x20c
ntdll!TppWorkerThread+0x4b3
KERNEL32!BaseThreadInitThunk+0x17
ntdll!RtlUserThreadStart+0x20

方法2: 通过调试找到uuid

在调用NdrClientCall3断下, 依次查看结构体:

1694084123706

和rpcview一致.

com接口的rpc NdrpClientCall3

com接口一般在进程内调用rpc时就用的 RPCRT4!NdrpClientCall3接口. 声明如下:

1
2
3
4
5
6
7
8
9
10
11
CLIENT_CALL_RETURN RPC_ENTRY
NdrpClientCall3
(
void * pThis, // rcx
MIDL_STUBLESS_PROXY_INFO *pProxyInfo, // rdx
ulong nProcNum, // r8
void *pReturnValue, // r9
NDR_PROC_CONTEXT *pContext, // poi(@rsp+0x28)
uchar *StartofStack // poi(@rsp+0x30)

)

第二个参数就是MIDL_STUBLESS_PROXY_INFO 结构体指针, 第三个是函数序号. 如果确认是进程内的调用, 可以直接在当前线程的RPCRT4!Invoke下断点, 直接找到相关处理函数.

示例堆栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
07 00000053`8d2fd8e0 00007ffe`ef6d7de3     eapahost!EapHost::HostAuthenticatorApis::EapHostAuthenticatorReceivePacket+0xde // 实际调用的函数

08 00000053`8d2fd960 00007ffe`ef73bc6d RPCRT4!Invoke+0x73
09 00000053`8d2fd9d0 00007ffe`ef6668b9 RPCRT4!Ndr64StubWorker+0xbfd
0a 00000053`8d2fe0a0 00007ffe`ef802209 RPCRT4!NdrStubCall3+0xc9
0b 00000053`8d2fe100 00007ffe`ef66a92b combase!CStdStubBuffer_Invoke+0x59 [d:\rs1\onecore\com\combase\ndr\ndrole\stub.cxx @ 1527]
0c 00000053`8d2fe140 00007ffe`ef84de3c RPCRT4!CStdStubBuffer_Invoke+0x3b
0d (Inline Function) --------`-------- combase!InvokeStubWithExceptionPolicyAndTracing::__l6::<lambda_76d9e92c799d246a4afbe64a2bf5673d>::operator()+0x24 [d:\rs1\onecore\com\combase\dcomrem\channelb.cxx @ 1824]
0e 00000053`8d2fe170 00007ffe`ef84e482 combase!ObjectMethodExceptionHandlingAction<<lambda_76d9e92c799d246a4afbe64a2bf5673d> >+0x4c [d:\rs1\onecore\com\combase\dcomrem\excepn.hxx @ 91]
0f (Inline Function) --------`-------- combase!InvokeStubWithExceptionPolicyAndTracing+0x8d [d:\rs1\onecore\com\combase\dcomrem\channelb.cxx @ 1822]
10 00000053`8d2fe1d0 00007ffe`ef81fab1 combase!DefaultStubInvoke+0x222 [d:\rs1\onecore\com\combase\dcomrem\channelb.cxx @ 1891]
11 00000053`8d2fe3f0 00007ffe`ef8054c0 combase!CCtxChnl::SendReceive+0x2b1 [d:\rs1\onecore\com\combase\dcomrem\crossctx.cxx @ 4138]
12 00000053`8d2fe660 00007ffe`ef737aed combase!NdrExtpProxySendReceive+0x1c0 [d:\rs1\onecore\com\combase\ndr\ndrole\proxy.cxx @ 1965]
13 00000053`8d2fe6d0 00007ffe`ef8014f4 RPCRT4!NdrpClientCall3+0x46d
14 00000053`8d2feae0 00007ffe`ef90cbb2 combase!ObjectStublessClient+0x144 [d:\rs1\onecore\com\combase\ndr\ndrole\amd64\stblsclt.cxx @ 371]
15 00000053`8d2fee70 00007ffe`e27a4808 combase!ObjectStubless+0x42 [d:\rs1\onecore\com\combase\ndr\ndrole\amd64\stubless.asm @ 176]

eapahost!ObjectStublessClient6(直接调用的 combase!ObjectStublessClient6)// 进入com接口调用

16 00000053`8d2feec0 00007ffe`e27a25d9 iassam!EAPSession::processEAPPacket+0x64

示例

来自 示例项目
下面提到的”Example1.h”是自动生成的.

server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// File Example1Server.cpp
#include <iostream>
#include "Example1.h"

// Server function.
void Output(const char* szOutput)
{
std::cout << szOutput << std::endl;
}

// Naive security callback.
RPC_STATUS CALLBACK SecurityCallback(RPC_IF_HANDLE /*hInterface*/, void* /*pBindingHandle*/)
{
return RPC_S_OK; // Always allow anyone.
}

int main()
{
RPC_STATUS status;

// Uses the protocol combined with the endpoint for receiving
// remote procedure calls.
status = RpcServerUseProtseqEp(
reinterpret_cast<unsigned char*>("ncacn_ip_tcp"), // Use TCP/IP protocol.
RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // Backlog queue length for TCP/IP.
reinterpret_cast<unsigned char*>("4747"), // TCP/IP port to use.
NULL); // No security.

if (status)
exit(status);

// Registers the Example1 interface.
status = RpcServerRegisterIf2(
Example1_v1_0_s_ifspec, // Interface to register.
NULL, // Use the MIDL generated entry-point vector.
NULL, // Use the MIDL generated entry-point vector.
RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, // Forces use of security callback.
RPC_C_LISTEN_MAX_CALLS_DEFAULT, // Use default number of concurrent calls.
(unsigned)-1, // Infinite max size of incoming data blocks.
SecurityCallback); // Naive security callback.

if (status)
exit(status);

// Start to listen for remote procedure
// calls for all registered interfaces.
// This call will not return until
// RpcMgmtStopServerListening is called.
status = RpcServerListen(
1, // Recommended minimum number of threads.
RPC_C_LISTEN_MAX_CALLS_DEFAULT, // Recommended maximum number of threads.
FALSE); // Start listening now.

if (status)
exit(status);
}

// Memory allocation function for RPC.
// The runtime uses these two functions for allocating/deallocating
// enough memory to pass the string to the server.
void* __RPC_USER midl_user_allocate(size_t size)
{
return malloc(size);
}

// Memory deallocation function for RPC.
void __RPC_USER midl_user_free(void* p)
{
free(p);
}

client

idl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// File Example1.idl
[
// A unique identifier that distinguishes this interface from other interfaces.
uuid(00000001-EAF3-4A7A-A0F2-BCE4C30DA77E),

// This is version 1.0 of this interface.
version(1.0)
]
interface Example1 // The interface is named Example1
{
// A function that takes a zero-terminated string.
void Output(
[in, string] const char* szOutput);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// File Example1Client.cpp
#include <iostream>
#include "../Example1/Example1.h"

int main()
{
RPC_STATUS status;
unsigned char* szStringBinding = NULL;

// Creates a string binding handle.
// This function is nothing more than a printf.
// Connection is not done here.
status = RpcStringBindingCompose(
NULL, // UUID to bind to.
reinterpret_cast<unsigned char*>("ncacn_ip_tcp"), // Use TCP/IP
// protocol.
reinterpret_cast<unsigned char*>("localhost"), // TCP/IP network
// address to use.
reinterpret_cast<unsigned char*>("4747"), // TCP/IP port to use.
NULL, // Protocol dependent network options to use.
&szStringBinding); // String binding output.

if (status)
exit(status);

// Validates the format of the string binding handle and converts
// it to a binding handle.
// Connection is not done here either.
status = RpcBindingFromStringBinding(
szStringBinding, // The string binding to validate.
&hExample1Binding); // Put the result in the implicit binding
// handle defined in the IDL file.

if (status)
exit(status);

RpcTryExcept
{
// Calls the RPC function. The hExample1Binding binding handle
// is used implicitly.
// Connection is done here.
Output("Hello Implicit RPC World!");
}
RpcExcept(1)
{
std::cerr << "Runtime reported exception " << RpcExceptionCode()
<< std::endl;
}
RpcEndExcept

// Free the memory allocated by a string.
status = RpcStringFree(
&szStringBinding); // String to be freed.

if (status)
exit(status);

// Releases binding handle resources and disconnects from the server.
status = RpcBindingFree(
&hExample1Binding); // Frees the implicit binding handle defined in
// the IDL file.

if (status)
exit(status);
}

// Memory allocation function for RPC.
// The runtime uses these two functions for allocating/deallocating
// enough memory to pass the string to the server.
void* __RPC_USER midl_user_allocate(size_t size)
{
return malloc(size);
}

// Memory deallocation function for RPC.
void __RPC_USER midl_user_free(void* p)
{
free(p);
}

参考

https://www.tiraniddo.dev/2021/08/how-to-secure-windows-rpc-server-and.html 

https://www.codeproject.com/Articles/4837/Introduction-to-RPC-Part-1#Implicitandexplicithandles17

致谢

感谢 k0shl, @XiaoWei___ 两位大佬帮我理解这个rpc.