【PE】inline hook的实现
hook思路
最基本的5字节的hook思路如下,有了这个思路,可以用更多的方式进行hook
- 通过修改目标函数开头的5个字节为jmp …,劫持程序执行流
- 跳转过去之后,再把API开头5字节改回来(UnHook)
- 然后调用这个API(这个时候栈帧还是原来的样子)
- API执行完毕后,返回到我们自己的函数上
- 根据需求修改API执行的结果
- 然后再进行Hook(将开头5字节改回来)
- 最后返回到最初函数调用的地方,等待下次调用
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步入,可以看到如下的汇编
mov edi, edi
这种汇编纯纯的没用
push ebp; mov ebp, esp;
,抬栈操作,往上抬一个新的执行空间
从第5个字节开始,才是MessageBoxA
这个函数真正有效的执行流。
如果我们将上面的5字节给patch成jmp ....
跳转到某个函数,那么就成功hook了
计算jmp偏移
注意,jmp有三种跳转形式:
短跳转(Short Jmp,只能跳转到256字节的范围内),对应机器码:EB
近跳转(Near Jmp,可跳至同一段范围内的地址),对应机器码:E9
远跳转(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);
}
效果
使用IDA动调看看关键部分,成功写入jmp backdoor
2.six bytes hook
6字节的hook原理同上,将jmp ...
的指令改写为push ...; ret;
的方式
但是由于上述MessageBox这种函数上方只有5字节的地址可以写,所以得针对热补丁
的形式进行hook。
热补丁的函数上方有较多的nop
与int 3
,将这些无关紧要的字节给hook成push ...; ret;
的方式
3.seven bytes hook
原理同上,只不过使用mov eax, addr; jmp eax;
的方式。
这种hook的方式会占用一个寄存器的存储空间。
总结
上述提及的代码以及hook方式都是基于x86
架构下的,针对x64
架构,其实也有对应的inline hook
x86 InlineHook | ret | jmp reg | jmp offset |
---|---|---|---|
影响字节数 | 6 字节 | 7 字节 | 5 字节 |
影响寄存器 | 无 | 影响一个寄存器的值 | 无 |
通用性 | 通用 | 通用 | 通用 |
x64 InlineHook | ret | jmp reg | jmp offset |
---|---|---|---|
影响字节数 | 14 字节 | 12 字节 | 6 字节 |
影响寄存器 | 无 | 影响一个寄存器的值 | 无 |
通用性 | 通用 | 通用 | 寻址范围低 |
通常情况下 x64 使用 ret 方式,x86 使用 jmp offset 方式即可