生成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文件中添加要序列化的类型。例如,以下定义一个简单类型:
|
|
然后创建同名的ACF文件(例如TYPES.IDL对应TYPES.ACF),并添加编码和解码属性:
|
|
使用MIDL编译IDL文件后,会得到客户端源代码(如TYPES_c.c),其中包含几个函数,最重要的是TEST_TYPE_Encode和TEST_TYPE_Decode,用于从字节流序列化(编码)和反序列化(解码)类型。这些函数的使用方式不重要,我们更关心如何配置NDR字节码以执行序列化,以便解析并生成自己的序列化器。
查看为X64目标编译的Decode函数时,应如下所示:
|
|
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):
|
|
从中提取以下值:
- MIDL_TYPE_PICKLING_INFO = 0x1800DEAF0
- MIDL_STUBLESS_PROXY_INFO = 0x1800D5EA0
- 类型偏移数组 = 0x1800F3138
- 类型偏移索引 = 5
这些地址使用库的默认加载地址,可能与DLL在内存中的加载地址不同。Get-NdrComplexType支持指定从基模块的相对地址,因此在使用前减去基地址0x180000000。以下脚本将提取类型信息:
|
|
只要此命令无错误,$types变量现在将包含解析的复杂类型,本例中会有多个。然后可以使用Format-RpcComplexType将其格式化为C#源代码文件以供应用程序使用:
|
|
这将生成一个C#文件,如下所示。代码包含Encoder和Decoder类,每个结构有静态方法。我们还向Format-RpcComplexType传递了Pointer参数,以便将结构包装在Unique Pointers中。这是使用真实RPC运行时的默认设置,尽管除了Conformant Structures外并非严格必需。如果不这样做,解码通常会失败,尤其是在这种情况下。
您可能会注意到生成代码的一个严重问题:没有正确的结构名称。这是不可避免的,MIDL编译器不会在NDR字节码中保留任何名称信息,仅保留结构信息。但是,如果您知道名称应该是什么,基本的Visual Studio重构工具可以快速重命名。您也可以在