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

一:背景1.讲故事有朋友咨询个问题,他每次在调试 WinDbg 的时候,进程初始化断点之前都会有一些 dll 加载到进程中,比如下面这样:
Microsoft (R) Windows Debugger Version 10.0.25200.1003 X86Copyright (c) Microsoft Corporation. All rights reserved.CommandLine: D:\net6\ConsoleApp1\Debug\ConsoleApplication3.exe************* Path validation summary **************ResponseTime (ms)LocationDeferredsrv*c:\mysymbols*https://msdl.microsoft.com/download/symbolsSymbol search path is: srv*c:\mysymbols*https://msdl.microsoft.com/download/symbolsExecutable search path is:ModLoad: 00400000 0041f000ConsoleApplication3.exeModLoad: 774b0000 77653000ntdll.dllModLoad: 753a0000 75490000C:\Windows\SysWOW64\KERNEL32.DLLModLoad: 75900000 75b14000C:\Windows\SysWOW64\KERNELBASE.dllModLoad: 79bc0000 79d36000C:\Windows\SysWOW64\ucrtbased.dllModLoad: 79ba0000 79bbe000C:\Windows\SysWOW64\VCRUNTIME140D.dll(44c.4b0c): Break instruction exception - code 80000003 (first chance)eax=00000000 ebx=00000000 ecx=afe00000 edx=00000000 esi=774c1ff4 edi=774c25bceip=77561a42 esp=0019fa20 ebp=0019fa4c iopl=0nv up ei pl zr na pe nccs=0023ss=002bds=002bes=002bfs=0053gs=002befl=00000246ntdll!LdrpDoDebuggerBreak+0x2b:77561a42 ccint3问是否可以用 WinDbg 解读下内部运作原理,哈哈,其实要了解运作原理,一定要熟知 PE 头,那这篇就安排上 。
二:理解 PE 头结构1. 测试代码为了方便讲述,先上一段测试代码,这里故意加载 combase.dll 是为了提取 PE 中的某些数据结构 , 代码如下:
#include <iostream>#include <Windows.h>int main(int argc, char* argv[]) { LoadLibrary(L"combase.dll"); getchar();}其实你仔细想一想也能知道,既然能做到初始化加载,必然在 PE 头上藏了什么东西,这些东西让 Windows 加载器可以顺利加载诸如 ntdll.dll, KERNEL32.dll 等等,接下来一起观察下 。
2. 可视化观察 PE 头【WinDBG详解进程初始化dll是如何加载的】要想可视化观察 PE 头,工具有很多,这里使用 PPEE 工具,截图如下:

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

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

文章插图
肯定有朋友说 , WinDbg 上显示的是 5 个,你这里才 3 个,还有 2 个为什么没有? 很简单,多余的 ntdll.dllKERNELBASE.dll 必然是依赖项哈 。
3. 用 WinDbg 深入探究玩 WinDbg 都喜欢刨根问底,拿可视化 PPEE 肯定忽悠不过去,那好吧,我们用 C 中的结构体去解剖它 。
  1. DOS Header 节
这一块信息在源码中是用 ntdll!_IMAGE_DOS_HEADER 结构来承载的 , 可以用 dt 输出,起始点就是我们的 ConsoleApplication3.dll 在进程的首位置,即: 0x400000
0:000> dt 0x400000 _IMAGE_DOS_HEADERConsoleApplication3!_IMAGE_DOS_HEADER+0x000 e_magic: 0x5a4d+0x002 e_cblp: 0x90+0x004 e_cp: 3...+0x024 e_oemid: 0+0x026 e_oeminfo: 0+0x028 e_res2: [10] 0+0x03c e_lfanew: 0n232
  1. NT Header 节
接下来就是 NT Header 节,它在源码中是由 _IMAGE_NT_HEADERS 结构来承载的 , 起始位置的偏移已经保存在上面的 e_lfanew 字段中,即 0n232
0:000> dt 0x400000+0n232 _IMAGE_NT_HEADERSConsoleApplication3!_IMAGE_NT_HEADERS+0x000 Signature: 0x4550+0x004 FileHeader: _IMAGE_FILE_HEADER+0x018 OptionalHeader: _IMAGE_OPTIONAL_HEADER
  1. _IMAGE_DATA_DIRECTORY
在 PPEE 的第一张截图中,我们查看的是 Data Directorys 数组中的第二项 DIRECTORY_ENTRY_IMPORT 内容,它里面定义了我们需要初始化导入的 dll , 我们可以用 dt r3 展开一下,然后一直点点点就好了,简化后如下:
0:000> dt -r3 0x400000+0n232 _IMAGE_NT_HEADERSConsoleApplication3!_IMAGE_NT_HEADERS+0x000 Signature: 0x4550+0x004 FileHeader: _IMAGE_FILE_HEADER+0x000 Machine: 0x14c...+0x012 Characteristics: 0x103+0x018 OptionalHeader: _IMAGE_OPTIONAL_HEADER+0x000 Magic: 0x10b+0x002 MajorLinkerVersion : 0xe ''...+0x05c NumberOfRvaAndSizes : 0x10+0x060 DataDirectory: [16] _IMAGE_DATA_DIRECTORY+0x000 VirtualAddress: 0+0x004 Size: 00:000> dx -r1 (*((ConsoleApplication3!_IMAGE_DATA_DIRECTORY (*)[16])0x400160))(*((ConsoleApplication3!_IMAGE_DATA_DIRECTORY (*)[16])0x400160))[Type: _IMAGE_DATA_DIRECTORY [16]][0][Type: _IMAGE_DATA_DIRECTORY][1][Type: _IMAGE_DATA_DIRECTORY][2][Type: _IMAGE_DATA_DIRECTORY]...[15][Type: _IMAGE_DATA_DIRECTORY]0:000> dx -r1 (*((ConsoleApplication3!_IMAGE_DATA_DIRECTORY *)0x400168))(*((ConsoleApplication3!_IMAGE_DATA_DIRECTORY *)0x400168))[Type: _IMAGE_DATA_DIRECTORY][+0x000] VirtualAddress: 0x1b1cc [Type: unsigned long][+0x004] Size: 0x50 [Type: unsigned long]

推荐阅读