Ring 3層的 IAT HOOK 和 EAT HOOK 其原理是通過替換IAT表中函數的原始地址從而實現Hook的,與普通的 InlineHook 不太一樣 IAT Hook 需要充分理解PE文件的結構才能完成 Hook,接下來將具體分析 IAT Hook 的實現原理,並編寫一個DLL注入文件,實現 IAT Hook ,廢話不多說先來給大家補補課。
在早些年系統中運行的都是DOS應用,所以DOS頭結構就是在那個年代產生的,那時候還沒有PE結構的概念,不過軟件行業發展到今天DOS頭部分的功能已經無意義了,但為了最大的兼容性微軟還是保留了DOS文件頭,有些軟件在識別程序是不是可執行文件的時候通常會讀取PE文件的前兩個字節來判斷是不是MZ。
上圖就是PE文件中的DOS部分,典型的DOS開頭ASCII字符串MZ幻數,MZ是Mark Zbikowski
的縮寫,Mark Zbikowski是MS-DOS的主要開發者之一,很顯然這個人給微軟做出了巨大的貢獻。
在DOS格式部分我們只需要關注標紅部分,標紅部分是一個偏移值000000F8h
該偏移值指向了PE文件中的標綠部分00004550
指向PE字符串的位置,此外標黃部分為DOS提示信息,當我們在DOS模式下執行一個可執行文件時會彈出This program cannot be run in DOS mode.
提示信息。
上圖中在PE字符串開頭位置向后偏移1字節,就能看到黃色的014C
此處代表的是機器類別的十六進制表示形式,在向后偏移1個字節是紫色的0006
代表的是程序中的區段數,繼續向后偏移1字節會看到藍色的5DB93874
此處是一個時間戳,代表的是自1970年1月1日至當前時間的總秒數,繼續向后可看到灰色的000C
此處代表的是鏈接器的具體版本。
上圖中我們以PE字符串為單位向后偏移36字節,即可看到文件偏移為120處的內容,此處的內容是我們要重點研究的對象。
在文件FOA偏移為120
的位置,可以看到標紅色的地址0001121C
此處代表的是程序裝入內存后的入口點(虛擬地址),而緊隨其后的橙色部分00001000
就是代碼段的基址,其后的粉色部分是數據段基址,在數據基址向后偏移1字節可看到紫色的00400000
此處就是程序的建議裝入地址,如果編譯器沒有開啟基址隨機化的話,此處默認就是00400000,開啟隨機化后建議裝入地址與實際地址將不符合。
繼續向下文件FOA偏移為130的位置,第一處淺藍色部分00001000
為區段之間的對齊值,深藍色部分00002000
為文件對其值。
上面只簡單的介紹了PE結構的基本內容,在PE結構的開頭我們知道了區段的數量是6個,接着我們可以在PE字符串向下偏移244個字節的位置就能夠找到區段塊,區塊內容如下:
上圖可以看到,我分別用不同的顏色標注了這六個不同的區段,區段的開頭一般以.xxx為標識符其所對應的機器碼是2E,其中每個區塊分別占用40個字節的存儲空間。
我們以.text節為例子,解釋下不同塊的含義,第一處綠色的位置就是區段名稱該名稱總長度限制在8字節以內,第二處深紅色標簽為虛擬大小,第三處深紫色標簽為虛擬偏移,第四處藍色標簽為實際大小,第五處綠色標簽為區段的屬性,其它的節區屬性與此相同,此處就不再贅述了。
接着繼續看一下導入表,導出表,基址重定位表,IAT表,這些表位於PE字符串向后偏移116個字節的位置,如下我已經將重要的字段備注了顏色:
首先第一處淺紅色部分就是導出表的地址與大小,默認情況下只有DLL文件才會導出函數所以此處為零,第二處深紅色位置為導入表地址而后面的黃色部分則為導入表的大小,繼續向下第三處淺藍色部分則為資源表地址與大小,第四處棕色部分就是基址重定位表的地址,默認情況下只有DLL文件才會重定位,最下方的藍色部分是IAT表的地址,后面的黃色為IAT表的大小。
此時我們重點關注一下導入表RVA地址 0001A1E0
我們通過該地址計算一下導入表對應到文件中的位置。
計算公式:FOA = 導入RVA表地址 - 虛擬偏移 + 實際偏移 = > 0001A1E0 - 11000 + 400 = 95E0
通過計算可得知,導入表位置對應到文件中的位置是0x95E0,我們直接跟隨過去但此時你會驚奇的發現這里全部都是0,這是因為Windows裝載器在加載時會動態的獲取第三方函數的地址並自動的填充到這些位置處,我們並沒有運行EXE文件所以也就不會填充,為了方便演示,我們將程序拖入x64dbg讓其運行起來,然后來看一個重要的結構。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // 指向導入表名稱的RVA
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 默認為0(非重點)
DWORD ForwarderChain; // 默認為0(非重點)
DWORD Name; // 指向DLL名字的RVA
DWORD FirstThunk; // 導入地址表IAT的RVA
} IMAGE_IMPORT_DESCRIPTOR;
該IMAGE_IMPORT_DESCRIPTOR
導入表結構的大小為4*5 = 20
個字節的空間,導入表結構結束的位置通常會通過使用一串連續的4*5個0
表示結束,接下來我們將從后向前逐一分析這個數據結構所對應到程序中的位置。
通過上面對導入表的分析我們知道了導入表RVA地址為 0001A1E0
此時我們還知道ImageBase地址是00400000
兩個地址相加即可得到導入表的虛擬VA地址0041a1e0
,此時我們可以直接通過x64dbg的數據窗口定位到0041a1e0
可看到如下地址組合,結合IMAGE_IMPORT_DESCRIPTOR
結構來分析。
如上所示,可以看到該程序一共有3個導入結構分別是紅紫黃色部分,最后是一串零結尾的字符串,標志着導入表的結束,我們以第1段紅色部分為例,最后一個地址偏移0001A15C
對應的就是導入表中的FirstThunk
字段,我們將其加上ImageBase地址,定位過去發現該地址剛好是LoadIconW
的函數地址,那么我們有理由相信緊隨其后的地址應該是下一個外部函數的地址,而事實也正是如此。
接着我們繼續來分析IMAGE_IMPORT_DESCRIPTOR
導入結構中的Name
字段,其對應的是第一張圖中的紅色部分0001A54A
將該偏移與基址00400000
相加后直接定位過去,可以看到0041A54A
對應的字符串正是USER32.dll
動態鏈接庫,而后面會有兩個00標志着字符串的結束。
最后我們來分析IMAGE_IMPORT_DESCRIPTOR
中最復雜的一個字段OriginalFirstThunk
為什么說它復雜呢?是因為他的內部並不是一個數值而是嵌套了另一個結構體 IMAGE_THUNK_DATA
,我們先來看一下微軟對該結構的定義:
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal; // 序號
DWORD AddressOfData; // 指向 PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
接着來找到OriginalFirstThunk
字段在內存中的位置,由第一張圖可知,圖中的標紅部分第一個四字節0001A38C
就是它丫的!我們加上基址00400000然后直接懟過去,並結合上方的結構定義研究一下!
該結構中我們需要關注AddressOfData
結構成員,該成員中的數據最高位(紅色)如果為1(去掉1)說明是函數的導出序號,而如果最高位為0則說明是一個指向IMAGE_IMPROT_BY_NAME
結構(導入表)的RVA(藍色)地址,此處因為我們找的是導入表所以最高位全部為零。
我們以上圖中的第一個RVA地址0001A53E
與基址相加,來看下該AddressOfData
字段中所指向的內容是什么。
上圖黃色部分是編譯器生成的,而藍色部分則為LoadIconW
字符串與FirstThunk
中的0041A15C
地址指針是相互對應的,而最后面的00則表明字符串的結束,對比以下結構聲明就很好理解了。
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; // 編譯器生成的
CHAR Name[1]; // 函數名稱,以0結尾的字符串
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
為了能更加充分的理解,我用Excel畫了一張圖,如下所示:
如上圖IMAGE_IMPORT_DESCRIPTO
導入表結構中的FirstThunk
和OriginalFirstThunk
分別指向兩個相同的IMAGE_THUNK_DATA
結構,其中內存INT(Improt Name Table)
表中存儲的就是導入函數的名稱,而IAT(Improt Address Table)
表中存放的是導入函數的地址,他們都共同指向IMAGE_IMPORT_BY_NAME
結構,而之所以使用兩份IMAGE_THUNK_DATA
結構,是為了最后還可以留下一份備份數據用來反過來查詢地址所對應的導入函數名,看了這張圖再結合上面的實驗相信你已經理解了!
經過了上面對導入表的學習,接着我們就來通過代碼的方式實現劫持MessageBox函數:
1.首先需要編寫一個DLL文件,在DLL文件中找出MessageBox的原函數地址。
2.接着通過代碼的方式找到DOS/NT/FILE/Optional頭偏移地址。
3.通過DataDirectory[1]數組得到導入表的起始RVA 並與ImageBase基址相加得到VA。
4.循環遍歷導入表中的IAT表,找到與MessageBox
地址相同的4字節位置。
5.找到后通過VirtualProtect
設置內存屬性可讀寫,並將自己的函數地址寫入到目標IAT表中。
6.沒有找到的話直接pFirstThunk++
循環遍歷后面的4字節位置,直到找到為止。
知道了流程,編寫並理解代碼就變得非常簡單了,代碼如下,你可以自行注入到進程中測試效果。
#include <stdio.h>
#include <Windows.h>
typedef int(WINAPI *pfMessageBoxA)(HWND, LPCSTR, LPCSTR, UINT);
pfMessageBoxA OldMessageBoxA = NULL;
int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
return OldMessageBoxA(hWnd, "hello lyshark", lpCaption, uType);
}
PIMAGE_NT_HEADERS GetLocalNtHead()
{
DWORD dwTemp = NULL;
PIMAGE_DOS_HEADER pDosHead = NULL;
PIMAGE_NT_HEADERS pNtHead = NULL;
HMODULE ImageBase = GetModuleHandle(NULL); // 取自身ImageBase
pDosHead = (PIMAGE_DOS_HEADER)(DWORD)ImageBase; // 取pDosHead地址
dwTemp = (DWORD)pDosHead + (DWORD)pDosHead->e_lfanew;
pNtHead = (PIMAGE_NT_HEADERS)dwTemp; // 取出NtHead頭地址
return pNtHead;
}
void IATHook()
{
PVOID pFuncAddress = NULL;
pFuncAddress = GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA"); // 取Hook函數地址
OldMessageBoxA = (pfMessageBoxA)pFuncAddress; // 保存原函數指針
PIMAGE_NT_HEADERS pNtHead = GetLocalNtHead(); // 獲取到程序自身NtHead
PIMAGE_FILE_HEADER pFileHead = (PIMAGE_FILE_HEADER)&pNtHead->FileHeader;
PIMAGE_OPTIONAL_HEADER pOpHead = (PIMAGE_OPTIONAL_HEADER)&pNtHead->OptionalHeader;
DWORD dwInputTable = pOpHead->DataDirectory[1].VirtualAddress; // 找出導入表偏移
DWORD dwTemp = (DWORD)GetModuleHandle(NULL) + dwInputTable;
PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)dwTemp;
PIMAGE_IMPORT_DESCRIPTOR pCurrent = pImport;
DWORD *pFirstThunk; //導入表子表,IAT存儲函數地址表.
//遍歷導入表
while (pCurrent->Characteristics && pCurrent->FirstThunk != NULL)
{
dwTemp = pCurrent->FirstThunk + (DWORD)GetModuleHandle(NULL);// 找到內存中的導入表
pFirstThunk = (DWORD *)dwTemp; // 賦值 pFirstThunk
while (*(DWORD*)pFirstThunk != NULL) // 不為NULl說明沒有結束
{
if (*(DWORD*)pFirstThunk == (DWORD)OldMessageBoxA) // 相等說明正是我們想要的地址
{
DWORD oldProtected;
VirtualProtect(pFirstThunk, 0x1000, PAGE_EXECUTE_READWRITE, &oldProtected); // 開啟寫權限
dwTemp = (DWORD)MyMessageBoxA;
memcpy(pFirstThunk, (DWORD *)&dwTemp, 4); // 將MyMessageBox地址拷貝替換
VirtualProtect(pFirstThunk, 0x1000, oldProtected, &oldProtected); // 關閉寫保護
}
pFirstThunk++; // 繼續遞增循環
}
pCurrent++; // 每次是加1個導入表結構.
}
}
BOOL APIENTRY DllMain(HMODULE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{
IATHook();
return TRUE;
}
原創作品,轉載請加出處,您添加出處是我創作的動力!