文章首發阿里雲先知社群:https://xz.aliyun.com/t/14033
什麼是 hook
hook 翻譯過來就是鉤子,它用於攔截並改變某個事件或操作的行為,比如我們大家在寫 shellcode loader 時,直接使用申請記憶體,copy 記憶體等高危操作可能會報毒,然後嘗試更換冷門的 api 或者直接使用核心函式時,成功繞過殺軟,這個時候可能就是因為殺軟 hook 了高危 api 但是沒有 hook 一些冷門 api 導致的。
inline hook
inline hook 只是眾多 hook 方式中的一種,它用到的是 jmp 指令,我們接下來用一個小 demo 來學習一下 inline hook。
demo
#include <iostream>
#include <Windows.h>
extern"C" __declspec(dllexport) void fun() {
while (1) {
Sleep(1000);
printf("hello world\n");
}
}
int main()
{
fun();
}
我們先隨便設個斷點,然後在除錯,檢視反彙編:
可以看到呼叫 fun 函式的時候 call 07FF778971280h
,我們跟到這個地址再去看看
發現這個地址相鄰都有很多 jmp 指令,jmp 的目的地就是各個函式,我們去 07FF778971920h
看一下:
可以發現 07FF778971920h
就是真正的 fun 的地址。
那麼我們到這裡就對一個呼叫流程有了一個基本的認知了,呼叫時首先 call 一下,在 call 的地方會有一個 jmp 指令等著你去跳轉到真正的 fun 函式處。
那麼我們辦法修改 jmp 指令的地址,使其跳轉到我們想要跳轉的函式,不就實現了一個 hook 嗎,我們可以在 x64dbg 中簡單的驗證一下想法:
符號處找到 main 函式
直接雙擊上圖紅框處
可以看到那一堆 jmp 指令處了,我們直接修改一下看看會發生什麼
直接會發生異常,因為這塊記憶體好像還是 fun 函式內部的東西,所以異常是正常的
我們修改一下原始碼,增加一個 fun1 函式,方便我們修改 jmp 地址:
#include <iostream>
#include <Windows.h>
extern"C" __declspec(dllexport) void fun() {
while (1) {
Sleep(1000);
printf("hello world\n");
}
}
extern"C" __declspec(dllexport) void fun1() {
MessageBox(0, 0, 0, 0);
}
int main()
{
fun();
}
我們直接進行修改:
注意到上圖程式碼和註釋不一樣,我們來執行一下
彈出來 messagebox,所以可以證明 hook 成功,接下來我們要做的事情就是將上述流程用程式碼來實現。
程式碼實現上述 demo
程式碼實現的難點是確定修改的位元組,我們可以先分析一下 jmp 處的詳情:
00007FF6996412E9 | E9 12060000 | jmp <hook.fun> |
00007FF6996412EE | E9 8D3D0000 | jmp <hook.__scrt_is_managed_app> |
00007FF6996412F3 | E9 58420000 | jmp <hook.__castguard_set_user_handler> |
E9 是 jmp,然後就是一個地址了,並且我們可以發現 <hook.fun>
的地址並不是 AB060000
,其他兩個例子同理,這兩個數是怎麼算出來的呢?
第一個數是硬編碼,第二個數是 <hook.fun>
的地址,他們之間有以下規則:
硬編碼 = 要跳轉的地址 - 指令完成的下一條地址
我們用上述 fun 的例子驗證一下,先看一下引數:
硬編碼 = 00007FF699641900-00007FF6996412EE = 612
由於計算機是小端格式儲存,所以說是 E9 12060000。
由於指令的長度是 5,所以公式也可以表示為:
硬編碼 = 要跳轉的地址 - (當前指令地址 + 5)
接下來就是程式碼實現了:
我們先來明確一下我們的目標,透過一個 hook 函式,讓我們在呼叫 fun 函式時,呼叫 fun1 函式。
所以我們先需要獲取 fun 和 fun1 函式的地址:
這裡的地址其實就是上面那堆 jmp 處的地址,所以我們只需要在這個地址的基礎上修改記憶體硬編碼即可。
我們先將地址強轉為 char*型別,這樣單位是一個位元組,所以chfunaddr + 1
就把 E9 跳過去了,直接來到了硬編碼的地方,方便我們修改
接下來套用上面的公式計算出硬編碼
但是注意我們此時還不能直接修改記憶體,因為我們的程式都在.text 端,記憶體屬性是隻讀的,所以我們需要修改記憶體屬性,然後再修改記憶體即可
這樣我們就完成了一次 hook ,看效果:
並沒有呼叫 fun,而是呼叫了 fun1,我們目前的程式碼如下 :
// hook.cpp : 此檔案包含 "main" 函式。程式執行將在此處開始並結束。
//
#include <iostream>
#include<Windows.h>
extern"C" __declspec(dllexport) void fun()
{
while (true)
{
Sleep(1000);
printf("Hello World!\n");
}
}
void fun1() {
MessageBox(0, 0, 0, 0);
}
auto funaddr = fun;
auto fun1addr = fun1;
void hook() {
char* chfunaddr = (char*)funaddr;
int* hardaddr = (int*)(chfunaddr + 1);
//計算fun1的硬編碼
int newhard = (int)fun1addr - (int)(chfunaddr + 5);
DWORD oldprotect;
VirtualProtect(hardaddr, 0x100, PAGE_EXECUTE_READWRITE, &oldprotect);
hardaddr[0] = newhard;
}
int main()
{
hook();
fun();
}
但是到這裡還沒有完善 hook,因為 hook 還有一個事情要做那就是還原,不僅先呼叫了 fun1,還要再呼叫 fun,不能讓使用者察覺到異常,所以我們接下來繼續完善程式碼:
我們需要申請一段記憶體空間,用來儲存修改之前的指令,然後在 hook 操作結束之後直接執行即可
我們在修改記憶體的時候提前儲存一下原來的硬編碼
在執行 fun1 的時候先 unhook 一下,恢復 funadd r原來的值,再直接呼叫即可
完整的程式碼如下:
#include <iostream>
#include <Windows.h>
extern"C" __declspec(dllexport) void fun()
{
while (true)
{
Sleep(1000);
printf("Hello World!\n");
}
}
void fun1();
auto funaddr = fun;
int oldAddrHard = 0;
auto fun1addr = fun1;
void fun1() {
memcpy(((char*)funaddr + 1), &oldAddrHard, 4);
MessageBox(0, 0, 0, 0);
funaddr();
}
void hook() {
char* chfunaddr = (char*)funaddr;
int* hardaddr = (int*)(chfunaddr + 1);
//計算fun1的硬編碼
int newhard = (int)fun1addr - (int)(chfunaddr + 5);
oldAddrHard = hardaddr[0];
DWORD oldprotect;
VirtualProtect(hardaddr, 0x100, PAGE_EXECUTE_READWRITE, &oldprotect);
hardaddr[0] = newhard;
}
int main()
{
hook();
fun();
}
這樣我們就實現了呼叫完 fun1 再呼叫 fun 的 hook。