Windows进程间通信技术深潜(第二部分):RPC实战与拆解

admin 2026-05-01 05:02:16 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入讲解WindowsRPC实战应用,通过编写简单RPC接口详细剖析客户端与服务端通信流程。内容涵盖协议序列选择、绑定字符串构建、IDL接口定义,并提供完整代码示例演示固定端口与动态端口的实现差异。文章还介绍使用RpcView验证接口暴露和Impacket工具探测端点映射器的实用方法,为理解RPC通信机制提供具体操作指导。 综合评分: 85 文章分类: 技术标准,实战经验,WEB安全,安全工具,内网渗透


cover_image

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)来写。看个最简单的例子:

我来翻译一下这几行:

  1. 第2行:uuid(...),这是接口的“身份证号”(GUID),全球唯一。
  2. 第3行:version(1.0),接口版本号。
  3. 第4行:implicit_handle(handle_t ExampleInterHandle),用了“隐式句柄”,这个后面详细说。
  4. 第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_allocateMIDL_user_free这两个函数,本质上就是包装一下mallocfree

微软官方给的流程图很清楚地说明了这个关系:

编译服务器

用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实战与拆解》

评论:0   参与:  0