easyhook簡要說明:
easyhook是一個開源的hook庫(http://easyhook.github.io/),其支持托管代碼(.NET)和非托管代碼(C/C++)hook,這里只分析了其非托管下的hook代碼,根據目前分析的情況來看,其有如下幾個特點:
1. 同時支持X86和X64。
2. 支持針對不同的線程進行hook,例如可以設置當線程ID為0x1234的線程執行時執行hook功能函數,而線程ID為0x4321執行時不執行hook功能函數。
3. 支持在hook功能函數中執行被hook的函數,例如hook了MessageBoxA函數,那么在hook功能函數中,可以繼續調用MessageBoxA函數,不會發生無限遞歸的情況。
4. 不支持任意點hook。
這里分析其代碼的主要目的是了解其如何對X64代碼進行hook,微軟的detours hook庫32位開源,但是64位則需要付費購買,通過分析easyhook庫中64位hook的實現可以幫助我們理解64位hook的原理。
Hook相關函數:
在easyhook中hook一個函數的完整流程需要使用以下幾個函數:
1. LhInstallHook,安裝鈎子函數。
2. LhSetInclusiveACL,設置包含ACL(access control list)。
3. LhSetExclusiveACL,設置排除ACL。
4. LhUninstallHook,卸載鈎子(並不還原,只是禁用)。
5. LhWaitForPendingRemovals,刪除鈎子(還原)。
所使用到的關鍵數據結構為LOCAL_HOOK_INFO結構體:
LhInstallHook:
該函數的原型為:
1. 調用LhAllocateHook函數,在該函數中主要做以下工作:
i. 調用LhAllocateMemory函數分配存放LOCAL_HOOK_INFO結構體和trampoline代碼的內存,在32位下,該內存大小為4K。在64位下,其會獲取當前系統下的內存頁大小,之后以hook點為起始,嘗試在其上下偏移0x7FFFFF00的范圍內以頁大小的間隔分配頁大小的內存空間。 0x7FFFFF00乘以2即0xFFFFFE00,將近4GB的內存范圍,剛好是jmp(E9)能夠跳轉的范圍。
ii. 將分配的內存頁的起始部分作為LOCAL_HOOK_INFO結構體的區域,並將部分值初始化。
iii. 獲取trampoline匯編代碼的地址,並拷貝到內存頁中LOCAL_HOOK_INFO結構體之后。在X64中無法使用內聯匯編,這里easyhook將32位下的Trampoline代碼和64位的Trampoline代碼放在了單獨的.asm文件當中,使用C與匯編混編的方式,將匯編代碼結合到程序中,這樣就不受X64下不支持內聯匯編的影響。
iv. 將hook點的頭幾個字節(保證能jmp的前提下完整指令的長度),拷貝到trampoline代碼之后。如果頭幾個字節是跳轉語句,這里對跳轉語句進行了重定位。
v. 計算從內存頁跳轉回hook點之后代碼的偏移,並拷貝。
vi. 如果是32位,那么還要替換trampoline匯編代碼中的一些硬編碼,在32位的trampoline匯編代碼中需要使用一些變量,這些變量在編譯時無法確定,使用類似0x12345678這種 硬編碼進行標識,這里對其進行了替換。X64下無須替換。(原因見下方PS)
在LhAllocateMemory函數執行完畢之后,內存頁的分布圖是這樣的:
2. 計算從hook點到trampoline代碼的偏移。
3. 根據計算得出的偏移生成跳轉代碼並拷貝到hook點。
4. 將LOCAL_HOOK_INFO結構體添加到全局GlobalHookListHead鏈當中,同時將LOCAL_HOOK_INFO結構體的指針賦給最后一個參數(OutHandle)輸出,設置線程ID以及刪除鈎子都需要該結構體。
PS:
1. 在整個過程中,easyhook在安裝鈎子的時候,未掛起其他線程,同時在拷貝跳轉代碼到hook點時,使用的也只是 *((ULONGLONG*)Hook->TargetProc) = AtomicCache 這樣的賦值語句,該語句在底層也並非是原子操作。
2. 上面提到,X64下並未對trampoline的代碼進行替換。在X64的匯編代碼開始處:
Intro: ;void* Entry; // fixed 0 (0) db 0 db 0 db 0 db 0 db 0 db 0 db 0 db 0 OldProc: ;BYTE* OldProc; // fixed 4 (8) db 0 db 0 db 0 db 0 db 0 db 0 db 0 db 0 NewProc: db ..... ;由於占篇幅,這里省略定義。 Outro: db.... IsExecutePtr: db.... ........ ;該處開始是實際的匯編代碼。
是類似這樣的定義,在拷貝trampoline代碼時,只拷貝了定義之下的代碼語句,這些定義並未拷貝過去,由於trampoline匯編代碼和LOCAL_HOOK_INFO結構體是挨着的,所以在匯編代碼中,其也就將LOCAL_HOOK_INFO中的成員值當作了這些定義的變量,看下LOCAL_HOOK_INFO的最后的幾個成員:
發現它們和trampoline匯編代碼中定義的變量的順序是相同的。所以只要賦值了LOCAL_HOOK_INFO結構體,那么在匯編代碼中就可以直接使用了。這點相當巧妙。
LhSetInclusiveACL與LhSetExclusiveACL
這兩個函數的函數原型為:
這兩個函數用來設置執行hook功能的線程ID。LhSetInclusiveACL函數執行成功后,其第一個參數中線程ID對應的線程執行到hook點時將會執行hook功能函數。LhSetExclusiveACL函數執行成功后,其第一個參數中線程ID對應的線程執行到hook點時將不會執行hook功能函數。
在LOCAL_HOOK_INFO結構體中的LocalACL成員結構體定義如下:
在LhSetInclusiveACL函數中,將包含線程ID的數組拷貝到LOCAL_HOOK_INFO結構體中的LocalACL結構體的Entries數組中,同時將IsExclusive設置為假,更新Count的值。
在LhSetExclusiveACL執行過程和LhSetInclusiveACL函數相同,唯一一點不同的是將IsExclusive設置為真。
LhUninstallHook
該函數原型為:
該函數將參數中指定的LOCAL_HOOK_INFO結構體指針從全局Hook鏈GlobalHookListHead中移除,並添加到全局移除Hook鏈GlobalRmovalListHead當中。
除此之外,還將LOCAL_HOOK_INFO結構體中的HookProc值置為NULL。
LhWaitForPendingRemovals
該函數原型為:
該函數會遍歷GlobalRemovalListHead鏈,根據LOCAL_HOOK_INFO結構體指針中的TargetBackup(64位為TargetBackup_x64)成員變量恢復hook點的原始指令,恢復之后釋放結構體資源。因此,要刪除hook點,必須先調用LhUninstallHook函數,再調用LhWaitForPendingRemovals函數。
32位HOOK執行流程
安裝完鈎子后,函數執行的流程拓撲如下:
關鍵點在於trampoline匯編代碼,trampoline的流程圖如下:
注意,在流程圖中的 HookIntro與HookOutro函數是在執行硬編碼替換時,將函數地址替換進去的。IsExecuted變量值是LOCAL_HOOK_INFO結構體中IsExecutedPtr指針指向的值,該值用來當調用LhWaitForPendingRemovals函數刪除hook點時判斷是否還有線程在trampoline中執行,如果有,則等待一段時間,再判斷,直到沒有線程執行trampoline時才刪除hook點和trampoline內存頁。
接下來以hook MessageBoxA函數為例說明上述流程如何執行。
一、調用LhInstallHook函數安裝鈎子后,執行MessageBoxA函數
1. 在MessageBoxA函數入口跳到trampoline代碼中,判斷HookProc函數處的值不為0,執行HookIntro函數。
2. 在該函數中,判斷當前線程信息是否在全局線程列表中(用來保存各個線程相關的hook信息,比如是否執行等),如果不存在就添加到全局線程列表中。之后判斷當前線程ID是否被設置到ACL中,顯然,由於這個時候未調用LhSetInclusiveACL或LhSetExclusiveACL函數,所以是未被設置到ACL中,那么函數返回假。
3. 執行OldProc函數,並跳轉回hook之后,正常執行MessageBoxA函數。
流程圖如下:
二、調用LhInstallHook,並調用LhSetInclusiveACL包含當前線程ID后,執行MessageBoxA函數
1. 在MessageBoxA函數入口跳到trampoline代碼中,判斷HookProc函數處的值不為0,執行HookIntro函數。
2. 在HookIntro函數中,判斷當前線程信息是否在全局線程列表中(用來保存各個線程相關的hook信息,比如是否執行等),如果不存在就添加到全局線程列表中。之后判斷線程ID是否被設置到ACL中,由於調用了LhSetInclusiveACL函數,所以被設置到ACL中,那么函數返回真。同時會更新該線程的hook信息,標識該線程已經是在trampoline中執行了的。
3. 接下來執行HookProc函數。
4. 在HookProc函數中又調用了MessageBoxA函數,因此便又會進入trampoline代碼執行HookIntro函數,在該函數中獲取該線程的hook信息,從而得知該線程已經是在trampoline中執行了的,所以返回假。
5. 由於返回假,所以執行OldProc,再跳回正常MessageBoxA函數執行。執行完畢后返回HookProc函數。
6. 執行HookOutro函數,在該函數中,重置線程的hook信息。同時更改返回地址為MessageBoxA函數的返回地址。
7. HookOutro函數返回到trampoline中,執行一些掃尾工作后,使用ret指令返回。
流程圖如下:
三、調用LhUninstallHook后,執行MessageBoxA函數
1. 調用LhUninstallHook函數后,會將LOCAL_HOOK_INFO結構體中的HookProc值置為0。
2. 調用MessageBoxA函數進入trampoline,首先判斷HookProc值是否為0,由於該值已經被LhUninstallHook函數置為0,所以跳到OldProc處繼續執行。
流程圖如下:
四、調用LhWaitForPendingRemovals后,執行MessageBoxA函數
1. 調用LhWaitForPendingRemovals后,hook點已經被恢復,MessageBoxA函數正常執行。
64位HOOK執行流程
整體來看,64位的Hook執行流程和32位的相同,只是個別細節不同,以下是不同點:
1. 在trampoline匯編代碼中,引用變量的方式不同。32位是通過硬編碼替換的,64位是通過和LOCAL_HOOK_INFO結構相近,引用LOCAL_HOOK_INFO結構體的變量。
2. 64位函數的調用約定不同,因此在trampoline代碼中,需要對64位函數的調用約定做一些額外的工作,64位的調用約定參考下方補充信息。
3. 分配頁內存的方式不同。
補充:
x64函數調用約定:
1. 一個函數在調用時,前四個參數(整數型)是從左至右依次存放於RCX、RDX、R8、R9寄存器里面;
2. 前四個浮點型和雙精度浮點則從左至右依次存放於XMM0、XMM1、XMM2、XMM3寄存器里面;
3. 剩下的參數從左至右順序入棧;
4. 調用者負責在棧上分配32字節的“shadow space”,用於存放那四個存放調用參數的寄存器的值(亦即前四個調用參數);
5. 調用者負責維護堆棧平衡。
引用