MinHook的原理就在於重寫目標函數。在此次分析的X86模式中,32位相對JMP覆蓋了整個地址空間。由於在相對地址計算中溢出的位被忽略,因此在X86模式中,函數的地址是容易掌控的。數組
直接來進入正題。函數
使用時須要經過#pragma來導出模塊定義文件 (.def)中的DLL函數:oop
LIBRARY EXPORTS SeInitialize SeUninitialize SeCreateHook SeEnableHook SeDisableHook
#if defined _M_X64 #pragma comment(lib, "libMinHook.x64.lib") #elif defined _M_IX86 #pragma comment(lib, "..\\Debug\\MiniHook.x86.lib") #endif
Hook的第一步是用HeapCreate建立了一個自動增加的堆,首先是提交了PAGE_SIZE大小,詳細內容的話能夠參考MSDN:測試
__HeapHandle = HeapCreate(0, 0, //提交 PAGE_SIZE 0); //If dwMaximumSize is 0, the heap can grow in size.自動增加
隨後開始建立Hook了。須要注意的是這裏的建立Hook只是寫好了進行Hook所須要的信息,原始指令備份,Trampline等,添加到HOOK_INFORMATION的HookEntry成員中。spa
填充的hook信息結構體:3d
typedef struct _HOOK_INFORMATION_ { PHOOK_ENTRY HookEntry; UINT MaximumLength; UINT Length; }HOOK_INFORMATION,*PHOOK_INFORMATION; typedef struct _HOOK_ENTRY { LPVOID TargetFunctionAddress; LPVOID FakeFunctionAddress; LPVOID MemorySlot; UINT8 Backup[8]; //恢復Hook使用的存放原先數據 // UINT8 patchAbove : 1; // Uses the hot patch area. 位域:1位 UINT8 IsEnabled : 1; // Enabled. // UINT8 queueEnable : 1; // Queued for enabling/disabling when != isEnabled. UINT Index : 4; // Count of the instruction boundaries.??? UINT8 OldIPs[8]; // Instruction boundaries of the target function.??? UINT8 NewIPs[8]; // Instruction boundaries of the trampoline function ??? } HOOK_ENTRY, *PHOOK_ENTRY; //44字節
首先第一步是檢查函數是否已經被Hook過,若是已經被Hook過的話,就沒必要再一次填充HOOK_INFORMATION結構了。code
UINT SeFindHookEntry(LPVOID FunctionAddress) { UINT i; for (i = 0; i < __Hooks.Length; ++i) { if ((ULONG_PTR)FunctionAddress == (ULONG_PTR)__Hooks.HookEntry[i].TargetFunctionAddress) return i; } return STATUS_NOT_FOUND; }
若是沒有被本身Hook過的話,就先準備一下TRAMPOLINE結構體:blog
#pragma pack(1) typedef struct _TRAMPOLINE { LPVOID TargetFunctionAddress; LPVOID FakeFunctionAddress; LPVOID MemorySlot; // MemorySlot 32字節 #if defined(_M_X64) || defined(__x86_64__) LPVOID pRelay; // [Out] Address of the relay function. #endif // BOOL patchAbove; // [Out] Should use the hot patch area? //Patch --->補丁 //0xA 0xB UINT Index; // [Out] Number of the instruction boundaries. UINT8 OldIPs[8]; // [Out] Instruction boundaries of the target function. //恢復 UINT8 NewIPs[8]; // [Out] Instruction boundaries of the trampoline function. //Hook } TRAMPOLINE, *PTRAMPOLINE;
typedef struct _MEMORY_SLOT { union { struct _MEMORY_SLOT *Flink; UINT8 BufferData[MEMORY_SLOT_SIZE]; }; } MEMORY_SLOT, *PMEMORY_SLOT; //32字節
對於TRAMPOLINE的構建是一塊相對複雜的內容。能夠看到結構體的前兩個成員分別是原函數地址和咱們想要到達的自定義函數地址,第三個成員MemorySlot,記錄的是原函數地址開頭的五個字節,以及跳轉回原函數的jmp指令,OldPos和NewPos則分別保存了在構建MemorySlot的過程當中,已經讀取的目標函數偏移字節數,和已經寫入的Detour函數字節數。內存
構建MemorySlot是經過一個do_While循環來實現的,首先它須要保存好被覆蓋的那些字節數,因此在原函數已經讀取的字節數少於jmp+四字節offset,一共五字節以前,MemorySlot要先備份下原來的指令,一旦原函數已經讀取的字節數超過或者等於覆蓋所須要的五字節時,也就沒必要再備份原來函數基地址處的指令了,由於僅僅只是覆蓋五字節的內容:
get
do { HDE hde; UINT CopyCodeLength; LPVOID CopyCodeData; //對於出現的相對偏移地址,在跳板中都要給出新的相對地址 /* 74CA8B80 8B FF mov edi,edi 74CA8B82 55 push ebp 74CA8B83 8B EC mov ebp,esp 74CA8B85 6A 00 push 0 74CA8B87 FF 75 14 push dword ptr [ebp+14h] 74CA8B8A FF 75 10 push dword ptr [ebp+10h] 74CA8B8D FF 75 0C push dword ptr [ebp+0Ch] 74CA8B90 FF 75 08 push dword ptr [ebp+8] 74CA8B93 E8 F8 FC FF FF call _MessageBoxExW@20 (74CA8890h) */ ULONG_PTR OldInstance = (ULONG_PTR)Trampoline->TargetFunctionAddress + OldPos; ULONG_PTR NewInstance = (ULONG_PTR)Trampoline->MemorySlot + NewPos; //指令長度 CopyCodeLength = HDE_DISASM((LPVOID)OldInstance, &hde); if (hde.flags & F_ERROR) return FALSE; CopyCodeData = (LPVOID)OldInstance; //第一次時,CopyCodeData是MessageBox入口地址 if (OldPos >= sizeof(JMP_REL)) { // The trampoline function is long enough. // Complete the function with the jump to the target function. #if defined(_M_X64) || defined(__x86_64__) jmp.address = pOldInst; #else //OldInstance = 74CA8B85 //目標 = 源 + Offset + 5 //Offset = 目標 - (源 + 5) jmp.Operand = (UINT32)(OldInstance - (NewInstance + sizeof(jmp))); //計算跳轉到目標的偏移 #endif CopyCodeData = &jmp; CopyCodeLength = sizeof(jmp); IsLoop = TRUE; } //......此處省略部分源碼,突出重點 Trampoline->OldIPs[Trampoline->Index] = OldPos; Trampoline->NewIPs[Trampoline->Index] = NewPos; Trampoline->Index++; // Avoid using memcpy to reduce the footprint. #ifndef _MSC_VER memcpy((LPBYTE)ct->pTrampoline + newPos, pCopySrc, copySize); #else __movsb((LPBYTE)Trampoline->MemorySlot + NewPos, (const unsigned char*)CopyCodeData, CopyCodeLength); #endif NewPos += CopyCodeLength; OldPos += hde.len; } while (!IsLoop);
在這段do_while循環裏,經過反彙編能夠清晰地看到MemorySlot一步步的構建,在測試代碼中我Hook的是MessageBox函數,因此在看MemorySlot的構造過程以前,先來看一看反彙編下原始的MessageBox的機器指令:
對應着MessageBox的機器指令,來看MemorySlot在do while循環中的構造過程:
局部變量窗口找到MemorySlot的地址:
先是申請了32字節的結構體大小:
經過反彙編引擎計算MessageBox函數基地址處開始,每幾個字節是一條完整的機器指令,記錄在Trampoline的OldIPs,NewIPs數組中:
//指令長度 CopyCodeLength = HDE_DISASM((LPVOID)OldInstance, &hde);
Trampoline->OldIPs[Trampoline->Index] = OldPos; Trampoline->NewIPs[Trampoline->Index] = NewPos; Trampoline->Index++;
每記錄一次指令字節長度的同時,也將即將被覆蓋的原指令備份到MemorySlot中:
如今備份的指令長度已經達到了5字節了,能夠開始填充跳轉回MessageBox基地址處+5字節的偏移地址跳轉指令了:
if (OldPos >= sizeof(JMP_REL)) { // The trampoline function is long enough. // Complete the function with the jump to the target function. #if defined(_M_X64) || defined(__x86_64__) jmp.address = pOldInst; #else //目標 = 源 + Offset + 5 //Offset = 目標 - (源 + 5) jmp.Operand = (UINT32)(OldInstance - (NewInstance + sizeof(jmp))); //計算跳轉到目標的偏移 #endif CopyCodeData = &jmp; CopyCodeLength = sizeof(jmp); IsLoop = TRUE; }
到此爲止MemorySlot填充好了10字節的內容,構造完畢了,整個Trampoline結構也構造好了,如今Trampoline的內容應該是這樣的:
建立好了Trampoline結構後,開始建立HookEntry結構體,對比兩個結構體咱們能夠看到:HookEntry結構體只比Trampoline結構多了兩個成員,因此其實HookEntry結構的構建只剩下兩個成員了——一個Backup備份被覆蓋的五字節內容,一個IsEnabled設置爲FALSE,暫時不開始實際的Hook。
HookEntry->IsEnabled = FALSE; //存儲源函數的數據內容 5字節的備份 memcpy(HookEntry->Backup, TargetFunctionAddress, sizeof(JMP_REL));
到此爲止整個CreateHook的流程就結束了,能夠進入下一話:EnableHook,使Hook生效——實際動手覆蓋五字節了
//SHELLCODE PJMP_REL jmp = (PJMP_REL)PatchData; jmp->Opcode = 0xE9; jmp->Operand = (UINT32)((LPBYTE)HookEntry->FakeFunctionAddress - (PatchData + sizeof(JMP_REL)));
而到了想要恢復的時候,將備份好的原五字節覆蓋回去也就搞定了:
memcpy(PatchData, HookEntry->Backup, sizeof(JMP_REL));
其實還有不恢復原五字節指令而可以正確調用MessageBox的方法,回頭一想,直接執行Trampoline結構中MemorySlot的指令,不久是正確執行了MessageBox的指令嗎嘛!——想到這一點的時候,我才明白了構建了MemorySlot以後,還要另外用Backup成員保存原五字節的用意:一個用來直接調用正確的MessageBox函數,一個用來進行UnHook恢復。
談到這裏,順便一提鉤子鏈的問題,當一個函數被屢次Hook的時候,就會出現Hook鏈了。Hook鏈中依次跳到各個模塊對目標函數Hook的Detour函數中,執行本身的函數。根據不一樣的模塊Hook的前後順序,造成鉤子鏈:
從圖中能夠看出,Module2是先掛的鉤子,它將CreateProcessW的其實5字節的指令複製到了它的Trampoline函數中,而且添加了一個jmp到CreateProcessW+5Byte的地址,從而繼續執行CreateProcessW以後的指令實現恢復。當Module1再次對CreateProcessW掛鉤子時,它將Module2寫的jmp到Module2地址的指令複製到了Module1 的Trampline 函數中,這樣就造成了鉤子鏈,即對同一個函數地址,屢次Hook時就造成了鉤子鏈。
從圖中也能夠看出,卸載鉤子回覆指令的方法有兩種:
1. 按照掛鉤的反順序卸載鉤子,也即先Module1卸載鉤子,而後Module2卸載鉤子,Module1會將JMP Hook2-Temp的指令恢復到了CreatProcessW掛鉤指令上,那麼這時CreateProcessW處就變成了單個掛鉤的狀況,不管此時調用CreateProcessW,仍是繼續Module2的UnHook都不會有問題。
2. 直接卸載第一個掛鉤,也即先讓Module2卸載,那麼CreateProcessW被Hook的地方指令被恢復爲了原始的狀況(Kernel32.dll初始映射的狀況)。這時調用CreateProcessW不會出現問題。也就是說雖然Module1沒有UnHook,可是它的鉤子被覆蓋掉了,也不會調用到Module1的Detours函數了。
可是~若是此時,Module1 繼續執行卸載,那麼很明顯 JMP Hook2-Temp 指令被覆蓋會了CreateProcessW Hook的五個字節指令上了,而很明顯它要跳轉到Module2的自定義Detours函數。而Trampoline函數所佔用的內存空間可能已經被釋放掉了,一旦釋放掉了,將跳轉到無效的內存空間,就要GG崩潰了~
Hook MessageBox的測試結果:
MessageBoxW(NULL, L"HelloWorld", L"HelloWorld", 0); int WINAPI DetourMessageBox( _In_opt_ HWND hWnd, _In_opt_ WCHAR* lpText, _In_opt_ WCHAR* lpCaption, _In_ UINT uType) { __OriginalMessageBoxW(hWnd,L"FakeMessageBox",L"FakeMessageBox",uType); return 0; } if (CreateHook(&MessageBoxW, &DetourMessageBox, reinterpret_cast<LPVOID*>(&__OriginalMessageBoxW)) != STATUS_SUCCESS) { return; }