在处理二进制数据时,我们经常使用dt命令将字节分组到有意义的字段中,例如。
0:000> dt ntdll!_PEB @$peb +0x000 InheritedAddressSpace : 0 '' +0x001 ReadImageFileExecOptions : 0 '' +0x002 BeingDebugged : 0x1 '' +0x003 BitField : 0x8 '' +0x003 ImageUsesLargePages : 0y0 +0x003 IsProtectedProcess : 0y0 +0x003 IsLegacyProcess : 0y0 +0x003 IsImageDynamicallyRelocated : 0y1 +0x003 SkipPatchingUser32Forwarders : 0y0 ...
当库所有者未在符号文件中提供类型信息时,会出现问题。我们通常只需要在二进制编辑器中手动分解字节(010编辑器有一个很好的模板系统)。如果调试器中也有一些可用的模板系统,不是很好吗?我有个好消息要告诉你:随着WinDbg的最新发布,我们收到了一个非常强大的功能:.natvis文件。甚至有两个碎片整理工具集专门用于此功能:Defrag Tools #138和Defrag Tools #139。我们首先分析.natvis文件是如何构建的,以便以后在二进制数据分析中使用它们。
.natvis文件
.natvis文件在Visual Studio中已经使用了一段时间来自定义变量在监视窗口中的显示方式。您可以在%VSINSTALLDIR%Common7PackagesDebuggerVisualizers中找到Visual Studio使用的.natvis文件。它们是XML文件,根据%VSINSTALLDIR%XMLSchemas1033 atvis.xsd文件中定义的架构构造。您可以在项目中定义自己的.natvis文件,Visual Studio会将它们嵌入到.pdb文件中(此处提供更多信息)。示例.natvis文件可能如下所示:
<?xml version="1.0" encoding="utf-8"?> <AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010"> <Type Name="tagRECT"> <AlternativeType Name="CRect"></AlternativeType> <DisplayString>{{LT({left}, {top}) RB({right}, {bottom}) [{right-left} x {bottom-top}]}}</DisplayString> <Expand> <Item Name="[top]">top,x</Item> <Item Name="[right]">right,x</Item> <Item Name="[width]">right - left</Item> <Item Name="[bottom]">bottom</Item> <Item Name="[left]">left</Item> </Expand> </Type> </AutoVisualizer>
0:000> .nvload c: empwindbg-dx est.natvis Successfully loaded visualizers in "c: empwindbg-dx est.natvis" 0:000> .nvlist Loaded NatVis Files: c: empwindbg-dx est.natvis 0:000> dx rect rect : {LT(1, 2) RB(3, 4) [2 x 2]} [Type: tagRECT] [<Raw View>] [top] : 0x2 [right] : 0x3 [width] : 2 [bottom] : 4 [left] : 1
WinDbg中类型模板
在前一段中,我们研究了使用.natvis文件的常用方法。但是,当我们没有可用的私有符号时,原始二进制数据呢?好消息是仍然可以使用dx命令。在下一个示例中,我们将使用以下C#类:
public struct TestClass { public Guid Id { get; set; } public int Count { get; set; } public String Name { get; set; } }
一个非常简单的程序:
public static void Main() { var t = new TestClass() { Id = Guid.NewGuid(), Count = 2, Name = "test class" }; Console.ReadLine(); }
让我们在应用程序等待用户输入时中断执行,并使用转储TestClass实例netext扩展名中的!wdo命令:
0:000> !Name2EE Test TestClass Module: 01263fbc Assembly: Test.exe Token: 02000002 MethodTable: 01264db0 EEClass: 012617b8 Name: TestClass 0:000> !wdo -mt 01264db0 0x010ff1d0 ... 629ae918 System.String +0000 _Name_k__BackingField 032f2754 test class 629b07a0 System.Int32 +0004 _Count_k__BackingField 2 (0n2) 629aba00 System.Guid +0008 _Id_k__BackingField -mt 629ABA00 00000000 {c41e14c4-95fc-402b-8e54-9f2ec1f4865e}
我们现在将尝试使用.natvis文件和dx命令模拟上述输出。我们的.natvis文件如下:
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010"> <Type Name="T1"> <DisplayString>CLR string</DisplayString> <Expand> <ArrayItems> <Size>*((int *)(this + 4))</Size> <ValuePointer>(NvWchar *)(this + 8)</ValuePointer> </ArrayItems> </Expand> </Type> <Type Name="T0"> <Expand> <Item Name="Id">*((NvGuid *)(this + 8))</Item> <Item Name="Count">*((int *)(this + 4))</Item> <Item Name="Name">*((T1 *)(*(int *)this))</Item> </Expand> </Type> </AutoVisualizer>
不要害怕字段定义中*的数量,因为我们没有任何依赖的符号,我们需要处理指针。我们的基本指针总是这样。为了使求值器工作,我们总是需要指定期望输出的类型。例如,Count字段位于TestClass实例的偏移量4处。因此,我们首先向这个地址添加4个字节,将地址强制转换为int*,然后在对其值感兴趣时解除对它的引用,从而得到表达式*((int*)(this+4)。CLR字符串稍微复杂一些,但规则是相同的。我最不需要解释的是类型名。您可能已经注意到模板中使用的那些奇怪的T0、T1和T2类型名称以及NvWchar和NvGuid。dx命令只能对其具有符号的类型进行操作。因此,如果我们在.natvis文件中创建一个完全虚构的类型并尝试向其强制转换内存地址,那么dx命令将不起作用。这里提供了一个NatvisTypes库的帮助,我在这里为您定义了一些模拟类型:T0、T1、T2、…、T9。另外还有像NvGuid和NvWchar这样的类型(我计划在将来添加其他类型)。源代码提交给与lld扩展相同的repo:https://github.com/lowleveldesign/lldext,可以在发布页面上找到二进制文件。但有一个问题:我们需要将NatvisTypes.dll加载到进程中。有人来帮忙!injectdll命令,我已经用lld WinDbg扩展名发布了该命令。Nv*类型的可视化工具在项目中定义,并自动添加到NatvisTypes.pdb文件中。WinDbg足够友好,可以用.pdb文件加载可视化工具。让我们看看示例TestClass实例的调试器输出是什么样子的:
0:000> .load lld 0:000> !injectdll d:devsrclldextWin32DebugNatvisTypes.dll 0:000> .nvload c: empTestClass.natvis Successfully loaded visualizers in "c: empTestClass.natvis" 0:000> ld NatvisTypes *** WARNING: Unable to verify checksum for d:devsrclldextWin32DebugNatvisTypes.dll Symbols loaded for NatvisTypes 0:000> dx *((T0 *)0x010ff1d0) *((T0 *)0x010ff1d0) : [Type: T0] [<Raw View>] Id : 0xc41e14c4-0x95fc-0x402b-0x8e0x54-0x9f0x2e0xc10xf40x860x5e [Type: NvGuid] Count : 2 Name : CLR string [Type: T1] 0:000> dx -r1 (*((NatvisTypes!T1 *)0x32f2754)) (*((NatvisTypes!T1 *)0x32f2754)) : CLR string [Type: T1] [<Raw View>] [0] : 116 't' [Type: NvWchar] [1] : 101 'e' [Type: NvWchar] [2] : 115 's' [Type: NvWchar] [3] : 116 't' [Type: NvWchar] [4] : 32 ' ' [Type: NvWchar] [5] : 99 'c' [Type: NvWchar] [6] : 108 'l' [Type: NvWchar] [7] : 97 'a' [Type: NvWchar] [8] : 115 's' [Type: NvWchar] [9] : 115 's' [Type: NvWchar]
我知道这个例子不是最好的,但是请注意,我们已经将原始二进制数据转换为有意义的数据。我还没有谈到死后调试的问题。无法将DLL插入转储。当您需要分析转储时,必须使用具有符号但通常不会使用的任何类型,例如:
0:000> dt ntdll!* ... ntdll!_ALTERNATIVE_ARCHITECTURE_TYPE ntdll!_KUSER_SHARED_DATA ntdll!_TP_POOL ntdll!_TP_CLEANUP_GROUP ntdll!_ACTIVATION_CONTEXT ntdll!_TP_CALLBACK_INSTANCE
您可以在.natvis文件中覆盖它们,然后投射内存。很乏味,但我还没有找到更好的办法。最后,如果您还没有对dx命令印象深刻,请查看WinDbg会话中dx调试器调用的输出。