1

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

 1 year ago
source link: https://www.cnblogs.com/huangxincheng/p/16873561.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

1.讲故事

有朋友咨询个问题,他每次在调试 WinDbg 的时候,进程初始化断点之前都会有一些 dll 加载到进程中,比如下面这样:


Microsoft (R) Windows Debugger Version 10.0.25200.1003 X86
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: D:\net6\ConsoleApp1\Debug\ConsoleApplication3.exe

************* Path validation summary **************
Response                         Time (ms)     Location
Deferred                                       srv*c:\mysymbols*https://msdl.microsoft.com/download/symbols
Symbol search path is: srv*c:\mysymbols*https://msdl.microsoft.com/download/symbols
Executable search path is: 
ModLoad: 00400000 0041f000   ConsoleApplication3.exe
ModLoad: 774b0000 77653000   ntdll.dll
ModLoad: 753a0000 75490000   C:\Windows\SysWOW64\KERNEL32.DLL
ModLoad: 75900000 75b14000   C:\Windows\SysWOW64\KERNELBASE.dll
ModLoad: 79bc0000 79d36000   C:\Windows\SysWOW64\ucrtbased.dll
ModLoad: 79ba0000 79bbe000   C:\Windows\SysWOW64\VCRUNTIME140D.dll
(44c.4b0c): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=afe00000 edx=00000000 esi=774c1ff4 edi=774c25bc
eip=77561a42 esp=0019fa20 ebp=0019fa4c iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!LdrpDoDebuggerBreak+0x2b:
77561a42 cc              int     3

问是否可以用 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 头

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

214741-20221109142711264-1933246601.png

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

214741-20221109142711269-290657285.png

肯定有朋友说,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_HEADER 
ConsoleApplication3!_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_HEADERS
ConsoleApplication3!_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_HEADERS
ConsoleApplication3!_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             : 0
0: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]

从输出的 VirtualAddress=0x1b1cc 中可以看到,我们 PPEE 截图二中的 DIRECTORY_ENTRY_IMPORT 真实内容是在偏移 0x1b1cc 处,它是一个 combase!_IMAGE_IMPORT_DESCRIPTOR 结构体,输出如下:


0:005> dt 0x400000+0x1b1cc combase!_IMAGE_IMPORT_DESCRIPTOR
   +0x000 Characteristics  : 0x1b21c
   +0x000 OriginalFirstThunk : 0x1b21c
   +0x004 TimeDateStamp    : 0
   +0x008 ForwarderChain   : 0
   +0x00c Name             : 0x1b40c
   +0x010 FirstThunk       : 0x1b000

到这里就很关键了,涉及到如下几点信息:

  • 加载的 dll 名字是什么?

可以从 Name 字段提取,参考如下代码:


0:005> da 0x400000+0x1b40c
0041b40c  "KERNEL32.dll"

  • 加载的 方法名 是什么?

这需要提取 OriginalFirstThunk 字段,这里是一个 _IMAGE_IMPORT_BY_NAME类型的指针数组,代码如下:


0:005> dp 0x400000+0x1b21c
0041b21c  0001b3e8 0001b3fc 0001b954 0001b942
0041b22c  0001b934 0001b924 0001b912 0001b906
0041b23c  0001b8fa 0001b8ea 0001b8d6 0001b8ba
...

0:005> dt 0x400000+0x0001b3e8 combase!_IMAGE_IMPORT_BY_NAME
   +0x000 Hint             : 0x382
   +0x002 Name             : [1]  "I"

0:005> da 0x400000+0x0001b3e8+0x2
0041b3ea  "IsDebuggerPresent"

结合上面的输出,我们知道 IsDebuggerPresent() 是属于 KERNEL32.dll 下的,有了这两点信息,Windows 加载器就可以用 LoadLibraryGetProcAddress 方法将其加载到进程中了,转化为 C 代码大概是这样的。


typedef BOOL(CALLBACK* DeubbgerFunc)();

int main(int   argc, char* argv[])
{
	HMODULE hModule = LoadLibrary(L"KERNEL32.dll");

	DeubbgerFunc func = (DeubbgerFunc)GetProcAddress(hModule, "IsDebuggerPresent");

	BOOL b= func();
}

  • func 函数地址会保存吗?

当然会保存了,会放在 _IMAGE_IMPORT_DESCRIPTOR 结构下的 FirstThunk 字段中,这是一个函数地址的指针数组,可以用 dds 观察。


0:005> dt 0x400000+0x1b1cc combase!_IMAGE_IMPORT_DESCRIPTOR
   +0x000 Characteristics  : 0x1b21c
   +0x000 OriginalFirstThunk : 0x1b21c
   +0x004 TimeDateStamp    : 0
   +0x008 ForwarderChain   : 0
   +0x00c Name             : 0x1b40c
   +0x010 FirstThunk       : 0x1b000

0:005> dds 0x400000+0x1b000
0041b000  753c20d0 KERNEL32!IsDebuggerPresentStub
0041b004  753c16c0 KERNEL32!LoadLibraryWStub
0041b008  753c2e80 KERNEL32!GetCurrentProcess
0041b00c  753bf550 KERNEL32!GetProcAddressStub
0041b05c  753b9910 KERNEL32!TerminateProcessStub
...

还有一点要注意,如果你在代码中使用 IsDebuggerPresent() 方法的话,它会从 0041b000 位置上取函数地址,参考如下汇编代码:

214741-20221109142711271-1940043213.png

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

214741-20221109142711241-1300026663.png
  • INT 是 Windows 需要加载的函数名列表。
  • IAT 是存放 GetProcAddress 返回函数地址的列表。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK