函数Hook技术(Trampoline 蹦床)

函数Hook技术(Trampoline 蹦床)
bahadir这一期主要分享Hook中的Trampoline(蹦床)技术,这个相当于是Detour的进化版。Trampoline的最大的优势是解决Detour调用被Hook函数带来的无限递归问题。
一,技术介绍
Trampoline和Detour的区别
1.Dertour(转向)
结合上一期内容Detour是拦截目标函数并把代码重定向到自己的函数(Hook函数)当中。比如当程序正常调用一个目标函数时实际上执行的是被我们修改后的指令,通过插入的Jmp指令让CPU跳转到我们的代码。本质就是拦截和重定向目标函数。
举例:汽车正常形式的一条公路上,我们设立了个路障(拦截)和指示牌(转向),强制让所有车开往我们指定的一条小路(Hook函数)上。
2.Trampoline(蹦床)
跟Detour不同的是,在实行Detour之前先备份被窃取的字节(原始指令),再分配一块内存空间(Trampoline空间)存放原始指令。在这原始指令后面紧接着一个Jmp指令用来跳转回被窃取的字节之后的位置,继续执行原始函数的剩余部分。这样就达到了备份原始指令的目的,我们先通过Detour跳转到我们的Hook函数执行想要的功能逻辑(如果Hook函数当中我们需要调用原始指令的话直接调用Trampoline的地址就行了)。执行完Hook函数之后CPU会跳转到Trampoline,Trampoline会执行之前备份好的原始指令,然后通过Trampoline中的Jmp指令跳转回原始函数继续执行。本质上可以在自己的函数当中可以随意调用原函数避免出现递归循环问题。
举例:跟上面的公路举例中,在设置的路障旁边修建一条匝道(Trampoline),这个匝道包含被我们通过路障覆盖掉的原始公路的一部分(备份原始指令),匝道的尽头就是原始公路路障后面的部分。当车进入我们的小路(Hook函数)后,必须经过我们新修建的匝道(Trampoline)才能会到主公路的后面部分继续行驶。
二,Hook函数(Trampoline)
1.函数的定义
首先是函数的声明,跟Detour创建的Hook函数一样需要三个参数。分别是想要拦截的目标函数地址,指向我们自定义的Hook函数的地址,需要被窃取的字节长度:
1 | void * TrampolineHook(void * toHook, void * ourFunct, int len); |
第一步跟之前一样先判断被窃取的字节长度是否小于5个字节,是的话就没有办法完整的写入Detour所需要的跳转指令返回NULL表示失败:
1 | if (len < 5) return NULL; |
之后可以为存放 Trampoline 代码分配一个新的内存空间,这里通过调用 Windows API 函数 VirtualAlloc 来实现。这个函数接受4个参数:
1 | LPVOID VirtualAlloc( |
根据介绍第一个参数是分配的内存起始地址填写 NULL
参数会让系统自动选择分配的基地址,第二个参数是分配内存区域的大小填写 len+5
后面的5代表跳转指令的长度,第三个参数填写 MEM_COMMIT | MEM_RESERVE
表示直接完成内存的预留和提交可以立即让内存区域可用,第四个参数 PAGE_EXECUTE_READWRITE
让这块地址的拥有执行权限和读写权限。
此函数返回成功能得到新创建的内存区域地址,把这个地址存储在 trampoLine 指针当中:
1 | void * trampoLine = VirtualAlloc(NULL, len + 5, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); |
得到了新内存区域后通过 memcpy 函数把原始指令复制到 trampoLine 区域当中:
1 | memcpy(trampoLine, toHook, len); |
原始指令后需要跳转到原函数的后面部分去才行,所以这里要先计算跳转回原始函数的地址:
1 | uintptr_t trampoJmpAddy = ((uintptr_t)toHook - (uintptr_t)trampoLine) - 5; |
将 Jmp 指令的操作码写入内存区域,0xE9
是Jmp指令的操作码。 (uintptr_t)trampoLine + len
计算出原始指令之后的第一个字节处:
1 | *(BYTE*)((uintptr_t)trampoLine + len) = 0xE9; |
Jmp指令后面紧接着需要跳转的地址,这个地址在上面计算过,直接写入即可:
1 | *(uintptr_t *)((uintptr_t)trampoLine + len + 1) = trampoJmpAddy; |
现在就可以执行Detour操作来修改目标函数了,这里为了方便可以直接调用在上一期创建的myHook函数:
1 | myHook(toHook, ourFunct, len); |
根据前面所讲执行完我们的Hook函数之后就可以返回 Trampoline 的地址:
1 | return trampoLine; |
下面是完整的代码:
1 | void * TrampolineHook(void * toHook, void * ourFunct, int len) |
2.Trampoline函数的实现
这里我就拿经典的 FPS 游戏 AssaultCube 来举例,这个游戏的渲染是通过 OpenGL 中的 wglSwapBuffers 函数来实现的。通过 IDA 查看 ac_client.exe 中的 Imports 就能搜到此函数的依赖:
1 | 004DA3E0 SDL_GL_SwapBuffers SDL |
确认了是OpenGL库之后通过 IDA 打开 opengl32.dll 搜索 Exports 窗口中的 wglSwapBuffers 函数:
1 | wglSwapBuffers 6923D080 362 |
双击进入此函数查看函数声明,因为不修改函数内部逻辑所以不用分析函数实现:
1 | int __stdcall wglSwapBuffers(HDC hDC); |
可以看到 stdcall 调用约定,返回类型为 int ,接受一个HDC类型参数的函数。那么我们通过 typedef 定义一个函数指针类型 wglSwapBuffers ,再声明一个全局函数指针变量用来存储原始函数的地址和 Trampoline 地址:
1 | typedef int(__stdcall* p_wglSwapBuffers)(HDC hDC); |
创建我们的Hook函数用来“替代”原始的 wglSwapBuffers ,每次调用 wglSwapBuffers 时会先调用我们的Hook函数。在 Hook 函数中先执行一段自己想要的代码最后再跳转到 Trampoline 函数:
1 | int __stdcall hook_wglSwapBuffers(HDC hDC) |
这样以来我们就写完了Hook函数,之后在我们的线程创建处要调用此钩子函数之前先把原始函数 wglSwapBuffers 的地址存放到之前声明的 o_wglSwapBuffers 当中:
1 | o_wglSwapBuffers = (p_wglSwapBuffers)GetProcAddress(GetModuleHandle(L"opengl32.dll"), "wglSwapBuffers"); //自动获取原始 wglSwapBuffers 的地址 |
然后就可以正常调用我们的 TrampolineHook 函数了,最重要的一点是要把此函数返回的新内存地址(Trampoline函数地址)赋值给 o_wglSwapBuffers 变量。这样就能实现完整的Hook了,要是这里不改 o_wglSwapBuffers 变量的值,一直保持原始函数地址的话会无限递归导致栈溢出程序崩溃:
1 | o_wglSwapBuffers = (p_wglSwapBuffers)mem::TrampolineHook((void*)o_wglSwapBuffers, (void*)hook_wglSwapBuffers, 5); |
在游戏中注入线程之后通过 Cheat Engine 查看 wglSwapBuffers 函数:
1 | OPENGL32.dll+3D07D - CC - int 3 |
加个断点一步步执行就能调转到Hook函数:
1 | AssaultCube_TrampolineHook.dll+158CC - CC - int 3 |
Visual Studio 的调试器显示了所有变量的地址,Cheat Engine 调转到蹦床函数的地址就能看到新创建的内存空间:
1 | 00FFFFFF - - ?? |
对此产生迷惑可以通过 Visual Studio 和 Cheat Engine进行配合调试一步步执行代码进行学习。
3.完整代码
下面是完整带注释的代码可自行实践学习:
1 | // 获取游戏主模块的基址,用于后续计算游戏内存中各功能的地址 |
每一种Hook技术都有各自的优势和缺点,根据当时的情况来选择适合的技术才是最优解,后续我会继续补充其他的Hook技术。文章当中有需要更改的部分请在评论区指出或者联系我个人邮箱进行反馈,谢谢观看!