最简单的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,//pushad0x9C,//pushfd0x54,//push esp0xB8, 0x90, 0x90, 0x90, 0x90,  //mov eax,hookFunc0xFF, 0xD0, //call eax0x9D, //popfd0x61, //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 = 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 hookRetAddr0xC3  // 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