WinDBG详解进程初始化dll是如何加载的

[复制链接]
发表于 2022-11-9 16:36:52 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
一:背景

1.讲故事

有朋友咨询个问题,他每次在调试 WinDbg 的时候,进程初始化断点之前都会有一些 dll 加载到进程中,比如下面这样:
  1. Microsoft (R) Windows Debugger Version 10.0.25200.1003 X86
  2. Copyright (c) Microsoft Corporation. All rights reserved.
  3. CommandLine: D:\net6\ConsoleApp1\Debug\ConsoleApplication3.exe
  4. ************* Path validation summary **************
  5. Response                         Time (ms)     Location
  6. Deferred                                       srv*c:\mysymbols*https://msdl.microsoft.com/download/symbols
  7. Symbol search path is: srv*c:\mysymbols*https://msdl.microsoft.com/download/symbols
  8. Executable search path is:
  9. ModLoad: 00400000 0041f000   ConsoleApplication3.exe
  10. ModLoad: 774b0000 77653000   ntdll.dll
  11. ModLoad: 753a0000 75490000   C:\Windows\SysWOW64\KERNEL32.DLL
  12. ModLoad: 75900000 75b14000   C:\Windows\SysWOW64\KERNELBASE.dll
  13. ModLoad: 79bc0000 79d36000   C:\Windows\SysWOW64\ucrtbased.dll
  14. ModLoad: 79ba0000 79bbe000   C:\Windows\SysWOW64\VCRUNTIME140D.dll
  15. (44c.4b0c): Break instruction exception - code 80000003 (first chance)
  16. eax=00000000 ebx=00000000 ecx=afe00000 edx=00000000 esi=774c1ff4 edi=774c25bc
  17. eip=77561a42 esp=0019fa20 ebp=0019fa4c iopl=0         nv up ei pl zr na pe nc
  18. cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
  19. ntdll!LdrpDoDebuggerBreak+0x2b:
  20. 77561a42 cc              int     3
复制代码
问是否可以用 WinDbg 解读下内部运作原理,哈哈,其实要了解运作原理,一定要熟知 PE 头,那这篇就安排上。
二:理解 PE 头结构

1. 测试代码

为了方便讲述,先上一段测试代码,这里故意加载 combase.dll 是为了提取 PE 中的某些数据结构,代码如下:
  1. #include <iostream>
  2. #include <Windows.h>
  3. int main(int argc, char* argv[]) {
  4.         LoadLibrary(L"combase.dll");
  5.         getchar();
  6. }
复制代码
其实你仔细想一想也能知道,既然能做到初始化加载,必然在 PE 头上藏了什么东西,这些东西让 Windows 加载器可以顺利加载诸如 ntdll.dll, KERNEL32.dll 等等,接下来一起观察下。
2. 可视化观察 PE 头

要想可视化观察 PE 头,工具有很多,这里使用 PPEE 工具,截图如下:

从图中可以看到,其实初始化加载什么,由可选头中的 DIRECTORY_ENTRY_IMPORT 数据目录项决定,哪这里包含了哪些初始化 dll 呢? 可以选中右边的 DIRECTORY_ENTRY_IMPORT 项即可,如下图所示:

肯定有朋友说,WinDbg 上显示的是 5 个,你这里才 3 个,还有 2 个为什么没有? 很简单,多余的 ntdll.dll 和 KERNELBASE.dll 必然是依赖项哈。
3. 用 WinDbg 深入探究

玩 WinDbg 都喜欢刨根问底,拿可视化 PPEE 肯定忽悠不过去,那好吧,我们用 C 中的结构体去解剖它。

  • DOS Header 节
这一块信息在源码中是用 ntdll!_IMAGE_DOS_HEADER 结构来承载的,可以用 dt 输出,起始点就是我们的 ConsoleApplication3.dll 在进程的首位置,即: 0x400000。
  1. 0:000> dt 0x400000 _IMAGE_DOS_HEADER
  2. ConsoleApplication3!_IMAGE_DOS_HEADER
  3.    +0x000 e_magic          : 0x5a4d
  4.    +0x002 e_cblp           : 0x90
  5.    +0x004 e_cp             : 3
  6.    ...
  7.    +0x024 e_oemid          : 0
  8.    +0x026 e_oeminfo        : 0
  9.    +0x028 e_res2           : [10] 0
  10.    +0x03c e_lfanew         : 0n232
复制代码

  • NT Header 节
接下来就是 NT Header 节,它在源码中是由 _IMAGE_NT_HEADERS 结构来承载的,起始位置的偏移已经保存在上面的 e_lfanew 字段中,即 0n232。
  1. 0:000> dt 0x400000+0n232 _IMAGE_NT_HEADERS
  2. ConsoleApplication3!_IMAGE_NT_HEADERS
  3.    +0x000 Signature        : 0x4550
  4.    +0x004 FileHeader       : _IMAGE_FILE_HEADER
  5.    +0x018 OptionalHeader   : _IMAGE_OPTIONAL_HEADER
复制代码

  • _IMAGE_DATA_DIRECTORY
在 PPEE 的第一张截图中,我们查看的是 Data Directorys 数组中的第二项 DIRECTORY_ENTRY_IMPORT 内容,它里面定义了我们需要初始化导入的 dll,我们可以用 dt r3 展开一下,然后一直点点点就好了,简化后如下:
  1. 0:000> dt -r3 0x400000+0n232 _IMAGE_NT_HEADERS
  2. ConsoleApplication3!_IMAGE_NT_HEADERS
  3.    +0x000 Signature        : 0x4550
  4.    +0x004 FileHeader       : _IMAGE_FILE_HEADER
  5.       +0x000 Machine          : 0x14c
  6.       ...
  7.       +0x012 Characteristics  : 0x103
  8.    +0x018 OptionalHeader   : _IMAGE_OPTIONAL_HEADER
  9.       +0x000 Magic            : 0x10b
  10.       +0x002 MajorLinkerVersion : 0xe ''
  11.       ...
  12.       +0x05c NumberOfRvaAndSizes : 0x10
  13.       +0x060 DataDirectory    : [16] _IMAGE_DATA_DIRECTORY
  14.          +0x000 VirtualAddress   : 0
  15.          +0x004 Size             : 0
  16. 0:000> dx -r1 (*((ConsoleApplication3!_IMAGE_DATA_DIRECTORY (*)[16])0x400160))
  17. (*((ConsoleApplication3!_IMAGE_DATA_DIRECTORY (*)[16])0x400160))                 [Type: _IMAGE_DATA_DIRECTORY [16]]
  18.     [0]              [Type: _IMAGE_DATA_DIRECTORY]
  19.     [1]              [Type: _IMAGE_DATA_DIRECTORY]
  20.     [2]              [Type: _IMAGE_DATA_DIRECTORY]
  21.     ...
  22.     [15]             [Type: _IMAGE_DATA_DIRECTORY]
  23. 0:000> dx -r1 (*((ConsoleApplication3!_IMAGE_DATA_DIRECTORY *)0x400168))
  24. (*((ConsoleApplication3!_IMAGE_DATA_DIRECTORY *)0x400168))                 [Type: _IMAGE_DATA_DIRECTORY]
  25.     [+0x000] VirtualAddress   : 0x1b1cc [Type: unsigned long]
  26.     [+0x004] Size             : 0x50 [Type: unsigned long]
复制代码
从输出的 VirtualAddress=0x1b1cc 中可以看到,我们 PPEE 截图二中的 DIRECTORY_ENTRY_IMPORT 真实内容是在偏移 0x1b1cc 处,它是一个 combase!_IMAGE_IMPORT_DESCRIPTOR 结构体,输出如下:
  1. 0:005> dt 0x400000+0x1b1cc combase!_IMAGE_IMPORT_DESCRIPTOR
  2.    +0x000 Characteristics  : 0x1b21c
  3.    +0x000 OriginalFirstThunk : 0x1b21c
  4.    +0x004 TimeDateStamp    : 0
  5.    +0x008 ForwarderChain   : 0
  6.    +0x00c Name             : 0x1b40c
  7.    +0x010 FirstThunk       : 0x1b000
复制代码
到这里就很关键了,涉及到如下几点信息:

  • 加载的 dll 名字是什么?
可以从 Name 字段提取,参考如下代码:
  1. 0:005> da 0x400000+0x1b40c
  2. 0041b40c  "KERNEL32.dll"
复制代码

  • 加载的 方法名 是什么?
这需要提取 OriginalFirstThunk 字段,这里是一个 _IMAGE_IMPORT_BY_NAME类型的指针数组,代码如下:
  1. 0:005> dp 0x400000+0x1b21c
  2. 0041b21c  0001b3e8 0001b3fc 0001b954 0001b942
  3. 0041b22c  0001b934 0001b924 0001b912 0001b906
  4. 0041b23c  0001b8fa 0001b8ea 0001b8d6 0001b8ba
  5. ...
  6. 0:005> dt 0x400000+0x0001b3e8 combase!_IMAGE_IMPORT_BY_NAME
  7.    +0x000 Hint             : 0x382
  8.    +0x002 Name             : [1]  "I"
  9. 0:005> da 0x400000+0x0001b3e8+0x2
  10. 0041b3ea  "IsDebuggerPresent"
复制代码
结合上面的输出,我们知道 IsDebuggerPresent() 是属于 KERNEL32.dll 下的,有了这两点信息,Windows 加载器就可以用 LoadLibrary 和 GetProcAddress 方法将其加载到进程中了,转化为 C 代码大概是这样的。
  1. typedef BOOL(CALLBACK* DeubbgerFunc)();
  2. int main(int   argc, char* argv[])
  3. {
  4.         HMODULE hModule = LoadLibrary(L"KERNEL32.dll");
  5.         DeubbgerFunc func = (DeubbgerFunc)GetProcAddress(hModule, "IsDebuggerPresent");
  6.         BOOL b= func();
  7. }
复制代码

  • func 函数地址会保存吗?
当然会保存了,会放在 _IMAGE_IMPORT_DESCRIPTOR 结构下的 FirstThunk 字段中,这是一个函数地址的指针数组,可以用 dds 观察。
  1. 0:005> dt 0x400000+0x1b1cc combase!_IMAGE_IMPORT_DESCRIPTOR
  2.    +0x000 Characteristics  : 0x1b21c
  3.    +0x000 OriginalFirstThunk : 0x1b21c
  4.    +0x004 TimeDateStamp    : 0
  5.    +0x008 ForwarderChain   : 0
  6.    +0x00c Name             : 0x1b40c
  7.    +0x010 FirstThunk       : 0x1b0000:005> dds 0x400000+0x1b0000041b000  753c20d0 KERNEL32!IsDebuggerPresentStub0041b004  753c16c0 KERNEL32!LoadLibraryWStub0041b008  753c2e80 KERNEL32!GetCurrentProcess0041b00c  753bf550 KERNEL32!GetProcAddressStub0041b05c  753b9910 KERNEL32!TerminateProcessStub...
复制代码
还有一点要注意,如果你在代码中使用 IsDebuggerPresent() 方法的话,它会从 0041b000 位置上取函数地址,参考如下汇编代码:

三:总结

对初学者来说,搞懂这些还是有一定困难的,我在网上找了一份很好的参考图,大家可以对照着这张图理解,在此感谢作者。


  • INT 是 Windows 需要加载的函数名列表。
  • IAT 是存放 GetProcAddress 返回函数地址的列表。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表