前言 看到一篇利用RPC实现免杀对抗的文章,不懂RPC的我读起来像天书,恍然大悟,我得先搞懂RPC是什么,本文的目的是掌握RPC的背景、概念、实现
RPC背景 随着计算机技术从“单机时代”进入“分布式时代”,传统的IPC(Inter-Process Communication,进程间通信)已经无法满足跨设备、跨网络的协作需求,如大型企业的财务系统,单台计算机的算力、存储、可靠性已经无法满足需求,需要多台计算机协同合作,其中包括负责存储的(数据库服务器)、负责计算的(应用服务器)、负责展示的(前端服务器),各个服务器的进程之间无法再通过传统的IPC(Inter-Process Communication,进程间通信)通信,需要一种新的通信机制
早期程序员们自己实现通信机制,包括自己基于TCP/IP协议编写数据发送/接收逻辑,处理数据序列化反序列化、考虑丢包等等,这些底层细节与“业务逻辑”无关,却占用了开发者大量的时间精力,急需一种全新的标准,这个时候RPC应运而生
RPC最早是由贝尔实验室的Bruce Jay Nelson在1984年的论文中提出的,随后Sun公司实现了自研的ONC RPC,有了这个成熟的机制,大幅降低了跨进程通信的开发门槛,微软在1993年自Windows NT起,在ONC RPC的基础上研发了适用于Windows的RPC机制,我们都知道Windows是组件化架构,典型的组件就是COM,而DCOM就是基于RPC实现的分布式COM组件
RPC概念 IPC(Inter-Process Communication,进程间通信)是用于本地的一套机制,RPC(Remote Procedure Call,远程过程调用)相比IPC是一套用于远程的机制,包含客户端和服务端,服务端提供服务(比如计算器服务),客户端发起请求(比如传入1+1,服务端返回结果2),当然这是最简化的场景,实际要复杂的多,RPC机制中涉及4个概念: RPC客户端:发起请求的代码 RPC客户端存根(也叫RPC客户端Stub):负责将客户端的请求参数打包成网络数据,并通过网络发送给服务端 RPC服务端存根(也叫RPC服务端Stub):负责接收并拆解数据包,然后调用本地方法 RPC服务端:实际处理请求的代码
RPC调用过程: 1、客户端以本地调用方式调用服务 2、客户端存根接收到调用后,负责将方法、参数等组装成能够进行网络传输的消息体(将消息对象序列化为二进制) 3、客户端存根通过socket将网络消息发送到服务端 4、服务端存根收到消息后进行解码(将消息对象反序列化) 5、服务端存根根据解码结果调用本地的服务 6、服务端执行本地过程并将执行结果返回给服务端存根 7、服务端存根将返回结果打包成网络消息(将结果消息进行发序列) 8、服务端存根通过socket将网络消息发送到客户端 9、客户端存根接收到结果消息,并进行解码(将结果消息反序列化) 10、客户端接收到返回结果
RPC的接口使用了IDL(Interface Description Language,接口描述语言)作为接口语言,熟悉COM的同学应该一眼就能认出来,COM也是用的这个接口语言,这个接口语言使用的编译器是MIDL,在Visual Studio套件中,通过IDL文件定义RPC客户端和RPC服务端之间的通信接口,只有实现了此接口的RPC客户端和RPC服务端才能互相通信
RPC实现 下面是一个Demo演示,创建一个文件夹包含下面三个文件
SimpleCalc.idl代码如下
1 2 3 4 5 6 7 8 9 10 11 12 [ uuid(8A4B8C38-6BBD-4A9D-AE8E-1234567890AB), version(1.0) ] interface SimpleCalc { // 两个输入参数,返回它们的和 int Add([in] int a, [in] int b); // 让服务端优雅退出(示例) void Shutdown(void); }
server.cpp代码如下
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 #include <windows.h> #include <stdio.h> #include "SimpleCalc.h" #pragma comment(lib, "Rpcrt4.lib") void* __RPC_USER MIDL_user_allocate(size_t size) { return malloc(size); } void __RPC_USER MIDL_user_free(void* p) { free(p); } extern "C" int Add(handle_t IDL_handle, int a, int b) { printf("[Server] Add(%d, %d)\n", a, b); return a + b; } extern "C" void Shutdown(handle_t IDL_handle) { printf("[Server] Shutdown requested\n"); RpcMgmtStopServerListening(NULL); RpcServerUnregisterIf(NULL, NULL, TRUE); } int main() { RPC_STATUS status; RPC_CSTR pszProtocolSequence = (RPC_CSTR)"ncalrpc"; RPC_CSTR pszEndpoint = (RPC_CSTR)"MyLocalRPC"; status = RpcServerUseProtseqEpA(pszProtocolSequence, RPC_C_PROTSEQ_MAX_REQS_DEFAULT, pszEndpoint, NULL); if (status != RPC_S_OK) { fprintf(stderr, "RpcServerUseProtseqEpA failed: 0x%lx\n", status); return 1; } status = RpcServerRegisterIf(SimpleCalc_v1_0_s_ifspec, NULL, NULL); if (status != RPC_S_OK) { fprintf(stderr, "RpcServerRegisterIf failed: 0x%lx\n", status); return 1; } RpcServerRegisterAuthInfoA(NULL, RPC_C_AUTHN_NONE, NULL, NULL); printf("[Server] Listening on ncacn_ip_tcp port %s\n", (char*)pszEndpoint); status = RpcServerListen(1, RPC_C_LISTEN_MAX_CALLS_DEFAULT, FALSE); if (status != RPC_S_OK) { fprintf(stderr, "RpcServerListen failed: 0x%lx\n", status); return 1; } printf("[Server] RpcServerListen returned, exiting.\n"); return 0; }
client.cpp代码如下
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 #include <windows.h> #include <stdio.h> #include "SimpleCalc.h" #pragma comment(lib, "Rpcrt4.lib") void* __RPC_USER MIDL_user_allocate(size_t size) { return malloc(size); } void __RPC_USER MIDL_user_free(void* p) { free(p); } int main() { RPC_STATUS status; RPC_BINDING_HANDLE binding = NULL; RPC_CSTR pszStringBinding = NULL; /* status = RpcStringBindingComposeA( NULL, (RPC_CSTR)"ncacn_ip_tcp", (RPC_CSTR)"127.0.0.1", (RPC_CSTR)"50000", NULL, &pszStringBinding ); */ status = RpcStringBindingComposeA( NULL, (RPC_CSTR)"ncalrpc", NULL, // LRPC不需要地址 (RPC_CSTR)"MyLocalRPC", NULL, &pszStringBinding ); if (status != RPC_S_OK) { fprintf(stderr, "RpcStringBindingComposeA failed: 0x%lx\n", status); return 1; } status = RpcBindingFromStringBindingA(pszStringBinding, &binding); if (status != RPC_S_OK) { fprintf(stderr, "RpcBindingFromStringBindingA failed: 0x%lx\n", status); RpcStringFreeA(&pszStringBinding); return 1; } RpcStringFreeA(&pszStringBinding); // ✅ 用 RpcTryExcept 捕获 RPC 异常 RpcTryExcept { int sum = Add(binding, 3, 7); printf("[Client] Add(3,7) = %d\n", sum); Shutdown(binding); } RpcExcept(1) { printf("[Client] RPC Exception code: 0x%lx\n", RpcExceptionCode()); } RpcEndExcept RpcBindingFree(&binding); return 0; }
安装好VS2022后,打开“x64 Native Tools Command Prompt for VS 2022”,切换到上述目录,先编译idl文件
执行后会生成头文件、客户端存根文件、服务端存根文件、等
然后编译服务端
1 cl /EHsc server.cpp SimpleCalc_s.c /link rpcrt4.lib
最后编译客户端
1 cl /EHsc client.cpp SimpleCalc_c.c /link rpcrt4.lib
成功编译后,先执行服务端,再执行客户端,结果如下图
上图显示成功进行了一次RPC通信,我们对代码进行一下初步讲解
服务端代码中,MIDL_user_allocate 与 MIDL_user_free是RPC运行时需要调用的分配/释放函数,需要在代码中实现它,所以需要下述2行代码,其实就是把malloc和free包装一下,符合RPC的要求
1 2 void* __RPC_USER MIDL_user_allocate(size_t size) { return malloc(size); } void __RPC_USER MIDL_user_free(void* p) { free(p); }
实现函数Add和Shutdown,并将其声明为C语言风格,确保编译后的函数名称不会有额外修饰
1 2 3 4 5 6 7 8 9 extern "C" int Add(handle_t IDL_handle, int a, int b) { printf("[Server] Add(%d, %d)\n", a, b); return a + b; } extern "C" void Shutdown(handle_t IDL_handle) { printf("[Server] Shutdown requested\n"); RpcMgmtStopServerListening(NULL); RpcServerUnregisterIf(NULL, NULL, TRUE); }
初始化一些参数,并调用RpcServerUseProtseqEpA注册协议
1 2 3 4 5 6 7 8 RPC_STATUS status; RPC_CSTR pszProtocolSequence = (RPC_CSTR)"ncalrpc"; RPC_CSTR pszEndpoint = (RPC_CSTR)"MyLocalRPC"; status = RpcServerUseProtseqEpA(pszProtocolSequence, RPC_C_PROTSEQ_MAX_REQS_DEFAULT, pszEndpoint, NULL); if (status != RPC_S_OK) { fprintf(stderr, "RpcServerUseProtseqEpA failed: 0x%lx\n", status); return 1; }
调用RpcServerRegisterIf将IDL的接口注册到RPC运行时
1 2 3 4 5 status = RpcServerRegisterIf(SimpleCalc_v1_0_s_ifspec, NULL, NULL); if (status != RPC_S_OK) { fprintf(stderr, "RpcServerRegisterIf failed: 0x%lx\n", status); return 1; }
调用RpcServerRegisterAuthInfoA并传入RPC_C_AUTHN_NONE允许匿名访问
1 RpcServerRegisterAuthInfoA(NULL, RPC_C_AUTHN_NONE, NULL, NULL);
调用RpcServerListen监听来自客户端的请求
1 2 3 4 5 status = RpcServerListen(1, RPC_C_LISTEN_MAX_CALLS_DEFAULT, FALSE); if (status != RPC_S_OK) { fprintf(stderr, "RpcServerListen failed: 0x%lx\n", status); return 1; }
客户端代码中,MIDL_user_allocate 与 MIDL_user_free同服务端一样
1 2 void* __RPC_USER MIDL_user_allocate(size_t size) { return malloc(size); } void __RPC_USER MIDL_user_free(void* p) { free(p); }
初始化参数,并调用RpcStringBindingComposeA创建一个字符串形式绑定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 RPC_STATUS status; RPC_BINDING_HANDLE binding = NULL; RPC_CSTR pszStringBinding = NULL; status = RpcStringBindingComposeA( NULL, (RPC_CSTR)"ncalrpc", NULL, // LRPC不需要地址 (RPC_CSTR)"MyLocalRPC", NULL, &pszStringBinding ); if (status != RPC_S_OK) { fprintf(stderr, "RpcStringBindingComposeA failed: 0x%lx\n", status); return 1; }
调用RpcBindingFromStringBindingA将字符串绑定转化为RPC绑定句柄
1 2 3 4 5 6 status = RpcBindingFromStringBindingA(pszStringBinding, &binding); if (status != RPC_S_OK) { fprintf(stderr, "RpcBindingFromStringBindingA failed: 0x%lx\n", status); RpcStringFreeA(&pszStringBinding); return 1; }
调用服务端提供的功能Add,包含在一个异常处理中
1 2 3 4 5 6 7 8 9 10 RpcTryExcept { int sum = Add(binding, 3, 7); printf("[Client] Add(3,7) = %d\n", sum); Shutdown(binding); } RpcExcept(1) { printf("[Client] RPC Exception code: 0x%lx\n", RpcExceptionCode()); } RpcEndExcept