使用汇编和反汇编引擎写一个x86任意地址hook

发布时间 2023-12-21 09:44:52作者: Python成长路

最简单的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日志

参考