经典的inline hook...

【PE】inline hook的实现

hook思路

最基本的5字节的hook思路如下,有了这个思路,可以用更多的方式进行hook

  1. 通过修改目标函数开头的5个字节为jmp ......,劫持程序执行流
  2. 跳转过去之后,再把API开头5字节改回来(UnHook)
  3. 然后调用这个API(这个时候栈帧还是原来的样子)
  4. API执行完毕后,返回到我们自己的函数上
  5. 根据需求修改API执行的结果
  6. 然后再进行Hook(将开头5字节改回来)
  7. 最后返回到最初函数调用的地方,等待下次调用

hook方式

1.five bytes hook

在x86的ms库中,大部分的库函数的前5字节是可有可无的数据

USER32.dll:77280C10 mov     edi, edi
USER32.dll:77280C12 push    ebp
USER32.dll:77280C13 mov     ebp, esp

这里用vs编译一个最简单的MessageBoxA来调试

#include<Windows.h>

int main() {
	MessageBoxA(NULL, "woodwhale", "Title", S_OK);
	return 0;
}

call MessageBoxA的位置下个断点,然后F7步入,可以看到如下的汇编

image-20230422231648595

mov edi, edi这种汇编纯纯的没用

push ebp; mov ebp, esp;,抬栈操作,往上抬一个新的执行空间

从第5个字节开始,才是MessageBoxA这个函数真正有效的执行流。

如果我们将上面的5字节给patch成jmp ....跳转到某个函数,那么就成功hook了

计算jmp偏移

注意,jmp有三种跳转形式:

  1. 短跳转(Short Jmp,只能跳转到256字节的范围内),对应机器码:EB

  2. 近跳转(Near Jmp,可跳至同一段范围内的地址),对应机器码:E9

  3. 远跳转(Far Jmp,可跳至任意地址),对应机器码: EA

其中,短跳转和近跳转都是eip的相对偏移

由于新写入的jmp指令一共5字节,所以执行完这条指令后,eip会加上5,然后再加上jmp的操作数,前往目的地址

被hook函数地址 + 5 + jmp偏移 = 目的函数地址

所以jmp的偏移量为目的函数地址 - 被hook函数地址 - 5

写内存

由于要修改的代码位于PE文件的代码段,而PE文件载入内存时默认代码段的权限为RX(可读可执行),所以得用VirtualProtect来改代码端的权限(和linux中的mprotect异曲同工)

来看看函数原型

BOOL VirtualProtect(  
  LPVOID lpAddress,  //基地址:内存起始位置,也就是要修改代码的地址
  DWORD dwSize,  //    长度  :要修改多少个字节的属性,此处为一条jmp指令的长度5字节
  DWORD flNewProtect,  //    新保护属性  :修改后的内存保护属性,此处为64代表“可执行可写”。
  PDWORD lpflOldProtect  //    旧保护属性:原始的内存保护属性
);   

将5字节的权限改为RWX(可读可写可执行),然后将jmp指令给写进去,写完之后再把权限改回来

实现

#include<Windows.h>
#include<stdio.h>

FARPROC MsgBoxAddr;
BYTE PatchCode[7] = { 0xe9,0 };
BYTE OldCode[7] = { 0 };
DWORD OldState;

void hook();
int WINAPI backdoor(HWND hWnd,
	LPCSTR lpText,
	LPCSTR lpCaption,
	UINT uType);
void main();

void hook() {
	// 获取user32.dll中的MessageBoxA函数地址
	MsgBoxAddr = GetProcAddress(GetModuleHandle(L"user32.dll"), "MessageBoxA");
	// 计算需要hook到的函数地址
	DWORD targetAddr = DWORD(&backdoor) - DWORD(MsgBoxAddr) - 5;
	memcpy(&(*(PatchCode + 1)), &targetAddr, 4);
	printf("jmp to %x\n", *((ULONG*)(PatchCode + 1)));
	// 读取原本的5字节数据
	ReadProcessMemory(GetCurrentProcess(), MsgBoxAddr, OldCode, 5, nullptr);
	for (int i = 0; i < 5; i++) {
		printf("%x ", OldCode[i]);
		if (i == 4) {
			printf("\n");
		}
	}
	// 申请写的权限,写入需要patch的字节数据
	VirtualProtect(MsgBoxAddr, 6, PAGE_EXECUTE_READWRITE, &OldState);
	WriteProcessMemory(GetCurrentProcess(), MsgBoxAddr, PatchCode, 5, nullptr);
	// 恢复权限
	VirtualProtect(MsgBoxAddr, 6, OldState, &OldState);
}

int WINAPI backdoor(
	HWND hWnd,
	LPCSTR lpText,
	LPCSTR lpCaption,
	UINT uType
) {
	puts("Successful Hook!");
	// 写入oldCode,暂时恢复MessageBoxA函数
	for (int i = 0; i < 5; i++) {
		printf("%x ", OldCode[i]);
		if (i == 4) {
			printf("\n");
		}
	}
	WriteProcessMemory(GetCurrentProcess(), MsgBoxAddr, OldCode, 5, nullptr);
	MessageBoxA(NULL, "Hook", "Successful", MB_OK);
	// 恢复hook
	WriteProcessMemory(GetCurrentProcess(), MsgBoxAddr, PatchCode, 5, nullptr);
	return 0;
}

void main() {
	hook();	// inline_hook
	MessageBoxA(NULL, "woodwhale", "Title", S_OK);
}

效果

image-20230423011703841

使用IDA动调看看关键部分,成功写入jmp backdoor

image-20230423012039195

2.six bytes hook

6字节的hook原理同上,将jmp ... 的指令改写为push ...; ret;的方式

但是由于上述MessageBox这种函数上方只有5字节的地址可以写,所以得针对热补丁的形式进行hook。

热补丁的函数上方有较多的nopint 3,将这些无关紧要的字节给hook成push ...; ret;的方式

3.seven bytes hook

原理同上,只不过使用mov eax, addr; jmp eax;的方式。

这种hook的方式会占用一个寄存器的存储空间。

总结

上述提及的代码以及hook方式都是基于x86架构下的,针对x64架构,其实也有对应的inline hook

x86 InlineHookretjmp regjmp offset
影响字节数6 字节7 字节5 字节
影响寄存器影响一个寄存器的值
通用性通用通用通用
x64 InlineHookretjmp regjmp offset
影响字节数14 字节12 字节6 字节
影响寄存器影响一个寄存器的值
通用性通用通用寻址范围低

通常情况下 x64 使用 ret 方式,x86 使用 jmp offset 方式即可