诱使进程加载你的 DLL¶
使用代码洞穴,你可以诱使远程进程在 DLL 上调用 LoadLibrary(),从而有效地把外部代码加载到它的内存空间。由于 LoadLibrary() 只接受一个参数,你可以创建代码洞穴像这样调用它:
// 把 DLL 名称写入内存
wchar_t* dllName = "c:\\something.dll";
int namelen = wcslen(dllName) + 1;
LPVOID remoteString =
VirtualAllocEx(process, NULL, namelen * 2,
MEM_COMMIT, PAGE_EXECUTE);
WriteProcessMemory(process, remoteString, dllName,
namelen * 2, NULL);
// 获取 LoadLibraryW() 的地址
HMODULE k32 = GetModuleHandleA("kernel32.dll");
LPVOID funcAdr = GetProcAddress(k32, "
LoadLibraryW");
// 创建线程调用 LoadLibraryW(dllName)
HANDLE thread =
CreateRemoteThread(process, NULL, NULL,
(LPTHREAD_START_ROUTINE)funcAdr,
remoteString, NULL, NULL);
// 让线程完成并清理
WaitForSingleObject(thread, INFINITE);
CloseHandle(thread);
这段代码有点像是第一百二十八页「生产中绕过 ASLR」中的线程注入代码与清单 7-2 和 7-3 中创建的调用 someFunction 的代码洞穴的混合体。与前一个一样,这个示例使用单参数 API 函数(即 LoadLibrary)的函数体作为代码洞穴的主体。与后一个一样,它必须向内存注入一个字符串,因为 LoadLibrary 期望字符串指针作为第一个参数。线程注入后,它迫使 LoadLibrary 加载名称被注入内存的 DLL,有效地把外部代码放入游戏。
注 给你计划注入的任何 DLL 一个独特的名称,比如 MySuperBotV2Hook.dll。更简单的名称(如 Hook.dll 或 Injected.dll)危险地过于通用。如果名称与已经加载的 DLL 冲突,LoadLibrary() 会假设它是同一个 DLL 而不加载它!
一旦 LoadLibrary() 代码洞穴把你的 DLL 加载到游戏,DLL 的入口点——称为 DllMain()——就会以 DLL_PROCESS_ATTACH 作为原因执行。进程被终止或对 DLL 调用 FreeLibrary() 时,它的入口点会以 DLL_PROCESS_DETACH 原因被调用。从入口点处理这些事件可能看起来像这样:
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
printf("DLL attached!\n");
break;
case DLL_PROCESS_DETACH:
printf("DLL detached!\n");
break;
}
return TRUE;
}
这个示例函数首先检查 DllMain() 被调用的原因。然后它输出文本,指示它被调用是因为 DLL 被附加还是分离,无论哪种都返回 TRUE。
请记住,DLL 的入口点在加载器锁内执行,加载器锁是读取或修改进程中已加载模块列表的所有函数使用的全局同步锁。GetModuleHandle()、GetModuleFileName()、Module32First() 和 Module32Next() 等函数都会使用这个加载器锁,这意味着从 DLL 入口点运行非平凡代码可能导致死锁,应该避免。
如果你需要从 DLL 入口点运行代码,请从新线程运行,如下所示:
// 运行你的机器人
return 1;
}
// 在 DllMain() 中为 DLL_PROCESS_ATTACH 情况执行
auto thread = CreateThread(NULL, 0, &runBot, NULL,
0, NULL);
CloseHandle(thread);
在 DllMain() 中,这段代码创建一个在 runBot() 函数开始的新线程。然后它立即关闭线程句柄,因为从 DllMain() 执行任何进一步操作都可能导致严重问题。在这个 runBot() 内部,你可以开始执行机器人的代码。代码在游戏内部运行,意味着你可以用类型转换方法直接操纵内存。你还可以做更多事情,正如你将在第 8 章看到的。
注入 DLL 时,确保你没有依赖问题。例如,如果你的 DLL 依赖一些非标准 DLL,你必须先把这些 DLL 注入游戏,或者把它们放在 LoadLibrary() 会搜索的文件夹中,比如 PATH 环境变量中的任何文件夹。前者在 DLL 没有自身依赖时有效,而后者实现起来有点棘手,容易发生名称冲突。最好的选择是静态链接所有外部库,让它们直接编译进你的 DLL。