65.9K
CodeProject 正在变化。 阅读更多。
Home

调试教程第四部分:编写 WINDBG 扩展

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (39投票s)

2004年3月25日

11分钟阅读

viewsIcon

239667

downloadIcon

7418

本教程我们将尝试编写一个简单的调试扩展。

引言

欢迎来到这个调试系列的第四部分。在本期中,我们将暂时远离实际调试,深入研究创建有用的调试辅助工具。我绝不会提倡仅仅为了编写而编写调试辅助工具。我会找到一个理由来编写一些东西,也许是你经常做但很繁琐的事情。一旦你找到了想要自动化的东西,我就会看看你如何能够实现它。

这就是我所做的。在我调试的过程中,我**总是**在堆栈和其他位置搜索字符串。我为什么要这样做?人不是计算机,我们理解语言而不是数字。因此,许多应用程序甚至驱动程序都是基于字符串编写的。我不会说一切都是字符串,但通常 somewhere 会有一个字符串。如果你仔细想想,你真的不需要字符串。我们可以只使用数字,再也不用字符串了。有些人可能会想,当你暴露一个 UI 时,当然,你最终会 somewhere 遇到一个字符串……嗯,不一定非得这样,对吧?我的意思是,我说的不仅仅是用户界面,我说的是那些甚至不向用户暴露的程序的内部。程序员仍然是人,喜欢用某种语言交流,而不是二进制。因此,即使是应用程序的内部,有时也会使用字符串来表示事物。你几乎可以在任何地方找到字符串,甚至在驱动程序中。

所以有字符串,那又怎样?

嗯,它们为应用程序提供了一个额外的可读性层面。这是第一条经验法则,也许如果你能在堆栈的某个地方找到一个字符串,你就能更好地追踪应用程序在做什么。这些字符串可以是环境变量、文件名、设备名称(COM1、\Device\xxxx 等)、其他对象的名称、用户名、GUID 等。这些信息可以更好地帮助你追踪应用程序在做什么以及它可能在你的程序中的哪个位置。

另一个有趣的观点是,我不确定这一点,但我的信念是,最常见的缓冲区溢出是由于无效的字符串操作。无论是忘记了 NULL 字符,还是由于 API 返回的是字符数而不是字节数而错误地估计了分配。如果一个字符串覆盖了你程序中的内存,并且你能找到这个字符串,那么就更容易追踪到是谁创建了它。

我们从哪里开始?

我从堆栈开始。我有一个习惯,在输入“KB”和“DDS ESP”之后,我做的第一件事就是使用“DC ESP”。这个命令在一侧转储 DWORD,在另一侧转储可打印字符。让我们看一个例子。

0:000> dc esp
0006febc  77d43a09 77d43c7d 0006fefc 00000000  .:.w}<.w........
0006fecc  00000000 00000000 00000000 0006ff1c  ................
0006fedc  010028e4 0006fefc 00000000 00000000  .(..............
0006feec  00000000 00000000 77e7ad86 00091ee7  ...........w....
0006fefc  001a03e4 00000118 0000ffff bf8a75ed  .............u..
0006ff0c  0768a2ca 00000229 00000251 00000000  ..h.)...Q.......
0006ff1c  0006ffc0 01006c54 01000000 00000000  ....Tl..........
0006ff2c  00091ee7 0000000a 00000000 00000000  ................
0:000> dc
0006ff3c  7ffdf000 80543940 f544fc5c 00000044  ....@9T.\.D.D...
0006ff4c  00092b28 00092b48 00092b70 00000000  (+..H+..p+......
0006ff5c  00000000 00000000 00000000 00000000  ................
0006ff6c  00000000 00000000 00000000 00000000  ................
0006ff7c  00000000 ffffffff ffffffff ffffffff  ................
0006ff8c  00091ee7 00000000 00000001 00272620  ............ &'.
0006ff9c  00272d00 00000000 00000000 0006ff34  .-'.........4...
0006ffac  e24296d0 0006ffe0 01006d14 01001888  ..B......m......
0:000>
0006ffbc  00000000 0006fff0 77e814c7 00000000  ...........w....
0006ffcc  00000000 7ffdf000 f544fcf0 0006ffc8  ..........D.....
0006ffdc  80534504 ffffffff 77e94809 77e91210  .ES......H.w...w
0006ffec  00000000 00000000 00000000 01006ae0  .............j..
0006fffc  00000000 78746341 00000020 00000001  ....Actx .......
0007000c  00000654 0000007c 00000000 00000020  T...|....... ...
0007001c  00000000 00000014 00000001 00000003  ................
0007002c  00000034 000000ac 00000001 00000000  4...............
0:000>
0007003c  00000000 00000000 00000000 00000000  ................
0007004c  00000002 00000000 00000000 00000000  ................
0007005c  00000168 00000190 00000000 2d59495b  h...........[IY-
0007006c  000002f8 00000032 0000032c 000002b8  ....2...,.......
0007007c  00000010 00000002 0000008c 00000002  ................
0007008c  00000001 000000ac 00000538 00000001  ........8.......
0007009c  00000002 000005e4 00000070 00000001  ........p.......
000700ac  64487353 0000002c 00000001 00000001  SsHd,...........
0:000>
000700bc  00000003 00000002 0000008c 00000001  ................
000700cc  00000000 0000002c 0000005e 0000005e  ....,...^...^...
000700dc  00000000 00000000 00000000 00000000  ................
000700ec  00000000 00000000 00000000 00000000  ................
000700fc  00000000 00000002 00000028 00000034  ........(...4...
0007010c  003a0043 0057005c 004e0049 004f0044  C.:.\.W.I.N.D.O.
0007011c  00530057 0030002e 0057005c 006e0069  W.S...0.\.W.i.n.
0007012c  00780053 005c0073 00000000 00000000  S.x.s.\.........

我启动了 notepad.exe 并中断了它,然后转储了主(也是唯一的)线程的堆栈。堆栈上的字符串是“局部数组”,例如在你的函数中声明 char x[10];。但这并不是程序中唯一的字符串。还有其他的,这些其他的存储在被声明为局部变量的指针中,甚至传递给像 CreateFile 这样的函数。CreateFile 的第一个参数接受一个字符串。

那么,我通常会做什么?然后我会在堆栈中搜索可能是字符串的内存位置,然后再次对它们执行“DC”,或者“DA”(Dump ANSI string)或“DU”(Dump Unicode String)。这样做的问题是它很慢而且很繁琐。我找不到任何调试器命令可以为我做这件事(如果有,请告诉我),所以我最终自己写了一个。

写你自己的?

只要它们导出函数并以 Microsoft 定义的方式运行,WINDBG 就支持任何人创建的 DLL。这意味着你可以编写插件!以前人们会编写插件来 !<mydatastructure> <address> ,它基本上会转储数据结构的成员及其名称。然而,WINDBG 支持“dt”命令,如果你有一个 PDB(符号文件),它可以为你做到这一点,而无需编写任何代码!让我们看一个例子。

使用“dt <yourdll>!<your data structure>”将转储结构的内容及其名称。让我们快速看一个例子。

0:000> dt ntdll!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar
   +0x003 SpareBool        : UChar
   +0x004 Mutant           : Ptr32 Void
   +0x008 ImageBaseAddress : Ptr32 Void
   +0x00c Ldr              : Ptr32 _PEB_LDR_DATA
   +0x010 ProcessParameters : Ptr32 _RTL_USER_PROCESS_PARAMETERS
   +0x014 SubSystemData    : Ptr32 Void
   +0x018 ProcessHeap      : Ptr32 Void
   +0x01c FastPebLock      : Ptr32 _RTL_CRITICAL_SECTION
   +0x020 FastPebLockRoutine : Ptr32 Void
   +0x024 FastPebUnlockRoutine : Ptr32 Void
   +0x028 EnvironmentUpdateCount : Uint4B
   +0x02c KernelCallbackTable : Ptr32 Void
   +0x030 SystemReserved   : [1] Uint4B
   +0x034 ExecuteOptions   : Pos 0, 2 Bits
   +0x034 SpareBits        : Pos 2, 30 Bits
   +0x038 FreeList         : Ptr32 _PEB_FREE_BLOCK
   +0x03c TlsExpansionCounter : Uint4B
   +0x040 TlsBitmap        : Ptr32 Void
   +0x044 TlsBitmapBits    : [2] Uint4B
   +0x04c ReadOnlySharedMemoryBase : Ptr32 Void
   +0x050 ReadOnlySharedMemoryHeap : Ptr32 Void
   +0x054 ReadOnlyStaticServerData : Ptr32 Ptr32 Void
   +0x058 AnsiCodePageData : Ptr32 Void
   +0x05c OemCodePageData  : Ptr32 Void
   +0x060 UnicodeCaseTableData : Ptr32 Void
   +0x064 NumberOfProcessors : Uint4B
   +0x068 NtGlobalFlag     : Uint4B
   +0x070 CriticalSectionTimeout : _LARGE_INTEGER
   +0x078 HeapSegmentReserve : Uint4B
   +0x07c HeapSegmentCommit : Uint4B
   +0x080 HeapDeCommitTotalFreeThreshold : Uint4B
   +0x084 HeapDeCommitFreeBlockThreshold : Uint4B
   +0x088 NumberOfHeaps    : Uint4B
   +0x08c MaximumNumberOfHeaps : Uint4B
   +0x090 ProcessHeaps     : Ptr32 Ptr32 Void
   +0x094 GdiSharedHandleTable : Ptr32 Void
   +0x098 ProcessStarterHelper : Ptr32 Void
   +0x09c GdiDCAttributeList : Uint4B
   +0x0a0 LoaderLock       : Ptr32 Void
   +0x0a4 OSMajorVersion   : Uint4B
   +0x0a8 OSMinorVersion   : Uint4B
   +0x0ac OSBuildNumber    : Uint2B
   +0x0ae OSCSDVersion     : Uint2B
   +0x0b0 OSPlatformId     : Uint4B
   +0x0b4 ImageSubsystem   : Uint4B
   +0x0b8 ImageSubsystemMajorVersion : Uint4B
   +0x0bc ImageSubsystemMinorVersion : Uint4B
   +0x0c0 ImageProcessAffinityMask : Uint4B
   +0x0c4 GdiHandleBuffer  : [34] Uint4B
   +0x14c PostProcessInitRoutine : Ptr32
   +0x150 TlsExpansionBitmap : Ptr32 Void
   +0x154 TlsExpansionBitmapBits : [32] Uint4B
   +0x1d4 SessionId        : Uint4B
   +0x1d8 AppCompatFlags   : _ULARGE_INTEGER
   +0x1e0 AppCompatFlagsUser : _ULARGE_INTEGER
   +0x1e8 pShimData        : Ptr32 Void
   +0x1ec AppCompatInfo    : Ptr32 Void
   +0x1f0 CSDVersion       : _UNICODE_STRING
   +0x1f8 ActivationContextData : Ptr32 Void
   +0x1fc ProcessAssemblyStorageMap : Ptr32 Void
   +0x200 SystemDefaultActivationContextData : Ptr32 Void
   +0x204 SystemAssemblyStorageMap : Ptr32 Void
   +0x208 MinimumStackCommit : Uint4B
0:000>

我们刚刚转储了 Windows 定义的 _PEB 结构的信息。现在我们将尝试找到我们的 PEB 并转储这个地址。

0:000> !teb
TEB at 7ffde000
    ExceptionList:        0006ffb0
    StackBase:            00070000
    StackLimit:           0005f000
    SubSystemTib:         00000000
    FiberData:            00001e00
    ArbitraryUserPointer: 00000000
    Self:                 7ffde000
    EnvironmentPointer:   00000000
    ClientId:             00000b80 . 00000f40
    RpcHandle:            00000000
    Tls Storage:          00000000
    PEB Address:          7ffdf000
    LastErrorValue:       0
    LastStatusValue:      c0000034
    Count Owned Locks:    0
    HardErrorMode:        0
0:000> dt ntdll!_PEB 7ffdf000
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0x1 ''
   +0x003 SpareBool        : 0 ''
   +0x004 Mutant           : 0xffffffff
   +0x008 ImageBaseAddress : 0x01000000
   +0x00c Ldr              : 0x00191ea0
   +0x010 ProcessParameters : 0x00020000
   +0x014 SubSystemData    : (null)
   +0x018 ProcessHeap      : 0x00090000
   +0x01c FastPebLock      : 0x77fc49e0
   +0x020 FastPebLockRoutine : 0x77f5b2a0
   +0x024 FastPebUnlockRoutine : 0x77f5b380
   +0x028 EnvironmentUpdateCount : 1
   +0x02c KernelCallbackTable : 0x77d42a38
   +0x030 SystemReserved   : [1] 0
   +0x034 ExecuteOptions   : 0y00
   +0x034 SpareBits        : 0y000000000000000000000000000000 (0)
   +0x038 FreeList         : (null)
   +0x03c TlsExpansionCounter : 0
   +0x040 TlsBitmap        : 0x77fc4680
   +0x044 TlsBitmapBits    : [2] 0x7ff
   +0x04c ReadOnlySharedMemoryBase : 0x7f6f0000
   +0x050 ReadOnlySharedMemoryHeap : 0x7f6f0000
   +0x054 ReadOnlyStaticServerData : 0x7f6f0688  -> (null)
   +0x058 AnsiCodePageData : 0x7ffb0000
   +0x05c OemCodePageData  : 0x7ffc1000
   +0x060 UnicodeCaseTableData : 0x7ffd2000
   +0x064 NumberOfProcessors : 1
   +0x068 NtGlobalFlag     : 0x70
   +0x070 CriticalSectionTimeout : _LARGE_INTEGER 0xffffe86d`079b8000
   +0x078 HeapSegmentReserve : 0x100000
   +0x07c HeapSegmentCommit : 0x2000
   +0x080 HeapDeCommitTotalFreeThreshold : 0x10000
   +0x084 HeapDeCommitFreeBlockThreshold : 0x1000
   +0x088 NumberOfHeaps    : 5
   +0x08c MaximumNumberOfHeaps : 0x10
   +0x090 ProcessHeaps     : 0x77fc5a80  -> 0x00090000
   +0x094 GdiSharedHandleTable : 0x00360000
   +0x098 ProcessStarterHelper : (null)
   +0x09c GdiDCAttributeList : 0x14
   +0x0a0 LoaderLock       : 0x77fc1774
   +0x0a4 OSMajorVersion   : 5
   +0x0a8 OSMinorVersion   : 1
   +0x0ac OSBuildNumber    : 0xa28
   +0x0ae OSCSDVersion     : 0x100
   +0x0b0 OSPlatformId     : 2
   +0x0b4 ImageSubsystem   : 2
   +0x0b8 ImageSubsystemMajorVersion : 4
   +0x0bc ImageSubsystemMinorVersion : 0
   +0x0c0 ImageProcessAffinityMask : 0
   +0x0c4 GdiHandleBuffer  : [34] 0
   +0x14c PostProcessInitRoutine : (null)
   +0x150 TlsExpansionBitmap : 0x77fc4660
   +0x154 TlsExpansionBitmapBits : [32] 0
   +0x1d4 SessionId        : 0
   +0x1d8 AppCompatFlags   : _ULARGE_INTEGER 0x0
   +0x1e0 AppCompatFlagsUser : _ULARGE_INTEGER 0x0
   +0x1e8 pShimData        : (null)
   +0x1ec AppCompatInfo    : (null)
   +0x1f0 CSDVersion       : _UNICODE_STRING "Service Pack 1"
   +0x1f8 ActivationContextData : 0x00080000
   +0x1fc ProcessAssemblyStorageMap : 0x000929a8
   +0x200 SystemDefaultActivationContextData : 0x00070000
   +0x204 SystemAssemblyStorageMap : (null)
   +0x208 MinimumStackCommit : 0
0:000>

如果你还记得,!teb 会给你“线程环境块”。这个块的一部分会显示进程的 PEB。现在,你看到它不仅打印了数据结构中包含的信息,它还知道**如何**根据数据类型来显示信息。我为什么给你看这个?这是因为我上面说的。人们做的第一件事或者想做的就是开始编写调试扩展来转储所有数据结构,但现在这已不可行!然后,你必须在每次添加或移动信息时更改你的调试代码。最好使用“dt”命令,轻松找到你内部数据结构中包含的所有数据。

开始编写扩展

所以,我提出的扩展是 !dumpstrings。它会遍历一个内存位置并转储所有指针。然后我可以执行 !dumpstrings esp 并转储堆栈上所有指针的字符串。

首先,让我们从如何编写扩展开始。WINDBG 要求你至少导出两个函数。这些函数在我下面的源代码中发布。

/***********************************************************
 * ExtensionApiVersion
 *
 * Purpose: WINDBG will call this function to get the version
 *          of the API
 *
 *  Parameters:
 *     Void
 *
 *  Return Values:
 *     Pointer to a EXT_API_VERSION structure.
 *
 ***********************************************************/              
LPEXT_API_VERSION WDBGAPI ExtensionApiVersion (void)
{
    return &g_ExtApiVersion;
}


/***********************************************************
 * WinDbgExtensionDllInit
 *
 * Purpose: WINDBG will call this function to initialize
 *          the API
 *
 *  Parameters:
 *     Pointer to the API functions, Major Version, Minor Version
 *
 *  Return Values:
 *     Nothing
 *
 ***********************************************************/              
VOID WDBGAPI WinDbgExtensionDllInit (PWINDBG_EXTENSION_APIS 
           lpExtensionApis, USHORT usMajorVersion, 
           USHORT usMinorVersion)
{
     ExtensionApis = *lpExtensionApis;
}

第一个函数 ExtensionApiVersion 只是返回一个版本,我们所做的只是提供 WINDBG 期望的版本号,让它满意。这是 g_ExtApiVersion 的声明。

/***********************************************************
 * Global Variable Needed For Versioning
 ***********************************************************/              
EXT_API_VERSION g_ExtApiVersion = {
         5 ,
         5 ,
         EXT_API_VERSION_NUMBER ,
         0
     } ;

EXT_API_VERSION_NUMBERwdbgexts.h 中声明。请注意,还有其他版本的调试器扩展 DLL,例如使用 ntsdexts.h。我只介绍一个简单的例子,它适用于你可以从 Microsoft 网站下载的当前 CDB 和 WinDbg 调试器。我还使用了 windbgexts.h,而不是 ntsdexts.h。如果你查看你的 SDK include 文件,你会注意到你同时拥有这两个头文件。

那么,EXT_API_VESRION_NUMBER 是如何声明的?在我的系统上,它声明为

#define EXT_API_VERSION_NUMBER   5

typedef struct EXT_API_VERSION {
    USHORT  MajorVersion;
    USHORT  MinorVersion;
    USHORT  Revision;
    USHORT  Reserved;
} EXT_API_VERSION, *LPEXT_API_VERSION;

你从哪里得到 5, 5?我只是调试了 WINDBG 附带的一些其他扩展来找到这些数字。我还发现较旧的 WINDBGs 使用 3, 5。但这真的无关紧要,我们只需要一个基本的框架,然后我们就可以编写我们想要的所有命令了!我不太喜欢让这样的事情成为大问题,或者深究它们的含义,因为对我来说,只要我的扩展能加载,它就无关紧要了。

WinDbgExtensionDllInit API 只是给你的应用程序一个虚拟函数表。这个表**必须**命名为某个特定名称。嗯,不一定**必须**,但命名相同会更容易。原因是 windbgexts.h 包含宏,可以调用存储在此结构中的函数。如果你不使用相同的名称,你将不得不自己创建调用。这是我代码中的全局变量。

/***********************************************************
 * Global Variable Needed For Functions
 ***********************************************************/              
WINDBG_EXTENSION_APIS ExtensionApis = {0};

这真的没什么大不了的,宏只是让事情变得容易一点,仅此而已。你可以随心所欲。这是 WINDBGEXTS.H 宏的转储。

#define dprintf          (ExtensionApis.lpOutputRoutine)
#define GetExpression    (ExtensionApis.lpGetExpressionRoutine)
#define GetSymbol        (ExtensionApis.lpGetSymbolRoutine)
#define Disassm          (ExtensionApis.lpDisasmRoutine)
#define CheckControlC    (ExtensionApis.lpCheckControlCRoutine)
#define ReadMemory       (ExtensionApis.lpReadProcessMemoryRoutine)
#define WriteMemory      (ExtensionApis.lpWriteProcessMemoryRoutine)
#define GetContext       (ExtensionApis.lpGetThreadContextRoutine)
#define SetContext       (ExtensionApis.lpSetThreadContextRoutine)
#define Ioctl            (ExtensionApis.lpIoctlRoutine)
#define StackTrace       (ExtensionApis.lpStackTraceRoutine)

接下来呢?除了这两个 API,你还可以有一个 CheckVersion() 函数来强制你的扩展只在特定版本的 WINDBG 上使用命令。我发现这完全没用,而且没有实现它,因为它不是必需的。所以,让我们写一个函数吧!

第一个函数很简单。我们将执行“!help”,它将转储帮助信息。

/***********************************************************
 * !help
 *
 * Purpose: WINDBG will call this API when the user types !help
 *          
 *
 *  Parameters:
 *     N/A
 *
 *  Return Values:
 *     N/A
 *
 ***********************************************************/
DECLARE_API (help)
{
    dprintf("Toby's Debug Extensions\n\n");
    dprintf("!dumpstrings <ADDRESS register> - Dumps 20 strings in"\
       "ANSI/UNICODE using this address as a pointer to strings (useful for" \
       "dumping strings on the stack) \n");
       /* String Split so it is readable in this article. */
}

dprintf(); 基本上是“debug printf”,它与 printf() 完全一样!它会将输出打印到调试器。DECLARE_API(<command>) 是声明 API 名称的一种简单方法。记住,你给函数的名称就是在调试器中使用的名称。我称之为 help,所以要使用它,只需输入 !help 或 !<dllname>.help。这是一个简单的函数,它只会向用户显示帮助消息。

接下来我们要实现我们的字符串函数。这个函数将接受一个内存地址作为参数。我希望它能像当前命令一样工作,如果你输入 dc <address> 然后再次输入 dc,它会继续从上次停下的地方继续转储内存。所以,让我们看看我写了什么。

/***********************************************************
 * !dumpstrings
 *
 * Purpose: WINDBG will call this API when the user types !dumpstrings
 *          
 *
 *  Parameters:
 *     !dumpstrings or !dumpstrings <ADDRESS register>
 *
 *  Return Values:
 *     N/A
 *
 ***********************************************************/
DECLARE_API (dumpstrings)
{
    static ULONG Address = 0;
    ULONG  GetAddress, StringAddress, Index = 0, Bytes;
    WCHAR MyString[51] = {0};
    
    
    GetAddress = GetExpression(args);

    if(GetAddress != 0)
    {
        Address = GetAddress;
    }
        
    dprintf("STACK   ADDR   STRING \n");

    for(Index = 0; Index < 4*20; Index+=4)
    {
        memset(MyString, 0, sizeof(MyString));
        
        Bytes = 0;

        ReadMemory(Address + Index, &StringAddress, 
                           sizeof(StringAddress), &Bytes);

        if(Bytes)
        {
           Bytes = 0;

           ReadMemory(StringAddress, MyString, 
                 sizeof(MyString) - 2, &Bytes);

           if(Bytes)
           {
              dprintf("%08x : %08x = (UNICODE) \"%ws\"\n", 
                       Address + Index, StringAddress, MyString);
              dprintf("%08x : %08x = (ANSI)    \"%s\"\n", 
                       Address + Index, StringAddress, MyString);
           }
           else
           {
              dprintf("%08x : %08x =  Address Not Valid\n", 
                             Address + Index, StringAddress);
           }
        }
        else
        {
           dprintf("%08x : Address Not Valid\n", Address + Index);
        }
    }

    Address += Index;
}

所以,我使用的第一个函数是 GetExpression()。在较新的 WINDBGs 上,它的工作方式如下。ADDRESS GetExpression(SYMBOLIC STRING)。你传入一个符号字符串,例如命令的参数,它会尝试将其解析为一个地址。命令的参数存储在 args 中,所以我传入 args。这将解析符号、地址甚至寄存器为数字,就像传入 ESP 的情况一样。

我现在有一个静态变量。如果 GetExpression() 返回 0,则可能没有参数。如果是这种情况,我使用 Address,我的静态变量。如果有人输入 !dumpstrings,它就能正常工作。它会继续从上次停下的地方继续。在函数结束时,我总是将 Address 保存为下一个要转储的位置。

我使用的下一个函数是 dprintf(),它的工作方式就像使用 printf 一样。我已经解释过这个了,所以接下来是下一个要点。地址是 4 字节长,所以每次循环我都会将这个地址增加 4。我想转储 20 个地址,所以我从 0 循环到 4*20。非常简单。

现在,你不能直接引用返回的内存,因为你不在应用程序的进程空间中。所以,WINDBG 提供了诸如 ReadMemory() 这样的函数。(windbgexts.h 提供了所有 API 的原型,所以如果你找不到 API 在线,你可以自己尝试。)ReadMemory() 函数有 4 个参数。

ReadMemory(Address In Process To Read,
           Local Variable to store the memory read, 
           size of the local variable, 
           pointer to a DWORD that returns the number 
           of bytes read from the memory location);

所以,我们将应用程序中内存的指针传递进去,一个用于接收返回数据的指针,然后接收字节数。如果没有返回字节,我们就打印一个无效内存地址;如果返回了字节,我们然后引用该内存位置读取最多 49 个字节(我们有 51 个字节,并在末尾加上两个 NULL 以支持 Unicode)。如果我能读取到任何内容,我就会尝试使用 dprintf() 函数以 ANSI 格式然后以 Unicode 格式显示它。如果内存返回 0 字节,我就会打印一个错误消息,说明它不是一个有效地址。这一切都非常简单。

接下来我们需要创建我们的 .DEF 文件。这个文件将简单地导出我们的函数。

LIBRARY "TDBG.DLL"

EXPORTS
    WinDbgExtensionDllInit
    ExtensionApiVersion
    dumpstrings
    help

现在,我们需要构建它。我喜欢使用 make 文件,并且我使用 Visual Slickedit 作为我的编辑器。我完全不使用 VC++ IDE。所以,在这个项目中,我创建了一个 make 文件。这是你设置的方法。首先,运行位于你的 VC++ 安装的 BIN 目录下的 VCVARS32.BAT。我把它移到了 C:\ 以便更容易使用。接下来要做的就是直接在源代码解压的目录中输入“nmake”。

C:\Programming\Programs\debugext\src\debug>\vcvars32
Setting environment for using Microsoft Visual C++ tools.
C:\Programming\Programs\debugext\src\debug>nmake

Microsoft (R) Program Maintenance Utility   Version 6.00.8168.0
Copyright (C) Microsoft Corp 1988-1998. All rights reserved.

        cl /nologo /MD /W3 /Oxs /Zi  /I ".\inc"  /D "WIN32" /DLL /D "_WINDOWS"
/Fr.\obj\i386\\ /Fo.\obj\i386\\ /Fd.\obj\i386\\ /c .\tdbg.c
tdbg.c
C:\PROGRA~1\MICROS~2\VC98\INCLUDE\wdbgexts.h(526) : warning C4101: 'li' : unrefe
renced local variable
        link.exe /DLL /nologo /def:tdbg.def /out:..\..\bin\tdbg.dll  /pdb:tdbg.p
db /debug /debugtype:both USER32.LIB  KERNEL32.LIB .\obj\i386\tdbg.obj
   Creating library ..\..\bin\tdbg.lib and object ..\..\bin\tdbg.exp
        rebase.exe -b 0x00100000 -x ..\..\bin -a ..\..\bin\tdbg.dll

REBASE: Total Size of mapping 0x00010000
REBASE: Range 0x00100000 -0x00110000

C:\Programming\Programs\debugext\src\debug>nmake clean

Microsoft (R) Program Maintenance Utility   Version 6.00.8168.0
Copyright (C) Microsoft Corp 1988-1998. All rights reserved.

Deleted file - C:\Programming\Programs\debugext\src\debug\obj\i386\tdbg.obj
Deleted file - C:\Programming\Programs\debugext\src\debug\obj\i386\tdbg.sbr
Deleted file - C:\Programming\Programs\debugext\src\debug\obj\i386\vc60.pdb

C:\Programming\Programs\debugext\src\debug>

如果你想再次构建,你需要更改源代码使日期更新,或者输入“nmake clean”来重新构建。现在你应该有一个二进制文件了。

 Volume in drive C has no label.
 Volume Serial Number is 2CF8-F7B5

 Directory of C:\Programming\Programs\debugext\bin

03/25/2004  08:56 PM    <DIR>          .
03/25/2004  08:56 PM    <DIR>          ..
03/25/2004  09:53 PM             2,412 tdbg.dbg
03/25/2004  09:53 PM            20,752 tdbg.dll
03/25/2004  09:53 PM             1,044 tdbg.exp
03/25/2004  09:53 PM             2,538 tdbg.lib
03/25/2004  09:53 PM            82,944 tdbg.pdb
               5 File(s)        109,690 bytes
               2 Dir(s)  12,229,009,408 bytes free

只需将此二进制文件复制到你的 WinDbg 可以找到的位置,例如你的 windows 目录。你可以使用 !load 指定加载这些扩展的路径,使用 !unload (如果构建了新版本/想强制调试器卸载以便重新加载下一个)来卸载扩展。你可以使用 !<dll name>.<function name> 或 !<function name>。提及二进制名称将强制 WINDBG 查找它来加载它。当多个 DLL 扩展导出同一个命令时,这也有助于区分使用哪个 DLL。

让我们试试

既然我们已经创建了调试扩展,我就把它移到一个 WINDBG 可以加载的位置(例如我的 windows 目录)。然后我使用“!tdbg.dumpstrings esp”来转储堆栈上的所有字符串。这是同一个应用程序(已重启,因此地址可能已更改),已重启(我用 dc esp 验证了,我在堆栈上得到了与之前相同的字符串)。我现在想转储堆栈上所有字符串的指针。让我们试试看会发生什么!

0:000> !tdbg.dumpstrings esp
STACK   ADDR   STRING
0006febc : 77d43a09 = (UNICODE) ""
0006febc : 77d43a09 = (ANSI)    "┬►"
0006fec0 : 77d43c7d = (UNICODE) ""
0006fec0 : 77d43c7d = (ANSI)    "ïN♦ü∙☻☺"
0006fec4 : 0006fefc = (UNICODE) ""
0006fec4 : 0006fefc = (ANSI)    "▐♥↔"
0006fec8 : 00000000 =  Address Not Valid
0006fecc : 00000000 =  Address Not Valid
0006fed0 : 00000000 =  Address Not Valid
0006fed4 : 00000000 =  Address Not Valid
0006fed8 : 0006ff1c = (UNICODE) ""
0006fed8 : 0006ff1c = (ANSI)    "└ ♠"
0006fedc : 010028e4 = (UNICODE) ""
0006fedc : 010028e4 = (ANSI)    "à└uöΦ├∩   5î¢"
0006fee0 : 0006fefc = (UNICODE) ""
0006fee0 : 0006fefc = (ANSI)    "▐♥↔"
0006fee4 : 00000000 =  Address Not Valid
0006fee8 : 00000000 =  Address Not Valid
0006feec : 00000000 =  Address Not Valid
0006fef0 : 00000000 =  Address Not Valid
0006fef4 : 77e7ad86 = (UNICODE) ""
0006fef4 : 77e7ad86 = (ANSI)    "â|$♦"
0006fef8 : 00091ee8 = (UNICODE) ""
0006fef8 : 00091ee8 = (ANSI)    ""
0006fefc : 001d03de = (UNICODE) ""
0006fefc : 001d03de = (ANSI)    ""
0006ff00 : 00000118 =  Address Not Valid
0006ff04 : 0000ffff =  Address Not Valid
0006ff08 : bf8a75ed =  Address Not Valid
0:000> !tdbg.dumpstrings
STACK   ADDR   STRING
0006ff0c : 077d5cc8 =  Address Not Valid
0006ff10 : 000001f3 =  Address Not Valid
0006ff14 : 000001df =  Address Not Valid
0006ff18 : 00000000 =  Address Not Valid
0006ff1c : 0006ffc0 = (UNICODE) ""
0006ff1c : 0006ffc0 = (ANSI)    "≡ ♠"
0006ff20 : 01006c54 = (UNICODE) ""
0006ff20 : 01006c54 = (ANSI)    "ï≡ëuä9]ΣuV §▄↕"
0006ff24 : 01000000 = (UNICODE) ""
0006ff24 : 01000000 = (ANSI)    "MZÉ"
0006ff28 : 00000000 =  Address Not Valid
0006ff2c : 00091ee8 = (UNICODE) ""
0006ff2c : 00091ee8 = (ANSI)    ""
0006ff30 : 0000000a =  Address Not Valid
0006ff34 : 00000000 =  Address Not Valid
0006ff38 : 00000000 =  Address Not Valid
0006ff3c : 7ffdf000 = (UNICODE) ""
0006ff3c : 7ffdf000 = (ANSI)    ""
0006ff40 : 80543940 =  Address Not Valid
0006ff44 : f4910c5c =  Address Not Valid
0006ff48 : 00000044 =  Address Not Valid
0006ff4c : 00092b30 = (UNICODE) ""
0006ff4c : 00092b30 = (ANSI)    ""
0006ff50 : 00092b50 = (UNICODE) ""
0006ff50 : 00092b50 = (ANSI)    "WinSta0\Default"
0006ff54 : 00092b78 = (UNICODE) ""
0006ff54 : 00092b78 = (ANSI)    "C:\WINDOWS.0\System32\notepad.exe"
0006ff58 : 00000000 =  Address Not Valid
0:000> !tdbg.dumpstrings
STACK   ADDR   STRING
0006ff5c : 00000000 =  Address Not Valid
0006ff60 : 00000000 =  Address Not Valid
0006ff64 : 00000000 =  Address Not Valid
0006ff68 : 00000000 =  Address Not Valid
0006ff6c : 00000000 =  Address Not Valid
0006ff70 : 00000000 =  Address Not Valid
0006ff74 : 00000000 =  Address Not Valid
0006ff78 : 00000000 =  Address Not Valid
0006ff7c : 00000000 =  Address Not Valid
0006ff80 : ffffffff =  Address Not Valid
0006ff84 : ffffffff =  Address Not Valid
0006ff88 : ffffffff =  Address Not Valid
0006ff8c : 00091ee8 = (UNICODE) ""
0006ff8c : 00091ee8 = (ANSI)    ""
0006ff90 : 00000000 =  Address Not Valid
0006ff94 : 00000001 =  Address Not Valid
0006ff98 : 00272620 = (UNICODE) ""
0006ff98 : 00272620 = (ANSI)    "(&'"
0006ff9c : 00272d00 = (UNICODE) ""
0006ff9c : 00272d00 = (ANSI)    "░-'"
0006ffa0 : 00000000 =  Address Not Valid
0006ffa4 : 00000000 =  Address Not Valid
0006ffa8 : 0006ff34 = (UNICODE) ""
0006ffa8 : 0006ff34 = (ANSI)    ""

我们在堆栈上找到了两个字符串。不算差,而且我只是启动了 Notepad,没有做任何其他事情。这是这个 API 如何使用的简单示例。获取一个内存位置并转储所有指向字符串的指针。你总会得到一些乱码和无效地址在堆栈上(或任何内存位置)转储出来。我们寻找的是包含字符串的有效地址。

结论

希望你喜欢学习如何编写调试扩展,并希望例子能有所帮助!还有其他 API 列出,你可以查找它们,或者也许会有另一个教程将来解释它们。读取内存和显示信息的基本知识已经涵盖。希望将来,我们可以探索更高级的调试命令。

© . All rights reserved.