干貨 | HOOK技術實戰
基礎知識
對于Windows系統,它是建立在事件驅動機制上的,說白了就是整個系統都是通過消息傳遞實現的。hook(鉤子)是一種特殊的消息處理機制,它可以監視系統或者進程中的各種事件消息,截獲發往目標窗口的消息并進行處理。所以說,我們可以在系統中自定義鉤子,用來監視系統中特定事件的發生,完成特定功能,如屏幕取詞,監視日志,截獲鍵盤、鼠標輸入等等。
鉤子的種類很多,每種鉤子可以截獲相應的消息,如鍵盤鉤子可以截獲鍵盤消息,外殼鉤子可以截取、啟動和關閉應用程序的消息等。鉤子可以分為線程鉤子和系統鉤子,線程鉤子可以監視指定線程的事件消息,系統鉤子監視系統中的所有線程的事件消息。因為系統鉤子會影響系統中所有的應用程序,所以鉤子函數必須放在獨立的動態鏈接庫(DLL) 中。
所以說,hook(鉤子)就是一個Windows消息的攔截機制,可以攔截單個進程的消息(線程鉤子),也可以攔截所有進程的消息(系統鉤子),也可以對攔截的消息進行自定義的處理。Windows消息帶了一些程序有用的信息,比如Mouse類信息,就帶有鼠標所在窗體句柄、鼠標位置等信息,攔截了這些消息,就可以做出例如金山詞霸一類的屏幕取詞功能。
hook原理
在正確使用鉤子函數前,我們先講解鉤子函數的工作原理。當創建一個鉤子時,WINDOWS會先在內存中創建一個數據結構,該數據結構包含了鉤子的相關信息,然后把該結構體加到已經存在的鉤子鏈表中去。新的鉤子將加到老的前面。當一個事件發生時,如果您安裝的是一個線程鉤子,您進程中的鉤子函數將被調用。如果是一個系統鉤子,系統就必須把鉤子函數插入到其它進程的地址空間,要做到這一點要求鉤子函數必須在一個動態鏈接庫中,所以如果您想要使用系統鉤子,就必須把該鉤子函數放到動態鏈接庫中去。
當然有兩個例外:工作日志鉤子和工作日志回放鉤子。這兩個鉤子的鉤子函數必須在安裝鉤子的線程中。原因是:這兩個鉤子是用來監控比較底層的硬件事件的,既然是記錄和回放,所有的事件就當然都是有先后次序的。所以如果把回調函數放在DLL中,輸入的事件被放在幾個線程中記錄,所以我們無法保證得到正確的次序。故解決的辦法是:把鉤子函數放到單個的線程中,譬如安裝鉤子的線程。
幾點需要說明的地方:
(1) 如果對于同一事件(如鼠標消息)既安裝了線程鉤子又安裝了系統鉤子,那么系統會自動先調用線程鉤子,然后調用系統鉤子。 (2) 對同一事件消息可安裝多個鉤子處理過程,這些鉤子處理過程形成了鉤子鏈。當前鉤子處理結束后應把鉤子信息傳遞給下一個鉤子函數。而且最近安裝的鉤子放在鏈的開始,而最早安裝的鉤子放在最后,也就是后加入的先獲得控制權。 (3) 鉤子特別是系統鉤子會消耗消息處理時間,降低系統性能。只有在必要的時候才安裝鉤子,在使用完畢后要及時卸載。
應用場景
我們知道殺軟有一個檢測的點就是一些敏感的api,如OpenProcess,VirtualAllocEx,WriteProcessMemory 等等。最常用的一個方式就是加一個jmp,跳轉到一個地址,這個地址一般就是殺軟寫代碼進行行為檢測的函數,那么jmp這個過程就是一個hook過程。當然我們如果想要殺軟不分析我們的木馬,那么unhook回來即可,這塊就涉及到硬編碼的知識,這里就不做延伸了。
IAThook
在實現IAThook之前就需要很多前置知識,主要要對PE結構有一定的了解才行。
PE結構
PE文件大致可以分為兩部分,即數據管理結構及數據部分。數據管理結構包含:DOS頭、PE頭、節表。數據部分包括節表數據(節表數據是包含著代碼、數據等內容)。詳情見下圖:

1.DOS頭
DOS頭分為兩個部分,分別是MZ頭及DOS存根,MZ頭是真正的DOS頭部,它的結構被定義為IMAGE_DOS_HEADER。DOS存根是一段簡單程序,主要是用于兼容DOS程序,當不兼容DOS程序時,輸出:"this program cannot be run in DOS mode"。
2.PE頭
PE頭分為三個部分,分別是PE標識(IMAGE_NT_SIGNATRUE)、文件頭(/images/hook技術/image_FILE_HEADER)、可選頭(IMAHE_OPTION_HEADER)。PE頭是固定不變的,位于DOS頭部中e_ifanew字段指出位置。
3.節表
程序中組織按照不同屬性存在不同的節中,如果PE中文件頭的NumberOfSections值中有N個節,那么節表就是由N個節表(IMAGE_SECTION_HEADER)組成。節表主要是存儲了何種借的屬性、文件位置、內存位置等。位置緊跟PE頭后。
4.節表數據
PE文件真正程序部分的存儲位置,有幾個節表就有幾個節表數據,根據節表的屬性、地址等信息,程序的程序就分布在節表的指定位置。位置緊跟節表后。
在了解IAT表之前,需要知道PE數據目錄項的第二個結構 -- 導入表

由于導入函數就是被程序調用但其執行代碼又不在程序中的函數,這些函數的代碼位于一個或者多個DLL 中。當PE 文件被裝入內存的時候,Windows 裝載器才將DLL 裝入,并將調用導入函數的指令和函數實際所處的地址聯系起來(動態連接),這操作就需要導入表完成,其中導入地址表就指示函數實際地址。
導入表是一個結構體,如下所示
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
這里VirtualAddress為導入表的RVA(PE文件在內存中會拉伸,拉伸后的文件偏移地址稱為RVA,原來的文件偏移地址稱為FOA,計算公式為FOA = 導入RVA表地址 - 虛擬偏移 + 實際偏移),Size為導入表的大小。但是上面的解雇姿勢說明導入表在哪里、有多大,并不是真正的導入表。VirtualAddress中存儲的是RVA,如果要在FileBuffer中定位,需要將RVA轉換成FOA,即內存偏移->文件偏移,通過轉換過后才能得到真正的導入表,結構如下
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //RVA 指向IMAGE_THUNK_DATA結構數組(即INT表)
};
DWORD TimeDateStamp; //時間戳
DWORD ForwarderChain;
DWORD Name; //RVA,指向dll名字,該名字已0結尾
DWORD FirstThunk; //RVA,指向IMAGE_THUNK_DATA結構數組(即IAT表)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
到真正的導入表這個地方,又涉及到兩個表,即INT表(Import Name Table)和IAT(Import Address Table),很明顯這里一個表是存儲名稱,一個表是存儲地址的。這里又有一個注意的地方,就是在加載之前INT、IAT表里面存放的都是函數的名稱并指向IMAGE_IMPORT_BY_NAME結構,如下圖所示

在PE文件加載到內存后,INT表的內容和指向的結構都不變,但是IAT表存放的就是函數的地址,也不指向IMAGE_IMPORT_BY_NAME結構了,如下所示

hook原理
若我們找到了想要 HOOK 函數在 IAT 表中的具體位置,我們就可以通過修改該位置(該位置存放的是指針)指針的值為我們自己編寫的函數的地址(在此之前肯定要把這個函數先加載到進程空間),但該函數的參數必須與被 HOOK 的函數完全一致
那么我們了解了導入表、INT表、IAT表之后,就來說說為什么要修改IAT表呢?
在調用api的時候,只要是通過LoadLibrary加載的dll,都會在IAT表里面,我們通上面了解到IAT表里面存放的地址,那么我們可以將IAT表里面的地址修改成我們自己寫的函數的地址來執行我們函數的功能,這就是IAThook想要達到的目的。但是這里有很多步驟和需要用到很多api,下面就說一下IAThook的實現過程。
這里我們選擇hookuser32.dll里面的MessageBoxW函數,我們這里首先定義一個自己的函數MyMessageBox,實現的功能就是獲取參數和返回值
首先定義一個指針
typedef int (WINAPI* PFNMESSAGEBOX)(HWND, LPCSTR, LPCSTR, UINT);
然后打印指針的參數
printf("Argument: hwnd-%x lpText-%ws lpCaption-%ws uType-%x", hwnd, lpText, lpCaption, uType);
因為我們將IAT表里面的地址改為了我們函數的地址,但是原來IAT表里面的函數我們還是要執行才可以,所以這里執行真正的函數,這里pOldFuncAddr就是原來IAT表指向函數的地址,使用GetProcess得到原MessageBoxW的地址
int ret = ((PFNMESSAGEBOX)pOldFuncAddr)(hwnd, lpText, lpCaption, uType); DWORD pOldFuncAddr = (DWORD)::GetProcAddress(LoadLibrary(L"USER32.dll"), "MessageBoxW");
我們再獲取返回值即可實現hookMessageBoxW,MyMessageBox的完整代碼如下
int WINAPI MyMessageBox(
HWND hwnd,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType)
{
//定義MyMessageBox的指針
typedef int (WINAPI* PFNMESSAGEBOX)(HWND, LPCSTR, LPCSTR, UINT);
//獲取參數
printf("Argument: hwnd-%x lpText-%ws lpCaption-%ws uType-%x", hwnd, lpText, lpCaption, uType);
//執行真正的函數
int ret = ((PFNMESSAGEBOX)pOldFuncAddr)(hwnd, lpText, lpCaption, uType);
//獲取返回值
printf("The return value is: %x", ret);
return ret;
}
再就是修改IAT表的函數編寫
首先定位導入表,位于數據目錄項的第二個,使用指針pImport指向導入表
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER); pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pOptionHeader->DataDirectory[1].VirtualAddress + dwImageBase);
然后使用GetModuleHandle獲取進程基址
dwImageBase = (DWORD)::GetModuleHandle(NULL);
然后使用指針pIAT指向IAT表
pIAT = (PDWORD)(pImport->FirstThunk + dwImageBase);
這里作一個判斷,因為我們之前通過GetProcess得到了原來MessageBoxW的地址,這里pIAT指向的也應該是原MessageBoxW的地址,所以正常情況下是相等的,使用*pIAT取值
if (*pIAT == pOldFuncAddr)
注意到這里有一個需要注意的地方,IAT表在默認的情況下是不能夠進行寫入的,如果這個地方直接修改IAT表的數據就會報錯0xc0000005,所以我們需要修改IAT表為可寫屬性,這里用到VirtualProtect這個api
BOOL VirtualProtect( LPVOID lpAddress, //要更改訪問保護屬性的頁面區域的起始頁面地址 SIZE_T dwSize, //要更改訪問保護屬性的區域的大小,以字節為單位 DWORD flNewProtect, //內存保護選項 PDWORD lpflOldProtect //指向一個變量的指針,該變量接收指定頁面區域中第一頁的先前訪問保護值。如果此參數為NULL或未指向有效變量,則函數失敗 );

這里修改第三個參數為PAGE_EXECUTE_READWRITE,即可讀可寫即可
VirtualProtect(pIAT, 0x2000, PAGE_EXECUTE_READWRITE, &oldProtected);
然后把IAT表的地址改向我們自己定義函數的地址即可實現IAThook
*pIAT = dwNewAddr;
那么我們實現IAThook之后如果想把原函數的地址還原回去,就可以寫一個UnSetIATHook,只要把*IAT指向舊的地址即可
在寫一個TestIATHook調用一下這兩個函數
int TestIATHook()
{
SetIATHook(pOldFuncAddr, (DWORD)MyMessageBox);
MessageBox(NULL, L"IAT HOOK", L"IATHOOK success!", MB_OK);
UnSetIATHook(pOldFuncAddr, (DWORD)MyMessageBox);
return 1;
}
完整代碼如下
// IAT hook.cpp : 此文件包含 "main" 函數。程序執行將在此處開始并結束。
//
#include
#include
DWORD pOldFuncAddr = (DWORD)::GetProcAddress(LoadLibrary(L"USER32.dll"), "MessageBoxW");
BOOL SetIATHook(DWORD dwOldAddr, DWORD dwNewAddr)
{
DWORD dwImageBase = 0;
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
PIMAGE_IMPORT_DESCRIPTOR pImport = NULL;
PDWORD pIAT = NULL;
DWORD oldProtected = 0;
bool Flag = FALSE;
dwImageBase = (DWORD)::GetModuleHandle(NULL);
pDosHeader = (PIMAGE_DOS_HEADER)dwImageBase;
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew);
pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4);
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pOptionHeader->DataDirectory[1].VirtualAddress + dwImageBase);
//定位IAT表
while (pImport->FirstThunk != 0 && Flag == FALSE)
{
pIAT = (PDWORD)(pImport->FirstThunk + dwImageBase);
while (*pIAT)
{
if (*pIAT == pOldFuncAddr)
{
VirtualProtect(pIAT, 0x4096, PAGE_EXECUTE_READWRITE, &oldProtected);
*pIAT = dwNewAddr;
Flag = TRUE;
printf("Hook success!");
break;
}
pIAT++;
}
pImport++;
}
return Flag;
}
DWORD UnSetIATHook(DWORD dwOldAddr, DWORD dwNewAddr)
{
DWORD dwImageBase = 0;
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
PIMAGE_IMPORT_DESCRIPTOR pImport = NULL;
PDWORD pIAT = NULL;
DWORD oldProtected = 0;
bool Flag = FALSE;
dwImageBase = (DWORD)::GetModuleHandle(NULL); //獲取進程基址
pDosHeader = (PIMAGE_DOS_HEADER)dwImageBase;
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew);
pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4);
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pOptionHeader->DataDirectory[1].VirtualAddress + dwImageBase);
while (pImport->FirstThunk != 0 && Flag == FALSE)
{
pIAT = (PDWORD)(pImport->FirstThunk + dwImageBase);
while (*pIAT)
{
if (*pIAT == dwNewAddr)
{
*pIAT = dwOldAddr;
Flag = TRUE;
break;
}
pIAT;
}
pImport;
}
return Flag;
}
int WINAPI MyMessageBox(
HWND hwnd,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType)
{
//定義MyMessageBox的指針
typedef int (WINAPI* PFNMESSAGEBOX)(HWND, LPCSTR, LPCSTR, UINT);
//獲取參數
printf("Argument: hwnd-%x lpText-%ws lpCaption-%ws uType-%x", hwnd, lpText, lpCaption, uType);
//執行真正的函數
int ret = ((PFNMESSAGEBOX)pOldFuncAddr)(hwnd, lpText, lpCaption, uType);
//獲取返回值
printf("The return value is: %x", ret);
return ret;
}
int TestIATHook()
{
SetIATHook(pOldFuncAddr, (DWORD)MyMessageBox);
MessageBox(NULL, L"IAT HOOK", L"IATHOOK success!", MB_OK);
UnSetIATHook(pOldFuncAddr, (DWORD)MyMessageBox);
return 1;
}
int main()
{
TestIATHook();
}
注意這里我寫的時候有兩個坑點,第一個地方就是打印的時候需要用tchar.h即寬字符,否則顯示不完全

再就是我一開始hook的是MessageBoxA這個api,但是獲取不到返回值,改成MessageBoxW即可

實現效果如下所示

Inlinehook
API函數都保存在操作系統提供的DLL文件中,當在程序中使用某個API函數時,在運行程序后,程序會隱式地將API所在的DLL加載入進程中。這樣,程序就會像調用自己的函數一樣調用API。
在進程中當EXE模塊調用CreateFile()函數的時候,會去調用kernel32.dll模塊中的CreateFile()函數,因為真正的CreateFile()函數的實現在kernel32.dll模塊中。
CreateFile()是API函數,API函數也是由人編寫的代碼再編譯而成的,也有其對應的二進制代碼。既然是代碼,那么就可以被修改。通過一種“野蠻”的方法來直接修改API函數在內存中的映像,從而對API函數進行HOOK。使用的方法是,直接使用匯編指令的jmp指令將其代碼執行流程改變,進而執行我們的代碼,這樣就使原來的函數的流程改變了。執行完我們的流程以后,可以選擇性地執行原來的函數,也可以不繼續執行原來的函數。
假設要對某進程的kernel32.dll的CreateFile()函數進行HOOK,首先需要在指定進程中的內存中找到CreateFile()函數的地址,然后修改CreateFile()函數的首地址的代碼為jmp MyProc的指令。這樣,當指定的進程調用CreateFile()函數時,就會首先跳轉到我們的函數當中去執行流程,這樣就完成了我們的HOOK了。
那么既然有了IAThook,我們為什么還要用Inlinehook呢,直接用IAThook不是更方便嗎?看硬編碼多麻煩。
我們思考一個問題,如果函數不是以LoadLibrary方式加載,那么肯定在導入表里就不會出現,那么IAThook就不能使用了,這就是Inlinehook誕生的條件。
硬編碼
何為硬編碼?
這里我就不生搬概念性的東西來解釋了,說說我自己的理解。硬編碼可以說就是用十六進制的字符組成的,他是給cpu讀的語言,我們知道在計算機里面只有0和1,如果你要讓他去讀c語言的那些字符他是讀不懂的,他只會讀0和1,這就是硬編碼。
硬編碼的結構如下,有定長指令、變長指令等等一系列指令,還跟各種寄存器相關聯起來,確實如果我們去讀硬編碼的話太痛苦了

這里就不過多延伸了,我們在Inlinehook里面只會用到一個硬編碼就是E9,對應的匯編代碼就是jmp
我們的思路還是跟之前的IAThook一樣,通過修改jmp跳轉的地址跳轉到我們的函數執行功能之后再跳轉到原函數的地址執行原函數
那么這里我們首先定義一個Add函數,定義三個變量,函數的功能就是實現三個數的相加
DWORD Add(int x, int y, int z)
{
return x + y + z;
}
我們在vs里面寫入Add函數并跟進反匯編進行查看,注意這里匯編代碼對應的硬編碼的字節數是確定的,例如55對應的就是push ebp,54對應的就是push esp,這是一個定長指令,也就是說push這個匯編代碼在硬編碼里面就是一字節

那么我們要實現jmp跳轉,執行的命令為jmp 0x123454678 ,對應的字節數為5(jmp也為定長指令),也就是說至少要有5個字節的空間才能夠寫入jmp跳轉的硬編碼。

這里我們可以先將這幾行代碼移到一塊空白的緩沖區里,再使用jmp即E9call跳轉到我們想執行的函數的地址,執行完成過后再執行這幾行代碼之后跳轉回來,如下所示。這里說一下E9的計算,E9后面要填的硬編碼計算公式為 要跳轉的地址 - (E9地址 + 5)

先編寫鉤子函數,首先定義一個裸函數,由我們自己來平衡堆棧,因為C語言默認為stdcall,是自動平衡堆棧的,即內平棧
extern "C" _declspec(naked) void Hook()
我們在進行函數的hook過程中,要保證寄存器和標志寄存器的數值不能改變,否則程序可能會報錯,所以這里先把標志寄存器的值壓入堆棧
_asm
{
pushad; //保留寄存器
pushfd; //保留標志寄存器
}
//讀取寄存器的值
_asm
{
mov reg.EAX, eax
mov reg.EBX, ebx
mov reg.ECX, ecx
mov reg.EDX, edx
mov reg.EDI, edi
mov reg.ESI, esi
mov reg.ESP, esp
mov reg.EBP, ebp
}
然后把我們函數的三個參數壓入堆棧,這里前面已經有10個值了,所以壓棧的地址為esp + 28
_asm
{
mov eax, DWORD PTR ss : [esp + 0x28]
mov x, eax
mov eax, DWORD PTR ss : [esp + 0x2c]
mov y, eax
mov eax, DWORD PTR ss : [esp + 0x30]
mov z, eax
}
然后我們實現的功能就是把寄存器的值打印出來,當然這里是實驗,實戰中師傅們自行發揮
printf("EAX:%x EBX:%x ECX:%x EDX:%x EDI:%x ESI:%x ESP:%x EBP:%x", reg.EAX, reg.EBX, reg.ECX, reg.EDX, reg.EDI, reg.ESI, reg.ESP, reg.EBP);
printf("x:%d y:%d z:%d", x, y, z);
然后還原寄存器和標志寄存器
_asm
{
popfd; //還原標志寄存器
popad; //還原寄存器
}
到這里我們想要執行的函數功能就已經執行完成了,那么我們還需要將之前覆蓋的匯編代碼給還原回去
//執行之前覆蓋的代碼
_asm
{
push ebp
mov ebp, esp
sub esp, 0C0h
}
完成后我們再跳轉回之前執行hook的地址
//執行完后跳轉回hook地址
_asm
{
jmp RetWriteHookAddr;
}
鉤子的完整代碼如下
extern "C" _declspec(naked) void Hook()
{
_asm
{
pushad; //保留寄存器
pushfd; //保留標志寄存器
}
//讀取寄存器的值
_asm
{
mov reg.EAX, eax
mov reg.EBX, ebx
mov reg.ECX, ecx
mov reg.EDX, edx
mov reg.EDI, edi
mov reg.ESI, esi
mov reg.ESP, esp
mov reg.EBP, ebp
}
//將參數壓入堆棧
_asm
{
mov eax, DWORD PTR ss : [esp + 0x28]
mov x, eax
mov eax, DWORD PTR ss : [esp + 0x2c]
mov y, eax
mov eax, DWORD PTR ss : [esp + 0x30]
mov z, eax
}
printf("EAX:%x EBX:%x ECX:%x EDX:%x EDI:%x ESI:%x ESP:%x EBP:%x", reg.EAX, reg.EBX, reg.ECX, reg.EDX, reg.EDI, reg.ESI, reg.ESP, reg.EBP);
printf("x:%d y:%d z:%d", x, y, z);
_asm
{
popfd; //還原標志寄存器
popad; //還原寄存器
}
//執行之前覆蓋的代碼
_asm
{
push ebp
mov ebp, esp
sub esp, 0C0h
}
//執行完后跳轉回hook地址
_asm
{
jmp RetWriteHookAddr;
}
}
然后我們再寫SetInlineHook函數,首先判斷傳入的鉤子函數的地址和鉤子函數是否存在再往下執行
if (HookAddr == NULL || HookProc == NULL)
{
printf("The address is error,please try again!");
return FALSE;
}
判斷是否大于5字節,小于5字節則報錯空間不夠寫不進去
if (dwLength < 5)
{
printf("The alloc is too small,please try adgin!");
return FALSE;
}
然后使用之前IAThook里面的VirtualProtect修改為可讀寫屬性
ReAdd = VirtualProtect((LPBYTE)HookAddr, dwLength, PAGE_EXECUTE_READWRITE, &OldProtect);
申請空間
szBuffer = malloc(dwLength * sizeof(char));
將內存全部首先置nop
memcpy(szBuffer, HookAddr, dwLength);
memset(HookAddr, 0x90, dwLength);
然后計算E9后面跟的硬編碼,使用要跳轉的地址 - E9地址 - 5
DWORD JmpAddr = (DWORD)HookProc - (DWORD)HookAddr - 5;
執行跳轉即可
*(LPBYTE)HookAddr = 0xE9;
*(PDWORD)((LPBYTE)HookAddr + 1) = JmpAddr;
WriteHookAddr = (DWORD)HookAddr;
RetWriteHookAddr = (DWORD)HookAddr + dwLength;
dwHookFlag = 1;
再寫一個解鉤函數,成功后把dwHookFlag置0,代碼如下
DWORD UnInlineHook(DWORD dwLength)
{
if (!dwHookFlag)
{
printf("UnInlineHook!");
return FALSE;
}
memcpy((LPVOID)WriteHookAddr, szBuffer, dwLength);
szBuffer = NULL;
dwHookFlag = 0;
return 1;
}
完整代碼如下
// InlineHook.cpp : 此文件包含 "main" 函數。程序執行將在此處開始并結束。
//
#include
#include
//保留硬編碼
BOOL ReAdd;
BOOL dwHookFlag;
LPVOID szBuffer;
LPBYTE HookAddr;
LPVOID HookProc;
DWORD dwLength;
DWORD WriteHookAddr, RetWriteHookAddr;
DWORD x, y, z;
DWORD Add(int x, int y, int z);
DWORD Sub(int i, int j, int k);
typedef struct _regeist
{
DWORD EAX;
DWORD EBX;
DWORD ECX;
DWORD EDX;
DWORD EBP;
DWORD ESP;
DWORD ESI;
DWORD EDI;
}regeist;
regeist reg = { 0 };
extern "C" _declspec(naked) void Hook()
{
_asm
{
pushad; //保留寄存器
pushfd; //保留標志寄存器
}
_asm
{
mov reg.EAX, eax
mov reg.EBX, ebx
mov reg.ECX, ecx
mov reg.EDX, edx
mov reg.EDI, edi
mov reg.ESI, esi
mov reg.ESP, esp
mov reg.EBP, ebp
}
_asm
{
mov eax, DWORD PTR ss : [esp + 0x28]
mov x, eax
mov eax, DWORD PTR ss : [esp + 0x2c]
mov y, eax
mov eax, DWORD PTR ss : [esp + 0x30]
mov z, eax
}
printf("EAX:%x EBX:%x ECX:%x EDX:%x EDI:%x ESI:%x ESP:%x EBP:%x", reg.EAX, reg.EBX, reg.ECX, reg.EDX, reg.EDI, reg.ESI, reg.ESP, reg.EBP);
printf("x:%d y:%d z:%d", x, y, z);
_asm
{
popfd; //還原標志寄存器
popad; //還原寄存器
}
//執行之前覆蓋的代碼
_asm
{
push ebp
mov ebp, esp
sub esp, 0C0h
}
//執行完后跳轉回hook地址
_asm
{
jmp RetWriteHookAddr;
}
}
//HookAddr為鉤子地址,HookProc為鉤子函數,dwLength為要修改的硬編碼長度
DWORD SetInlineHook(LPBYTE HookAddr, LPVOID HookProc, DWORD dwLength)
{
if (HookAddr == NULL || HookProc == NULL)
{
printf("The address is error,please try again!");
return FALSE;
}
if (dwLength < 5)
{
printf("The alloc is too small,please try adgin!");
return FALSE;
}
DWORD OldProtect;
ReAdd = VirtualProtect((LPBYTE)HookAddr, dwLength, PAGE_EXECUTE_READWRITE, &OldProtect);
if (!ReAdd)
{
printf("SetInlineHook_VirtualProtect - Error!");
return FALSE;
}
szBuffer = malloc(dwLength * sizeof(char));
memcpy(szBuffer, HookAddr, dwLength);
memset(HookAddr, 0x90, dwLength); //將Hook的內存全部初始化為nop(dwLength > 5)
//E9后面的值 = 要跳轉的地址 - E9 - 5
DWORD JmpAddr = (DWORD)HookProc - (DWORD)HookAddr - 5;
*(LPBYTE)HookAddr = 0xE9;
*(PDWORD)((LPBYTE)HookAddr + 1) = JmpAddr;
WriteHookAddr = (DWORD)HookAddr;
RetWriteHookAddr = (DWORD)HookAddr + dwLength;
dwHookFlag = 1;
}
DWORD UnInlineHook(DWORD dwLength)
{
if (!dwHookFlag)
{
printf("UnInlineHook!");
return FALSE;
}
memcpy((LPVOID)WriteHookAddr, szBuffer, dwLength);
szBuffer = NULL;
dwHookFlag = 0;
return 1;
}
DWORD Add(int x, int y, int z)
{
return x + y + z;
}
DWORD TestInlineHook()
{
PBYTE Addr = (BYTE*)Add + 1;
Addr += *(DWORD*)Addr + 4;
SetInlineHook((LPBYTE)Addr, Hook, 9);
Add(8, 1, 5);
UnInlineHook(9);
Add(8, 1, 5);
return 0;
}
int main()
{
//Add(8, 1, 5);
TestInlineHook();
return 0;
}
實現效果如下,因為在TestInlineHook調用了兩個Add函數,在第二個Add函數之前調用了UnInlineHook,最后的結果只顯示了一次,所以UnInlineHook也執行成功了
