x86平臺inline hook原理和實現

概念

inline hook是一種通過修改機器碼的方式來實現hook的技術。

原理

對於正常執行的程序,它的函數調用流程大概是這樣的:

0x1000地址的call指令執行後跳轉到0x3000地址處執行,執行完畢後再返回執行call指令的下一條指令。

我們在hook的時候,可能會讀取或者修改call指令執行之前所壓入棧的內容。那麼,我們可以將call指令替換成jmp指令,jmp到我們自己編寫的函數,在函數裏call原來的函數,函數結束後再jmp回到原先call指令的下一條指令。如圖:

通過修改機器碼實現的inline hook,不僅不會破壞原本的程序邏輯,而且還能執行我們的代碼,讀寫被hook的函數的數據。

inline hook流程

(1)尋找hook位置

在逆向的時候,會遇到不同類型的call,它們所佔的字節可能是不一樣的,本文構造一個長度爲5字節的jmp指令(jmp的機器碼佔用1字節,跳轉到的地址偏移佔用4字節)來替換原來的5字節的call指令。即我們需要尋找長度爲5字節的call,來進行inline hook。5字節的call形如:

(2)inline hook代碼實現

在x86彙編中,同樣有很多類型的jmp,本文構造inline hook使用的是近距離地址跳轉的jmp指令,它的機器碼爲E9,這種類型的jmp指令需要一個參數,參數是當前jmp指令地址距離目標函數地址的字節數。

假設需要hook的call的指令的內存地址爲:0x1000,我們想要它執行後跳轉到我們的函數(假設函數在內存中的地址:0x5000),那麼,構造jmp指令時,指令應爲:

jmp (0x5000-(0x1000 + 5))

即:

jmp 0x3FFB

對於5字節指令的hook,上面的計算公式是固定的,jmp指令本身佔用5字節,所以加上5

懂了這些知識,就可以動手編寫hook代碼了:

int StartHook(DWORD hookAddr, BYTE backCode[5], void(*FuncBeCall)()) {
    DWORD jmpAddr = (DWORD)FuncBeCall - (hookAddr + 5);
    BYTE jmpCode[5];
    *(jmpCode + 0) = 0xE9;
    *(DWORD *)(jmpCode + 1) = jmpAddr;
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, GetCurrentProcessId());
    if (ReadProcessMemory(hProcess, (LPVOID)hookAddr, backCode, 5 , NULL) == 0) {
        return -1;
    }

    if (WriteProcessMemory(hProcess, (LPVOID)hookAddr, jmpCode, 5, NULL) == 0) {
        return -1;
    }

    return 0;
}

上面的StartHook函數,hookAddr接收一個將被替換的call指令的內存地址;backCode接收一個長度爲5字節的數組緩衝區,用於備份原有的call指令;FuncBeCall參數接收一個返回值爲void函數地址。

StartHook函數的邏輯是:根據FuncBeCall的地址計算jmp的地址,並構造一條完整的jmp指令,存入數組。我們要hook當前的進程,所以調用OpenProcess打開當前進程。調用ReadProcessMemory讀取當前進程hookAddr處的指令,寫入backCode數組。調用WriteProcessMemory將構造好的jmp指令寫入當前進程hookAddr處。

StartHook函數第3個參數接收一個函數地址,這個函數地址指向的函數應該是這樣的:

_declspec(naked) void OnCall() {
    ......
}

OnCall函數用_declspec(naked)修飾,被它修飾的函數我們常稱它爲裸函數,裸函數的特點是在編譯生成的時候不會產生過多用於平衡堆棧的指令,這意味着在裸函數中我們要編寫內聯彙編控制堆棧平衡。一個比較簡單寫法是備份所有的寄存器,做完其他操作後再把寄存器的值還原回去,代碼示例如下:

DWORD tEax = 0,tEcx = 0,tEdx = 0,tEbx = 0,tEsp = 0,tEbp = 0,tEsi = 0,tEdi = 0;
_declspec(naked) void OnCall() {
    __asm {
        mov tEax, eax
        mov tEcx, ecx
        mov tEdx, edx
        mov tEbx, ebx
        mov tEsp, esp
        mov tEbp, ebp
        mov tEsi, esi
        mov tEdi, edi
    }
    //do something
    __asm {
        mov eax, tEax
        mov ecx, tEcx
        mov edx, tEdx
        mov ebx, tEbx
        mov esp, tEsp
        mov ebp, tEbp
        mov esi, tEsi
        mov edi, tEdi
        call ...
        jmp ...
    }
}

裸函數編寫規則可以參考msdn上的這篇文檔

當我們替換到進程的jmp代碼被執行,它就會跳轉到該裸函數。在裸函數裏,先備份所有的寄存器,然後編寫我們的hook代碼,編寫hook代碼時可以通過esp寄存器讀取或者修改原call的參數,或者通過修改eax寄存器以修改原call的返回值,再或者調用其他函數等等。執行完我們的hook代碼再把寄存器的值還原回去。這樣就不會導致程序邏輯出錯而崩潰。

卸載inline hook流程

卸載hook的流程比較簡單,也是打開當前進程,把hook時備份的call指令寫回原來的位置

int Unhook(DWORD hookAddr, BYTE backCode[5]) {
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, GetCurrentProcessId());
    if (WriteProcessMemory(hProcess, (LPVOID)hookAddr, backCode, 5, NULL) == 0) {
        return -1;
    }
    return 0;
}

參數hookAddr是原來hook的call的內存地址,參數backCode是原來備份下來的call指令。

總結

本文是針對5字節的call進行inline hook,在尋找call的時候可能會遇到許多不同的call,比如6字節的call,或者7字節的call。對於不同的call,只要掌握了inline hook原理,就可以根據實際情況編寫hook代碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章