使用汇编和反汇编引擎写一个x86任意地址hook
最簡單的Hook
剛開始學的時候,用的hook都是最基礎的5字節hook,也不會使用hook框架,hook流程如下:
- 構建一個jmp指令跳轉到你的函數(函數需定義為裸函數)
- 保存被hook地址的至少5字節機器碼,然后寫入構建的jmp指令
- 接著在你的代碼里做你想要的操作
- 以內聯匯編的形式執行被hook地址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日志
參考
- https://blog.csdn.net/sunflover454/article/details/49029615
總結
以上是生活随笔為你收集整理的使用汇编和反汇编引擎写一个x86任意地址hook的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 手机吃鸡卧倒锁定怎么解除(手机手机报价)
- 下一篇: 水弹枪能打塑料子弹吗