概述:ALPC 调用过程
学习 RPC 调用过程看到了 csandker 的这篇文章,学习记录一下吧,供大家参考 [Offensive Windows IPC] Internals 3: ALPC · csandker.io](https://csandker.io/2022/05/24/Offensive-Windows-IPC]-3-ALPC.html)
补一张作者画的图,是 alpc 的客户端和服务端创建及交互的过程。
补充一个 RPC 函数被调用时的堆栈,如下所示为调用 INetListManager::get_IsConnectedToInternet 时,服务端调用到 CImplINetworkListManager::IsConnectedToInternet 时,服务端的调用堆栈。
[0x0] netprofmsvc!CImplINetworkListManager::IsConnectedToInternet 0xa4edffe048 0x7ffc5c7fa2d3
[0x1] RPCRT4!Invoke+0x73 0xa4edffe050 0x7ffc5c85beeb
[0x2] RPCRT4!Ndr64StubWorker+0xb0b 0xa4edffe0a0 0x7ffc5c7919e9
[0x3] RPCRT4!NdrStubCall3+0xc9 0xa4edffe760 0x7ffc5df9c490
[0x4] combase!CStdStubBuffer_Invoke+0x60 0xa4edffe7c0 0x7ffc5c7dd17b
[0x5] RPCRT4!CStdStubBuffer_Invoke+0x3b 0xa4edffe800 0x7ffc5df469c3
[0x6] combase!RoGetAgileReference+0x7313 0xa4edffe830 0x7ffc5df4674e
[0x7] combase!RoGetAgileReference+0x709e 0xa4edffe890 0x7ffc5df9efb6
[0x8] combase!HSTRING_UserSize+0x116 0xa4edffe9f0 0x7ffc5df270b3
[0x9] combase!DllGetClassObject+0x683 0xa4edffea30 0x7ffc5df98d5d
[0xa] combase!CoGetApartmentType+0x1cd 0xa4edffed80 0x7ffc5df0eb26
[0xb] combase!RoGetActivatableClassRegistration+0x87f6 0xa4edffedd0 0x7ffc5dfdc0c8
[0xc] combase!InternalDoATClassCreate+0x9c98 0xa4edfff190 0x7ffc5df10ae9
[0xd] combase!RoGetActivatableClassRegistration+0xa7b9 0xa4edfff4b0 0x7ffc5c7db128
[0xe] RPCRT4!DispatchToStubInCNoAvrf+0x18 0xa4edfff4e0 0x7ffc5c7b8146
[0xf] RPCRT4!RPC_INTERFACE::DispatchToStubWorker+0x1a6 0xa4edfff530 0x7ffc5c7b7d76
[0x10] RPCRT4!RPC_INTERFACE::DispatchToStubWithObject+0x186 0xa4edfff610 0x7ffc5c7c4eff
[0x11] RPCRT4!LRPC_SCALL::DispatchRequest+0x16f 0xa4edfff6b0 0x7ffc5c7c44b8
[0x12] RPCRT4!LRPC_SCALL::HandleRequest+0x7f8 0xa4edfff780 0x7ffc5c7c3aa1
[0x13] RPCRT4!LRPC_ADDRESS::HandleRequest+0x341 0xa4edfff890 0x7ffc5c7c350e
[0x14] RPCRT4!LRPC_ADDRESS::ProcessIO+0x89e 0xa4edfff930 0x7ffc5c7c7b62
[0x15] RPCRT4!LrpcIoComplete+0xc2 0xa4edfffa70 0x7ffc5e710330
[0x16] ntdll!TppAlpcpExecuteCallback+0x260 0xa4edfffb10 0x7ffc5e73d566
[0x17] ntdll!TppWorkerThread+0x456 0xa4edfffb90 0x7ffc5dd17374
[0x18] KERNEL32!BaseThreadInitThunk+0x14 0xa4edfffe90 0x7ffc5e73cc91
[0x19] ntdll!RtlUserThreadStart+0x21 0xa4edfffec0 0x0 RPC 相关函数
NdrClientCall 与 NdrServerCall 的区别
-
基本概念与函数定位
函数 所属层次 调用者 主要职责 NdrClientCall 客户端存根(client stub) 客户端代码(通过 MIDL 生成的代理函数) 将调用请求的输入参数 编组(marshal)为 NDR 流,发送到服务器;等待响应后 解组(unmarshal)返回结果。 NdrServerCall 服务器端存根(server stub) RPC 运行时(RPCRT4.dll)在收到请求后 将收到的 NDR 流 解组 为本地函数参数,调用实际的服务器实现;随后把输出参数和返回值 编组 回传给客户端。 简单来说,
NdrClientCall负责“把本地调用变成网络消息”,NdrServerCall负责“把网络消息还原为本地调用”。它们是NDR(Network Data Representation)引擎的核心入口,分别位于 RPC 的客户端与服务器两侧。 -
调用流程与交互细节
客户端发起
- 用户的代理函数(如
ISomeInterface_SomeMethod_Proxy)内部会调用NdrClientCall。 NdrClientCall接收一个 RPC 消息结构(RPC_MESSAGE),该结构已由运行时填充好协议序列(如 TCP、LRPC)和调用句柄(handle_t)。
编组(Marshal)
NdrClientCall按照 MIDL 生成的 类型信息(type format string)递归地将所有 [in] 参数序列化到 Buffer 中。- 包括基本类型、结构体、指针、数组以及上下文句柄等,全部按照 NDR 规范进行编码(字节顺序、对齐、指针引用模型等)。
发送 & 接收
- 通过
RpcChannelBuffer:: SendReceive(底层调用 Rpcrt4 的传输层)将消息发送到服务器。 - 收到回复后,
NdrClientCall再把响应缓冲区中的 [out] 参数 解组 到客户端的变量中。
服务器接收
- RPC 运行时在服务器的 线程池 中分配一个调用上下文,然后调用
NdrServerCall(实际入口是NdrServerCall2或NdrServerCall3,取决于 OS 版本)。
解组 & 分派
NdrServerCall先解析请求缓冲区的 NDR 流,把参数 unpack 到栈/堆上,随后调用 服务器实现函数(由 MIDL 生成的 server stub 直接调用业务代码)。
返回
- 业务函数完成后,
NdrServerCall再把 [out] 参数和返回值 编组 到响应缓冲区,交给 RPC 运行时发送回客户端。
整个过程可以看作一次 “本地函数调用 → 网络编码 → 传输 → 网络解码 → 远程执行 → 结果编码 → 传输 → 本地解码 → 返回” 的完整往返。
- 用户的代理函数(如
-
数据编组(Marshalling)关键点
关键要素 NdrClientCall NdrServerCall 编组/解组方向 将本地参数 → NDR 字节流(客户端 → 网络) 将 NDR 字节流 → 本地参数(网络 → 服务器) 类型信息 使用 MIDL 生成的 type format string(Ndr_TypeFormatString)来决定每个字段的编码方式。同上,只是角色相反。 指针处理 对 [in] 指针进行 引用计数、全局指针或 堆指针的序列化;对 [out] 指针则分配临时内存并在返回后复制回客户端。 对 [in] 指针进行 解引用;对 [out] 指针在服务器端分配内存并将其地址写回 NDR 流。 [[【Go简明手册】错误处理 错误处理]] 若网络或解码出错, NdrClientCall会抛回 RPC_S_* 错误码,调用方通常会捕获RPC_STATUS。上下文句柄 通过 NdrClientCall的pAsync参数传递context handle,用于维护有状态的会话。NdrServerCall接收的pServerContext参数即为上下文句柄,用于后续的RpcServerFreeAuthIdentity等操作。 -
小结
NdrClientCall:客户端的 “发送‑等待‑接收” 入口,负责把所有 [in] 参数序列化为 NDR 流并在收到回复后解组 [out] 参数。NdrServerCall:服务器端的 “接收‑解组‑执行‑回复” 入口,把网络传来的 NDR 流恢复为本地参数,调用实际业务函数后再将结果编组返回。 两者共同构成 RPC 跨进程/跨机器的编组/解组桥梁,其实现细节被 MIDL 生成的存根隐藏,但在自定义通道或底层调试时,了解它们的工作原理非常有帮助。
一句话概括:NdrClientCall 把“本地调用”变成“网络报文”,NdrServerCall 把“网络报文”还原为“本地调用”。掌握它们各自的职责与数据流向,是深入 Windows RPC 机制的关键。
NdrClientCall2 与 NdrClientCall3 的区别
NdrClientCall2 和 NdrClientCall3 是 Windows RPC 运行时中客户端存根的核心入口函数,后者是前者的演进版本,主要区别体现在以下几个方面:
- 版本演进与引入时间
NdrClientCall2是较早版本的 RPC 客户端调用接口,自 Windows XP/Server 2003 时期引入。而NdrClientCall3是在 Windows Vista/Server 2008 及以后版本中引入的,旨在支持更多新特性和改进。从兼容性的角度考虑,系统保留了这两个版本的入口,应用程序可根据需求选择使用。 - 参数结构与扩展性
NdrClientCall2使用的参数结构相对较小,主要包含基本的调用信息,如 RPC 消息句柄、接口 UUID、操作索引等基础元素。NdrClientCall3则扩展了参数结构,引入了额外的标志位和扩展字段,能够传递更多的调用控制和元数据信息,这种扩展设计使得新版本的函数能够支持更丰富的 RPC 特性,同时保持向后兼容。 - 异步 RPC 支持
NdrClientCall3在异步调用支持方面有显著增强。它提供了更完善的异步操作句柄管理机制,允许客户端更灵活地发起非阻塞调用并处理完成通知。相比之下,NdrClientCall2的异步能力较为有限,主要依赖于基础的 RPC 异步 API。因此,在需要高性能并发 RPC 调用的场景中,NdrClientCall3是更优的选择。 - NDR 编码版本支持
NdrClientCall3原生支持 NDR64 编码规范,这是 Windows 7/Server 2008 R2 引入的 64 位数据表示标准,能够更高效地处理大型数据结构和高位平台的数据传输。而NdrClientCall2主要基于传统的 NDR32 编码,虽然也能工作,但在处理复杂数据结构时效率略低。 - 内存管理与缓冲机制
NdrClientCall3引入了改进的内存管理机制,包括对缓冲池(buffer pool)和自定义内存分配器的更好支持。它允许调用方指定自定义的内存管理回调函数,从而在特定场景下优化内存使用和减少分配开销。此外,NdrClientCall3还增强了对安全上下文句柄和会话密钥的直接处理能力。
RPC 结构体
NdrClientCall2 参数 1 对应的结构体解析
/*
* MIDL Stub Descriptor
*/
typedef struct _MIDL_STUB_DESC
{
void * RpcInterfaceInformation;
void * ( __RPC_API * pfnAllocate)(size_t);
void ( __RPC_API * pfnFree)(void *);
union
{
handle_t * pAutoHandle;
handle_t * pPrimitiveHandle;
PGENERIC_BINDING_INFO pGenericBindingInfo;
} IMPLICIT_HANDLE_INFO;
const NDR_RUNDOWN * apfnNdrRundownRoutines;
const GENERIC_BINDING_ROUTINE_PAIR * aGenericBindingRoutinePairs;
const EXPR_EVAL * apfnExprEval;
const XMIT_ROUTINE_QUINTUPLE * aXmitQuintuple;
const unsigned char * pFormatTypes;
int fCheckBounds;
/* Ndr library version. */
unsigned long Version;
MALLOC_FREE_STRUCT * pMallocFreeStruct;
long MIDLVersion;
const COMM_FAULT_OFFSETS * CommFaultOffsets;
// New fields for version 3.0+
const USER_MARSHAL_ROUTINE_QUADRUPLE * aUserMarshalQuadruple;
// Notify routines - added for NT5, MIDL 5.0
const NDR_NOTIFY_ROUTINE * NotifyRoutineTable;
/*
* Reserved for future use.
*/
ULONG_PTR mFlags;
// International support routines - added for 64bit post NT5
const NDR_CS_ROUTINES * CsRoutineTables;
void * ProxyServerInfo;
const NDR_EXPR_DESC * pExprInfo;
// Fields up to now present in win2000 release.
} MIDL_STUB_DESC;RpcInterfaceInformation 对应的结构体为 _RPC_CLIENT_INTERFACE
typedef struct _RPC_CLIENT_INTERFACE
{
unsigned int Length;
RPC_SYNTAX_IDENTIFIER InterfaceId;
RPC_SYNTAX_IDENTIFIER TransferSyntax;
PRPC_DISPATCH_TABLE DispatchTable;
unsigned int RpcProtseqEndpointCount;
PRPC_PROTSEQ_ENDPOINT RpcProtseqEndpoint;
ULONG_PTR Reserved;
void const __RPC_FAR * InterpreterInfo;
unsigned int Flags ;
} RPC_CLIENT_INTERFACE, __RPC_FAR * PRPC_CLIENT_INTERFACE;补一张图来看下 RPC_CLIENT_INTERFACE 结构体解析示意,简单对照下相关偏移:
