一:背景
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 工具,截圖如下:

從圖中可以看到,其實初始化加載什么,由可選頭中的 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,
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
- 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
- _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 加載器就可以用 LoadLibrary 和 GetProcAddress 方法將其加載到行程中了,轉化為 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 位置上取函式地址,參考如下匯編代碼:

三:總結
對初學者來說,搞懂這些還是有一定困難的,我在網上找了一份很好的參考圖,大家可以對照著這張圖理解,在此感謝作者,

- INT 是 Windows 需要加載的函式名串列,
- IAT 是存放 GetProcAddress 回傳函式地址的串列,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/530488.html
標籤:.NET技术
下一篇:乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - .NET 7正式發布,看看ASP.NET Core 7.0和EF Core 7新增哪些功能
