概述:ALPC 调用过程

学习 RPC 调用过程看到了 csandker 的这篇文章,学习记录一下吧,供大家参考 [Offensive Windows IPC] Internals 3: ALPC · csandker.io](https://csandker.io/2022/05/24/Offensive-Windows-IPC]-3-ALPC.html)

补一张作者画的图,是 alpc 的客户端和服务端创建及交互的过程。

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 的区别

  1. 基本概念与函数定位

    函数所属层次调用者主要职责
    NdrClientCall客户端存根(client stub)客户端代码(通过 MIDL 生成的代理函数)将调用请求的输入参数 编组(marshal)为 NDR 流,发送到服务器;等待响应后 解组(unmarshal)返回结果。
    NdrServerCall服务器端存根(server stub)RPC 运行时(RPCRT4.dll)在收到请求后将收到的 NDR 流 解组 为本地函数参数,调用实际的服务器实现;随后把输出参数和返回值 编组 回传给客户端。

    简单来说,NdrClientCall 负责“把本地调用变成网络消息”,NdrServerCall 负责“把网络消息还原为本地调用”。它们是 NDR(Network Data Representation) 引擎的核心入口,分别位于 RPC 的客户端与服务器两侧。

  2. 调用流程与交互细节

    客户端发起

    • 用户的代理函数(如 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(实际入口是 NdrServerCall2NdrServerCall3,取决于 OS 版本)。

    解组 & 分派

    • NdrServerCall 先解析请求缓冲区的 NDR 流,把参数 unpack 到栈/堆上,随后调用 服务器实现函数(由 MIDL 生成的 server stub 直接调用业务代码)。

    返回

    • 业务函数完成后,NdrServerCall 再把 [out] 参数和返回值 编组 到响应缓冲区,交给 RPC 运行时发送回客户端。

    整个过程可以看作一次 “本地函数调用 → 网络编码 → 传输 → 网络解码 → 远程执行 → 结果编码 → 传输 → 本地解码 → 返回” 的完整往返。

  3. 数据编组(Marshalling)关键点

    关键要素NdrClientCallNdrServerCall
    编组/解组方向将本地参数 → NDR 字节流(客户端 → 网络)将 NDR 字节流 → 本地参数(网络 → 服务器)
    类型信息使用 MIDL 生成的 type format stringNdr_TypeFormatString)来决定每个字段的编码方式。同上,只是角色相反。
    指针处理对 [in] 指针进行 引用计数、全局指针或 堆指针的序列化;对 [out] 指针则分配临时内存并在返回后复制回客户端。对 [in] 指针进行 解引用;对 [out] 指针在服务器端分配内存并将其地址写回 NDR 流。
    [[【Go简明手册】错误处理错误处理]]若网络或解码出错,NdrClientCall 会抛回 RPC_S_* 错误码,调用方通常会捕获 RPC_STATUS
    上下文句柄通过 NdrClientCall pAsync 参数传递 context handle,用于维护有状态的会话。NdrServerCall 接收的 pServerContext 参数即为上下文句柄,用于后续的 RpcServerFreeAuthIdentity 等操作。
  4. 小结

    NdrClientCall:客户端的 “发送‑等待‑接收” 入口,负责把所有 [in] 参数序列化为 NDR 流并在收到回复后解组 [out] 参数。 NdrServerCall:服务器端的 “接收‑解组‑执行‑回复” 入口,把网络传来的 NDR 流恢复为本地参数,调用实际业务函数后再将结果编组返回。 两者共同构成 RPC 跨进程/跨机器的编组/解组桥梁,其实现细节被 MIDL 生成的存根隐藏,但在自定义通道或底层调试时,了解它们的工作原理非常有帮助。

一句话概括:NdrClientCall 把“本地调用”变成“网络报文”,NdrServerCall 把“网络报文”还原为“本地调用”。掌握它们各自的职责与数据流向,是深入 Windows RPC 机制的关键。

NdrClientCall2 与 NdrClientCall3 的区别

NdrClientCall2 NdrClientCall3 是 Windows RPC 运行时中客户端存根的核心入口函数,后者是前者的演进版本,主要区别体现在以下几个方面:

  1. 版本演进与引入时间 NdrClientCall2 是较早版本的 RPC 客户端调用接口,自 Windows XP/Server 2003 时期引入。而 NdrClientCall3 是在 Windows Vista/Server 2008 及以后版本中引入的,旨在支持更多新特性和改进。从兼容性的角度考虑,系统保留了这两个版本的入口,应用程序可根据需求选择使用。
  2. 参数结构与扩展性 NdrClientCall2 使用的参数结构相对较小,主要包含基本的调用信息,如 RPC 消息句柄、接口 UUID、操作索引等基础元素。NdrClientCall3 则扩展了参数结构,引入了额外的标志位和扩展字段,能够传递更多的调用控制和元数据信息,这种扩展设计使得新版本的函数能够支持更丰富的 RPC 特性,同时保持向后兼容。
  3. 异步 RPC 支持 NdrClientCall3 在异步调用支持方面有显著增强。它提供了更完善的异步操作句柄管理机制,允许客户端更灵活地发起非阻塞调用并处理完成通知。相比之下,NdrClientCall2 的异步能力较为有限,主要依赖于基础的 RPC 异步 API。因此,在需要高性能并发 RPC 调用的场景中,NdrClientCall3 是更优的选择。
  4. NDR 编码版本支持 NdrClientCall3 原生支持 NDR64 编码规范,这是 Windows 7/Server 2008 R2 引入的 64 位数据表示标准,能够更高效地处理大型数据结构和高位平台的数据传输。而 NdrClientCall2 主要基于传统的 NDR32 编码,虽然也能工作,但在处理复杂数据结构时效率略低。
  5. 内存管理与缓冲机制 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 结构体解析示意,简单对照下相关偏移:

image-20260228123528266