easyhook源碼分析一


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. 調用者負責維護堆棧平衡。

 

 

 

引用

easyhook庫代碼簡要分析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM