生成C# NDR类型序列化器的完整指南

生成C# NDR类型序列化器

在更新NtApiDotNet至v1.1.28版本时,我添加了对Kerberos认证令牌的支持。为实现该功能,需要编写票据解析代码。Kerberos协议大部分使用ASN.1编码,但微软特定部分(如特权属性证书PAC)采用网络数据表示(NDR),这是因为这些协议部分源自使用MSRPC的旧版NetLogon协议,而MSRPC又使用NDR。

我需要实现解析NDR流并返回结构化信息的代码。虽然已有处理NDR的类,可以手动编写C#解析器,但这将耗时且需谨慎处理所有用例。若能直接使用现有NDR字节码解析器从KERBEROS DLL提取结构信息会更高效。幸运的是,我已实现该功能,但其使用方法并不直观。因此,本文概述如何从现有DLL提取NDR结构数据并创建独立C#类型序列化器。

KERBEROS如何解析NDR结构?

KERBEROS可能采用手动实现,但事实上,Windows上MSRPC运行时的一个较少知特性是能够生成独立结构和过程序列化器,而无需使用RPC通道。文档中称此为序列化服务。

要实现类型序列化器,需在C/C++项目中执行以下步骤。首先,在IDL文件中添加要序列化的类型。例如,以下定义了一个简单序列化类型:

interface TypeEncoders
{
    typedef struct _TEST_TYPE
    {
        [unique, string] wchar_t* Name;
        DWORD Value;
    } TEST_TYPE;
}

接着创建与IDL文件同名的ACF文件(例如TYPES.IDL对应TYPES.ACF),并添加编码和解码属性:

interface TypeEncoders
{
    typedef [encode, decode] TEST_TYPE;
}

使用MIDL编译IDL文件后,将得到客户端源代码(如TYPES_c.c),其中包含几个函数,最重要的是TEST_TYPE_Encode和TEST_TYPE_Decode,它们分别对字节流进行序列化(编码)和反序列化(解码)。这些函数的具体使用并不关键,我们更关注如何配置NDR字节码以执行序列化,以便解析并生成自己的序列化器。

解析NDR字节码

查看为X64目标编译的Decode函数时,应如下所示:

void TEST_TYPE_Decode(
    handle_t _MidlEsHandle,
    TEST_TYPE * _pType)
{
    NdrMesTypeDecode3(
         _MidlEsHandle,
         ( PMIDL_TYPE_PICKLING_INFO  )&__MIDL_TypePicklingInfo,
         &TypeEncoders_ProxyInfo,
         TypePicklingOffsetTable,
         0,
         _pType);
}

NdrMesTypeDecode3是RPC运行时DLL中实现的API。该函数及其对应的NdrMesTypeEncode3未在MSDN中文档化,但SDK头文件包含足够信息以理解其工作原理。

API接受6个参数:

  1. 序列化句柄,用于维护状态(如当前流位置),可多次使用以在流中编码或解码多个结构。
  2. MIDL_TYPE_PICKLING_INFO结构,提供基本信息(如NDR引擎标志)。
  3. MIDL_STUBLESS_PROXY_INFO结构,包含DCE和NDR64语法编码的格式字符串和传输类型。
  4. 类型偏移数组列表,包含所有类型序列化器在格式字符串中的字节偏移(来自代理信息结构)。
  5. 第4个参数中类型偏移的索引。
  6. 指向要序列化或反序列化的结构的指针。

仅需参数2至5即可正确解析NDR字节码。注意NdrMesType3 API用于双DCE和NDR64序列化器。若编译为32位,则将使用仅支持DCE的NdrMesType2 API。稍后将提及解析仅DCE API所需内容,但目前大多数要提取的内容都有64位构建,即使我的工具仅解析DCE NDR字节码,也几乎总是使用NdrMesType*3。

要解析类型序列化器,需使用LoadLibrary将DLL加载到内存中(以确保处理任何重定位),然后使用Get-NdrComplexType PS命令或NdrParser::ReadPicklingComplexType方法,并传递4个参数的地址。

实际示例:KERBEROS.DLL中的PAC_DEVICE_INFO

以PAC_DEVICE_INFO结构为例,因为它相当复杂,手动编写解析器需要大量工作。反汇编PAC_DecodeDeviceInfo函数时,可以看到对NdrMesTypeDecode3的调用(来自Windows 10 2004 SHA1:173767EDD6027F2E1C2BF5CFB97261D2C6A95969中的DLL):

mov     [rsp+28h], r14  ; pObject
mov     dword ptr [rsp+20h], 5 ; nTypeIndex
lea     r9, off_1800F3138 ; ArrTypeOffset
lea     r8, stru_1800D5EA0 ; pProxyInfo
lea     rdx, stru_1800DEAF0 ; pPicklingInfo
mov     rcx, [rsp+68h]  ; Handle
call    NdrMesTypeDecode3

从中可提取以下值:

  • MIDL_TYPE_PICKLING_INFO = 0x1800DEAF0
  • MIDL_STUBLESS_PROXY_INFO = 0x1800D5EA0
  • 类型偏移数组 = 0x1800F3138
  • 类型偏移索引 = 5

这些地址使用库的默认加载地址(可能与DLL在内存中的加载地址不同)。Get-NdrComplexType支持指定来自基模块的相对地址,因此在使用前减去基地址0x180000000。以下脚本将提取类型信息:

PS> $lib = Import-Win32Module KERBEROS.DLL
PS> $types = Get-NdrComplexType -PicklingInfo 0xDEAF0 -StublessProxy 0xD5EA0 `
     -OffsetTable 0xF3138 -TypeIndex 5 -Module $lib

只要该命令无错误,$types变量现在将包含解析的复杂类型(此情况下会有多个)。然后可以使用Format-RpcComplexType将其格式化为C#源代码文件以供应用程序使用:

PS> Format-RpcComplexType $types -Pointer

这将生成一个C#文件,其中包含具有每个结构静态方法的Encoder和Decoder类。我们还向Format-RpcComplexType传递了Pointer参数,以便将结构包装在唯一指针内。这是使用真实RPC运行时的默认设置,尽管除一致性结构外并非严格必需。若不这样做,解码通常会失败(在此情况下肯定如此)。

处理命名问题和替代方案

您可能注意到生成代码的一个严重问题:没有正确的结构名称。这是不可避免的,因为MIDL编译器不会随NDR字节码保留任何名称信息,仅保留结构信息。但是,若知道应有名称,基本Visual Studio重构工具可以快速重命名。您也可以在
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
公众号二维码

posted @ 2025-08-25 21:31  qife  阅读(16)  评论(0)    收藏  举报