前言
匯入地址表鉤取的方法容易實現但是存在缺陷,若需要鉤取的函式不存在匯入地址表中,那麼我們就無法進行鉤取,出現以下幾種情況時,匯入函式是不會儲存在匯入地址表中的。
-
延遲載入:當匯入函式還沒呼叫時,匯入函式還未寫入到匯入地址表中。
-
動態連結:使用
LoadLibrary
與GetProcAddress
函式時,程式是顯示獲取函式地址的,因此不會寫入到匯入地址表中。 -
手動解析匯入函式:即程式自身實現一套匯入方法,那麼此時也不會將匯入函式寫入到匯入地址表中。
有一種鉤取方法解決上述問題即內聯鉤取(inline hook
)。
內聯鉤取(inline hook)
內聯鉤取實際是找到需要鉤取的函式地址,這裡與匯入地址表鉤取不同的是我們不再侷限於匯入地址表,而是程式中所有的函式地址都能夠作為鉤取的物件。
這裡以CreateProcessW
函式為例,在CreateProcessW
函式中,第一條指令是mov edi,edi
那麼根據鉤取的思路,我們將mov edi,edi
這條指令修改為jmp xxx
(xxx
為我們自定義函式的地址),那麼在執行CreateaProcessW
函式時即可跳轉到我們的自定義函式中。
我們獲取mov edi,edi
指令的地址,並且將該指令篡改為jmp
指令,並且把mov edi,edi
指令的資料進行儲存,那麼在執行到CreateProcessW
函式時就會執行jmp
指令跳轉到自定義函式中,在鉤取操作時需要將指令寫回,還原CreateProcessW
函式的執行邏輯,就可以在鉤取的同時無礙的執行程式。
那麼總結一下內聯鉤取函式的流程
-
找到需要鉤取的函式的指令地址,這個指令並不僅限於函式起始的指令。
-
將該指令篡改成跳轉指令,跳轉的目的就是自定義的函式。
-
在自定義函式內需要還原被鉤取函式的指令。
因此內聯鉤取的實際就是修改程式執行邏輯,劫持程式的執行流程。由於32位程式與64位程式的組合語言與定址方式有些許差異,因此不同機器位數的程式的內聯鉤取方式不同。
機器碼的獲取
由於在篡改記憶體時需要將jmp xxx
的機器碼填寫到記憶體中,因此做內聯鉤取時需要獲取指令對應的機器碼。在C語言中支援內聯彙編,因此可以使用內聯彙編然後檢視對應的機器碼即可。
但是直接使用visual studio
編譯64位程式的內聯彙編程式碼會出錯,這是因為visual studio
自帶的編譯工具不支援x64
的內聯彙編。
因此需要先安裝clang
編譯器
在專案的編譯工具選擇clang
即可
在反彙編視窗中就有機器碼了。
【----幫助網安學習,以下所有學習資料免費領!加vx:dctintin,備註 “部落格園” 獲取!】
① 網安學習成長路徑思維導圖
② 60+網安經典常用工具包
③ 100+SRC漏洞分析報告
④ 150+網安攻防實戰技術電子書
⑤ 最權威CISSP 認證考試指南+題庫
⑥ 超1800頁CTF實戰技巧手冊
⑦ 最新網安大廠面試題合集(含答案)
⑧ APP客戶端安全檢測指南(安卓+IOS)
32位的內聯鉤取
首先第一步是確定在32位程式下是如何進行跳轉的,在32位情況使用跳轉指令是根據偏移獲取目的地址,偏移的計算公式如下
跳轉偏移 = 跳轉目的地址 - 當前指令地址 - 指令長度
因此jmp xxx
中,xxx
是偏移值而不是目的函式的絕對地址。
緊接著需要確定在32位下跳轉指令的機器碼是多少,用下面例子看看
void MyCreateProcess()
{
}
int main()
{
__asm {
jmp MyCreateProcess;
};
}
可以看到對應的機器碼為E9 EB FF FF FF
可以看到目標函式的地址為0xA71000
,使用上述公式計算一下偏移為0xA71000 - 0x0A71010 - 5 = 0xffffffeb
,因此E9
為jmp
的機器碼
因此需要將待鉤取函式的第一條指令修改為E9 XX XX XX XX XX
,長度為5個位元組
然後選擇一個目標函式,這裡還是使用CreateProcessW
函式作為例子,需要先獲取CreateProcessW
函式的地址
...
hMoudle = GetModuleHandleA(szDllName); //獲取Kernel32.dll模組的地址
if (hMoudle == NULL)
{
GetLastError();
}
pfnOld = GetProcAddress(hMoudle, funName);//獲取CreateProcessW函式地址
if (pfnOld == NULL)
{
GetLastError();
}
...
然後需要儲存原始指令,然後修改區域為可寫許可權,緊接著計算一下偏移把完整的指令寫進到待鉤取函式即可。
...
//修改許可權
VirtualProtect(pfnOld, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
//儲存原始的5個位元組
memcpy(pOrgBytes, pfnOld, 5);
//計算需要跳轉到的地址
//跳轉偏移 = 跳轉目的地址 - 當前指令地址 - 指令長度
dwAddress = (ULONGLONG)pfnNew - (ULONGLONG)pfnOld - 5;
//將目標函式的地址寫入到指令中
memcpy(&pBuf[1], &dwAddress, 4);
//篡改為跳轉指令
memcpy(pfnOld, pBuf, 5);
//還原許可權
VirtualProtect(pfnOld, 5, dwOldProtect, &dwOldProtect);
...
64位的內聯鉤取
64位下的規則會與32位有差異,但是總體思路是一致的。在32位下我們採用了偏移的方式找到目標函式,在64位下可以換種方式,採用mov rax, xxx; jmp rax
,將函式的絕對地址寫入暫存器,然後跳轉到指定暫存器的方式。
如下例子,我們首先獲取自定義函式的絕對地址,緊接著將它存放於暫存器中,緊接著跳轉即可。
int main()
{
__asm {
mov rax, 0x1122334455667788;
jmp rax;
};
}
可以看到mov rax, xxx; jmp rax
指令的機器碼為48 B8 xx xx xx xx xx xx xx xx FF E0
,其中由於64位地址都是8位元組的,因此需要xx
需要填充8位元組
因此總體程式碼與32位區別不大,這裡需要注意的是篡改的指令長度需要根據實際進行更改。
/*
* 48 B8 88 77 66 55 44 33 22 11 mov rax, 0x1122334455667788
* FF E0 jmp rax
* 需要12個位元組進行跳轉
*/
//修改區域許可權
VirtualProtect((LPVOID)pfnOrg, 12, PAGE_EXECUTE_READWRITE, &dwOldProtect);
//儲存原有的12位元組資料
memcpy(pOrgBytes, pfnOrg, 12);
//將HOOK函式的地址填進緩衝區
//將目標地址複製到指令中
memcpy(&pBuf[2], &pfnNew, 8);
//篡改待鉤取函式
memcpy(pfnOrg, pBuf, 12);
//恢復許可權
VirtualProtect((LPVOID)pfnOrg, 12, dwOldProtect, &dwOldProtect);
因此任意可以修改函式執行流程的彙編指令實際都可以例如push xxx; ret
。
完整程式碼可以參考:
https://github.com/h0pe-ay/HookTechnology/tree/main/Hook-InlineHook
總結
優勢
-
內聯鉤取相較於匯入表鉤取的選擇性更廣,可以選擇任意的函式及函式內的任意指令地址。
劣勢
-
每次都需要脫鉤後再進行掛鉤,影響效率
-
多執行緒寫入時可能會出錯
更多網安技能的線上實操練習,請點選這裡>>