第十七章:Windows API与HOOK技术
说实话,逆向工程里最让我着迷的技术,就是Hook。你想想看,一个程序本来在正常跑,你突然在中间插了一脚,把它的函数调用给截胡了——这种掌控感,真的很爽。
我最早接触Hook是在做游戏外挂分析的时候。那时候看到一个程序怎么都抓不到它的通信包,后来才发现,人家用了Inline Hook把send函数给改了。嗯,从那以后我就开始系统研究Hook技术了。
这一章,我会带你从Windows API基础讲起,然后深入到IAT Hook和Inline Hook的原理与实现。最后,咱们动手写一个实战例子:Hook一个API来监控程序行为。
17.1 Windows API基础
Windows API,说白了就是操作系统给你的一堆函数。你想创建进程?调用CreateProcess。你想读写内存?调用ReadProcessMemory。这些API就是你和内核之间的桥梁。
我个人习惯把常用API分成四类:进程、线程、内存、文件。咱们一个个来看。
17.1.1 进程操作API
| API函数 | 功能 | 备注 |
|---|---|---|
| CreateProcess | 创建新进程 | 最常用的进程创建函数 |
| OpenProcess | 打开已有进程 | 获取进程句柄 |
| TerminateProcess | 终止进程 | 强制结束,不推荐 |
| GetExitCodeProcess | 获取进程退出码 | 判断进程是否结束 |
我在分析恶意软件时,经常看到它们用CreateProcess来启动自身副本,或者用OpenProcess去打开其他进程的句柄。这里有个坑:OpenProcess的第二个参数是进程ID,但如果你权限不够,会返回NULL。我曾经因为这个排查了半天,最后发现是没提权。
17.1.2 线程操作API
| API函数 | 功能 | 备注 |
|---|---|---|
| CreateThread | 创建线程 | 线程入口点是个函数指针 |
| SuspendThread | 挂起线程 | 调试时常用 |
| ResumeThread | 恢复线程 | 配合挂起使用 |
| GetThreadContext | 获取线程上下文 | 拿到寄存器值 |
线程这块,我最常用的是GetThreadContext。调试时想看看EIP指向哪里?想改某个寄存器的值?都得靠它。注意,调用前要先SuspendThread,否则拿到的上下文可能不一致。
17.1.3 内存操作API
| API函数 | 功能 | 备注 |
|---|---|---|
| VirtualAlloc | 分配虚拟内存 | 可以指定保护属性 |
| VirtualProtect | 修改内存保护属性 | 绕过DEP时常用 |
| ReadProcessMemory | 读取远程进程内存 | 跨进程操作 |
| WriteProcessMemory | 写入远程进程内存 | 注入代码时必备 |
内存操作是Hook的基础。你想想看,要修改一个函数的代码,首先得把那块内存改成可写。VirtualProtect就是干这个的。我遇到过最头疼的情况是,有些程序把关键代码段设成了PAGE_EXECUTE_READ,你想写都写不进去。这时候就得先改保护属性,再写,最后再改回来。
17.1.4 文件操作API
| API函数 | 功能 | 备注 |
|---|---|---|
| CreateFile | 打开或创建文件 | 最底层的文件操作 |
| ReadFile | 读取文件内容 | 同步/异步都支持 |
| WriteFile | 写入文件 | 注意缓冲区大小 |
| DeleteFile | 删除文件 | 文件必须关闭 |
文件API在Hook监控中很常见。比如你想监控一个程序写了什么日志,Hook掉WriteFile就行了。但要注意,有些程序会用NtWriteFile这种内核层函数,绕过应用层的Hook。嗯,这就是为什么后面要讲内核Hook的原因。
17.2 IAT Hook原理与实现
IAT Hook,全称是Import Address Table Hook。说白了,就是修改程序的导入地址表。
每个PE文件都有一个导入表,里面记录了它调用了哪些DLL的哪些函数。程序运行时,加载器会把函数地址填到IAT里。IAT Hook就是把这个地址改成你自己的函数地址。
核心思路:找到目标进程的IAT,找到你要Hook的函数条目,把里面的函数指针改成你的代理函数地址。这样,程序每次调用那个函数,实际上都在调用你的代码。
具体实现步骤是这样的:
- 用OpenProcess打开目标进程
- 读取目标进程的PE头,找到导入表
- 遍历导入表,找到目标函数所在的DLL和函数名
- 修改IAT中的函数地址,指向你的代理函数
我写一个简单的示例代码:
// IAT Hook示例:Hook MessageBoxA
// 注意:这是简化版,实际使用需要处理更多边界情况
// 代理函数
int WINAPI MyMessageBoxA(
HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
// 先记录日志
printf("[Hook] MessageBoxA被调用!文本: %s\n", lpText);
// 调用原始函数
return OriginalMessageBoxA(hWnd, lpText, lpCaption, uType);
}
// 安装Hook
BOOL InstallIATHook(HMODULE hModule, const char* szDllName,
const char* szFuncName, PROC pNewFunc)
{
// 1. 获取导入表
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = ...;
// 2. 遍历找到目标DLL
while (pImportDesc->Name)
{
// 3. 找到目标函数
PIMAGE_THUNK_DATA pThunk = ...;
// 4. 修改函数地址
DWORD dwOldProtect;
VirtualProtect(&pThunk->u1.Function, sizeof(DWORD),
PAGE_READWRITE, &dwOldProtect);
pThunk->u1.Function = (DWORD)pNewFunc;
VirtualProtect(&pThunk->u1.Function, sizeof(DWORD),
dwOldProtect, &dwOldProtect);
return TRUE;
}
return FALSE;
}
避坑指南:我曾经在Hook一个程序时,发现IAT里根本没有我要Hook的函数。后来才意识到,那个函数是通过GetProcAddress动态获取的,不走IAT。这种情况下,IAT Hook就失效了,得用Inline Hook。
17.3 Inline Hook原理与实现
Inline Hook,也叫内联钩子。它的原理更暴力:直接修改目标函数的机器码。
具体做法是:在目标函数的开头写入一条跳转指令(比如jmp),跳转到你的代理函数。这样,只要有人调用这个函数,第一步就是跳到你的代码里。
我画了一张图来说明这个过程:
实现Inline Hook的关键步骤:
- 保存原始指令:把目标函数开头的5个字节(jmp指令长度)保存下来
- 写入跳转指令:在函数开头写入jmp指令,跳转到你的代理函数
- 构建跳板函数:在代理函数里,先执行你保存的原始指令,再跳回目标函数+5的位置继续执行
代码实现:
// Inline Hook示例:Hook MessageBoxW
// 保存原始指令的缓冲区
BYTE g_OriginalBytes[5] = {0};
PROC g_pOriginalFunc = NULL;
// 代理函数
int WINAPI MyMessageBoxW(
HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType)
{
wprintf(L"[Hook] MessageBoxW被调用!文本: %s\n", lpText);
// 先恢复原始指令
WriteProcessMemory(GetCurrentProcess(), g_pOriginalFunc,
g_OriginalBytes, 5, NULL);
// 调用原始函数
int result = ((int (WINAPI*)(HWND, LPCWSTR, LPCWSTR, UINT))
g_pOriginalFunc)(hWnd, lpText, lpCaption, uType);
// 重新安装Hook
InstallInlineHook(g_pOriginalFunc, MyMessageBoxW);
return result;
}
// 安装Inline Hook
BOOL InstallInlineHook(PROC pTargetFunc, PROC pNewFunc)
{
DWORD dwOldProtect;
// 1. 保存原始指令
memcpy(g_OriginalBytes, pTargetFunc, 5);
g_pOriginalFunc = pTargetFunc;
// 2. 修改内存保护属性
VirtualProtect(pTargetFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
// 3. 写入jmp指令
// jmp指令格式: E9 + 相对偏移
BYTE jmpCode[5] = {0xE9, 0, 0, 0, 0};
DWORD dwOffset = (DWORD)pNewFunc - (DWORD)pTargetFunc - 5;
memcpy(&jmpCode[1], &dwOffset, 4);
memcpy(pTargetFunc, jmpCode, 5);
// 4. 恢复保护属性
VirtualProtect(pTargetFunc, 5, dwOldProtect, &dwOldProtect);
return TRUE;
}
重要提醒:Inline Hook有个大坑——多线程安全。如果你的Hook函数里调用了原始函数,而另一个线程也同时调用了这个函数,可能会出问题。我建议在代理函数里用互斥锁保护一下。
17.4 实战:Hook一个API监控程序行为
好了,理论讲完了,咱们动手写个实战例子。这次的目标是:Hook CreateFileW,监控一个程序打开了哪些文件。
为什么选CreateFileW?因为几乎所有文件操作最终都会调用它。你监控了它,就能知道程序在读什么、写什么。
完整代码:
// 实战:Hook CreateFileW 监控文件操作
#include <windows.h>
#include <stdio.h>
// 原始函数指针
typedef HANDLE (WINAPI *pCreateFileW)(
LPCWSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES,
DWORD, DWORD, HANDLE);
pCreateFileW OriginalCreateFileW = NULL;
// 保存原始指令
BYTE g_OriginalBytes[5] = {0};
// 代理函数
HANDLE WINAPI HookedCreateFileW(
LPCWSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile)
{
// 记录日志
wprintf(L"[监控] 打开文件: %s\n", lpFileName);
wprintf(L"[监控] 访问模式: 0x%X\n", dwDesiredAccess);
// 恢复原始指令
WriteProcessMemory(GetCurrentProcess(), OriginalCreateFileW,
g_OriginalBytes, 5, NULL);
// 调用原始函数
HANDLE hFile = OriginalCreateFileW(
lpFileName, dwDesiredAccess, dwShareMode,
lpSecurityAttributes, dwCreationDisposition,
dwFlagsAndAttributes, hTemplateFile);
// 重新安装Hook
InstallHook();
return hFile;
}
// 安装Hook
void InstallHook()
{
HMODULE hKernel32 = GetModuleHandle(L"kernel32.dll");
OriginalCreateFileW = (pCreateFileW)
GetProcAddress(hKernel32, "CreateFileW");
// 保存原始指令
memcpy(g_OriginalBytes, OriginalCreateFileW, 5);
// 写入jmp
DWORD dwOldProtect;
VirtualProtect(OriginalCreateFileW, 5,
PAGE_EXECUTE_READWRITE, &dwOldProtect);
BYTE jmpCode[5] = {0xE9};
DWORD dwOffset = (DWORD)HookedCreateFileW -
(DWORD)OriginalCreateFileW - 5;
memcpy(&jmpCode[1], &dwOffset, 4);
memcpy(OriginalCreateFileW, jmpCode, 5);
VirtualProtect(OriginalCreateFileW, 5,
dwOldProtect, &dwOldProtect);
}
int main()
{
printf("文件监控器启动...\n");
InstallHook();
// 测试:打开一个文件
HANDLE hFile = CreateFileW(
L"test.txt",
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hFile != INVALID_HANDLE_VALUE)
CloseHandle(hFile);
printf("监控完成。\n");
return 0;
}
扩展思路:你可以把这个思路扩展到其他API。比如Hook WriteFile来监控文件写入,Hook RegOpenKeyEx来监控注册表访问。我曾经用这套方法分析过一个勒索软件,成功抓到了它加密文件的完整过程。
说实话,Hook技术是逆向工程的必修课。不管是做安全分析、游戏修改,还是恶意软件分析,都离不开它。我建议你多动手写几个例子,把IAT Hook和Inline Hook都跑一遍。只有亲手写过,才能真正理解里面的门道。
嗯,这一章的内容就到这儿。记住,Hook是把双刃剑,用好了是安全工具,用歪了就是攻击手段。咱们做逆向的,心里得有杆秤。