API hook - 自定义代码
一、介绍
开源hook库已被用于实现 API 挂钩。然而,这种方法的一个主要问题是这些库的源代码是公开可用的,使得安全研究人员和安全产品供应商可以很直接地构建 IoC。因此,本文将手动实现 API 挂钩,虽然不如前面演示的库复杂,但足以在没有 IoC 的情况下实现预期结果,如果只想挂钩单个函数,自定义挂钩代码会是一个更好的选择。这样可以避免链接其他库的额外工作,以及避免这些库给二进制文件大小带来的额外负担。
二、创建跳转代码
64 位跳转 Shellcode
64 位跳转 Shellcode 如下:
mov r10, pAddress jmp r10 |
其中 pAddress
是要跳转到的函数地址(例如 0x0000FFFEC32A300
)。在代码中使用这些指令之前,必须先将其转换为 opcode。
0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, pAddress 0x41, 0xFF, 0xE2 // jmp r10 |
32 位 跳转 Shellcode如下:
32 位版本:
mov eax, pAddress jmp eax |
同样,将指令转换为操作码。
1 2 | 0xB8, 0x00, 0x00, 0x00, 0x00, // mov eax, pAddress 0xFF, 0xE0 // jmp eax |
请注意,pAddress
表示为 NULL
,这就解释了 0x00
序列。这些 0x00
操作码是占位符,在运行时将被覆盖。
检索 pAddress
Hook 是在运行时安装的,因此必须在运行时检索并向 shellcode 添加 pAddress
值。可以使用 GetProcAddress
检索地址,一经完成,memcpy
用于将地址复制到 shellcode 中的正确位置。
64位补丁
uint8_t uTrampoline[] = { 0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 将 r10 寄存器设置为 pFunctionToRun 的值 0x41, 0xFF, 0xE2 // 跳转到 r10 寄存器的值 }; uint64_t uPatch = (uint64_t)pAddress; memcpy(&uTrampoline[2], &uPatch, sizeof (uPatch)); // 将地址复制到 uTrampoline 中偏移量为 '2' 的位置 |
uint8_t uTrampoline[] = { 0xB8, 0x00, 0x00, 0x00, 0x00, // mov eax, pFunctionToRun 0xFF, 0xE0 // jmp eax }; uint32_t uPatch = (uint32_t)pAddress; memcpy(&uTrampoline[1], &uPatch, sizeof (uPatch)); // 将地址复制到 uTrampoline 中的偏移量“1”处 |
如前所述,pAddress
是目标函数的地址。uint32_t
和uint64_t
数据类型用于确保地址为正确数量的字节,即32位机器为4字节,64位机器为8字节。uint32_t
的大小为4字节,uint64_t
的大小为8字节。memcpy
将通过覆盖0x00
占位字节,将地址放入跳转代码中。
编写跳转代码
在使用准备好的 shellcode 覆盖目标函数的前几个指令之前,将跳转代码要写入的内存空间标记为可写非常重要。在大多数情况下,内存区域不可写,需要使用 VirtualProtect
WinAPI 将内存权限更改为 PAGE_EXECUTE_READWRITE
。值得注意的是,该内存区域必须可写且可执行,因为当程序调用该函数时,它需要执行在只写内存中不允许的指令。
考虑到这一点,跳转代码应首先修改目标函数的权限,然后再复制 shellcode。
// 将 pFunctionToHook 处的内存权限更改为 PAGE_EXECUTE_READWRITE if (!VirtualProtect(pFunctionToHook, sizeof (uTrampoline), PAGE_EXECUTE_READWRITE, &dwOldProtection)) { return FALSE; } // 将跳转 shellcode 复制到 pFunctionToHook memcpy(pFunctionToHook, uTrampoline, sizeof (uTrampoline)); |
其中 pFunctionToHook
是要挂钩的函数地址,uTrampoline
是跳转 shellcode。
取消钩子
当被钩取的函数被调用时,跳转外壳代码应该同时适用于 64 位和 32 位架构。然而,我们还没有讨论如何取消钩子。要做到这一点,需要使用在安装跳转外壳代码之前创建的包含这些字节的缓冲区,还原被跳转外壳覆盖的原始字节。然后,取消钩子时应将此缓冲区用作 memcpy
函数中的源缓冲区。
memcpy(pFunctionToHook, pOriginalBytes, sizeof (pOriginalBytes)); |
其中,pFunctionToHook
是被钩取的函数的地址,pOriginalBytes
是保存函数原始字节的缓冲区,这些字节应该在钩取前保存,可以通过 memcpy
调用来完成。pOriginalBytes
缓冲区的大小应与跳转外壳代码大小相同,这样只能覆盖外壳代码。最后,建议还原内存权限,可以通过以下代码段完成。
if (!VirtualProtect(pFunctionToHook, sizeof (uTrampoline), dwOldProtection, &dwOldProtection)) { return FALSE; } |
其中,dwOldProtection
是第一个 VirtualProtect
调用返回的旧内存权限。
HookSt 结构体
为了方便实现,创建了 HookSt
结构体。此结构体将包含用来对特定函数进行挂接和取消挂接所需的信息。 对于设置为编译为 64 位应用程序的程序,将 TRAMPOLINE_SIZE
值设置为 13;而对于设置为在 32 位模式下编译的程序,则将其设置为 7。值 13 和 7 是 trampoline(跳转代码)shellcode 的大小,分别在前面显示的 uTrampoline
变量中表示 64 位和 32 位系统。
typedef struct _HookSt { PVOID pFunctionToHook; // 要挂接的函数的地址 PVOID pFunctionToRun; // 要改为运行的函数的地址 BYTE pOriginalBytes[TRAMPOLINE_SIZE]; // 缓冲区,用于存储一些原始字节(清理时需要) DWORD dwOldProtection; // 保存“要挂接的函数”地址的旧内存保护(清理时需要) } HookSt, *PHookSt; |
通过以下预处理程序代码设置 TRAMPOLINE_SIZE
值:
// 如果编译为 64 位 #ifdef _M_X64 #define TRAMPOLINE_SIZE 13 #endif // _M_X64 // 如果编译为 32 位 #ifdef _M_IX86 #define TRAMPOLINE_SIZE 7 #endif // _M_IX86 |
安装钩子
以下函数使用 HookSt
来安装钩子。
BOOL InstallHook (IN PHookSt Hook) { #ifdef _M_X64 // 64 位跳转代码 uint8_t uTrampoline [] = { 0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, pFunctionToRun 0x41, 0xFF, 0xE2 // jmp r10 }; // 将调用地址 (pFunctionToRun) 补丁到 shellcode 中 uint64_t uPatch = (uint64_t)(Hook->pFunctionToRun); // 将调用地址复制到 uTrampoline 中的偏移量 '2' memcpy(&uTrampoline[2], &uPatch, sizeof (uPatch)); #endif // _M_X64 #ifdef _M_IX86 // 32 位跳转代码 uint8_t uTrampoline[] = { 0xB8, 0x00, 0x00, 0x00, 0x00, // mov eax, pFunctionToRun 0xFF, 0xE0 // jmp eax }; // 将调用地址 (pFunctionToRun) 补丁到 shellcode 中 uint32_t uPatch = (uint32_t)(Hook->pFunctionToRun); // 将调用地址复制到 uTrampoline 中的偏移量 '1' memcpy(&uTrampoline[1], &uPatch, sizeof (uPatch)); #endif // _M_IX86 // 放置跳转代码函数 - 安装钩子 memcpy(Hook->pFunctionToHook, uTrampoline, sizeof (uTrampoline)); return TRUE; } |
卸载钩子
下面的函数使用 HookSt
移除钩子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | BOOL RemoveHook (IN PHookSt Hook) { DWORD dwOldProtection = NULL; // 复制原始字节 memcpy(Hook->pFunctionToHook, Hook->pOriginalBytes, TRAMPOLINE_SIZE); // 清理我们的缓冲区 memset(Hook->pOriginalBytes, '\0' , TRAMPOLINE_SIZE); // 将旧内存保护设置回钩入前的状态 if (!VirtualProtect(Hook->pFunctionToHook, TRAMPOLINE_SIZE, Hook->dwOldProtection, &dwOldProtection)) { printf( "[!] VirtualProtect 失败,错误代码:%d \n" , GetLastError()); return FALSE; } // 全部设为 null Hook->pFunctionToHook = NULL; Hook->pFunctionToRun = NULL; Hook->dwOldProtection = NULL; return TRUE; } |
填充 HookSt 结构
InitializeHookStruct
函数用于用执行挂钩所需的信息填充 HookSt
结构。
BOOL InitializeHookStruct(IN PVOID pFunctionToHook, IN PVOID pFunctionToRun, OUT PHookSt Hook) { // 填充结构 Hook->pFunctionToHook = pFunctionToHook; Hook->pFunctionToRun = pFunctionToRun; // 保存我们将覆盖的相同大小的原始字节(即 TRAMPOLINE_SIZE) // 这是为了在完成时能够进行清理 memcpy(Hook->pOriginalBytes, pFunctionToHook, TRAMPOLINE_SIZE); // 将保护更改为 RWX 以便我们可以修改字节 // 我们将旧保护保存到结构中(以便在清理时重新放置它) if (!VirtualProtect(pFunctionToHook, TRAMPOLINE_SIZE, PAGE_EXECUTE_READWRITE, &Hook->dwOldProtection)) { printf( "[!] VirtualProtect 失败,错误代码:%d \n" , GetLastError()); return FALSE; } return TRUE; } |
完整代码
#include <stdio.h> #include <Windows.h> #include <stdint.h> // 如果是x64编译 #ifdef _M_X64 #define TRAMPOLINE_SIZE 13 // _M_X64 #endif // 如果是x32编译 #ifdef _M_IX86 #define TRAMPOLINE_SIZE 7 // _M_X32 #endif // 自定义的 MessageBoxA 函数 int (WINAPI MyMessageBoxA)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) { // 打印原始参数 printf( "[+] 原始参数 \n" ); printf( "\t - lpText : %s\n" , lpText); printf( "\t - lpCaption : %s \n" , lpCaption); // 由于基于跳板的钩子方法,无法调用全局的原始函数指针来恢复执行。因此,调用 MessageBoxW 函数来替代。 return MessageBoxW(hWnd, L "不同的 lpText" , L "不同的 lpCaption" , uType); } // 存储钩子安装和卸载所需的信息的结构体 typedef struct _HookSt { PVOID pFunctionToHook; // 要钩取的函数的地址 PVOID pFunctionToRun; // 替代运行的函数的地址 BYTE pOriginalBytes[TRAMPOLINE_SIZE]; // 用于保存被覆盖的原始字节 DWORD dwOldProtection; // 保存“要钩取函数”的原始内存保护标志 }HookSt, * PHookSt; // 初始化钩子结构体 BOOL InitializeHookStruct(IN PVOID pFunctionToHook, IN PVOID pFunctionToRun, OUT PHookSt Hook) { // 填充结构体 Hook->pFunctionToHook = pFunctionToHook; Hook->pFunctionToRun = pFunctionToRun; // 保存原始字节,大小为我们将要覆盖的字节数(即 TRAMPOLINE_SIZE) // 这样做是为了在移除钩子时能够恢复原始内容 memcpy(Hook->pOriginalBytes, pFunctionToHook, TRAMPOLINE_SIZE); // 更改内存保护为 RWX,这样我们就可以修改字节 // 我们将原始保护标志保存到结构体中,以便在清理时恢复 if (!VirtualProtect(pFunctionToHook, TRAMPOLINE_SIZE, PAGE_EXECUTE_READWRITE, &Hook->dwOldProtection)) { printf( "[!] VirtualProtect 失败,错误码 : %d \n" , GetLastError()); return FALSE; } return TRUE; } // 安装钩子 BOOL InstallHook(IN PHookSt Hook) { #ifdef _M_X64 // 64位的跳板代码 uint8_t uTrampoline[] = { 0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, pFunctionToRun // 指针是 8 字节,在 x64 中 // 地址表示为 NULL。 0x41, 0xFF, 0xE2 // jmp r10 }; // uPatch 是目标函数地址。我们使用 uint32_t 和 uint64_t 数据类型来确保地址的大小正确。 // uint32_t 是 4 字节,uint64_t 是 8 字节。 // 将跳转地址填充到跳板中 uint64_t uPatch = (uint64_t)(Hook->pFunctionToRun); // 将跳转地址填充到 uTrampoline 数组的第 2 个字节位置 memcpy(&uTrampoline[2], &uPatch, sizeof (uPatch)); #endif // _M_X64 #ifdef _M_IX86 // 32位的跳板代码 uint8_t uTrampoline[] = { 0xB8, 0x00, 0x00, 0x00, 0x00, // mov eax, pFunctionToRun 0xFF, 0xE0 // jmp eax }; // 将目标函数的地址填充到跳板代码中 uint32_t uPatch = (uint32_t)(Hook->pFunctionToRun); // 将地址填充到 uTrampoline 数组的第 1 个字节位置 memcpy(&uTrampoline[1], &uPatch, sizeof (uPatch)); #endif // _M_IX86 // 将跳板代码写入要钩取的函数地址,安装钩子 memcpy(Hook->pFunctionToHook, uTrampoline, sizeof (uTrampoline)); return TRUE; } // 卸载钩子 BOOL RemoveHook(IN PHookSt Hook) { DWORD dwOldProtection = NULL; // 将原始字节恢复到被钩取的函数 memcpy(Hook->pFunctionToHook, Hook->pOriginalBytes, TRAMPOLINE_SIZE); // 清空我们的字节缓冲区 memset(Hook->pOriginalBytes, '\0' , TRAMPOLINE_SIZE); // 恢复原始的内存保护权限 if (!VirtualProtect(Hook->pFunctionToHook, TRAMPOLINE_SIZE, Hook->dwOldProtection, &dwOldProtection)) { printf( "[!] VirtualProtect 失败,错误码 : %d \n" , GetLastError()); return FALSE; } // 将钩子结构体中的所有字段重置为 NULL Hook->pFunctionToHook = NULL; Hook->pFunctionToRun = NULL; Hook->dwOldProtection = NULL; return TRUE; } int main() { // 初始化结构体(在安装或移除钩子之前需要) HookSt st = { 0 }; // 初始化钩子结构体 if (!InitializeHookStruct(&MessageBoxA, &MyMessageBoxA, &st)) { return -1; } // 原始函数将会执行 MessageBoxA(NULL, "这是一个恶意软件开发例子吗?" , "原始 MsgBox" , MB_OK | MB_ICONQUESTION); // 安装钩子 if (!InstallHook(&st)) { return -1; } // 安装了钩子后,这行代码将不会执行 MessageBoxA(NULL, "恶意软件开发是不好的" , "原始 MsgBox" , MB_OK | MB_ICONWARNING); // 卸载钩子 if (!RemoveHook(&st)) { return -1; } // 卸载钩子后,恢复原始的 MessageBoxA 函数 MessageBoxA(NULL, "恢复正常的 MsgBox" , "原始 MsgBox" , MB_OK | MB_ICONINFORMATION); return 0; }<br><br> |
点击->使程序运行至断点处
步进到函数内部看到call &MessageBoxA步进
可以看到汇编指令为我们替换过的
本文来自博客园,作者:aoaoaoao,转载请注明原文链接:https://www.cnblogs.com/websecyw/p/18692834
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构