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, NULL , NULL , RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, RPC_C_LISTEN_MAX_CALLS_DEFAULT, (unsigned )-1 , SecurityCallback);
第四个参数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
:
其中+4位置是rpc服务对应的UUID.
再看[50h]
位置的Example1_ServerInfo
:
再看[8h]
位置的SERVER_ROUTINE
表:
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!");
, 实际上, 它真实的调用是如下:
通过调用NdrClientCall3
函数来实现调用. 第二个参数就是目标server的函数编号, 此处就是第0
号函数(即Example1_ServerRoutineTable[0]
), 从第四个参数开始, 就是目标函数所需要的参数.
观察一下它的 Example1_ProxyInfo
Example1_StubDesc
:
从这个结构体可以看到, +18h位置是&hExample1Binding
, 指向RPC_BINDING_HANDLE
.
再看Example1___RpcClientInterface
对象(_RPC_CLIENT_INTERFACE
结构体), +4位置是server对应的uuid
下面从调试角度看看它的关系:
继续handle对应的结构体:
如图, 在[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 , 0 i64, v79, &v90, v61, v59).Pointer; NTSTATUS __fastcall IsOkayToExec (_QWORD *a1) { if ( (DllState & 0x40000000 ) != 0 ) { if ( a1 ) { if ( !SecDllClient ) return -1073741502 ; *a1 = SecDllClient; } 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(0x40 u, 0x30 ui64); if ( SecDllClient ) { *((_QWORD *)SecDllClient + 3 ) = v4; __int64 __fastcall CreateRpcConnection (__int64 a1, int a2, __int64 a3, __int64 a4, __int64 a5) { result = SecpGetRpcBinding(&Binding); if ( (int )result >= 0 ) { v14.Simple = 0 i64; v12 = a2; v10.Pointer = NdrClientCall3((MIDL_STUBLESS_PROXY_INFO *)&sspirpc_ProxyInfo, 0 , 0 i64, Binding, a1, v12, a5, a4, a3).Pointer; int __fastcall SecpGetRpcBinding (RPC_BINDING_HANDLE *a1) { Binding = 0 i64; v2 = RpcStringBindingComposeW( 0 i64, (RPC_WSTR)L"ncalrpc" , 0 i64, (RPC_WSTR)L"lsasspirpc" , word_180030B10, &StringBinding); v2 = RpcBindingFromStringBindingW(StringBinding, &Binding);
因为名字有isass, 估计是进程isass.exe
, 通过rpcview 查看:
rpcview不会自动识别端口来自哪个dll的rpc接口, 因此需要我们猜一下.
因为是sspi组件, 所以查看左下方所有的dll, 猜测应该是sspisrv.dll对应的服务是目标lsasspirpc
, 右下方就是rpc的调用表.
rpcview的Flags
栏, 鼠标放上去, 可以看到它的flag是什么意思.
如果想有符号, 通过下列方式添加(目录不要包含空格):
类似于windbg. 添加后重启rpcview.
它不会自动下载符号, 需要把符号先下载到对应目录才行, 下面是示例下载符号到C:\symbols
目录的方法, 如果只需要一个dll的符号, 可以只下载那个dll, 不用全部下载.
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
另外, 在选中dll后, 点击右键, 选择decompile
, 可以得到接口对应的idl.
在sspisvc.dll里, 我们查找RpcServerRegisterIf3
引用, 找到:
跟踪dword_180006380
:
查看[50h]
:
查看[8h]
:
自此我们找到了它的调用接口.
一般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
断下, 依次查看结构体:
和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
RPC接口的序列化 当我们构造rpc的请求时, 需要知道server端需要什么样的参数, 如果是自定义的结构体, 如果rpcview无法解析出结构体, 就需要我们逆向server的结构体格式化描述, 逆向出结构体. 本节将介绍server端如何构造的结构体格式化描述, 通过ida比对, 来逆向server的结构体.
假如我们的idl定义如下:
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 [ uuid(3 d267954-eeb7-11 d1-b94e-00 c04fa3080d), version(1.0 ), ] interface Example1 { error_status_t Proc0_TLSRpcGetVersion ( [in][context_handle] void * arg_0, [in][out]long * arg_1) ; error_status_t Proc1_TLSRpcConnect ( [out][context_handle] void ** arg_1) ; struct uknow1 { int f1; int f2; int f3; int f4; [size_is(f3)] unsigned char * buff; int f18; int f1c; [size_is(f18)] unsigned char * buff2; }; struct uknow2 { int f1; int f2; char * buff; int f10; int f14; int f18; int f1c; }; error_status_t Proc44_TLSRpcChallengeServer ( [in][context_handle] void * arg_0, [in]long arg_1, [in]struct uknow1* arg_2, [out][ref]struct uknow2** arg_3, [out][ref]struct uknow1** arg_4, [in][out]long * arg_5) ;]
在本文示例的server中, 添加如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 error_status_t Proc0_TLSRpcGetVersion (void *handle, long *ver) { return 0 ; } error_status_t Proc1_TLSRpcConnect (void ** handle) { return 2 ; } error_status_t Proc44_TLSRpcChallengeServer ( void * arg_0, long arg_1, struct uknow1* arg_2, struct uknow2** arg_3, struct uknow1** arg_4, long * arg_5) { return 1 ; }
这里, 我们一共定义了三个接口函数. 并声明了两种特别的结构体ukonw1
, uknow2
.
在自动生成的.c文件里, 就可以看到如下信息:
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 static const RPC_SERVER_INTERFACE Example1___RpcServerInterface = { sizeof (RPC_SERVER_INTERFACE), {{0x3d267954 ,0xeeb7 ,0x11d1 ,{0xb9 ,0x4e ,0x00 ,0xc0 ,0x4f ,0xa3 ,0x08 ,0x0d }},{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 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 }; static const MIDL_SYNTAX_INFO Example1_SyntaxInfo [ 2 ] = { { {{0x8A885D04 ,0x1CEB ,0x11C9 ,{0x9F ,0xE8 ,0x08 ,0x00 ,0x2B ,0x10 ,0x48 ,0x60 }},{2 ,0 }}, (RPC_DISPATCH_TABLE*)&Example1_v1_0_DispatchTable, Example1__MIDL_ProcFormatString.Format, Example1_FormatStringOffsetTable, Example1__MIDL_TypeFormatString.Format, 0 , 0 , 0 } ,{ {{0x71710533 ,0xbeba ,0x4937 ,{0x83 ,0x19 ,0xb5 ,0xdb ,0xef ,0x9c ,0xcc ,0x36 }},{1 ,0 }}, (RPC_DISPATCH_TABLE*)&Example1_NDR64__v1_0_DispatchTable, 0 , (unsigned short *) Example1_Ndr64ProcTable, 0 , 0 , 0 , 0 } };
其中序列化有关的就是 Example1_SyntaxInfo
结构体.
在ida中如下:
以下是Example1__MIDL_ProcFormatString
结构体的部分内容:
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 81 82 83 84 static const Example1_MIDL_PROC_FORMAT_STRING Example1__MIDL_ProcFormatString = { 0 , { ...... 0x0 , 0x48 , NdrFcLong( 0x0 ), NdrFcShort( 0x2 ), NdrFcShort( 0x38 ), 0x30 , 0x40 , NdrFcShort( 0x0 ), 0x0 , 0x0 , NdrFcShort( 0x48 ), NdrFcShort( 0x24 ), 0x47 , 0x7 , 0xa , 0x7 , NdrFcShort( 0x1 ), NdrFcShort( 0x1 ), NdrFcShort( 0x0 ), NdrFcShort( 0x0 ), NdrFcShort( 0x8 ), NdrFcShort( 0x0 ), NdrFcShort( 0x2 ), NdrFcShort( 0x48 ), NdrFcShort( 0x8 ), 0x8 , 0x0 , NdrFcShort( 0x10b ), NdrFcShort( 0x10 ), NdrFcShort( 0x2e ), NdrFcShort( 0x2013 ), NdrFcShort( 0x18 ), NdrFcShort( 0x48 ), NdrFcShort( 0x2013 ), NdrFcShort( 0x20 ), NdrFcShort( 0x64 ), NdrFcShort( 0x158 ), NdrFcShort( 0x28 ), 0x8 , 0x0 , NdrFcShort( 0x70 ), NdrFcShort( 0x30 ), 0x10 , 0x0 , 0x0 } }; static const unsigned short Example1_FormatStringOffsetTable[] = { 0 , 50 , 88 };
Example1_FormatStringOffsetTable
结构体里的值就是每一个rpc接口函数在Example1__MIDL_ProcFormatString.Format
内存里的偏移 , 因为我们声明了三个函数, 所以有三个值. 因为是从Format
字段开始算的, 需要偏移2字节.
在ida中反汇编server, 在偏移88+2 即 0x5a
位置, 就是0, 0x48, ...
这里的2 dup(0)
是指有两个重复的0. 偏移0x60+0x58 = 0xb8, 0x140006cb8 位置的值为 10h, 0, 0, 48h, 0, 0, 0, 0, 2....
Example1__MIDL_ProcFormatString
结构体会具象化到 Example1_Ndr64ProcTable 里.
让我们看一下Example1_Ndr64ProcTable
结构体:
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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 static const FormatInfoRef Example1_Ndr64ProcTable[] = { &__midl_frag2, &__midl_frag7, &__midl_frag11 }; static const __midl_frag11_t __midl_frag11 ={ { (NDR64_UINT32) 23986240 , (NDR64_UINT32) 56 , (NDR64_UINT32) 76 , (NDR64_UINT32) 157 , (NDR64_UINT16) 0 , (NDR64_UINT16) 0 , (NDR64_UINT16) 7 , (NDR64_UINT16) 8 }, { { 0x70 , (NDR64_UINT8) 64 , 0 , (NDR64_UINT8) 0 , (NDR64_UINT8) 0 }, (NDR64_UINT16) 0 }, { &__midl_frag12, { 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , (NDR64_UINT16) 0 , 0 }, (NDR64_UINT16) 0 , 0 , }, { &__midl_frag13, { 0 , 0 , 0 , 1 , 0 , 0 , 1 , 1 , 0 , 0 , 0 , 0 , 0 , (NDR64_UINT16) 0 , 0 }, (NDR64_UINT16) 0 , 8 , }, { &__midl_frag15, { 1 , 1 , 0 , 1 , 0 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 , (NDR64_UINT16) 0 , 0 }, (NDR64_UINT16) 0 , 16 , }, { &__midl_frag22, { 0 , 1 , 0 , 0 , 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , (NDR64_UINT16) 0 , 1 }, (NDR64_UINT16) 0 , 24 , }, { &__midl_frag26, { 1 , 1 , 0 , 0 , 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , (NDR64_UINT16) 0 , 1 }, (NDR64_UINT16) 0 , 32 , }, { &__midl_frag29, { 0 , 0 , 0 , 1 , 1 , 0 , 1 , 0 , 1 , 0 , 0 , 0 , 0 , (NDR64_UINT16) 0 , 0 }, (NDR64_UINT16) 0 , 40 , }, { &__midl_frag30, { 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 0 , 0 , 0 , 0 , 0 , (NDR64_UINT16) 0 , 0 }, (NDR64_UINT16) 0 , 48 , } };
这里可以看到, 每一个参数都有描述符.
在IDA中如下:
我们查看一下__midl_frag11_t
结构体的定义:
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 typedef struct { struct _NDR64_PROC_FORMAT frag1 ; struct _NDR64_BIND_AND_NOTIFY_EXTENSION frag2 ; struct _NDR64_PARAM_FORMAT frag3 ; struct _NDR64_PARAM_FORMAT frag4 ; struct _NDR64_PARAM_FORMAT frag5 ; struct _NDR64_PARAM_FORMAT frag6 ; struct _NDR64_PARAM_FORMAT frag7 ; struct _NDR64_PARAM_FORMAT frag8 ; struct _NDR64_PARAM_FORMAT frag9 ; } __midl_frag11_t ;typedef struct _NDR64_PARAM_FORMAT { PNDR64_FORMAT Type; NDR64_PARAM_FLAGS Attributes; NDR64_UINT16 Reserved; NDR64_UINT32 StackOffset; } NDR64_PARAM_FORMAT, *PNDR64_PARAM_FORMAT; typedef struct _NDR64_PARAM_FLAGS { NDR64_UINT16 MustSize : 1 ; NDR64_UINT16 MustFree : 1 ; NDR64_UINT16 IsPipe : 1 ; NDR64_UINT16 IsIn : 1 ; NDR64_UINT16 IsOut : 1 ; NDR64_UINT16 IsReturn : 1 ; NDR64_UINT16 IsBasetype : 1 ; NDR64_UINT16 IsByValue : 1 ; NDR64_UINT16 IsSimpleRef : 1 ; NDR64_UINT16 IsDontCallFreeInst : 1 ; NDR64_UINT16 SaveForAsyncFinish : 1 ; NDR64_UINT16 IsPartialIgnore : 1 ; NDR64_UINT16 IsForceAllocate : 1 ; NDR64_UINT16 Reserved : 2 ; NDR64_UINT16 UseCache : 1 ; } NDR64_PARAM_FLAGS;
以第二个参数的描述符举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { &__midl_frag15, { 1 , 1 , 0 , 1 , 0 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 , (NDR64_UINT16) 0 , 0 }, (NDR64_UINT16) 0 , 16 , },
它的 Attribute为 10Bh
, 即IsSimpleRef
, IsIn
, MustFree
, MustSize
.
查看__midl_frag15
:
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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 typedef struct { struct _NDR64_STRUCTURE_HEADER_FORMAT { NDR64_FORMAT_CHAR FormatCode; NDR64_ALIGNMENT Alignment; NDR64_STRUCTURE_FLAGS Flags; NDR64_UINT8 Reserve; NDR64_UINT32 MemorySize; } { 0x31 , (NDR64_UINT8) 7 , { 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 }, (NDR64_UINT8) 0 , (NDR64_UINT32) 40 }, struct { struct _NDR64_NO_REPEAT_FORMAT { NDR64_FORMAT_CHAR FormatCode; NDR64_UINT8 Flags; NDR64_UINT16 Reserved1; NDR64_UINT32 Reserved2; } { 0x80 , (NDR64_UINT8) 0 , (NDR64_UINT16) 0 , (NDR64_UINT32) 0 }, struct _NDR64_POINTER_INSTANCE_HEADER_FORMAT { NDR64_UINT32 Offset; NDR64_UINT32 Reserved; } { (NDR64_UINT32) 16 , (NDR64_UINT32) 0 }, struct _NDR64_POINTER_FORMAT { NDR64_FORMAT_CHAR FormatCode; NDR64_UINT8 Flags; NDR64_UINT16 Reserved; PNDR64_FORMAT Pointee; } { 0x21 , (NDR64_UINT8) 32 , (NDR64_UINT16) 0 , &__midl_frag16 }, struct _NDR64_NO_REPEAT_FORMAT { 0x80 , (NDR64_UINT8) 0 , (NDR64_UINT16) 0 , (NDR64_UINT32) 0 }, struct _NDR64_POINTER_INSTANCE_HEADER_FORMAT { (NDR64_UINT32) 32 , (NDR64_UINT32) 0 }, struct _NDR64_POINTER_FORMAT { 0x21 , (NDR64_UINT8) 32 , (NDR64_UINT16) 0 , &__midl_frag19 }, NDR64_FORMAT_CHAR frag7; 0x93 } frag2; } __midl_frag15_t ;
这里我把数值和结构体声明放到了一起, 方便理解. 通过这个结构体信息, 可以知道, 结构体大小为0x28. 有两个指针, 分别在结构体偏移 0x10, 0x20处.
查看PNDR64_FORMAT __midl_frag16
:
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 static const __midl_frag16_t __midl_frag16 ={ typedef struct _NDR64_CONF_ARRAY_HEADER_FORMAT { NDR64_FORMAT_CHAR FormatCode; NDR64_ALIGNMENT Alignment; NDR64_ARRAY_FLAGS Flags; NDR64_UINT8 Reserved; NDR64_UINT32 ElementSize; PNDR64_FORMAT ConfDescriptor; } NDR64_CONF_ARRAY_HEADER_FORMAT; { 0x41 , (NDR64_UINT8) 0 , { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 }, (NDR64_UINT8) 0 , (NDR64_UINT32) 1 , &__midl_frag17 }, typedef struct _NDR64_ARRAY_ELEMENT_INFO { NDR64_UINT32 ElementMemSize; PNDR64_FORMAT Element; } NDR64_ARRAY_ELEMENT_INFO; { (NDR64_UINT32) 1 , &__midl_frag21 } }; static const __midl_frag21_t __midl_frag21 =0x10 ; static const __midl_frag17_t __midl_frag17 ={ (NDR64_UINT32) 1 , typedef struct _NDR64_EXPR_VAR { NDR64_FORMAT_CHAR ExprType; NDR64_FORMAT_CHAR VarType; NDR64_UINT16 Reserved; NDR64_UINT32 Offset; } NDR64_EXPR_VAR; { 0x3 , 0x5 , (NDR64_UINT16) 0 , (NDR64_UINT32) 8 } };
通过__midl_frag16
我们可以知道, 数组的size取决于偏移为8字段的值, 数组成员为char类型.
Rpcview 获取 RPC 接口 IDL 通过rpcview的decompile
操作, 可以得到接口的idl.
如果遇到rpcview解析idl报错, 比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [ uuid(3d267954-eeb7-11d1-b94e-00c04fa3080d), version(1.0), ] interface DefaultIfName { [ERROR] unable to get list of all types sorted ...... error_status_t Proc8_TLSRpcRequestNewLicense( [in][context_handle] void* arg_0, [in]long arg_1, [in]struct Struct_302_t* arg_2, [in][string] wchar_t* arg_3, [in][string] wchar_t* arg_4, [in][range(0,16384)] long arg_5, [in][ref][size_is(arg_5)]/*[range(0,16384)]*/ byte *arg_6, [in]long arg_7, [out]long *arg_8, [out][ref][size_is(, *arg_8)]/*[range(0,0)]*/ byte **arg_9, [in][out]long *arg_10); ....
[size_is(, *arg_8)]
表示arg_9
的堆大小取决于*arg_8
的值
因为解析错误, 导致我们需要逆向struct Struct_302_t
结构体. 有两个方法可以逆向结构体.
方法一 :
找到client端, 然后查看client是怎么调用的. 找client的方法如下, 用powershell安装 NtObjectManager
1 2 3 PS > Install-Module -Name NtObjectManager -RequiredVersion 1.1 .33 PS > $rpc = ls c:\windows\system32\*.dll | Get-RpcServer -ParseClients PS > $rpc | ? {$_ .Client -and $_ .InterfaceId -eq '44d1520b-6133-41f0-8a66-d37305ecc357' } | Select FilePath
原理就是遍历dll, 找到调用了rpc的操作, 并找出其中调用了目标接口的dll. 根据dll(或者dll的引用方), 看client是怎么操作的. 这个方法不一定好用, 比如一些size_is
标签是看不出来的. 此处感谢k0shl师父的分享.
方法二 :
逆向server的 MIDL_SYNTAX_INFO
结构体.
示例lserver.dll
:
这里, 我关心的是第45个rpc函数的结构体描述符, 即1800A29B0:
查看arg2对应的描述符:
可以看到, 结构体大小为0x28, 字段大致如下:
1 2 3 4 5 6 7 8 9 10 struct{ int f_0; int f_4; int f_8; int f_Ch;//pad void *f_10h; int f_18h; int f_1Ch; void *f_20h; };
跟入unk_1800A22C0
:
查看第一个指针的描述符:
可以知道, 结构体是一个数组, 类型为char, size偏移为8.
1 2 3 4 5 6 7 8 9 10 struct{ int f_0; int f_4; int f_8; int f_Ch;//pad [size_is(f_8)]char *f_10h; int f_18h; int f_1Ch; void *f_20h; };
f_20h的分析:
所以, arg2对应的结构体如下:
1 2 3 4 5 6 7 8 9 10 struct{ int f_0; int f_4; int f_8; int f_Ch;//pad [size_is(f_8)]char *f_10h; int f_18h; int f_1Ch; [size_is(f_18h)]char *f_20h; };
RPC项目示例 来自 示例项目 代码里的”Example1.h”是根据idl自动生成的.
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 #include <iostream> #include "Example1.h" void Output (const char * szOutput) { std ::cout << szOutput << std ::endl ; } RPC_STATUS CALLBACK SecurityCallback (RPC_IF_HANDLE , void * ) { return RPC_S_OK; } int main () { RPC_STATUS status; status = RpcServerUseProtseqEp( reinterpret_cast<unsigned char *>("ncacn_ip_tcp" ), RPC_C_PROTSEQ_MAX_REQS_DEFAULT, reinterpret_cast<unsigned char *>("4747" ), NULL ); if (status) exit (status); status = RpcServerRegisterIf2( Example1_v1_0_s_ifspec, NULL , NULL , RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, RPC_C_LISTEN_MAX_CALLS_DEFAULT, (unsigned )-1 , SecurityCallback); if (status) exit (status); status = RpcServerListen( 1 , RPC_C_LISTEN_MAX_CALLS_DEFAULT, FALSE); if (status) exit (status); } void * __RPC_USER midl_user_allocate (size_t size) { return malloc (size); } void __RPC_USER midl_user_free (void * p) { free (p); }
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); }
Client :
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); }
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 FC64_ZERO = 0 0x0 FC64_UINT8 = 1 0x1 FC64_INT8 = 2 0x2 FC64_UINT16 = 3 0x3 FC64_INT16 = 4 0x4 FC64_INT32 = 5 0x5 FC64_UINT32 = 6 0x6 FC64_INT64 = 7 0x7 FC64_UINT64 = 8 0x8 FC64_INT128 = 9 0x9 FC64_UINT128 = 10 0xa FC64_FLOAT32 = 11 0xb FC64_FLOAT64 = 12 0xc FC64_FLOAT80 = 13 0xd FC64_FLOAT128 = 14 0xe FC64_CHAR = 16 0x10 FC64_WCHAR = 17 0x11 FC64_IGNORE = 18 0x12 FC64_ERROR_STATUS_T = 19 0x13 FC64_POINTER = 20 0x14 FC64_RP = 32 0x20 FC64_UP = 33 0x21 //表示这个字段是个指针 FC64_OP = 34 0x22 FC64_FP = 35 0x23 FC64_IP = 36 0x24 FC64_STRUCT = 48 0x30 FC64_PSTRUCT = 49 0x31 FC64_CONF_STRUCT = 50 0x32 FC64_CONF_PSTRUCT = 51 0x33 FC64_BOGUS_STRUCT = 52 0x34 FC64_FORCED_BOGUS_STRUCT = 53 0x35 FC64_CONF_BOGUS_STRUCT = 54 0x36 FC64_FORCED_CONF_BOGUS_STRUCT = 55 0x37 FC64_SYSTEM_HANDLE = 60 0x3c FC64_FIX_ARRAY = 64 0x40 FC64_CONF_ARRAY = 65 0x41 FC64_VAR_ARRAY = 66 0x42 FC64_CONFVAR_ARRAY = 67 0x43 FC64_FIX_FORCED_BOGUS_ARRAY = 68 0x44 FC64_FIX_BOGUS_ARRAY = 69 0x45 FC64_FORCED_BOGUS_ARRAY = 70 0x46 FC64_BOGUS_ARRAY = 71 0x47 FC64_ENCAPSULATED_UNION = 80 0x50 FC64_NON_ENCAPSULATED_UNION = 81 0x51 FC64_CHAR_STRING = 96 0x60 FC64_WCHAR_STRING = 97 0x61 FC64_STRUCT_STRING = 98 0x62 FC64_CONF_CHAR_STRING = 99 0x63 FC64_CONF_WCHAR_STRING = 100 0x64 FC64_CONF_STRUCT_STRING = 101 0x65 FC64_BIND_CONTEXT = 112 0x70 FC64_BIND_GENERIC = 113 0x71 FC64_BIND_PRIMITIVE = 114 0x72 FC64_AUTO_HANDLE = 115 0x73 FC64_CALLBACK_HANDLE = 116 0x74 FC64_SUPPLEMENT = 117 0x75 FC64_NO_REPEAT = 128 0x80 FC64_FIXED_REPEAT = 129 0x81 FC64_VARIABLE_REPEAT = 130 0x82 FC64_FIXED_OFFSET = 131 0x83 FC64_VARIABLE_OFFSET = 132 0x84 FC64_STRUCTPADN = 144 0x90 FC64_EMBEDDED_COMPLEX = 145 0x91 FC64_BUFFER_ALIGN = 146 0x92 FC64_END = 147 0x93 FC64_TRANSMIT_AS = 160 0xa0 FC64_REPRESENT_AS = 161 0xa1 FC64_USER_MARSHAL = 162 0xa2 FC64_PIPE = 163 0xa3 FC64_RANGE = 164 0xa4 FC64_PAD = 165 0xa5
通过以下方式获取:
参考来源: Building More Windows RPC Tooling for Security Research - James Forshaw
RPC 反序列化流程(不完整) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Ndr64StubWorker Ndr64pServerUnMarshal check func arg tag; IsBasetype..... Ndr64pPointerUnmarshall or Ndr64ConformantStringUnmarshall or Ndr64ComplexStructUnmarshall // 针对结构体指针类型 Ndr64ComplexStructMemorySize // 检查结构体描述符, 计算结构体长度 解码数据.... Ndr64SimpleTypeUnmarshall Ndr64EmbeddedPointerUnmarshall Ndr64ConformantArrayUnmarshall Ndr64pEarlyCheckCorrelation EvaluateExpr Ndr64pGetAllocateAllNodesContext Ndr64ConformantArrayMemorySize a1->pfnAllocate()// 申请数组的堆 ..... or Ndr64UnmarshallHandle // 针对 context_handle 类型 ..... Invoke target rpc func
参考 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.