[原創]MinHook測試與分析(x64下 E9,EB,CALL指令測試,且逆推測試微軟熱補丁)
依稀記得第一次接觸Hook的概念是在周偉民先生的書中-><<多任務下的數據結構與算法>>,當時覺得Hook很奇妙,有機會要學習到,正好近段日子找來了MiniHook,就一起分享一下。
本篇文章是在x64下測試與分析jmp+offset類型的Hook,並且逆推測出熱補丁的簡單用法,MinHook它的中心就是覆蓋重寫並且可以復原。知道大概的思路後後讓我們先來具體的實現MinHook再去做測試。
首先是堆的申請(申請PAGE_SIZE大小自動生長的堆),以下是實現與卸載
1 NTSTATUS WINAPI Initialize(VOID) 2 { 3 NTSTATUS Status = STATUS_SUCCESS; 4 5 EnterSpinLock(); 6 7 if (__HeapHandle == NULL) 8 { 9 __HeapHandle = HeapCreate(0,//申請堆棧 10 0, //提交 PAGE_SIZE 11 0); //If dwMaximumSize is 0, the heap can grow in size.自動增長 12 if (__HeapHandle != NULL) 13 { 14 //沒有實現 15 } 16 else 17 { 18 Status = STATUS_MEMORY_NOT_ALLOCATED; 19 } 20 } 21 else 22 { 23 Status = STATUS_ADDRESS_ALREADY_EXISTS; 24 } 25 26 LeaveSpinLock(); 27 28 return Status; 29 } 30 31 NTSTATUS WINAPI Uninitialize(VOID) 32 { 33 NTSTATUS Status = STATUS_SUCCESS; 34 35 return Status; 36 }
第一幕CreateHook
CreateHook 第一步:判斷內存是否申請好了,是否可執行,判斷是否已經Hook過了,如果已經Hook過,當讓他返回其所在位置,因為此時他的地址位置已經可以用來啟動Hoook,如下代碼詳解
1 UINT FindHookEntry(LPVOID FunctionAddress) 2 { 3 UINT i; 4 for (i = 0; i < __Hooks.Length; ++i) 5 { 6 if ((ULONG_PTR)FunctionAddress == (ULONG_PTR)__Hooks.Items[i].TargetFunctionAddress) 7 return i; 8 } 9 return STATUS_NOT_FOUND; 10 }
CreateHook 第二步:進行Hook,在這裏用到TRAMPOLINE結構體,我稱之為跳板結構體,作為數據的中間傳輸過渡,TRAMPOLINE中幾個註意的成員是1.Relay:在x64下Fake函數到原函數的中轉站(x86用不到),2.OldIPs:原函數地址的偏移字節的保存3.NewIPs: 已經寫入FakeFunctionAddress函數的字節數 4.MemorySlot:32字節原函數地址的前7個字節和跳轉指令後的字節 5.PachAbove:熱補丁
1 typedef struct _TRAMPOLINE 2 { 3 LPVOID TargetFunctionAddress; // [In] Address of the target function. 4 LPVOID FakeFunctionAddress; // [In] Address of the detour function. 5 LPVOID MemorySlot; // MemorySlot 32字節原函數地址的前五個字節和跳轉指令後的字節 6 7 #if defined(_M_X64) || defined(__x86_64__) 8 LPVOID Relay; // [Out] Address of the relay function. 9 #endif 10 BOOL PatchAbove; // [Out] Should use the hot patch area? //Patch --->熱補丁哦 //0xA 0xB 11 UINT IP; // [Out] Number of the instruction boundaries. 12 UINT8 OldIPs[8]; // [Out] Instruction boundaries of the target function. 13 UINT8 NewIPs[8]; // [Out] Instruction boundaries of the trampoline function. 14 } TRAMPOLINE, *PTRAMPOLINE;
CreateHook 第三步: 分配一塊內存用來保存Trampoline裏的MemorySlot數據 ,以下是MemorySlot結構體定義(MemorySlot內存構建放到最後的代碼鏈接中):
1 #define MEMORY_BLOCK_SIZE 0x1000 2 #if defined(_M_X64) || defined(__x86_64__) 3 #define MEMORY_SLOT_SIZE 64 4 #else 5 #define MEMORY_SLOT_SIZE 32 6 #endif 7 8 // Max range for seeking a memory block. (= 1024MB) 9 #define MAX_MEMORY_RANGE 0x40000000 10 11 typedef struct _MEMORY_SLOT 12 { 13 union 14 { 15 struct _MEMORY_SLOT *Flink;//下一指針 16 UINT8 BufferData[MEMORY_SLOT_SIZE]; 17 }; 18 } MEMORY_SLOT, *PMEMORY_SLOT; //32字節 19 20 typedef struct _MEMORY_BLOCK 21 { 22 _MEMORY_BLOCK* Flink; 23 PMEMORY_SLOT FreeMeorySlotHead; // First element of the free slot list.空閑插槽列表的第一個元素。 24 UINT UsedCount; 25 } MEMORY_BLOCK, *PMEMORY_BLOCK; //12字節
CreateHook 第四步:CreateTrampoline
Hook的Target我們這裏先使用MessageBoxW,作為一個詳細的jmp跳轉流程解釋,然後我寫了幾個匯編程序去進行其他E8,Call等指令的跳轉實現,不過它是怎麽跳轉的我會在下面跳轉的時候貼出來,首先來玩X64下的MessageBoxW,
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
前面講過我們是通過跳轉加指令形式跳轉到我們需要到的地址處,上面代碼註釋中我們了解到OldPos與NewPos是在MemorySlot創建過程對原函數地址的偏移字節的保存和已經寫入FakeFunctionAddress函數的字節數,如下
1 ULONG_PTR OldInstance = (ULONG_PTR)Trampoline->TargetFunctionAddress + OldPos; 2 ULONG_PTR NewInstance = (ULONG_PTR)Trampoline->MemorySlot + NewPos; 3 //數據 4 //OldPos是指的指令的偏移字節 即5個字節中的第2345位.OldInstance地址 5 //指令長度
了解到一些後,我們就應該去真正的對MemorySlot去構建,他的構建用了一個超級大的do-While()循壞(因為實踐了好幾種跳轉指令,心累),x86下的MessageBoxW跳轉在5字節處,所以為了之後的恢復,我們需要把7字節的內容做一個保存,這就是所謂的OriginalDataBackup數組的作用->用來恢復也就是解除Hook,後面會逐步解析他的作用和位置,我們這裏先記住即可
MemorySlot開始申請32字節的長度,,我們利用反匯編引擎HDE計算出MessageBoxW函數基地址,從上面給出的MessageBoxW的地址內容中,我們可以看到到達5字節的加法是先加4個字節到下一地址,然後加3到跳轉位置,記錄在OldPos,NewPos中
CopyCodeLength = HDE_DISASM((LPVOID)OldInstance, &hde); if (hde.flags & F_ERROR) { return FALSE; } CopyCodeData = (LPVOID)OldInstance; ..... Trampoline->OldIPs[Trampoline->IP] = OldPos; Trampoline->NewIPs[Trampoline->IP] = NewPos; Trampoline->IP++;
到達7字節了,我們就可以去做跳回MessageBoxW基地址加5字節偏移跳轉指令了
1if (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; }
1 //這裏是熱補丁的判斷 是否有足夠的位置長跳轉 2 if (OldPos < sizeof(JMP_REL) 3 && !IsCodePadding((LPBYTE)Trampoline->TargetFunctionAddress + OldPos, sizeof(JMP_REL) - OldPos)) 4 { 5 6 // Is there enough place for a short jump? 7 //沒有有足夠的位置長跳轉,那是否有足夠的位置短跳轉? 8 if (OldPos < sizeof(JMP_REL_SHORT) 9 && !IsCodePadding((LPBYTE)Trampoline->TargetFunctionAddress + OldPos, sizeof(JMP_REL_SHORT) - OldPos)) 10 { 11 return FALSE; 12 } 13 //只能寫短跳轉,使用熱補丁 14 // Can we place the long jump above the function? 15 //熱補丁:目標地址之前地址是否可執行? 16 if (!SeIsExecutableAddress((LPBYTE)Trampoline->TargetFunctionAddress - sizeof(JMP_REL))) 17 return FALSE; 18 //目標地址之前是否是可被覆蓋的空白 19 if (!IsCodePadding((LPBYTE)Trampoline->TargetFunctionAddress - sizeof(JMP_REL), sizeof(JMP_REL))) 20 return FALSE; 21 //標誌可以熱補丁 22 Trampoline->PatchAbove = TRUE;
做了這麽多工作,無非是為了MemorySlot裏有數據前7個字節和跳轉回MessageBoxW基地址+5字節的的偏移,構造好後,我們的TRAPOLINE結構也就完成
CreateHook第五步:添加Hook信息了(TRAMPLIONE結構體過渡),我們需要再去創建一個HookEntry的結構體去完成接收信息
1 // Hook information. 2 typedef struct _HOOK_ENTRY 3 { 4 LPVOID TargetFunctionAddress; //目標地址 5 LPVOID FakeFunctionAddress; //Fake地址即覆蓋地址 6 LPVOID TrampolineMemorySlot; // Address of the trampoline function. 7 UINT8 OriginalDataBackup[8]; // Original prologue of the target function.目標功能的原始序幕- //恢復Hook使用的存放原先數據 8 9 UINT8 PatchAbove : 1; // Uses the hot patch area. 備份原函數的5字節,重要!!! 10 UINT8 IsEnabled : 1; // Enabled.啟用或者關閉 11 UINT8 queueEnable : 1; // Queued for enabling/disabling when != isEnabled. 12 13 UINT IP : 4; // Count of the instruction boundaries.索引 想到匯編的IP就很明白了 14 UINT8 OldIPs[8]; // Instruction boundaries of the target function.原地址的字節變化就靠它了 15 UINT8 NewIPs[8]; // Instruction boundaries of the trampoline function 用在後續解釋的MemorySlot中 16 } HOOK_ENTRY, *PHOOK_ENTRY; //44字節 17 18 19 typedef struct _HOOK_INFORMATION_ 20 { 21 PHOOK_ENTRY Items; // Data heap 22 UINT MaximumLength; // Size of allocated data heap, items 23 UINT Length; // Actual number of data items 24 }HOOK_INFORMATION,*PHOOK_INFORMATION;
當有了這個結構體後就可以去CreateHook了,下面是構建過程:
1 if (CreateTrampoline(&Tl)) 2 { 3 PHOOK_ENTRY HookEntry = AddHookEntry(); //填充一個HookInfo信息 4 if (HookEntry != NULL) 5 { 6 HookEntry->TargetFunctionAddress = Tl.TargetFunctionAddress; 7 #if defined(_M_X64) || defined(__x86_64__) 8 HookEntry->FakeFunctionAddress = Tl.pRelay;//跳轉在trampoline 9 #else 10 HookEntry->FakeFunctionAddress = Tl.FakeFunctionAddress; 11 #endif 12 HookEntry->TrampolineMemorySlot = Tl.MemorySlot; 13 HookEntry->PatchAbove = Tl.PatchAbove 14 HookEntry->IsEnabled = FALSE; 15 //HookEntry->QueueEnable = FALSE; 16 HookEntry->IP = Tl.IP; 17 18 memcpy(HookEntry->OldIPs, Tl.OldIPs, ARRAYSIZE(Tl.OldIPs)); 19 memcpy(HookEntry->NewIPs, Tl.NewIPs, ARRAYSIZE(Tl.NewIPs)); 20 21 // Back up the target function. 22 23 if (Tl.PatchAbove)//這就是熱補丁 24 { 25 memcpy( 26 HookEntry->OriginalDataBackup, 27 (LPBYTE)TargetFunctionAddress - sizeof(JMP_REL), 28 sizeof(JMP_REL) + sizeof(JMP_REL_SHORT)); 29 } 30 else 31 { //存儲源函數的數據內容 32 memcpy(HookEntry->OriginalDataBackup, TargetFunctionAddress, sizeof(JMP_REL)); 33 } 34 if (OriginalWhitelist != NULL)//白名單,用來恢復 35 { 36 *OriginalWhitelist = HookEntry->TrampolineMemorySlot; 37 }
到這裏為止終於是創建了Hook
第二幕 EnableHook
顧名思義就是啟動Hook,顯而易見得知它的作用無非就是覆蓋原函數我們記錄的那7字節,如下:
1 //SHELLCODE 2 PJMP_REL jmp = (PJMP_REL)PatchData; 3 jmp->Opcode = 0xE9;//跳轉 4 jmp->Operand = (UINT32)((LPBYTE)HookEntry->FakeFunctionAddress - (PatchData + sizeof(JMP_REL))); 5
當需要解除Hook時候我們就可以用到在前面說過的OriginalDataBackup去恢復原函數,或者直接調用MemorySlot中記錄下的原始序幕
1 else 2 { 3 memcpy(PatchData, HookEntry->OriginalDataBackup, sizeof(JMP_REL)); 4 }
第三幕 MessageBoxW測試
1 if (CreateHook(&MessageBoxW, &FakeMessageBox, 2 reinterpret_cast<LPVOID*>(&__OriginalMessageBoxW)) != STATUS_SUCCESS)//告知要hook成什麽樣子 3 { 4 return; 5 } 6 7 MessageBoxW(0, L"MessageBoxW", L"MessageBoxW", 0);//沒有Hook還是原先,不要也行 8 if (EnableHook(MessageBoxW) != STATUS_SUCCESS) 9 { 10 printf("EnableHook is wrong\r\n"); 11 return; 12 } 13 MessageBoxW(NULL, L"CreateHook()", L"CreateHook()", 0);//啟動Hook後,現在是FakeHOOK 14 15 printf("Input AnyKey To Exit\r\n"); 16 getchar(); 17 18 Uninitialize();//返回釋放 19 } 20 21 int WINAPI FakeMessageBox( 22 _In_opt_ HWND DialogHwnd, 23 _In_opt_ WCHAR* DialogText, 24 _In_opt_ WCHAR* DialogCaption, 25 _In_ UINT Type 26 ) 27 { 28 __OriginalMessageBoxW(DialogHwnd, L"FakeMessageBox", L"FakeMessageBox", Type); 29 return 0; 30 }
編譯運行後出結果啦,先是原先的MessageBoxW:
這是成功Hook後的:
一切順利,沒有白費功夫,下面是我對EB,call,熱補丁的匯編源碼,我們仿照MessageBoxW的形式在test.cpp中定義函數指針,與Fake函數的輸出形式。
在這裏花費了功夫探索出了熱補丁的簡單定義是申請5字節空的內存然後 mov edi,edi,能應用正確,匯編代碼如下
.DATA MessageBoxW dq 0 .CODE Asm_OnInitMember PROC mov qword ptr[rsp+8h],rcx push rbp push rdi sub rsp,28h mov rax,qword ptr[rsp+28h+8h+8h+8h] mov MessageBoxW,rax add rsp,28h pop rdi pop rbp ret Asm_OnInitMember ENDP Asm_1 PROC mov qword ptr[rsp+8h],rcx push rbp push rdi sub rsp,28h xor rbx,rbx ;00007FF77A8012BC E9 7A 0B 00 00 jmp Asm_4 (07FF77A801E3Bh) 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 Asm_3 PROC jmp Label1 Label1: jmp Label2 Label2: mov eax,-3 ret Asm_3 ENDP 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_10 PROC db 0CCh db 0CCh db 0CCh db 0CCh db 0CCh mov edi,edi ret Asm_10 ENDP END
1 //熱補丁測試 typedef void(*LPFN_SUB_10)(); void FakeSub_10(); //熱補丁 LPFN_SUB_10 __OriginalSub_10 = NULL; 2 PVOID v10 = Asm_1(Asm_10); 3 4 if (SeCreateHook((PVOID)((ULONG_PTR)v10 + 5), &FakeSub_10, 5 reinterpret_cast<LPVOID*>(&__OriginalSub_10)) != STATUS_SUCCESS) 6 { 7 return; 8 } 9 //對於熱補丁函數調用 10 ((LPFN_SUB_10)(((ULONG_PTR)v10 + 5)))(); 11 if (SeEnableHook(ALL_HOOKS) != STATUS_SUCCESS) 12 { 13 printf("SeEnableHook() Error\r\n"); 14 return; 15 } 16 ((LPFN_SUB_10)(((ULONG_PTR)v10 + 5)))();
E9的測試只需要自寫一個函數調用測試調用即可,如下面這樣就行了然後在仿照上面自行測試即可
1 1 //E9指令,這樣就行了 2 2 3 3 void Sub_2() 4 4 { 5 5 printf("Sub_2\n\r"); 6 6 }
下面是所有的正確輸出結果:
好了,x86下的MiniHook終於是測試完了,寫了一遍後又是更懂了,如果有什麽差錯,望大家糾正
[原創]MinHook測試與分析(x64下 E9,EB,CALL指令測試,且逆推測試微軟熱補丁)