生成C#的NDR类型序列化器:从Kerberos到MSRPC的实战指南

本文详细介绍了如何从Windows DLL中提取NDR结构数据,并生成独立的C#类型序列化器,涵盖Kerberos认证令牌解析、MSRPC运行时特性及实用PowerShell命令操作。

生成C#的NDR类型序列化器

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

我需要实现代码来解析NDR流并返回结构化信息。由于已有处理NDR的类,可以手动编写C#解析器,但这很耗时且需仔细处理所有用例。如果能用现有的NDR字节码解析器从KERBEROS DLL提取结构信息会更简单。幸运的是,我已编写此功能,但使用方法不明显。因此,本文概述如何从现有DLL提取NDR结构数据并创建独立的C#类型序列化器。

首先,KERBEROS如何解析NDR结构?可能有手动实现,但Windows上MSRPC运行时的一个较少知特性是能生成独立结构和过程序列化器,无需使用RPC通道。文档中称为序列化服务。

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

1
2
3
4
5
6
7
8
interface TypeEncoders
{
    typedef struct _TEST_TYPE
    {
        [unique, string] wchar_t* Name;
        DWORD Value;
    } TEST_TYPE;
}

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

1
2
3
4
interface TypeEncoders
{
    typedef [encode, decode] TEST_TYPE;
}

使用MIDL编译IDL文件后,会得到客户端源代码(如TYPES_c.c),其中包含几个函数,最重要的是TEST_TYPE_Encode和TEST_TYPE_Decode,用于从字节流序列化(编码)和反序列化(解码)类型。这些函数的使用方式不重要,我们更关心如何配置NDR字节码以执行序列化,以便解析并生成自己的序列化器。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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个参数:

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

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

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

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

1
2
3
4
5
6
7
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。以下脚本将提取类型信息:

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

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

1
PS> Format-RpcComplexType $types -Pointer

这将生成一个C#文件,如下所示。代码包含Encoder和Decoder类,每个结构有静态方法。我们还向Format-RpcComplexType传递了Pointer参数,以便将结构包装在Unique Pointers中。这是使用真实RPC运行时的默认设置,尽管除了Conformant Structures外并非严格必需。如果不这样做,解码通常会失败,尤其是在这种情况下。

您可能会注意到生成代码的一个严重问题:没有正确的结构名称。这是不可避免的,MIDL编译器不会在NDR字节码中保留任何名称信息,仅保留结构信息。但是,如果您知道名称应该是什么,基本的Visual Studio重构工具可以快速重命名。您也可以在

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计