生成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个参数:
- 序列化句柄,用于维护状态(如当前流位置),可多次使用以在流中编码或解码多个结构。
- 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位构建,即使我的工具仅解析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智能小助手)
公众号二维码