hook初識之inline hook

fdx_xdf發表於2024-04-19

文章首發阿里雲先知社群: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();
}

我們先隨便設個斷點,然後在除錯,檢視反彙編:
image.png
可以看到呼叫 fun 函式的時候 call 07FF778971280h,我們跟到這個地址再去看看
image.png
發現這個地址相鄰都有很多 jmp 指令,jmp 的目的地就是各個函式,我們去 07FF778971920h 看一下:
image.png
可以發現 07FF778971920h就是真正的 fun 的地址。
那麼我們到這裡就對一個呼叫流程有了一個基本的認知了,呼叫時首先 call 一下,在 call 的地方會有一個 jmp 指令等著你去跳轉到真正的 fun 函式處。
那麼我們辦法修改 jmp 指令的地址,使其跳轉到我們想要跳轉的函式,不就實現了一個 hook 嗎,我們可以在 x64dbg 中簡單的驗證一下想法:
image.png
符號處找到 main 函式
image.png
直接雙擊上圖紅框處
image.png
可以看到那一堆 jmp 指令處了,我們直接修改一下看看會發生什麼
image.png
直接會發生異常,因為這塊記憶體好像還是 fun 函式內部的東西,所以異常是正常的
image.png
image.png
我們修改一下原始碼,增加一個 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();
}

我們直接進行修改:
image.png
注意到上圖程式碼和註釋不一樣,我們來執行一下
image.png
彈出來 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 的例子驗證一下,先看一下引數:
image.png
硬編碼 = 00007FF699641900-00007FF6996412EE = 612
由於計算機是小端格式儲存,所以說是 E9 12060000。
由於指令的長度是 5,所以公式也可以表示為:

硬編碼 = 要跳轉的地址 - (當前指令地址 + 5)

接下來就是程式碼實現了:
我們先來明確一下我們的目標,透過一個 hook 函式,讓我們在呼叫 fun 函式時,呼叫 fun1 函式。
所以我們先需要獲取 fun 和 fun1 函式的地址:
image.png
這裡的地址其實就是上面那堆 jmp 處的地址,所以我們只需要在這個地址的基礎上修改記憶體硬編碼即可。
我們先將地址強轉為 char*型別,這樣單位是一個位元組,所以chfunaddr + 1就把 E9 跳過去了,直接來到了硬編碼的地方,方便我們修改
image.png
接下來套用上面的公式計算出硬編碼
image.png
但是注意我們此時還不能直接修改記憶體,因為我們的程式都在.text 端,記憶體屬性是隻讀的,所以我們需要修改記憶體屬性,然後再修改記憶體即可
image.png
這樣我們就完成了一次 hook ,看效果:
image.png
並沒有呼叫 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 操作結束之後直接執行即可
我們在修改記憶體的時候提前儲存一下原來的硬編碼
image.png
在執行 fun1 的時候先 unhook 一下,恢復 funadd r原來的值,再直接呼叫即可
image.png
完整的程式碼如下:

#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。

相關文章