1. 程式人生 > >[原創]MinHook測試與分析(x64下 E9,EB,CALL指令測試,且逆推測試微軟熱補丁)

[原創]MinHook測試與分析(x64下 E9,EB,CALL指令測試,且逆推測試微軟熱補丁)

caption set enable wchar put .com 探索 cti wid

依稀記得第一次接觸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指令測試,且逆推測試微軟熱補丁)