文章总结: 本文深入讲解WindowsRPC实战应用,通过编写简单RPC接口详细剖析客户端与服务端通信流程。内容涵盖协议序列选择、绑定字符串构建、IDL接口定义,并提供完整代码示例演示固定端口与动态端口的实现差异。文章还介绍使用RpcView验证接口暴露和Impacket工具探测端点映射器的实用方法,为理解RPC通信机制提供具体操作指导。 综合评分: 85 文章分类: 技术标准,实战经验,WEB安全,安全工具,内网渗透
Windows进程间通信技术深潜(第二部分):RPC实战与拆解
幻泉之洲
2026年4月29日 16:54 北京
在小说阅读器读本章
去阅读
在第一部分讲解了RPC的基础概念后,本文直接进入实战,从编写一个简单的RPC接口开始,详细剖析客户端与服务端的通信流程、代码实现,并对比静态与动态端口的区别,最后通过工具验证RPC接口的暴露情况。阅读本文,你将亲手搭建一个能跑起来的RPC通信模型。
我们继续聊Windows RPC。上一期讲了概览和网络层面,这期我们不说空话,直接上手敲代码,看看一个RPC接口究竟是怎么从定义到调用的。
事先说明,这篇文章里的东西,大部分是我自己折腾出来的,还参考了MSDN官方文档和一位叫“@0xcsandker”的大牛的研究,他的《Windows IPC内部机制解析》系列写得相当透彻,给了我不少启发,链接我也放在这:Offensive Windows IPC Internals (https://csandker.io/2021/02/21/Offensive-Windows-IPC-2-RPC.html)。
好了,开工。
RPC到底是什么?
字面意思叫“远程过程调用”,但别被“远程”骗了。它就是一种让你一个程序里的函数,能跑去调用另一个程序(另一个进程)里函数的机制。这两个程序可以在同一台电脑上,也可以隔着网络。
核心思想很简单:客户端和服务器不共享内存,它们通过一个定义好的“接口”来对话,底层用什么传数据(TCP、管道啥的)它们不用操心。它和LPC、ALPC、命名管道一样,都属于Windows IPC 这个大家族。
架构上,它玩了个“替身”(Stub)的把戏。你(客户端)调用一个本地函数,这个函数其实是个替身。替身负责把你的参数打包、转换成标准格式、通过网络发出去。服务器那边也有个替身,它负责收包裹、解包、调用真正的函数。函数执行完,再按原路把结果返回来。对你来说,感觉就像调了个本地函数一样。
先搞清楚怎么“说话”:协议序列
客户端和服务器得先约好用哪种“语言”交流,这叫协议序列(Protocol Sequence)。它不是一个单一协议,而是一个组合,告诉你用哪种RPC协议、哪种传输层、要不要走网络。
微软主要支持三种:
- NCACN:面向连接的,像TCP,可靠。比如
ncacn_ip_tcp(走TCP/IP)和ncacn_np(走命名管道)。 - NCADG:无连接的,像UDP,现在用得少了。
- NCALRPC:本地专用的,同一台机器上进程间通信最快,不走网络栈。
具体有哪些,可以去微软官方文档查 (https://docs.microsoft.com/en-us/windows/win32/rpc/protocol-sequence-constants)。
地址怎么写:绑定字符串
光知道“语言”不行,还得知道“地址”在哪。绑定字符串就是把所有连接信息拼成一个人能看懂的地址。
格式是:协议序列:网络地址[端点]
举个例子:
- 走TCP连IP是192.168.177.132的135端口:
ncacn_ip_tcp:192.168.177.132[135] - 走SMB(命名管道):
ncacn_np:192.168.0.1[\pipe\spoolss]
本地通信(ncalrpc)的话,网络地址经常留空。
心脏:接口定义与IDL
两边具体能“聊”什么,由接口决定。接口定义了服务器程序提供了哪些远程函数,包括函数名、参数、返回值。
这东西用一种专门的“接口定义语言”(IDL)来写。看个最简单的例子:
我来翻译一下这几行:
- 第2行:
uuid(...),这是接口的“身份证号”(GUID),全球唯一。 - 第3行:
version(1.0),接口版本号。 - 第4行:
implicit_handle(handle_t ExampleInterHandle),用了“隐式句柄”,这个后面详细说。 - 第8行:定义了一个函数
PrintString,它接收一个字符串,返回一个整数。
写好的IDL文件(比如叫exampleInterface.idl)不能直接用,得用微软的MIDL编译器(midl.exe)来“编译”一下。
编译命令很简单:
midl.exe exampleInterface.idl /app_config
这个/app_config参数和句柄类型有关,我们放后面说。
编译完会生成几个文件:
- exampleInterface.h:接口头文件,客户端和服务器都得包含它。
- exampleInterface_c.c:客户端存根(stub),帮你干打包发送的脏活。
- exampleInterface_s.c:服务器存根(stub),帮你干接收解包的脏活。
原版IDL文件在我的GitHub上:exampleInterface.idl (https://github.com/sud0Ru/Blog/blob/main/IPC/part2/simple_client_server/exampleInterface.idl)
动手写服务器
服务器代码(server.c)的核心逻辑其实就几步,我挑重点说,完整代码在GitHub:server.c (https://github.com/sud0Ru/Blog/blob/main/IPC/part2/simple_client_server/server.c)。
1. 包含头文件和接口定义
首先得包含MIDL生成的头文件exampleInterface.h。
2. 告诉RPC运行时用什么协议和端口
调用RpcServerUseProtseqEp函数。既然我们做本地测试,就用最快的ncalrpc,端口名随便起一个,比如example_endpoint。
RpcServerUseProtseqEp( (unsigned char *)”ncalrpc”, // 协议序列 RPC_C_LISTEN_MAX_CALLS_DEFAULT, // 最大并发调用数 (unsigned char *)”example_endpoint”, // 端点(端口名) NULL // 安全描述符,先不管 );
3. 注册接口
调用RpcServerRegisterIf2,把我们的接口“告诉”系统。第一个参数ExampleInter_v1_0_s_ifspec,这个看起来有点怪的名字是MIDL编译器自动生成的,代表“服务器端的接口1.0版本规范”。
4. 开始监听
调用RpcServerListen,服务器就进入待命状态,等着客户端来呼叫了。
5. 实现远程函数
在代码里写一个函数,签名必须和IDL里定义的一模一样。我们这个就是打印字符串并返回3。
int PrintString(const char* str) { printf(“Received string: %s\n”, str); return 3; }
6. 内存管理
RPC在打包解包数据时需要分配内存,所以我们必须提供MIDL_user_allocate和MIDL_user_free这两个函数,本质上就是包装一下malloc和free。
微软官方给的流程图很清楚地说明了这个关系:
编译服务器
用Visual Studio的命令行工具,把服务器主文件和生成的服务器存根文件一起编译,并链接RPC运行时库。
cl server.c exampleInterface_s.c /link rpcrt4.lib
生成一个server.exe。运行它,如果顺利,你会看到它开始监听。
验证:用RpcView看看我们的接口
服务器跑起来了,怎么确认它真的在“广播”自己?有个神器叫RpcView (https://github.com/silverf0x/RpcView),它能列出系统里所有进程注册的RPC接口和端点。
打开它,找到我们的server.exe进程,你就能看到它注册的接口UUID和端点信息。这证明我们没白干。
再来写客户端
客户端(client.c)的任务是找到服务器并调用它的函数。完整代码:client.c (https://github.com/sud0Ru/Blog/blob/main/IPC/part2/simple_client_server/client.c)。
1. 组装“地址”
调用RpcStringBindingCompose,把协议、端口等信息拼成完整的绑定字符串。本地通信,网络地址填NULL就行。
RpcStringBindingCompose( NULL, // 对象UUID,不用 (unsigned char *)”ncalrpc”, // 协议序列 NULL, // 网络地址(本地) (unsigned char *)”example_endpoint”, // 端点 NULL, // 选项 &stringBinding // 输出的绑定字符串 );
结果会是这样:ncalrpc:[example_endpoint]。
2. 创建绑定句柄
调用RpcBindingFromStringBinding,把刚才那个字符串变成RPC运行时能用的一个连接句柄(binding handle)。
3. 发起调用!
接下来就是最激动人心的一步:直接调用PrintString函数!看起来和调用本地函数没区别。
int result = PrintString(“Hello, RPC Server!”); printf(“Server returned: %d\n”, result);
底层的打包、发送、接收、解包,全由客户端存根和运行时库默默完成了。
编译与运行
编译客户端类似:
cl client.c exampleInterface_c.c /link rpcrt4.lib
生成client.exe。
先运行服务器,再运行客户端。如果一切正常,客户端会打印出“Server returned: 3”,服务器会打印出“Received string: Hello, RPC Server!”。
这个流程,微软也有一张图总结得很好:
进阶:固定端口 vs. 动态端口
刚才我们用的是“固定/已知端点”,端口名(example_endpoint)是预先写死在代码里的。这适合内部可控的环境。
但在真实网络环境里,你可能不想(或不能)硬编码一个TCP端口。这时就要用“动态端点”。
服务器端变化:
- 不调用
RpcServerUseProtseqEp(指定端口),而是调用RpcServerUseProtseq(不指定端口)。 - 然后调用
RpcServerInqBindings获取系统分配的绑定信息。 - 最后调用
RpcEpRegister,把接口和动态分配的端口注册到Windows的“RPC端点映射器”服务里。
客户端变化:
客户端还是用ncacn_ip_tcp和服务器名字去连接,但不用指定端口。RPC运行时会自动去查询远端的“端点映射器”,问:“那个叫XXX的接口在哪个端口啊?”拿到端口号再连接。
我写了一个动态端口的例子,代码在这里:
- server_dynamic_endpoint.c (https://github.com/sud0Ru/Blog/blob/main/IPC/part2/simple_client_server/server_dynamic_endpoint.c)
- client_dynamic_endpoint.c (https://github.com/sud0Ru/Blog/blob/main/IPC/part2/simple_client_server/client_dynamic_endpoint.c)
编译运行,效果和固定端口一样。
窥探端点映射器
动态注册的信息去哪了?在“RPC端点映射器”数据库里。有个很实用的工具可以把它“倒”出来看,就是Impacket工具包里的impacket-rpcdump。
关键点来了:默认情况下,查询远程机器的端点映射器是不需要认证的。 这意味着你可以从网络上去探测目标机器开放了哪些RPC接口。当然,你可以通过组策略(计算机配置 -> 管理模板 -> 系统 -> 远程过程调用 -> 启用RPC端点映射器客户端身份验证)来要求认证。
运行命令:
impacket-rpcdump <目标IP>
在输出列表里,你就能找到自己注册的接口UUID、版本号和动态分配的实际端口号。
好了,一个能跑通的RPC模型就搭完了。你应该对客户端和服务器的角色、接口定义、通信流程有了具体的认识。限于篇幅,关于绑定句柄的详细类型、安全机制以及更深层的调用流程,我们放到下一期再彻底讲清楚。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:幻泉之洲 《Windows进程间通信技术深潜(第二部分):RPC实战与拆解》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论