【调试技术】RPC
概述:windwos RPC 调试记录
相关文章:
- 【RPC】Tools
- 【RPC】ALPC
- 【RPC】漏洞
- 【RPC】RpcView 相关
- https://github.com/klezVirus/RpcProxyInvoke/blob/3f07a06891bc234ec1a1ea93f62a8b3e8e57b263/[Slides] Unraveling an RPC Thread - How Attackers Abuse Server Calls for Code Execution.pdf - 阅力值 ⭐⭐⭐⭐⭐
- Making Windows RPC Calls: An Overview of Client-Server Interaction - 阅力值 ⭐⭐⭐⭐
- LPC Communication
RPCSS 服务:
- Essential Windows Services: Remote Procedure Call (RPC) / RpcSs | The Core Technologies Blog - 阅力值 ⭐⭐⭐⭐
StackOver Flow:
前言
windows RPC 是常用的远程调用,有关 rpc 的使用可以在本博客中搜索 RPC 关键字查看相关使用。或者直接查看 RPC 标签。RPC 工具也可以使用开源 RPCView。
RPC 及相关工具
RpcView 是一款非常棒的 Rpc 工具,非常有助于我们在分析调试过程中查看 Rpc 的相关信息。如果你需要调试 RPC 过程,那么这个工具可以是你的得力帮手。
简单使用
可以看到 RpcView 可以看到 RPC 进程使用的协议 ncacn_np ,以及 ncacn_np 协议对应的管道名 \\pipe\\hello
。那本次调试的目标就是对标 RpcView。在 windbg 中可以查看一个进程 RPC 详细详细。

调试步骤
使用微软官方的 dbgrpc.exe
rpc 调试主要用到的 windows kits 中带的文件 dbgrpc.exe。详见微软官方文档 使用 DbgRpc 工具 - Windows drivers | Microsoft Learn
使用 Windbg
参考 StackOver Flow 上的那个回答,正式开始对 RPC 的调试。
调试环境
使用 Win11,自己写一个 Demo 用来调试。例如 Github 的 MasqueradePEBtoCopyFile。
windbg 调试获取 RPC 接口 GUID
-
首先说明下原理,RPC 客户端进程最终都会调用到
NdrClientCall2
这个接口。这里补充下这个函数的一个调用,从 NdrClientCall4 调用
可以看到第一个参数为 PMIDL_STUB_DESC
1
2
3
4
5
6
7CLIENT_CALL_RETURN NdrClientCall4(PMIDL_STUB_DESC pStubDescriptor, PFORMAT_STRING pFormat, ...)
{
va_list va; // [esp+10h] [ebp+10h] BYREF
va_start(va, pFormat);
return NdrClientCall2(pStubDescriptor, pFormat, va);
} -
在 windbg 中添加断点并触发
1
bp RPCRT4!NdrClientCall2
-
断点触发后,查看入参
1
2
3
4
5
6
70:000> .echo "Arguments:"; dds esp L5
Arguments:
00fcd930 752a7174 RPCRT4!NdrClientCall4+0x14
00fcd934 74d81b98 combase!ILocalObjectExporter_StubDesc
00fcd938 74db86fa combase!lclor__MIDL_ProcFormatString+0x2
00fcd93c 00fcd950
00fcd940 00fcdf58其中第一个参数为
74d81b98
-
查看第一个参数,输出如下所示,RPC 相关的信息都保存在了 RpcInterfaceInformation 成员,其对应的结构体为
RPC_CLIENT_INTERFACE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
210:000> dt combase!ILocalObjectExporter_StubDesc 74d81b98
+0x000 RpcInterfaceInformation : 0x74db86b0 Void
+0x004 pfnAllocate : 0x74e851b0 void* combase!MIDL_user_allocate+0
+0x008 pfnFree : 0x74eaa150 void combase!MIDL_user_free+0
+0x00c IMPLICIT_HANDLE_INFO : <unnamed-tag>
+0x010 apfnNdrRundownRoutines : (null)
+0x014 aGenericBindingRoutinePairs : (null)
+0x018 apfnExprEval : (null)
+0x01c aXmitQuintuple : (null)
+0x020 pFormatTypes : 0x74dc56ba ""
+0x024 fCheckBounds : 0n1
+0x028 Version : 0xa000c
+0x02c pMallocFreeStruct : (null)
+0x030 MIDLVersion : 0n134283892
+0x034 CommFaultOffsets : 0x74dc5684 _COMM_FAULT_OFFSETS
+0x038 aUserMarshalQuadruple : 0x74d86894 _USER_MARSHAL_ROUTINE_QUADRUPLE
+0x03c NotifyRoutineTable : (null)
+0x040 mFlags : 1
+0x044 CsRoutineTables : (null)
+0x048 ProxyServerInfo : (null)
+0x04c pExprInfo : 0x74d86b58 _NDR_EXPR_DESC -
查看
RpcInterfaceInformation
,到这里就看到了保存接口信息的成员变量,其对应的结构体为_RPC_SYNTAX_IDENTIFIER
1
2
3
4
5
6
7
8
9
10
110:000> dt RPC_CLIENT_INTERFACE 0x74db86b0
combase!RPC_CLIENT_INTERFACE
+0x000 Length : 0x44
+0x004 InterfaceId : _RPC_SYNTAX_IDENTIFIER
+0x018 TransferSyntax : _RPC_SYNTAX_IDENTIFIER
+0x02c DispatchTable : (null)
+0x030 RpcProtseqEndpointCount : 0
+0x034 RpcProtseqEndpoint : (null)
+0x038 Reserved : 0
+0x03c InterpreterInfo : (null)
+0x040 Flags : 0 -
查看接口 GUID,可以看到接口 GUID 为
{E60C73E6-88F9-11CF-9AF1-0020AF6E72F4}
1
2
3
40:000> dx -r1 (*((combase!_RPC_SYNTAX_IDENTIFIER *)0x74db86b4))
(*((combase!_RPC_SYNTAX_IDENTIFIER *)0x74db86b4)) [Type: _RPC_SYNTAX_IDENTIFIER]
[+0x000] SyntaxGUID : {E60C73E6-88F9-11CF-9AF1-0020AF6E72F4} [Type: _GUID]
[+0x010] SyntaxVersion [Type: _RPC_VERSION]
使用 RpcView 查看接口
如果是 32 位的 RPC,则需要用 RpcView32,如果是 64 位的 RPC,就用 RpcView64。
使用 RpcView 查找接口的流程如下图所示,可以看到当前 RPC Client 请求的服务端为 RPCSS。

查看调用的函数
这里补充下上图中 Interface Properties 的部分

到这里可以看到主要调用的 dll 为 rpcss.dll
。如果我们还要继续查明调用的是哪个接口,那可以继续看 NdrClientCall2
的第二个参数。
-
第二个参数的结构体如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21typedef struct _NDR_PROC_HEADER_RPC
{
unsigned char handle_type;
unsigned char Oi_flags;
/*
* RPCF_Idempotent = 0x0001 - [idempotent] MIDL attribute
* RPCF_Broadcast = 0x0002 - [broadcast] MIDL attribute
* RPCF_Maybe = 0x0004 - [maybe] MIDL attribute
* Reserved = 0x0008 - 0x0080
* RPCF_Message = 0x0100 - [message] MIDL attribute
* Reserved = 0x0200 - 0x1000
* RPCF_InputSynchronous = 0x2000 - unknown
* RPCF_Asynchronous = 0x4000 - [async] MIDL attribute
* Reserved = 0x8000
*/
unsigned int rpc_flags;
unsigned short proc_num;
unsigned short stack_size;
} NDR_PROC_HEADER_RPC; -
查看第二个参数
pFormat
1
2
30:000> dt combase!lclor__MIDL_ProcFormatString 74db86fa
+0x000 Pad : 0n26624
+0x002 Format : [961] ""1
2
30:000> db 74db86fa
74db86fa 00 68 00 00 00 00 00 00-84 00 32 00 00 00 16 00 .h........2.....
74db870a 5c 02 47 20 08 43 01 00-00 00 00 00 0b 00 04 00 \.G .C..........在 Win11 上该结构体应该是有改动的,不过大致参考一下 StackOver Flow 上的分析
1
2
3
4
5
6
7# StackOver Flow 上的结构体对照
00 48 00 00 00 00 06 00-4c 00 30 40 00 00 00 00 ...
handle_type = 0x00
Oi_flags = 0x48
rpc_flags = 0x00000000
proc_num = 0x0006
stack_size = 0x004C本次数据:
1
2
3
4
5
600 68 00 00 00 00 00 00-84 00 32 00 00 00 16 00
handle_type = 0x00
Oi_flags = 0x68
rpc_flags = 0x00000000
proc_num = 0x0000
stack_size = 0x0084可以看到调用的
proc_num
为 0 -
查看调用参数
这里可以看到调用的函数地址为
0x00007ff83303ab50
,注意这个地址是该函数在当前进程中的地址,实际在 DLL 中的偏移地址还需要减去加载地址。实际上在 RpcView 左侧 Interface Properties 窗口的 Main 栏 中就有 DLL 的 Base 基址。不用别的软件再看 DLL 基址了。 下面这步有点多余。 -
查看实际偏移
查看进程加载 rpcss.dll 的地址:可以看到 load Address 的值为:
0x00007FF833030000
,实际偏移为:0xab50
-
使用 IDA 查看 rpcss.dll 相关偏移处的内容:
-
ab50
处的内容1
.text:000000018000AB50 ; __int64 __fastcall Connect(void *, unsigned __int16 *, unsigned __int16 *, struct __MIDL_ILocalObjectExporter_0001 *, unsigned int, unsigned __int16, volatile signed __int32 **, _DWORD *, LPVOID *, _QWORD *, unsigned int, unsigned __int64 *, unsigned int *, _DWORD *, _DWORD *, _DWORD *, unsigned __int16 **, _DWORD *, _DWORD *, unsigned int *, unsigned __int16 **, unsigned int *, struct __MIDL_ILocalObjectExporter_0002 **, unsigned int *, struct _GUID **, _DWORD *, DWORD *, __int64 *, _OWORD *, unsigned int *, _DWORD *, _QWORD *)
可以看到调用的 Connect 函数。
到此,一次完整的调用就被展现出来了。在被调用看到的堆栈如下所示:
1 |
|
StackOver Flow 真是非常有含金量。尽管在之前的学习了解 RPC 过程中已经有观察到 RpcInterfaceInformation
这个关键变量。如 RPCCraft 中甚至已经对该结构体详细说明,但是由于对 NdrClientCall 的调试不够到位,遂一直没有头绪。这次调试也算是了解到了 RPC 的一些关键调用。
Command 断点
RPC 客户端
在 NdrClientCall2 添加命令断点查看 RPC 调用信息
1 |
|
使用示例,断点触发时,会直接打印相关信息,但是必须是从 NdrClientCall4 函数调用过去的才可以,从 combase
调用过去的是从过 eax 传参:
1 |
|
GUID 对应的 RPC 不存在?
32 位和 64 位的都看看
RPC 服务端
查看被调用接口
1 |
|
附录:
相关结构体
RPC_MESSAGE
该结构体在 RpcCraft 项目中被用来填充自定义 Rpc 消息。

NdrClientCall2 分析脚本
dbgtools.js
如何使用
1 |
|