第十七章: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的函数条目,把里面的函数指针改成你的代理函数地址。这样,程序每次调用那个函数,实际上都在调用你的代码。

具体实现步骤是这样的:

  1. 用OpenProcess打开目标进程
  2. 读取目标进程的PE头,找到导入表
  3. 遍历导入表,找到目标函数所在的DLL和函数名
  4. 修改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 原理示意图 正常调用流程: 程序代码 目标函数 Hook后流程: 程序代码 目标函数(已修改) jmp 指令 代理函数 返回 原始函数(跳板)

实现Inline Hook的关键步骤:

  1. 保存原始指令:把目标函数开头的5个字节(jmp指令长度)保存下来
  2. 写入跳转指令:在函数开头写入jmp指令,跳转到你的代理函数
  3. 构建跳板函数:在代理函数里,先执行你保存的原始指令,再跳回目标函数+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是把双刃剑,用好了是安全工具,用歪了就是攻击手段。咱们做逆向的,心里得有杆秤。