Hook在過去十幾年中早被其他人詳細講解過,為了你們及讓我能更深刻理解Hook,我試寫了一篇邏輯性的教程。Hook是惡意軟件,逆向工程,或隨便哪個涉及OS內存領域中非常重要的主題之一。當其與進程注入一起出現時,Hooking能讓你了解該進程想做甚麼,或惡意截斷並更改對WinAPI的任意調用。

背景

我會介紹一種很流行的技術: in-line hook,它僅需修改目標進程DLL所導出的函數中前幾個字節。修改後,若進入函數,就會跳向進程中你所指定的內存地址。嘿嘿! 這時你就可以做壞事了: 對所截斷的調用做任何你想做的事。比如,你可以Hook CreateFile函數,當調用被攔截時,取消其調用並返回失敗。在此例中,實現的效果是拒絕創建任何文件,又或是更有針對性,僅拒絕創建特定文件。

可想而知,這種強大的技術非常的好用。很多軟件都用到了Hook技術,反作弊,反病毒/EDR,及惡意軟件都有使用此種技術。

經典的5-Byte Hook

我們會Hook MessageBoxA,用jmp指令修改其前5個字節,並jmp到我們自訂的函數裡。當調用MessageBoxA函數時,它會彈出一個對話框,其中包含標題和顯示的文字。我們可以Hook它並修改其參數。

1

我反彙編了user32.dll,找到了MessageBoxA,其便是我們Hook的目標。標示出的5個字節和右邊的彙編代碼互相對應,這組指令在許多API函數中非常常見。用jmp覆寫前5字節,便可將函數重定向到我們自己的函數中。我們要保存原始的指令,以便將執行傳回該函數時可以引用(注: 用來恢復函數)。jmp指令是種相對跳轉,其跳向一個偏移地址。jmp的操作碼是E9,而其需要一組4字節的偏移量(注: 目標地址),這需要我們自己計算。

2

首先,從內存中取得MessageBoxA的地址。

1
2
3
// 1. get memory address of the MessageBoxA function from user32.dll 
hinstLib= LoadLibraryA(TEXT("user32.dll"));
function_address= GetProcAddress(hinstLib, "MessageBoxA");

通過動態連接技術,我們調用LoadLibraryA來載入包含所需函數的DLL,用GetProcAddress讀取MessageBoxA在內存中的地址。用ReadProcessMemory將函數的前5個字節保存到緩衝區中。

1
2
// 2. save the first 5 bytes into saved_buffer
ReadProcessMemory(GetCurrentProcess(), function_address, saved_buffer, 5, NULL);

修改函數之前,我們得計算MessageBoxA到代理函數(馬上就寫! )的偏移(距離)。jmp 指令會令EIP步過當前指令(5字節),並加上偏移: eip = eip + 5 + offset

1
偏移 = <目標地址> - (<指令地址> + 5)
1
2
3
4
proxy_address= &proxy_function;
src= (DWORD)function_address + 5;
dst= (DWORD)proxy_address;
relative_offset= (DWORD *)(dst-src);

以下是完整的實現過程,其會將我們寫的補丁寫入內從中的MessageBoxA。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void install_hook()
{
HINSTANCE hinstLib;
VOID *proxy_address;
DWORD *relative_offset;
DWORD src;
DWORD dst;
CHAR patch[5]= {0};

// 1. get memory address of the MessageBoxA function from user32.dll
hinstLib= LoadLibraryA(TEXT("user32.dll"));
function_address= GetProcAddress(hinstLib, "MessageBoxA");

// 2. save the first 5 bytes into saved_buffer
ReadProcessMemory(GetCurrentProcess(), function_address, saved_buffer, 5, NULL);

// 3. overwrite the first 5 bytes with a call to proxy_function
proxy_address= &proxy_function;
src= (DWORD)function_address + 5;
dst= (DWORD)proxy_address;
relative_offset= (DWORD *)(dst-src);

memcpy(patch, 1, "\xE9", 1);
memcpy(patch + 1, 4, &relative_offset, 4);

WriteProcessMemory(GetCurrentProcess(), (LPVOID)function_address, patch, 5, NULL);
}

說明: WriteProcessMemory和ReadProcessMemory會查詢要訪問的內存權限並修改它們,它真的很希望你能成功诶~

我們的代理函數要用與原函數一模一樣的參數,調用約定,以及返回值類型。

1
2
3
4
5
6
7
8
9
10
11
12
// The proxy function we will jump to after the hook has been installed
int __stdcall proxy_function(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
std::cout << "Hello from MessageBox!\n";
std::cout << "Text: " << (LPCSTR)lpText << "\nCaption: " << (LPCSTR)lpCaption << "\n";

// unhook the function (re-write the saved buffer) to prevent infinite recursion
WriteProcessMemory(GetCurrentProcess(), (LPVOID)hooked_address, saved_buffer, 5, NULL);

// return to the original function and modify the intended parameters
return MessageBoxA(NULL, "yeet", "yeet", uType);
}

現在我們可以輸出MessageBoxA的參數,修改它們,並繼續執行原本的MessageBoxA函數。但如果此時我們直接調用MessageBoxA,便會進入不斷被Hook的死循環中,然後造成堆棧溢出。為了避免此種情況發生,我們要將之前儲存在緩衝區的字節重新寫入MessageBoxA的開頭。

此例只會引響一進程中的MessageBoxA調用,若想從導入的DLL中修改其他進程的函數,我會在另一篇文章中教你,你可以參考這個github範例。

因為代理函數會將舊字節重新寫入函數中(unhook),我們還得不斷重新Hook該函數以攔截接下來的調用。讓我們談談TrampolineHook。

Trampolines

運用Trampoline函數,可以在保持Hook的狀態下防止死循環。Trampoline的作用是執行被修改掉的5字節指令的工作,並跳過已安裝的Hook。其通過代理函數調用。

3

在原函數處跳過5字節,故不會執行jmp指令,也不會運行代理函數,我們直接傳遞已安裝的Hook。我們把被hook的函數+5 的地址push進棧,然後用ret實現跳轉。這兩條指令用4字節地址,總共要6字節。故需要11字節(注: 原先5字節,加上後來的6字節)。修改原本的install_hook()函數實現Trampoline的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void install_hook()
{
HINSTANCE hinstLib;
VOID *proxy_address;
DWORD *relative_offset;
DWORD *hook_address;
DWORD src;
DWORD dst;
CHAR patch[5]= {0};
char saved_buffer[5]; // buffer to save the original bytes
FARPROC function_address= NULL;

// 1. get memory address of the MessageBoxA function from user32.dll
hinstLib= LoadLibraryA(TEXT("user32.dll"));
function_address= GetProcAddress(hinstLib, "MessageBoxA");

// 2. save the first 5 bytes into saved_buffer
ReadProcessMemory(GetCurrentProcess(), function_address, saved_buffer, 5, NULL);

// 3. overwrite the first 5 bytes with a jump to proxy_function
proxy_address= &proxy_function;
src= (DWORD)function_address + 5;
dst= (DWORD)proxy_address;
relative_offset= (DWORD *)(dst-src);
memcpy(patch, "\xE9", 1);
memcpy(patch + 1, &relative_offset, 4);
WriteProcessMemory(GetCurrentProcess(), (LPVOID)function_address, patch, 5, NULL);

// 4. Build the trampoline
trampoline_address= VirtualAlloc(NULL, 11, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
hook_address= (DWORD *)((DWORD)function_address + 5);
memcpy((BYTE *)trampoline_address, &saved_buffer, 5);
memcpy((BYTE *)trampoline_address + 5, "\x68", 1);
memcpy((BYTE *)trampoline_address + 6, &hook_address, 4);
memcpy((BYTE *)trampoline_address + 10, "\xC3", 1);
}

我們首先調用VirtualAlloc來分配11字節的內存空間,並將其指定為可執行,可讀,且可寫。這樣才能讓我們修改已分配的字節並執行它。在將trampoline寫入內存後,可以通過代理函數調用它。

1
2
3
4
5
6
7
8
int __stdcall proxy_function(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
std::cout << "----------intercepted call to MessageBoxA----------\n";
std::cout << "Text: " << (LPCSTR)lpText << "\nCaption: " << (LPCSTR)lpCaption << "\n";
// pass to the trampoline with altered arguments which will then return to MessageBoxA
defTrampolineFunc trampoline= (defTrampolineFunc)trampoline_address;
return trampoline(hWnd, "yeet", "yeet", uType);
}

可在github找到完整代碼,在此處可以找到更多關於Hooking的例子。

原文連接: https://medium.com/geekculture/basic-windows-api-hooking-acb8d275e9b8