使用組合和反組合引擎寫一個x86任意地址hook

2023-12-21 12:00:29

最簡單的Hook

剛開始學的時候,用的hook都是最基礎的5位元組hook,也不會使用hook框架,hook流程如下:

  1. 構建一個jmp指令跳轉到你的函數(函數需定義為裸函數)
  2. 儲存被hook地址的至少5位元組機器碼,然後寫入構建的jmp指令
  3. 接著在你的程式碼裡做你想要的操作
  4. 以內聯組合的形式執行被hook地址5位元組機器碼對應的組合指令
  5. 跳轉回被hook的地址下一條指令

這樣操作比較繁瑣,每次hook都要定義一堆東西,還得自己補充hook地址被修改的組合指令,最重要的是這種hook無法擴充套件到Python裡使用。

加入反組合和組合引擎

csdn有一篇文章說了可以通過引入組合和反組合引擎來去掉第二步和第四步,也就是不需要關心hook地址的組合是什麼。

文章中用的組合引擎是XEDParse,我試了下用vs2017編譯不通過,看了檔案和issue,必須得使用vs2013及以下的版本才能編譯成功,所以就放棄了,改成使用keystone。想編譯keystone和Beaengine可以看另一篇文章keystone和beaengine的編譯

我也對文章中的程式碼進行了一些小優化,這也是為了方便引入到Python中使用。

開始寫程式碼

下面的說明可能會囉嗦一些,對每行程式碼都做了解釋。你也可以去看c++ 原始碼,也對每行程式碼做了註釋。

定義一個hook函數, 引數有四個,返回值是被修改的位元組數:

  • hookAddress: 要hook的地址
  • hookFunc: hook的回撥函數
  • hookOldCode:儲存被修改的位元組
  • hookOldSize:hookOldCode的緩衝區大小

size_t HookAnyAddress(__in DWORD hookAddress, __in AnyHookFunc hookFunc, __out BYTE* hookOldCode, __in size_t hookOldSize)

AnyHookFunc的函數指標定義:

typedef void(_stdcall * AnyHookFunc)(RegisterContext*);

RegisterContext結構體的定義

struct RegisterContext
{
	DWORD EFLAGS;
	DWORD EDI;
	DWORD ESI;
	DWORD EBP;
	DWORD ESP;
	DWORD EBX;
	DWORD EDX;
	DWORD ECX;
	DWORD EAX;
};

首先定義一個記憶體的shellcode,用來存放裸函數裡的指令

BYTE ShellCode[0x40] = {
	0x60,	//pushad
	0x9C,	//pushfd
	0x54,	//push esp
	0xB8, 0x90, 0x90, 0x90, 0x90,  //mov eax,hookFunc
	0xFF, 0xD0,	 //call eax
	0x9D, //popfd
	0x61, //popad
};

這裡的4個0x90是存放hook回撥函數的地址,接著寫入回撥函數地址

memcpy(&ShellCode[0x4], &hookFunc, 4);

分配一塊可執行的記憶體, 用於存放這段shellcode

DWORD shellcodeMemAddr = (DWORD)VirtualAlloc(NULL, 0x100, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (shellcodeMemAddr == 0) {
	return 0;
}

因為shellcode已經寫了0xC個位元組,所以後面的指令從+0xC開始寫

DWORD shellcodeMemAddrStart = shellcodeMemAddr + 0xC;

定義反組合引擎和組合引擎,keystone也是老朋友了,之前x86發訊息的時候就已經用過了:

// 定義反組合引擎
DISASM MyDisasm;
memset(&MyDisasm, 0, sizeof(DISASM));
MyDisasm.EIP = (UIntPtr)hookAddress;
// 設定為32位元x86平臺
MyDisasm.Archi = 32;
MyDisasm.Options = PrefixedNumeral + ShowSegmentRegs;
// PrefixedNumeral: 數值前加0x,ShowSegmentRegs: 顯示段暫存器的值

// 定義組合引擎
ks_engine *ks;
ks_err err = ks_open(KS_ARCH_X86, KS_MODE_32, &ks);
if (err != KS_ERR_OK) {
	return 0;
}

開始計算hook地址的指令,並將指令寫到shellcodeMemAddr裡

// 儲存返回hook地址下一條指令的地址
DWORD hookRetAddr = 0;
// 記錄被修改的指令長度
size_t hookSize = 0;
// 開始迴圈反組合,直到滿足5個位元組
while (true) {
	// 開始反組合,每次反組合一條指令,返回這條指令的長度
	int DisasmCodeSize = Disasm(&MyDisasm);
	if (DisasmCodeSize < 1) {
		return 0;
	}
	// hook的地址不能包含ret指令
	if (MyDisasm.Instruction.BranchType == RetType)
	{
		return 0;
	}
	hookSize += DisasmCodeSize;
	// 儲存組合指令條數
	size_t encodingCount;
	// 儲存組合後的指令
	unsigned char *encodingCode;
	// 儲存組合後的指令長度
	size_t encodingSize;
	// 利用keystone將反組合後的指令再轉為機器碼,這麼操作可以自動處理相對地址
	// 前三個引數是輸入引數,第二個引數是反組合會的指令,第三個引數是指令所在的記憶體地址(用於計算相對偏移)
	// 後三個引數為輸出引數,見定義處
	if (ks_asm(ks, MyDisasm.CompleteInstr, shellcodeMemAddrStart, &encodingCode, &encodingSize, &encodingCount) != KS_ERR_OK) {
		return 0;
	}
	// 將組合後的機器碼寫到shellcode
	memcpy(&ShellCode[shellcodeMemAddrStart - shellcodeMemAddr], encodingCode, encodingSize);
	ks_free(encodingCode);
	// 注意: 反組合和組合的機器碼和長度可能是不一樣的
	shellcodeMemAddrStart += encodingSize;
	// 開始下一條指令的反組合和組合
	MyDisasm.EIP += DisasmCodeSize;
	// 如果指令達到5個位元組就結束
	if (hookSize >= 5)
	{
		hookRetAddr = MyDisasm.EIP;
		break;
	}
}
ks_close(ks);

開始構建跳轉指令,跳轉回hook地址的下一條指令的位置

// 儲存原始記憶體屬性值
DWORD dwOldProtect = 0;
// 給hook的地址賦予可寫許可權
BOOL bRet = VirtualProtect((LPVOID)hookAddress, 0x20, PAGE_EXECUTE_READWRITE, &dwOldProtect);
if (!bRet) {
	return 0;
}
// 儲存被覆蓋的機器碼
memcpy(hookOldCode, (LPVOID)hookAddress, hookSize);
// 構建跳轉指令
BYTE pushRetCode[6] = {
	0x68, 0x90, 0x90, 0x90, 0x90, // push hookRetAddr
	0xC3  // ret
};
memcpy(&pushRetCode[1], &hookRetAddr, 4);

將構架的跳轉指令寫入到shellcode裡,並將shellcode寫到申請的記憶體shellcodeMemAddr裡

memcpy(&ShellCode[shellcodeMemAddrStart - shellcodeMemAddr], pushRetCode, sizeof(pushRetCode));
// 將shellcode寫入申請的記憶體地址
memcpy((LPVOID)shellcodeMemAddr, ShellCode, sizeof(ShellCode));

開始修改hook地址的機器碼,跳轉到申請的記憶體地址shellcodeMemAddr

BYTE jmpCode[5] = { 0xE9, 0xFF, 0xFF, 0xFF, 0xFF };
*(DWORD*)(jmpCode + 1) = shellcodeMemAddr - (DWORD)hookAddress - 5;
memcpy((LPVOID)hookAddress, jmpCode, 5);
BYTE nopCode[2] = { 0x90,0x90};

如果被修改的指令超過了五個位元組,其他位元組用nop填充

if (hookSize > 5) {
	memset((LPVOID)(hookAddress + 5), 0x90, hookSize - 5);
}

最後還原記憶體屬性,返回被修改的指令長度

VirtualProtect((LPVOID)hookAddress, 0x20, dwOldProtect, &dwOldProtect);
return hookSize;

取消hook,只需要將儲存的機器碼還原:

DWORD UnHookAnyAddress(__in DWORD hookAddress, __in BYTE* hookOldCode, __in size_t hookOldSize) {
	DWORD dwOldProtect = 0;
	VirtualProtect((LPVOID)hookAddress, 0x20, PAGE_EXECUTE_READWRITE, &dwOldProtect);
	memcpy((LPVOID)hookAddress, hookOldCode, hookOldSize);
	VirtualProtect((LPVOID)hookAddress, 0x20, dwOldProtect, &dwOldProtect);
	return 0;
}

Python中使用

將這個編譯成dll就能在Python裡載入了,不過dll只能用於hook當前程序,這是因為函數不能跨程序呼叫,你建立的回撥函數,其他程序無法呼叫。

解決這個問題也很簡單,可以在目標程序申請一塊可執行的記憶體,用組合引擎和反組合引擎將回撥函數寫到這塊記憶體裡。

不過我的使用場景是將Python注入到了程序,Python作為執行緒在目標程序裡執行,不用這麼繁瑣。使用案例看另一篇文章封裝32位元和64位元hook框架實戰hook紀錄檔

參考