Inline hook

Inline hook(内联钩子)是一种再程序运行时修改函数执行流程的技术。通过修改函数原始代码 讲目标函数的执行路径重定向到自定义的代码段,从而实现对目标函数的拦截和修改。

image-20240111101326816

image-20240111103102751

对一个函数hook的方法:

  1. 从哪来到哪去
  2. 如何实现跳转
    E9 + 四字节相对地址(E9是jmp的值)(相当于一共5字节)
    修改时,如果指令长度大于5,则多余的值会和下一条代码结合为新代码,所以防止崩溃可以写成nop(90)
    这个相对地址不算这条指令本身
  3. 如何修改代码
    当已经有一个我们想要hook的函数时,需要先获得它的传入参数,并令hook函数的参数与想要hook的函数的参数类型,个数相同,以保证堆栈平衡
    为了在运行时修改代码需要virtualprotect函数(windows下)来修改某处的访问权限

注:实验环境为x86,因为在x64下相对地址的值大于4字节导致不能正常跳转

那么尝试修改并hook一个函数:

0x1

1
2
3
4
5
6
int main()
{
hooker(MessageBoxW, mybox, 5);
MessageBoxW(0, L"Hello", 0, 0);
return 0;
}

main函数先调用hooker函数,修改messagebox的内容,然后执行被修改的messagebox函数

那么hooker函数就要传入messagebox函数的地址,和我们想要修改成的函数的地址,这里的长度可以是固定的,可以不写5

1
2
3
4
5
6
7
8
9
10
void hooker(void* src, void* dst, int len) {
DWORD old;
VirtualProtect(src, len, 0x40, &old);
memcpy(back, src, len);
*(BYTE*)src = 0xE9;
uintptr_t ra = (uintptr_t)dst - (uintptr_t)src - 5;
*(DWORD*)((BYTE*)src + 1) = ra;
VirtualProtect(src, len, old, &old);

}

在函数外面创建一个备份BYTE back[5];用来保存原始数据,然后用VirtualProtect修改函数前5个字节变成可写属性(0x40),然后备份原始属性到old并修改,核心公式是:

1
2
*(BYTE*)src = 0xE9;
uintptr_t ra = (uintptr_t)dst - (uintptr_t)src - 5;

然后改回去

最后就是实现自己的函数,一定要注意函数的传入参数必须要和原函数相同

1
2
3
4
5
6
7
8
9
10
11
int WINAPI mybox(_In_opt_ HWND hWnd, 
_In_opt_ LPCWSTR lpText,
_In_opt_ LPCWSTR lpCaption,
_In_ UINT uType)
{
lpText = L"hooked";//想要修改的效果
unhook(MessageBoxW, back, 5);
MessageBoxW(hWnd, lpText, lpCaption, uType);
hooker(MessageBoxW, mybox, 5);
return 0;
}

这里通过去官网直接抄传入参数就可以直接得到参数名
同时注意要先解除钩子再调用原函数不然会陷入循环递归

1
2
3
4
5
6
void unhook(void* src, void* back, int len) {
DWORD old;
VirtualProtect(src, len, 0x40, &old);
memcpy(src, back, 5);
VirtualProtect(src, len, old, &old);
}

在调用完目标函数后,还要再次钩住函数,等待下一次调用


0x2

由于刚刚的hook函数,每次必须要先hook,然后再unhook,十分麻烦。同时,每次调用messagebox的效果都是一样的,如果想要实现显示其它东西,需要另一种方法。

对于第一个问题:如果不对hook过的函数unhook,那么就会陷入死循环,无限调用自己,如果现在有一个和messagebox的函数效果相同的函数,在hook后,不调用messagebox,而是调用这个函数,就不会内存溢出

对于第二个问题:如果有一个和messagebox函数相同的函数,那么直接调用那个函数并修改参数就可以显示其它东西。

因此,只要能构造出一个和我们想hook的函数功能相同的函数就可以解决上面两个问题。

hook前 hook后

那么先构造trampoline函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void* trampoline(void* src, void* dst, int len)
{
BYTE* boxin = (BYTE*)VirtualAlloc(0, len + 5, 0x00001000, 0x40);
memcpy(boxin, src, len);
*(boxin + len) = 0xe9;
*(DWORD*)(boxin + len + 1) = (BYTE*)src - boxin - 5;
//以下为原hooker的内容
DWORD old;
VirtualProtect(src, len, 0x40, &old);
*(BYTE*)src = 0xE9;
uintptr_t ra = (uintptr_t)dst - (uintptr_t)src - 5;
*(DWORD*)((BYTE*)src + 1) = ra;
VirtualProtect(src, len, old, &old);
return boxin;
}

前4句都是在创建功能相同函数,直接申请内存把messagebox前5个字节复制过来,然后jmp到messagebox里(偷懒)

第5句到10句的hooker和之前的hooker一样

1
2
3
4
5
6
7
8
void hooker(void* src, void* dst, int len) {
DWORD old;
VirtualProtect(src, len, 0x40, &old);
*(BYTE*)src = 0xE9;
uintptr_t ra = (uintptr_t)dst - (uintptr_t)src - 5;
*(DWORD*)((BYTE*)src + 1) = ra;
VirtualProtect(src, len, old, &old);
}

最后一句return是为了之后使用不被hook的函数

然后先把相同功能函数的调用写出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using PFUN = int (WINAPI*)(
_In_opt_ HWND hWnd,
_In_opt_ LPCWSTR lpText,
_In_opt_ LPCWSTR lpCaption,
_In_ UINT uType);
PFUN pfun = nullptr;
int main()
{
pfun = (PFUN)trampoline(MessageBoxW, mybox, 5);
MessageBoxW(0, L"Hello", 0, 0);
MessageBoxW(0, L"Hello", 0, 0);
MessageBoxW(0, L"Hello", 0, 0);
pfun(0, L"123123", 0, 0);
return 0;
}

最后添上hook函数

1
2
3
4
5
6
7
8
9
int WINAPI mybox(_In_opt_ HWND hWnd, 
_In_opt_ LPCWSTR lpText,
_In_opt_ LPCWSTR lpCaption,
_In_ UINT uType)
{
lpText = L"hooked";
pfun(hWnd, lpText, lpCaption, uType);
return 0;
}

这样调用时,会先显示3个hook,在最后显示123123.