在X64模式中,存在的問題是JMP指令和整個地址空間相比僅僅覆蓋了很窄的範圍。所以引入一箇中繼函數(Relay Function)來實現對64位Detour函數地址的跳轉。git
在hook的分析以前,先談一下前一篇帖子評論中的相關話題。github
以前發佈的一篇Minhook分析,有大牛說沒有寫出多線程安全,指令緩存,以及捕獲上下文,從中提取EIP / RIP的問題,實際上源碼當中都是有涉及的,這裏再次感謝大牛提出的問題,只是上一篇我想的是着重於單純的hook分析因此沒有將多線程安全等部分展現出來,因此在這一篇中我先來將源碼中上述問題的對應的解決方案作一次分析。數組
(先附上github上源碼的下載地址:https://github.com/TsudaKageyu/minhook)緩存
0x01 多線程的安全問題安全
在MinHook中構建hook相關結構的幾個函數中(好比MH_CreateHook函數中),進入函數後開始操做以前,都會首先調用EnterSpinLock()函數來確保某一肯定的時間片內,只有惟一一個線程在調用當前函數,來看EnterSpinLock函數具體內容:多線程
static VOID EnterSpinLock(VOID) { SIZE_T spinCount = 0; // Wait until the flag is FALSE. /* LONG InterlockedCompareExchange( _Inout_ LONG volatile *Destination , _In_ LONG Exchange , _In_ LONG Comparand ); 把目標操做數(第1參數所指向的內存中的數)與一個值(第3參數)比較,若是相等, 則用另外一個值(第2參數)與目標操做數(第1參數所指向的內存中的數)交換; 返回值是 Destination 指針的初始值。 整個操做過程是鎖定內存的,其它處理器不會同時訪問內存,從而實現多處理器環境下的線程互斥 */ while (InterlockedCompareExchange(&g_isLocked, TRUE, FALSE) != FALSE) { // No need to generate a memory barrier here, since InterlockedCompareExchange() // generates a full memory barrier itself. // Prevent the loop from being too busy. if (spinCount < 32) Sleep(0); else Sleep(1); spinCount++; } }
EnterSpinLock函數內部調用了InterlockedCompareExchange函數,這個函數的功能是把目標操做數(第1參數所指向的內存中的數)與一個值(第3參數)比較,若是相等,則用另外一個值(第2參數)與目標操做數(第1參數所指向的內存中的數)交換。函數的返回值則是 Destination 指針的初始值。這整個操做過程是鎖定內存的,其它處理器不會同時訪問內存,從而實現多處理器環境下的線程互斥。ide
當咱們第一次調用MH_CreateHook函數來構建HookEntry結構的時候,g_isLocked的值仍是初始化時的值FALSE。函數
// Spin lock flag for EnterSpinLock()/LeaveSpinLock(). volatile LONG g_isLocked = FALSE;
因此第一次進入的時候不會進入while循環(注意InterlockedCompareExchange函數的返回值是初始值FALSE,而不是發生交換後的值TRUE,因此沒有進入while循環),假定當前線程正在調用MH_CreateHook函數尚未結束,那麼再次進入EnterSpinLock時,g_isLocked已經被置爲TRUE了,進入while循環,假如在第二個線程while循環的過程當中,g_isLocked的值遲遲不被恢復成FALSE的話(MH_CreateHook函數結束退出前會調用LeaveSpinLock函數將g_isLocked的值恢復成FALSE),那麼第二個線程首先會反覆地調用Sleep(0),調用次數達到32次之後,開始反覆地調用Sleep(1),直至沒有線程在調用MH_CreateHook函數爲止(標誌就是g_isLocked的值恢復成FALSE),就退出while循環,正式地進入MH_CreateHook函數。oop
這裏有必要談一下Sleep(0)與Sleep(1)了。測試
Sleep 的意思是告訴操做系統本身要休息 n 毫秒,這段時間片可讓給另外一個就緒的線程。
當 n=0 的時候,意思是要當前線程放棄本身剩下的時間片,可是仍然是就緒狀態。不過Sleep(0) 只容許那些優先級相等或更高的線程使用當前的CPU,其它線程只能等待了。若是沒有合適的線程,那當前線程會從新使用 CPU 時間片。
當 n=1 的時候,意思是要當前線程放棄剩下的時間片,並休息 一下(這裏的休息時間並不必定就是1毫秒,具體的休息時間要看系統的時間精度,好比系統的時間精度只能達到10ms的話,那麼將會休息10ms)。而且全部其它就緒狀態的線程都有機會競爭時間片,而不用在意優先級。
因此在while循環的整個等待過程當中,也一直在嘗試讓出時間片給可能須要的其餘線程。
0x02 指令緩存
咱們hook了原函數跳轉到咱們本身的Detour函數,雖然修改了內存中的指令,但有可能被修改的指令已經被緩存起來了,再執行的話,CPU可能會優先執行緩存中的指令,使得修改的指令得不到執行。因此咱們須要使用一個隱藏的系統調用來刷新一下緩存,當心駛得萬年船~:
FlushInstructionCache(GetCurrentProcess(), pPatchTarget, patchSize);
在這裏值得一提的是,若是使用WriteProcessMemory寫其餘進程內存來注入亦或其餘目的的話,是不須要再額外調用FlushInstructionCache函數刷新緩存的,由於WriteProcessMemory自己就會調用NtFlushInstructionCache函數來刷新緩存:
0x03 暫停線程,捕獲上下文,提取EIP / RIP
當咱們掛起了線程要重寫目標函數時,首先要捕獲其上下文。線程的上下文實際上就是其寄存器的狀態。從上下文中提取咱們關注的寄存器EIP / RIP。若是剛好命很差當前的EIP / RIP指向的地址是咱們準備hook重寫的目標函數地址處,那麼就須要修正EIP / RIP中的地址值,修正的新數據將用來恢復現場保證線程的順利執行。
在上一篇帖子中詳細描述了x86中jmp + 4字節offset類型的hook,咱們暫以此類型來展開具體分析。
當咱們要用jmp指令去覆蓋重寫原函數的入口指令時,咱們會先將全部線程掛起,等待咱們的覆蓋重寫操做完成以後,再恢復線程,可是~若是實在命很差的話——可能會有某一個線程排隊等待恢復的EIP/RIP正好是咱們hook的5個字節的第3個字節。這個時候咱們就無法讓本次的函數調用被hook了,只能戰戰兢兢如履薄冰地指望線程恢復後可以繼續執行後續的指令避免GG崩潰掉。在上一篇中咱們是以掛鉤MessagBox函數爲例,這裏就以它爲例:
反彙編下的MessageBox機器指令:
如今假定某個被掛起的線程在被掛起以前已經取指執行了「8B FF」這條指令,它的EIP保存的是「55」這條指令的地址0x77068b82,那麼當咱們恢復這個線程的時候,就應當讓它繼續正確地執行後續的指令,由於咱們沒法指望這個線程可以執行「jmp + offset」五字節指令了——爲時晚矣,不如放它一馬~
這時候就應該回想起HOOK_ENTRY這個結構體了:
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字節
回想上一篇中介紹的這三個成員變量:Index,OldIPs[8],NewIPs[8],它們記錄的是,在Trampoline的構建過程中,Index記錄的是指令數目,OldIPs[8]數組記錄的是目標函數的在while循環過程當中記錄下的當前指令長度,,NewIPs[8]數組記錄的是Trampoline在每一次while循環過程當中構建的指令長度,while循環的源代碼以下:
do { HDE hde; UINT CopyDataLength; LPVOID CopyData; //對於出現的相對偏移地址,在跳板中都要給出新的相對地址 /* 32位 MessageBox 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) 64位 MessageBox 00007FF97B4485A0 48 83 EC 38 sub rsp,38h 00007FF97B4485A4 45 33 DB xor r11d,r11d 00007FF97B4485A7 44 39 1D 7A 33 03 00 cmp dword ptr [gfEMIEnable (07FF97B47B928h)],r11d 00007FF97B4485AE 74 2E je MessageBoxW+3Eh (07FF97B4485DEh) 00007FF97B4485B0 65 48 8B 04 25 30 00 00 00 mov rax,qword ptr gs:[30h] 00007FF97B4485B9 4C 8B 50 48 mov r10,qword ptr [rax+48h] 00007FF97B4485BD 33 C0 xor eax,eax 00007FF97B4485BF F0 4C 0F B1 15 98 44 03 00 lock cmpxchg qword ptr [gdwEMIThreadID (07FF97B47CA60h)],r10 00007FF97B4485C8 4C 8B 15 99 44 03 00 mov r10,qword ptr [gpReturnAddr (07FF97B47CA68h)] 00007FF97B4485CF 41 8D 43 01 lea eax,[r11+1] 00007FF97B4485D3 4C 0F 44 D0 cmove r10,rax 00007FF97B4485D7 4C 89 15 8A 44 03 00 mov qword ptr [gpReturnAddr (07FF97B47CA68h)],r10 00007FF97B4485DE 83 4C 24 28 FF or dword ptr [rsp+28h],0FFFFFFFFh 00007FF97B4485E3 66 44 89 5C 24 20 mov word ptr [rsp+20h],r11w 00007FF97B4485E9 E8 A2 FE FF FF call MessageBoxTimeoutW (07FF97B448490h) 00007FF97B4485EE 48 83 C4 38 add rsp,38h */ ULONG_PTR OldInstance = (ULONG_PTR)Trampoline->TargetFunctionAddress + OldPos; ULONG_PTR NewInstance = (ULONG_PTR)Trampoline->MemorySlot + NewPos; //指令長度 CopyDataLength = HDE_DISASM((LPVOID)OldInstance, &hde); if (hde.flags & F_ERROR) return FALSE; CopyData = (LPVOID)OldInstance; if (OldPos >= sizeof(JMP_REL)) { // The trampoline function is long enough. #if defined(_M_X64) || defined(__x86_64__) //OldInstance = 00007FF97B4485A7; jmp.Address = OldInstance; #else //OldInstance = 74CA8B85 //目標 = 源 + Offset + 5 //Offset = 目標 - (源 + 5) jmp.Operand = (UINT32)(OldInstance - (NewInstance + sizeof(jmp))); //計算跳轉到目標的偏移 #endif CopyData = &jmp; CopyDataLength = sizeof(jmp); IsLoop = TRUE; } #if defined(_M_X64) || defined(__x86_64__) else if ((hde.modrm & 0xC7) == 0x05) { // Instructions using RIP relative addressing. (ModR/M = 00???101B) // Modify the RIP relative address. /* PUINT32 pRelAddr; // Avoid using memcpy to reduce the footprint. #ifndef _MSC_VER memcpy(instBuf, (LPBYTE)pOldInst, copySize); #else __movsb(instBuf, (LPBYTE)pOldInst, copySize); #endif pCopySrc = instBuf; // Relative address is stored at (instruction length - immediate value length - 4). pRelAddr = (PUINT32)(instBuf + hs.len - ((hs.flags & 0x3C) >> 2) - 4); *pRelAddr = (UINT32)((pOldInst + hs.len + (INT32)hs.disp.disp32) - (pNewInst + hs.len)); // Complete the function if JMP (FF /4). if (hs.opcode == 0xFF && hs.modrm_reg == 4) finished = TRUE;*/ } #endif else if (hde.opcode == 0xE8) { // Direct relative CALL ULONG_PTR Destination = OldInstance + hde.len + (INT32)hde.imm.imm32; #if defined(_M_X64) || defined(__x86_64__) call.Address = Destination; #else //計算源地址和Trampoline之間的偏移值 call.Operand = (UINT32)(Destination - (NewInstance + sizeof(call))); #endif //CopyData 被拷貝到Trampoline中保存的內容 CopyData = &call; CopyDataLength = sizeof(call); } else if ((hde.opcode & 0xFD) == 0xE9) //F 1111 D 1101 { //E 1110 9 1001 //E 1110 B 1011 // Direct relative JMP (EB or E9) ULONG_PTR Destination = OldInstance + hde.len; // /* 0xDE EB 00 0xE0 xor eax,eax */ if (hde.opcode == 0xEB) // isShort jmp Destination += (INT8)hde.imm.imm8; else Destination += (INT32)hde.imm.imm32; // Simply copy an internal jump. if ((ULONG_PTR)Trampoline->TargetFunctionAddress <= Destination && Destination < ((ULONG_PTR)Trampoline->TargetFunctionAddress + sizeof(JMP_REL))) { //比較越界 /* Asm_5 PROC jmp Label1 Lable2: xor eax,eax Loop Lable2 mov eax,-5 ret Label1: mov ecx,2 jmp Lable2 Asm_5 ENDP */ if (JmpDest < Destination) JmpDest = Destination; } else { #if defined(_M_X64) || defined(__x86_64__) jmp.Address = Destination; #else // jmp.Operand = (UINT32)(Destination - (NewInstance + sizeof(jmp))); #endif CopyData = &jmp; CopyDataLength = sizeof(jmp); // Exit the function If it is not in the branch IsLoop = (OldInstance >= JmpDest); } } else if ((hde.opcode & 0xF0) == 0x70 || (hde.opcode & 0xFC) == 0xE0 || (hde.opcode2 & 0xF0) == 0x80) { /* & 0xF0 0x70 jo 後有一個字節的偏移 0x71 jno 後有一個字節的偏移 0x72 jb 後有一個字節的偏移 .. .. 0x7F jg 後有一個字節的偏移 & 0xFC 0xE0 loopne 後有一個字節的偏移 0xE1 0xE2 0xE3 */ // Direct relative Jcc ULONG_PTR Destination = OldInstance + hde.len; if ((hde.opcode & 0xF0) == 0x70 // Jcc || (hde.opcode & 0xFC) == 0xE0) // LOOPNZ/LOOPZ/LOOP/JECXZ Destination += (INT8)hde.imm.imm8; else Destination += (INT32)hde.imm.imm32; // Simply copy an internal jump. if ((ULONG_PTR)Trampoline->TargetFunctionAddress <= Destination && Destination < ((ULONG_PTR)Trampoline->TargetFunctionAddress + sizeof(JMP_REL))) { if (JmpDest < Destination) JmpDest = Destination; } else if ((hde.opcode & 0xFC) == 0xE0) { // LOOPNZ/LOOPZ/LOOP/JCXZ/JECXZ to the outside are not supported. return FALSE; } else { UINT8 v1 = ((hde.opcode != 0x0F ? hde.opcode : hde.opcode2) & 0x0F); #if defined(_M_X64) || defined(__x86_64__) // Invert the condition in x64 mode to simplify the conditional jump logic. jcc.Opcode = 0x71 ^ v1; jcc.Address = Destination; #else jcc.Opcode1 = 0x80 | v1; jcc.Operand = (UINT32)(Destination - (NewInstance + sizeof(jcc))); #endif CopyData = &jcc; CopyDataLength = sizeof(jcc); } } else if ((hde.opcode & 0xFE) == 0xC2) { // RET (C2 or C3) // Complete the function if not in a branch. IsLoop = (OldInstance >= JmpDest); } // Can't alter the instruction length in a branch. if (OldInstance < JmpDest && CopyDataLength != hde.len) return FALSE; // Trampoline function is too large. if ((NewPos + CopyDataLength) > TRAMPOLINE_MAX_SIZE) return FALSE; // Trampoline function has too many instructions. if (Trampoline->Index >= ARRAYSIZE(Trampoline->OldIPs)) return FALSE; Trampoline->OldIPs[Trampoline->Index] = OldPos; Trampoline->NewIPs[Trampoline->Index] = NewPos; Trampoline->Index++; // Avoid using memcpy to reduce the footprint. #ifndef _MSC_VER memcpy((LPBYTE)Trampoline->MemorySlot + NewPos, CopyData, CopyDataLength); #else __movsb((LPBYTE)Trampoline->MemorySlot + NewPos, (const unsigned char*)CopyData, CopyDataLength); #endif NewPos += CopyDataLength; OldPos += hde.len; } while (!IsLoop);
在Trampoline構建完成以後,它的內容是這樣的(部分紅員未寫出,由於此處不須要):
在MessageBox函數將會被覆蓋的五字節中,是3條指令:
因此累計起來的指令長度是2,3,5(指令長度的計算是經過反彙編引擎HDE實現的)。
當前假定的狀況是咱們準備實際的hook重寫目標函數覆蓋前5字節以前,咱們掛起了線程,卻發現被掛起線程已經執行了「88 FF」這條指令,當前的EIP/RIP指向了下一條指令「55」所在的地址,因此等到這個被掛起線程被咱們恢復了以後,咱們應當確保它能正確的執行下去,這時候來看一看MemorySlot這個結構所保存的內容:
MemorySlot當中此時已經保存好了備份的5字節,以及一個跳轉到原函數開始地址後5字節的地址的跳轉指令,因此當咱們恢復線程的時候,只須要將EIP/RIP修正爲指向MemorySlot對應的第二條指令「55」,線程就可以順利的執行了。源代碼以下:
DWORD_PTR SeFindNewIP(PHOOK_ENTRY HookEntry, DWORD_PTR Ip) { UINT i; for (i = 0; i < HookEntry->Index; ++i) { if (Ip == ((DWORD_PTR)HookEntry->TargetFunctionAddress + HookEntry->OldIPs[i])) return (DWORD_PTR)HookEntry->MemorySlot + HookEntry->NewIPs[i]; } return 0; }
0x04 x64下各種型目標函數的hook
接下來進入x64下的hook的內容了。
1.對於普通的目標函數(函數的初始指令不涉及jmp,call等指令),MinHook採用的是構建ff 25的一個64位8字節絕對地址跳轉來實現目標函數重寫,這個函數叫作Relay(中繼函數),它是到繞道函數的64位跳轉,被放置在目標函數的附近。
JMP_ABS jmp = { //64位絕對地址jmp 13字節 0xFF, 0x25, 0x00000000, // FF25 00000000: JMP [RIP+6] 0x0000000000000000ULL // Absolute destination address
先來看看x64模式下的Trampoline結構體:
#pragma pack(1) typedef struct _TRAMPOLINE { LPVOID TargetFunctionAddress; LPVOID FakeFunctionAddress; LPVOID MemorySlot; // MemorySlot 32字節 #if defined(_M_X64) || defined(__x86_64__) LPVOID Relay; // [Out] Address of the relay function. 原函數 到 Fake函數的中轉站 #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;
能夠看到其中多出的一個成員:Relay,它存放的是對繞道函數Detour的絕對地址跳轉:
#if defined(_M_X64) || defined(__x86_64__) // Create a relay function. jmp.Address = (ULONG_PTR)Trampoline->FakeFunctionAddress; Trampoline->Relay = (LPBYTE)Trampoline->MemorySlot + NewPos; memcpy(Trampoline->Relay, &jmp, sizeof(jmp)); #endif
話很少說,調試見真章:
首先仍是測試MessageBox這個API:
// WindowsAPI 測試 if (MHCreateHook(&MessageBoxW, &DetourMessageBox, reinterpret_cast<LPVOID*>(&__OriginalMessageBoxW)) != STATUS_SUCCESS) { return; } MessageBoxW(NULL, L"MessageBoxW()", L"MessageBoxW()", 0); //單個函數的Hook if (EnableHook(&MessageBoxW) != STATUS_SUCCESS) { printf("EnableHook() Error\r\n"); return; } 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; }
主要仍是關注Trampoline的構建過程:
局部變量窗口找到MemorySlot的地址,準備開始經過反彙編引擎計HDE算指令長度,構建MemorySlot中的內容,這裏經過對比目標函數MessageBox的反彙編指令來觀察MemorySlot的構建過程:
while循環(while循環的代碼上面已貼出)中第一條指令的備份保存:
第二條指令備份保存:
此時的指令備份長度達到了7字節,知足了備份的長度條件(>=5字節):
if (OldPos >= sizeof(JMP_REL)) { // The trampoline function is long enough.
開始寫跳轉指令了:
咱們對比跳轉到的地址0x00007ffa851285a7與Trampoline中已經保存好的目標函數MessageBox的絕對地址0x00007ffa851285a0
0x00007ffa851285a7-0x00007ffa851285a0 = 7,這七字節的長度正好就是已經備份到MemorySlot中的7字節,所以要成功調用已經被Hook過的目標函數,只須要執行MemorySlot中的指令便可,也就是說到了這裏咱們的MemorySlot成員已經構建成功了。
接下來是第二個關鍵成員的構造:Relay Function!
在MemorySlot構建完成的while循環推出後,就開始構造Relay Function了:
#if defined(_M_X64) || defined(__x86_64__) // Create a relay function. jmp.Address = (ULONG_PTR)Trampoline->FakeFunctionAddress; Trampoline->Relay = (LPBYTE)Trampoline->MemorySlot + NewPos; memcpy(Trampoline->Relay, &jmp, sizeof(jmp)); #endif
從代碼中能夠看出Realy的地址是緊跟在MemorySlot結構以後的,因此咱們不妨直接用當前的內存窗口觀察Realy所指向的地址內容:
能夠看到Relay指向的地址內容中保存的是對咱們的Detour(即FakeFunction)繞道函數的絕對地址(0x00007ff7cff51389)的跳轉:
那麼到目前爲止,Trampoline結構中最關鍵的兩個成員內容就是這樣的:
隨後將在MHCreateHook函數中,將Trampoline結構中的各個成員對應賦值給HookEntry結構中的各個成員,並備份好五字節的指令在Backup成員中。與x86模式下不一樣的一點,也是最重要的一點就是,x64模式下的FakeFunction(Detour Funciton)再也不是直接由Trampoline中的FakeFunction成員直接賦值,而是由Realy成員複製給HookEntry中的FakeFunction成員,這是x64與x86的最大區別之處,也是x64hook的點睛之筆!這裏值得一提的是,當初經過VirtualAlloc()API爲MemorySlot申請地址的時候,就是儘可能在目標函數附近申請的,而Realy的地址又緊跟在MemorySlot以後,因此才能保證了Realy的地址與目標函數地址相近,從而進一步保證了經過E9相對地址跳轉指令覆蓋重寫目標函數時四字節的相對偏移可以成功達到目的。
#if defined(_M_X64) || defined(__x86_64__) HookEntry->FakeFunctionAddress = Trampoline.Relay; #else HookEntry->FakeFunctionAddress = Trampoline.FakeFunctionAddress;
最後調用EnableHook函數真正覆蓋重寫目標函數的時候,咱們只須要用本身的Detour繞道函數地址減去原目標函數的地址,再減去5字節的指令長度,獲得相對偏移地址,經過E9相對地址跳轉指令使得目標函數的調用繞道到咱們的Detour函數。
2.目標函數初始指令爲E9 EB類型的跳轉
首先分析E9近跳轉指令。
這裏只要經過Hook一個自定義的函數便可看到目標函數初始指令爲E9近跳轉指令的狀況:
//E9指令 if (MHCreateHook(&Sub_2, &DetourSub_2, reinterpret_cast<LPVOID*>(&__OriginalSub_2)) != STATUS_SUCCESS) { return; } Sub_2(); void Sub_2() { printf("Sub_2\n\r"); } void DetourSub_2() { printf("DetourSub_2\n\r"); __OriginalSub_2(); }
首先經過Trampoline結構中的目標函數Sub_2()的首地址來看它對應的反彙編指令:
不出所料第一條指令是E9跳轉到真正的Sub_2()入口地址處。
這種狀況下Trampoline中有對應的狀況判斷與處理:
else if ((hde.opcode & 0xFD) == 0xE9) { //F 1111 D 1101 //E 1110 9 1001 //E 1110 B 1011 // Direct relative JMP (EB or E9) ULONG_PTR Destination = OldInstance + hde.len; // /* 0xDE EB 00 0xE0 xor eax,eax */ if (hde.opcode == 0xEB) // isShort jmp Destination += (INT8)hde.imm.imm8; else Destination += (INT32)hde.imm.imm32; // Simply copy an internal jump. if ((ULONG_PTR)Trampoline->TargetFunctionAddress <= Destination && Destination < ((ULONG_PTR)Trampoline->TargetFunctionAddress + sizeof(JMP_REL))) { //比較越界 if (JmpDest < Destination) JmpDest = Destination; } else { #if defined(_M_X64) || defined(__x86_64__) jmp.Address = Destination; #else // jmp.Operand = (UINT32)(Destination - (NewInstance + sizeof(jmp))); #endif CopyData = &jmp; CopyDataLength = sizeof(jmp); // Exit the function If it is not in the branch IsLoop = (OldInstance >= JmpDest); } }
當第一天指令是E9或者EB時將會進入else if的判斷以內,而後經過反彙編引擎HDE,將E9指令所在地址,即目標函數的地址進行修正,首先會加上E9指令的長度,而後經過反彙編引擎爲目標地址加上到真正Sub_2()函數所須要的便宜,這個時候的Destination就成爲真正的Sub_2()函數入口地址了,這時候再將這個地址做爲ff 25跳轉的絕對地址,寫入到MemorySlot當中:
__movsb((LPBYTE)Trampoline->MemorySlot + NewPos, (const unsigned char*)CopyData, CopyDataLength);
下一步就是構建Relay成員指向地址的內容:
#if defined(_M_X64) || defined(__x86_64__) // Create a relay function. jmp.Address = (ULONG_PTR)Trampoline->FakeFunctionAddress; Trampoline->Relay = (LPBYTE)Trampoline->MemorySlot + NewPos; memcpy(Trampoline->Relay, &jmp, sizeof(jmp)); #endif
此時MemorySlot和Relay就構建好了,當前,它們的內容是這樣的:
接下來的步驟就與以前敘述的相同了,這裏就再也不贅述,並且EB類型的狀況與E9狀況相同,也再也不贅述了。進入下一種call指令類型的hook。
3.目標函數初始指令爲call類型的跳轉
爲了可以使目標函數第一條指令是call,方便Minhook的測試,在這裏將經過彙編指令來構造第一條指令是call的自定義函數.
先來看這段彙編代碼:
Asm_1 PROC mov qword ptr[rsp+8h],rcx push rbp push rdi sub rsp,28h xor rbx,rbx mov rax,qword ptr[rsp+28h+8h+8h+8h] mov ebx,dword ptr[rax+1] add rax,rbx add rax,5 add rsp,28h pop rdi pop rbp ret Asm_1 ENDP
這段彙編代碼的做用就在於抹去調用函數時jmp + 4字節偏移的指令,返回被調用函數真正地址。
當咱們在main函數中調用自定義的函數時,當代碼執行到被調用函數處,第一條指令將是一條jmp指令,跳轉到真正的被調用函數入口地址,這一點在上述的自定義函數Sub_2()中也有所體現。
這段彙編指令很是簡單,它首先將Asm_1傳進來的第一個參數放到rsp+8字節的位置(這裏涉及到x64下寄存器的傳參問題,x64下函數調用的參數傳遞中,前四個參數分別用這四個寄存器傳遞:rcx,rdx,r8,r9),實際上這裏的參數也就是被調用函數的地址,進一步說就是第一條指令jmp的地址。爲何是將這個地址放到rsp+8字節的位置而不是直接放在棧頂處呢?這是由於Asm_1做爲函數被調用時,棧頂是必需要保存函數調用結束後下一條指令的地址的,因此只能退到距離棧頂8字節處放置第一條指令jmp的地址。
隨後將rbp,rdi壓棧,再經過棧頂指針rsp的偏移定位到傳進去的參數,第一條指令jmp的地址,將它賦值保存到rax中,再將rax保存的地址越過一個字節,也就能夠獲得距離真正Sub_2函數的的偏移值,將這4字節的偏移值放到ebx保存,最後用當前地址加上偏移地址,再加上5字節的指令長度,就獲得了真正的Sub_2()函數入口地址了,放在rax中做爲Asm_1()的返回值返出去,就此還不算大功告成,只是萬事俱備只欠東風了——咱們還須要構建一條call指令出來,依然用匯編硬寫出call指令:
Asm_4 PROC call Label0 jmp Exit; Label0: mov rcx,0; call Label1; //Call db 'H' db 0 db 'e' db 0 db 'l' db 0 db 'l' db 0 db 'o' db 0 db 'S' db 0 db 'u' db 0 db 'b' db 0 db '_' db 0 db '4' db 0 db 0 db 0 Label1: pop rdx call Label2; db 'H' db 0 db 'e' db 0 db 'l' db 0 db 'l' db 0 db 'o' db 0 db 'S' db 0 db 'u' db 0 db 'b' db 0 db '_' db 0 db '4' db 0 db 0 db 0 Label2: pop r8 mov r9,0 call MessageBoxW ret Exit: ret Asm_4 ENDP
而後將Asm_4做爲Asm_1的參數傳進去,其返回值的地址內就將是咱們一直搓手想要的call指令了:
//Call指令 PVOID v4 = Asm_1(Asm_4); Asm_4(); if (SeCreateHook(v4, &FakeSub_4, reinterpret_cast<LPVOID*>(&__OriginalSub_4)) != STATUS_SUCCESS) { return; }
(待補充)