标题: 用RPC/ALPC调试手段分析Win10 FQDN解析过程 创建: 2021-11-16 12:10 更新: 2021-11-23 14:16 链接: https://scz.617.cn/windows/202111161210.txt -------------------------------------------------------------------------- 目录: ☆ 用RPC/ALPC调试手段分析Win10 FQDN解析过程 1) 背景介绍 2) Inside WS2_32!GetAddrInfoW 3) 拦载RPCRT4!NdrpClientCall3获取RPC Server的IID/远程过程号 4) 用RpcView定位RPC Server的PID及远程过程 5) dnsrslvr!R_ResolverQuery调用栈回溯 6) 拦截RPCRT4!DispatchToStubInCNoAvrf获取IID/远程过程号/远程过程 7) 内核态获取ALPC目标进程 8) 利用ALPCLogger获取ALPC目标进程 ☆ 后记 ☆ 参考资源 -------------------------------------------------------------------------- ☆ 用RPC/ALPC调试手段分析Win10 FQDN解析过程 1) 背景介绍 参看 《DNS系列(11)--研究Win10 FQDN解析》 https://scz.617.cn/windows/202103071208.txt 假设用ping、Opera触发FQDN解析,最终通过RPC交由其他进程完成真正的53/UDP通信; 单就Win10 FQDN解析而言,这个RPC Server是Dnscache(DNS Client)服务。 一个通用问题,如何普适地利用调试手段找到RPC Client所对应的RPC Server,如何 在RPC Server中对RPC Client进行识别、过滤?本文给出部分解答,某些技术手段属 于Hacking,不绝对可靠,但大多数时候可行。假设读者了解RPC/ALPC相关基础知识, 不做科普。 Guest环境如下 -------------------------------------------------------------------------- Win10 Win10企业版2016 LTSB 1607(OS Build 14393.4704) ping.exe 10.0.14393.0 (rs1_release.160715-1616) dnsapi.dll 10.0.14393.4350 (rs1_release.210407-2154) dnsrslvr.dll 10.0.14393.4350 (rs1_release.210407-2154) ntdll.dll 10.0.14393.4704 (rs1_release.211004-1917) rpcrt4.dll 10.0.14393.4704 (rs1_release.211004-1917) -------------------------------------------------------------------------- 2) Inside WS2_32!GetAddrInfoW 在Guest中 "C:\temp\cdb.exe" -noinh -snul -hd -o ping.exe www.baidu.com .prompt_allow +reg +ea +dis bc * bp WS2_32!GetAddrInfoW "kpn;du @rcx" 会有两次命中WS2_32!GetAddrInfoW # Child-SP RetAddr Call Site 00 00000034`8ab5ed08 00007ff7`1daa1154 WS2_32!GetAddrInfoW 01 00000034`8ab5ed10 00007ff7`1daa1ead ping!ResolveTarget+0x60 02 00000034`8ab5eda0 00007ff7`1daa32fd ping!wmain+0x439 03 00000034`8ab5f8d0 00007fff`885b84d4 ping!NlsFPutMsgW+0x2f5 04 00000034`8ab5f910 00007fff`88d41791 KERNEL32!BaseThreadInitThunk+0x14 05 00000034`8ab5f940 00000000`00000000 ntdll!RtlUserThreadStart+0x21 # Child-SP RetAddr Call Site 00 00000034`8ab5ed08 00007ff7`1daa11bf WS2_32!GetAddrInfoW 01 00000034`8ab5ed10 00007ff7`1daa1ead ping!ResolveTarget+0xcb 02 00000034`8ab5eda0 00007ff7`1daa32fd ping!wmain+0x439 03 00000034`8ab5f8d0 00007fff`885b84d4 ping!NlsFPutMsgW+0x2f5 04 00000034`8ab5f910 00007fff`88d41791 KERNEL32!BaseThreadInitThunk+0x14 05 00000034`8ab5f940 00000000`00000000 ntdll!RtlUserThreadStart+0x21 参看 https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow https://docs.microsoft.com/en-us/windows/win32/api/ws2def/ns-ws2def-addrinfow WS2_32!GetAddrInfoW函数原型如下 -------------------------------------------------------------------------- INT WSAAPI GetAddrInfoW ( [in,optional] PCWSTR pNodeName, // rcx [in,optional] PCWSTR pServiceName, // rdx [in,optional] ADDRINFOW *pHints, // r8 [out] PADDRINFOW *ppResult // r9 ) -------------------------------------------------------------------------- WS2_32!GetAddrInfoW第3形参类型是ADDRINFOW结构 -------------------------------------------------------------------------- typedef struct addrinfoW { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; size_t ai_addrlen; PWSTR ai_canonname; struct sockaddr *ai_addr; struct addrinfoW *ai_next; } ADDRINFOW, *PADDRINFOW; -------------------------------------------------------------------------- addrinfoW结构的第一个成员ai_flags会影响GetAddrInfoW()的行为。ping调用 GetAddrInfoW()时,pHints.ai_flags第一次传值4,第二次传值2,它们的含义是 -------------------------------------------------------------------------- AI_CANONNAME(2) The canonical name is returned in the first ai_canonname member. AI_NUMERICHOST(4) pNodeName以点分十进制形式指定 -------------------------------------------------------------------------- "www.baidu.com"是FQDN,不是点分十进制IP地址,故第一次WS2_32!GetAddrInfoW调 用失败,返回值WSAHOST_NOT_FOUND(11001),参看 https://docs.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2 可以快速查看11001的意义 $ net helpmsg 11001 No such host is known. 我们关心第二次WS2_32!GetAddrInfoW调用,对应FQDN解析。 Win10上WS2_32!GetAddrInfoW内部用到"RPC Over ALPC"。注意RPC和ALPC是两个独立 的概念,二者不具有必然的捆绑关系,换句话说,存在其他ALPC,但与RPC无关。 单次WS2_32!GetAddrInfoW内部会两次调用ntdll!NtAlpcSendWaitReceivePort,第一 次对应RPC的BIND操作,第二次才是真正的远程过程调用。 bp ntdll!NtAlpcSendWaitReceivePort "kpn;r rcx" 断点命中时rcx是ClientCommunicationPortHandle,调用栈回溯整理如下 -------------------------------------------------------------------------- DNSAPI!Rpc_ResolverQuery+0xf4 RPCRT4!NdrClientCall3 RPCRT4!NdrClientCall3+0xed RPCRT4!NdrpClientCall3 RPCRT4!NdrpClientCall3+0x4e9 // BIND RPCRT4!GenericHandleMgr RPCRT4!GenericHandleMgr+0x92 ntdll!LdrpDispatchUserCallTarget DNSAPI!DNS_RPC_HANDLE_bind DNSAPI!DNS_RPC_HANDLE_bind+0xd0 RPCRT4!RpcBindingBind RPCRT4!RpcBindingBind+0x55 RPCRT4!LRPC_FAST_BINDING_HANDLE::Bind RPCRT4!LRPC_FAST_BINDING_HANDLE::Bind+0x192 RPCRT4!LRPC_CASSOCIATION::Bind RPCRT4!LRPC_CASSOCIATION::Bind+0x6ef ntdll!NtAlpcSendWaitReceivePort // 对应RPC BIND RPCRT4!NdrpClientCall3+0xf71 // 远程过程调用 // call qword ptr [RPCRT4!_guard_dispatch_icall_fptr] ntdll!LdrpDispatchUserCallTarget // "u @rax l 1"检查目标函数 // "jmp rax"进行流程转移 RPCRT4!LRPC_BASE_CCALL::SendReceive RPCRT4!LRPC_BASE_CCALL::SendReceive+0x128 ntdll!NtAlpcSendWaitReceivePort // 对应RPC远程过程调用 -------------------------------------------------------------------------- 3) 拦载RPCRT4!NdrpClientCall3获取RPC Server的IID/远程过程号 从前一小节调用栈看出,WS2_32!GetAddrInfoW内部会过RPCRT4!NdrpClientCall3, 拦截后者可以得到RPC Server的IID、远程过程号。若对RPC本身不太了解,参[1]。 RPCRT4!NdrpClientCall3函数原型如下 -------------------------------------------------------------------------- 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) ) -------------------------------------------------------------------------- 第3形参nProcNum就是远程过程号。IID可以通过第2形参pProxyInfo间接获取。 在Guest中 "C:\temp\cdb.exe" -noinh -snul -hd -o ping.exe www.baidu.com .prompt_allow +reg +ea +dis bc * bp WS2_32!GetAddrInfoW 2 "kpn;du @rcx" 断点命中后设置新断点 bp RPCRT4!NdrpClientCall3 "kpn;dt -io ntdll!_GUID poi(poi(@rdx))+4;r r8" 新断点命中时依次显示IID、远程过程号 IID 45776b01-5956-4485-9f80-f428f7d60129 ProcNum 4 能获取什么,就能对什么设置条件断点,复杂的RPC Client可以利用之。 4) 用RpcView定位RPC Server的PID及远程过程 参看 《RpcView简介》 https://scz.617.cn/windows/202110270957.txt 在Guest中启动RpcView RpcView.exe /f 假设已用第3小节的办法获取RPC Server的IID、远程过程号,在RpcView中搜IID RpcView Filter Interfaces 45776b01-5956-4485-9f80-f428f7d60129 上述操作直接过滤出RPC Server的PID及远程过程,比如 svchost.exe(564) dnsrslvr.dll R_ResolverQuery(4) 检查svchost(564) $ tasklist /svc /fi "services eq dnscache" $ tasklist /svc /fi "pid eq 564" Image Name PID Services ========================= ======== ============================================ svchost.exe 564 CryptSvc, Dnscache, LanmanWorkstation, NlaSvc, TermService svchost(564)中有好几个服务,RDP服务也在其中。 5) dnsrslvr!R_ResolverQuery调用栈回溯 dnsrslvr!R_ResolverQuery第3形参对应FQDN -------------------------------------------------------------------------- dnsrslvr!R_ResolverQuery ( x, x, FQDN, // r8 ... ) -------------------------------------------------------------------------- 调试svchost(564),设置条件断点 bp dnsrslvr!R_ResolverQuery ".if(qwo(@r8)==0x2e007700770077 and qwo(@r8+8)==0x64006900610062 and qwo(@r8+0x10)==0x6f0063002e0075 and wo(@r8+0x18)==0x6d){kpn}.else{du @r8;gc}" FQDN是"www.baidu.com"时断下,否则显示FQDN后继续。dnsrslvr!R_ResolverQuery 会被频繁命中,若不设过滤条件,无法有效调试。 用"ping www.baidu.com"触发上述断点,调用栈回溯如下 # Child-SP RetAddr Call Site 00 000000d8`fd3fedf8 00007fff`8817a583 dnsrslvr!R_ResolverQuery 01 000000d8`fd3fee00 00007fff`881d6162 RPCRT4!Invoke+0x73 02 000000d8`fd3fee90 00007fff`8814a284 RPCRT4!Ndr64AsyncServerWorker+0x392 03 000000d8`fd3fefb0 00007fff`8814919d RPCRT4!DispatchToStubInCNoAvrf+0x24 04 000000d8`fd3ff000 00007fff`88149a4b RPCRT4!RPC_INTERFACE::DispatchToStubWorker+0x1bd 05 000000d8`fd3ff0d0 00007fff`881310ac RPCRT4!RPC_INTERFACE::DispatchToStub+0xcb 06 000000d8`fd3ff130 00007fff`8813152c RPCRT4!LRPC_SCALL::DispatchRequest+0x34c 07 000000d8`fd3ff210 00007fff`8811ae1c RPCRT4!LRPC_SCALL::HandleRequest+0x2bc 08 000000d8`fd3ff330 00007fff`8811c67b RPCRT4!LRPC_ADDRESS::HandleRequest+0x36c 09 000000d8`fd3ff3e0 00007fff`88143a2a RPCRT4!LRPC_ADDRESS::ProcessIO+0x91b 0a 000000d8`fd3ff520 00007fff`88d0d35e RPCRT4!LrpcIoComplete+0xaa 0b 000000d8`fd3ff5c0 00007fff`88d0ecc9 ntdll!TppAlpcpExecuteCallback+0x25e 0c 000000d8`fd3ff670 00007fff`885b84d4 ntdll!TppWorkerThread+0x8d9 0d 000000d8`fd3ffa70 00007fff`88d41791 KERNEL32!BaseThreadInitThunk+0x14 0e 000000d8`fd3ffaa0 00000000`00000000 ntdll!RtlUserThreadStart+0x21 6) 拦截RPCRT4!DispatchToStubInCNoAvrf获取IID/远程过程号/远程过程 第4小节用RpcView快速定位了RPC Server的远程过程。假设没有RpcView可用,又该 如何?本小节介绍一种偏Hacking的方案。 因故,不建议在RPC Server中拦截这些函数 RPCRT4!Invoke RPCRT4!Ndr64StubWorker RPCRT4!NdrServerCallAll RPCRT4!Ndr64AsyncServerWorker RPCRT4!RPC_INTERFACE::DispatchToStubWorker RPCRT4!LRPC_SCALL::DispatchRequest RPCRT4!LRPC_SCALL::HandleRequest 推荐拦截 RPCRT4!DispatchToStubInCNoAvrf 调试svchost(564),设置条件断点 r $t9=0x1088 bp RPCRT4!DispatchToStubInCNoAvrf "r $t0=poi(@rdx+0x68);.if((@$t0&0xffffffff00000000)==(@rdx&0xffffffff00000000)){.if(qwo(@$t0+8)==@$t9){}.else{gc}}.else{gc}" 给$t9指定RPC Client的PID,上述断点对RPC Client的PID进行检查,匹配时才断下。 windbg条件断点.if语句不支持短路求值?只好.if中嵌套.if。 断点命中时用如下命令显示IID、远程过程号、远程过程 dt ntdll!_GUID poi(@rdx+0x28)+4 ? dwo(@rdx+0x1c) dqs poi(poi(poi(@rdx+0x28)+0x50)+8)+dwo(@rdx+0x1c)*8 l 1 显然,此处不只可以过滤PID,也可过滤IID、远程过程号,从而定位远程过程。当 RPC Client是ping这种很简单的进程时,过滤PID足矣。 7) 内核态获取ALPC目标进程 第4小节用RpcView快速定位了RPC Server的PID。假设没有RpcView可用,又该如何? 本小节介绍一种偏Hacking的方案。 参[2],本小节技术方案用到sysinternals的livekd。在Guest中这样启动livekd "\livekd.exe" -k "\kd.exe" livekd必须与kd相配合,不能单独使用。但livekd不涉及双机联调,不需要提前 bcdedit配置,不要求Guest进入"Test Mode",正常启动的Win10中可以直接使用 livekd。对于只读型内核数据访问,livekd非常方便。若发现livekd不能用,最好去 下载最新版本。livekd启动时动态释放并加载一个驱动 C:\Windows\System32\drivers\LiveKdD.SYS 在RPC Client(比如ping)中拦截ntdll!NtAlpcSendWaitReceivePort,断点命中时rcx 是ClientCommunicationPortHandle,假设其值为0xcc。 接下来在livekd中获取句柄0xcc对应的对象地址,这步需要指定ping的PID,可在用 户态"? @$tpid",也可在内核态"!process 0 0 ping.exe",假设ping的PID是5376。 kd> !handle 0xcc 0 0n5376 ... 00cc: Object: ffffda88435ace20 GrantedAccess: 001f0001 (Protected) (Inherit) (Audit) 获取ClientCommunicationPort(0xffffda88435ace20) 用!alpc根据ClientCommunicationPort获取ConnectionPort kd> !alpc /p 0xffffda88435ace20 Port ffffda88435ace20 Type : ALPC_CLIENT_COMMUNICATION_PORT CommunicationInfo : ffffa08d1a1953c0 ConnectionPort : ffffda88402ebe20 (DNSResolver) ClientCommunicationPort : ffffda88435ace20 ServerCommunicationPort : ffffda8843123590 OwnerProcess : ffffda884345b080 (PING.EXE) ... 获取ConnectionPort(0xffffda88402ebe20) 用!alpc查看ConnectionPort所属进程 kd> !alpc /p 0xffffda88402ebe20 Port ffffda88402ebe20 Type : ALPC_CONNECTION_PORT CommunicationInfo : ffffa08d187183a0 ConnectionPort : ffffda88402ebe20 (DNSResolver) ClientCommunicationPort : 0000000000000000 ServerCommunicationPort : 0000000000000000 OwnerProcess : ffffda883ff6a800 (svchost.exe) ... kd> !process 0xffffda883ff6a800 0 PROCESS ffffda883ff6a800 SessionId: 0 Cid: 0234 Peb: d8fa688000 ParentCid: 030c DirBase: 649e2000 ObjectTable: ffffa08d18408340 HandleCount: Image: svchost.exe 至此已知WS2_32!GetAddrInfoW触发的"RPC Over ALPC"目标进程svchost(564)。 用".detach"退出livekd。 8) 利用ALPCLogger获取ALPC目标进程 一般而言,若RPCRT4!NdrpClientCall3的底层走ALPC,即"RPC Over ALPC",可用 ALPCLogger获取ALPC目标进程。 参[3],ALPCLogger是一款C#开发的工具,记录ALPC的Client、Server,作者是Pavel Yosifovich,《Windows Internals, 7th Edition》的作者之一。 ALPCLogger用了ETW技术,调用栈回溯中有内核态的地址。但是,调用栈回溯没有符 号信息,只有绝对地址,只能在调试器中查看到底是什么。调用栈回溯所用控件无法 一次性全选、复制,只能单条选中地址并复制。ALPCLogger开源,但我没兴趣改它。 ALPCLogger算是PoC,用起来非常不便,但确实能用。曾小结过如何使用ALPCLogger, 但由于RpcView、livekd非常顺手,不在此啰嗦。 与RpcView、livekd相比,ALPCLogger相当鸡肋,不推荐,此间仅备忘。 ☆ 后记 第6小节的技术方案非常Hacking化,莫在生产环境中使用。 RPC Client不是都调RPCRT4!NdrpClientCall3。若碰上RPCRT4!NdrpClientCall2,其 形参不直接提供远程过程号,需要从pFormat中析取,本文没有举例说明,有刚需的 读者自行练习。 感慨一下,很多人不识货。 ☆ 参考资源 [1] Offensive Windows IPC Internals 2 - [2021-02-21] https://csandker.io/2021/02/21/Offensive-Windows-IPC-2-RPC.html [2] livekd https://docs.microsoft.com/en-us/sysinternals/downloads/livekd [3] ALPCLogger - Pavel Yosifovich https://github.com/zodiacon/ALPCLogger (调用栈回溯不支持符号)