使用匯編和反彙編引擎寫一個x86任意地址hook

Python成長路發表於2023-12-21

最簡單的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日誌

參考

相關文章