【技術分享】Hook_IAT實現調包Win32API函數

說明
如何調包Win32API函數?其實就是HookPE文件自己的IAT表。
PE文件在加載到內存后,IAT中存儲對應函數名(或函數序號)的地址,所以我們只需要把用作替換的函數地址,覆蓋掉IAT中對應函數名(或函數序號)的地址,就能實現調包導入模塊的函數。(不僅包括Win32API,包括所有通過dll模塊導入的函數,在exe中都有一塊導入表與之對應)。
下面先回顧一下PE文件導入表知識,再操作hook IAT。
環境:Win10
語言:C
編譯:VS2019-x86
1、導入表及IAT大致工作原理
這部分涉及到PE文件導入表的知識,所以又回顧了一下PE文件的導入表及IAT大致工作原理。
導入表在目錄項中的第二項(導出表之后)。對應目錄項中的VirtualAddress(RVA)即指向的導入表。

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)} IMAGE_IMPORT_DESCRIPTOR;typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
這個結構體有5個4字節數據,占20字節,但只需特別記住這三個RVA即可,下面會分別詳細說明。
三個RVA所指向的地址大概是這樣的:

注意這是PE文件在加載內存前的樣子!
上面涉及到的IMAGE_THUNK_DATA這個結構數組,其實就是一個4字節數,本來是一個union類型,能表示4個數,但我們只需掌握兩種即可,其余兩種已經成為歷史遺留了。
(1)OriginalFirstThunk
OriginalFirstThunk這個RVA所指向的是INT表(Import Name Table),這個表每個數據占4個字節。顧名思義就是表示要導入的函數的名字表。
但是之前學導出表有了解到,導出函數可以以名字導出,亦可以序號導出。所以為了方便區分,就將這INT表的每個值做了細微調整:
INT:如果這個4字節數的最高位(二進制)為1,那么抹去這個最高位之后,所表示的數就是要導入的函數的序號(即這個函數通過序號導入);如果最高位是0,那這個數就也是一個RVA,指向IMAGE_IMPORT_BY_NAME結構體(包含真正的導入函數的名字字符串,以0結尾)。INT表以4字節0結尾。
IMAGE_IMPORT_BY_NAME:前兩個字節是一個序號,不是導入序號,一般無用,后面接著就是導入函數名字的字符串,以0結尾。

(2)Name
這個結構體變量也是一個RVA,直接指向一個字符串,這個字符串就是這個導入表對應的DLL的名字。說到這,大家明白,一個導入表只對應一個DLL。那肯定會有多個導入表。所以對應目錄項里的VirtualAddress(RVA)指向的是所有導入表的首地址,每個導入表占20字節,挨著。最后以一個空結構體作為結尾(20字節全0結構體)。
(3)FirstAddress
FirstAddress(RVA)指向的就是IAT表!IAT表也是每個數據占4個字節。最后以4字節0結尾。
注意上圖PE文件加載內存前,IAT表和INT表的完全相同的,所以此時IAT表也可以判斷函數導出序號,或指向函數名字結構體。
而在加載內存后,差別就是IAT表發生變化,系統會先根據結構體變量Name加載對應的dll(拉伸),讀取dll的導出表,對應原程序的INT表,匹配dll導出函數的地址,返回其地址,貼在對應的IAT表上,挨個修正地址(也就是GetProcAddress的功能)。
所以上文說到,IAT表會存儲dll的函數的地址,方便調用該函數時,直接取IAT表這個地址內的值,作為函數地址,去CALL。

(這是PE文件加載內存后的樣子,注意IAT表發生變化!)
2、根據函數名Hook IAT表
上面大概回顧了一下PE文件導入表的知識,現在就直接嘗試寫hook IAT的代碼,把這一塊封裝成一個函數。
(1)函數定義
#include<Windows.h>//hook自己pe文件的IAT導入表//參數1:自己進程的句柄//參數2:要Hook的函數名稱指針//參數3:需要覆蓋的新的函數指針。//返回值:為0則代表失敗(不是PE文件則返回0且彈MessageBox,沒有找到被hook函數僅僅返回0),//返回值:正常返回被hook函數的原始地址。int Hook_IAT_By_FuncName(HANDLE hMyProcess, PBYTE pOldFuncName, PDWORD pNewFuncAddr);
(2)定位到導入表
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hMyProcess;
PIMAGE_NT_HEADERS32 pNtHeader = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER pOptionHeader = (PIMAGE_OPTIONAL_HEADER)((DWORD)pNtHeader + 4 + IMAGE_SIZEOF_FILE_HEADER); //判斷參數一句柄指向的模塊是否為PE文件
if (*(PWORD)pDosHeader != 0x5A4D || *(PDWORD)pNtHeader != 0x4550) {
MessageBox(NULL, L"Not PE File!!", L"error!", NULL); return 0;
} //定位到可選頭目錄項
PIMAGE_DATA_DIRECTORY pDateDirectory = (PIMAGE_DATA_DIRECTORY)pOptionHeader->DataDirectory; //定位到導入表
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pDosHeader + pDateDirectory[1].VirtualAddress);
(3)遍歷每塊導入表
這一部分,遍歷每塊導入表的每個函數名,根據導入表中INT表指向的函數名,逐個對比。
直到找到我們尋找的函數名,切記先更改IAT表中對應地址內存的讀寫權限,再寫入hook函數的地址。
//遍歷每塊導入表
while (pImportDescriptor->Name)
{ //指向INT
PDWORD pThunkINT = (PDWORD)((DWORD)pDosHeader + pImportDescriptor->OriginalFirstThunk); //指向IAT
PDWORD pThunkIAT = (PDWORD)((DWORD)pDosHeader + pImportDescriptor->FirstThunk); while (*pThunkINT)
{ //因為們是根據函數名Hook,所以默認排除以序號導入的函數
if (*pThunkINT & 0x80000000) {
;
} else
{ //尋址函數名字結構體
PIMAGE_IMPORT_BY_NAME pImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pDosHeader + *pThunkINT); //比較導入表中的函數名和我們參數2提供的函數名
//下面這行代碼:如果pOldFuncName指向“MessageBox”,但導入表中只有"MessageBoxW",也比較成功,進入if內。
if (!memcmp(pOldFuncName, pImportByName->Name, strlen((char*)pOldFuncName))) { //找到對應函數名后,
//先更改IAT表中對應地址內存的讀寫權限
DWORD lpflOldProtect;
BOOL flag = VirtualProtect((LPVOID)pThunkIAT, sizeof(DWORD), PAGE_EXECUTE_READWRITE, &lpflOldProtect); //記錄被hook函數的原始地址
DWORD OldAddr = *pThunkIAT; //寫入hook函數的地址,即第三個參數
*pThunkIAT = (DWORD)pNewFuncAddr; //返回被hook函數的原始地址
return OldAddr;
}
} //每次循環,如果函數名不對應,那么這兩個指針同時增加。
//為滿足pThunkINT指向的名字和pThunkIAT指向的地址是一一對應的!
pThunkINT++;
pThunkIAT++;
} //結構體指針自增,表示指向下一塊導入表。
pImportDescriptor++;
} //最后跳出while循環,表示沒有找到對應函數名的導入函數
//則直接return 0;
return 0;
3、測試
上述封裝好hookIAT的函數,現在編寫main函數調用測試一下,
我們選擇測試hook MessageBox函數。
(1)編寫hook函數
編寫hook函數用于調包被hook函數,即替換掉,表面代碼是調用MessageBox函數,彈出框,實際上并不會執行MessageBox函數,而是執行我們的hook函數。
所以這里有一個細節:hook函數的定義最好與被hook函數一致。
未避免報錯,最好連調用約定都定義為一樣的,否則很可能會報如下錯誤:

由于MessageBox的函數定義為:
WINUSERAPIintWINAPIMessageBoxW(
_In_opt_ HWND hWnd,
_In_opt_ LPCWSTR lpText,
_In_opt_ LPCWSTR lpCaption,
_In_ UINT uType);#define MessageBox MessageBoxW
且上網查了一下MessageBox的調用約定為__stdcall,
所以進行如下定義hook函數(函數內容只是簡單測試一下):
int __stdcall NewFunc(HWND x, LPCWSTR y, LPCWSTR z, UINT m) { printf("\n\n"); printf("x=%d\n", x); printf("y=%s\n", y); printf("z=%s\n", z); printf("m=%d\n", m); printf("\n\n"); printf("Sorry! :\"MessageBox\" Function has been hooked!\n "); return 1;
}
(2)編寫main函數進行調用測試
int main() { //我們要hook函數的函數名
char FuncName[] = "MessageBox"; //參數字符串
char str[] = "Hello World!\n"; //定義函數指針類型
typedef int(__stdcall* MessageBoxFunc)(HWND, LPCWSTR, LPCWSTR, UINT); //先調用正常MessageBox函數
MessageBox(NULL,L"HOOK IAT",L"Tip",NULL); //調用先前編寫的hookIAT函數,進行hook
//同時返回被hook函數的地址,定義函數指針變量接收
MessageBoxFunc OldFunc = (MessageBoxFunc)Hook_IAT_By_FuncName(GetModuleHandle(NULL), (PBYTE)FuncName, (PDWORD)NewFunc); //測試函數指針變量接收的函數地址
OldFunc(NULL, L"MessageBox is here", L"Tip", NULL); //此時MessageBox函數已經被hook,不會再彈出框,
//用于調包的hook函數是在控制臺輸出
MessageBox((HWND)1, (LPCWSTR)FuncName, (LPCWSTR)str, (UINT)2); printf("Got it !\n"); return 0;
}
(3)測試
第一個正常的MessageBox

點擊確認后執行hookIAT函數
第二個MessageBox是定義的函數指針變量接收的hookIAT函數返回的地址。

點擊確認后,再次執行MessageBox,但此時已經被hook調包了,在控制臺輸出語句。

看來MessageBox已經被hook了。
4、所有源碼
因為代碼量并不多,所以直接寫到一個cpp文件里即可。
環境:Win10
語言:C
編譯:VS2019-x86
#include<Windows.h>#include<stdio.h>//hook自己pe文件的IAT導入表//參數1:自己進程的句柄//參數2:要Hook的函數名稱指針//參數3:需要覆蓋的新的函數指針。//返回值:為0則代表失敗(不是PE文件則返回0且彈MessageBox,沒有找到被hook函數僅僅返回0),//返回值:正常返回被hook函數的原始地址。int Hook_IAT_By_FuncName(HANDLE hMyProcess, PBYTE pOldFuncName, PDWORD pNewFuncAddr);int __stdcall NewFunc(HWND x, LPCWSTR y, LPCWSTR z, UINT m);int main() { char FuncName[] = "MessageBox"; char str[] = "Hello World!\n"; typedef int(__stdcall* MessageBoxFunc)(HWND, LPCWSTR, LPCWSTR, UINT);
MessageBox(NULL,L"HOOK IAT",L"Tip",NULL);
MessageBoxFunc OldFunc = (MessageBoxFunc)Hook_IAT_By_FuncName(GetModuleHandle(NULL), (PBYTE)FuncName, (PDWORD)NewFunc);
OldFunc(NULL, L"MessageBox is here", L"Tip", NULL);
MessageBox((HWND)1, (LPCWSTR)FuncName, (LPCWSTR)str, (UINT)2); printf("Got it !\n"); return 0;
}int __stdcall NewFunc(HWND x, LPCWSTR y, LPCWSTR z, UINT m) { printf("\n\n"); printf("x=%d\n", x); printf("y=%s\n", y); printf("z=%s\n", z); printf("m=%d\n", m); printf("\n\n"); printf("Sorry! :\"MessageBox\" Function has been hooked!\n "); return 1;
}//hook自己pe文件的IAT導入表//參數1:自己進程的句柄//參數2:要Hook的函數名稱指針//參數3:需要覆蓋的新的函數指針。//返回值:為0則代表失敗(不是PE文件則返回0且彈MessageBox,沒有找到被hook函數僅僅返回0),//返回值:正常返回被hook函數的原始地址。int Hook_IAT_By_FuncName(HANDLE hMyProcess, PBYTE pOldFuncName, PDWORD pNewFuncAddr) {
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hMyProcess;
PIMAGE_NT_HEADERS32 pNtHeader = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER pOptionHeader = (PIMAGE_OPTIONAL_HEADER)((DWORD)pNtHeader + 4 + IMAGE_SIZEOF_FILE_HEADER); //判斷參數一句柄指向的模塊是否為PE文件
if (*(PWORD)pDosHeader != 0x5A4D || *(PDWORD)pNtHeader != 0x4550) {
MessageBox(NULL, L"Not PE File!!", L"error!", NULL); return 0;
} //定位到可選頭目錄項
PIMAGE_DATA_DIRECTORY pDateDirectory = (PIMAGE_DATA_DIRECTORY)pOptionHeader->DataDirectory; //定位到導入表
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pDosHeader + pDateDirectory[1].VirtualAddress); while (pImportDescriptor->Name)
{
PDWORD pThunkINT = (PDWORD)((DWORD)pDosHeader + pImportDescriptor->OriginalFirstThunk);
PDWORD pThunkIAT = (PDWORD)((DWORD)pDosHeader + pImportDescriptor->FirstThunk); while (*pThunkINT)
{ if (*pThunkINT & 0x80000000) {
;
} else
{ //尋址函數名字結構體
PIMAGE_IMPORT_BY_NAME pImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pDosHeader + *pThunkINT); if (!memcmp(pOldFuncName, pImportByName->Name, strlen((char*)pOldFuncName))) {
DWORD lpflOldProtect;
BOOL flag = VirtualProtect((LPVOID)pThunkIAT, sizeof(DWORD), PAGE_EXECUTE_READWRITE, &lpflOldProtect);
DWORD OldAddr = *pThunkIAT;
*pThunkIAT = (DWORD)pNewFuncAddr; return OldAddr;
}
}
pThunkINT++;
pThunkIAT++;
}
pImportDescriptor++;
} return 0;
}