跳转至

找到并冻结主线程

冻结主线程的代码有点棘手。首先,你获取线程的唯一标识符。这与获取 PID 非常相似,你可以用 TlHelp32.h 中的 CreateToolhelp32Snapshot()、Thread32First() 和 Thread32Next()做到。正如第一百二十页「获取游戏的进程标识符」所述,这些函数基本上用于遍历列表。一个进程可以有许多线程,但以下示例假设游戏进程创建的第一个线程就是需要劫持的那个:

DWORD GetProcessThreadID(HANDLE Process) {
    THREADENTRY32 entry;
    entry.dwSize = sizeof(THREADENTRY32);
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32
CS_SNAPTHREAD, 0);
    if (Thread32First(snapshot, &entry) == TRUE) {
        DWORD PID = GetProcessId(Process);
        while (Thread32Next(snapshot, &entry) == 
TRUE) {
            if (entry.th32OwnerProcessID == PID) {
                CloseHandle(snapshot);
                return entry.th32ThreadID;
            }
        }
    }
    CloseHandle(snapshot);
    return NULL;
}

这段代码只是遍历系统上所有线程的列表,找到第一个与游戏 PID 匹配的线程。然后它从快照条目获取线程标识符。一旦知道线程标识符,像这样获取线程当前的寄存器状态:

HANDLE thread = OpenThread(
    (THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUME | 
THREAD_SET_CONTEXT),
    false, threadID);
SuspendThread(thread);
CONTEXT threadContext;
threadContext.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(thread, &threadContext);

这段代码用 OpenThread() 获取线程句柄。然后用 SuspendThread() 挂起线程,用 GetThreadContext() 获取其寄存器值。之后,清单 7-8 中的 memcpy() 代码应该拥有完成生成 shellcode 所需的所有变量。

生成 shellcode 后,可以像清单 7-4 中那样把代码洞穴写入分配的内存:

WriteProcessMemory(process, remoteString, string, 
stringlen, NULL);
WriteProcessMemory(process, remoteCave, codeCave, 
cavelen, NULL);

洞穴准备好并等待在内存中后,你只需把线程的 EIP 设置为代码洞穴的地址,让线程恢复执行,如下所示:

threadContext.Eip = (DWORD)remoteCave;
threadContext.ContextFlags = CONTEXT_CONTROL;
SetThreadContext(thread, &threadContext);
ResumeThread(thread);

这段代码使线程在代码洞穴的地址恢复执行。由于代码洞穴的编写方式,线程完全不知道发生了任何变化。洞穴存储线程的原始状态,执行有效载荷,恢复线程的原始状态,然后以一切完好无损的状态返回原始代码。

使用任何形式的代码注入时,理解你的代码洞穴接触哪些数据也很重要。例如,如果你创建了一个调用游戏内部函数来创建和发送网络数据包的代码洞穴,你需要确保函数接触的任何全局变量(比如数据包缓冲区、数据包位置标记等等)在完成后安全恢复。你永远不知道代码洞穴执行时游戏做什么——它可能正在调用和你一样的函数!

 入 DLL 获得完全控制

代码洞穴非常强大(你可以用汇编 shellcode 让游戏做任何事),但手工制作 shellcode 并不实际。直接注入 C++ 代码会更方便,不是吗?这是可能的,但过程复杂得多:代码必须编译成汇编,打包成与位置无关的格式,让它知道外部依赖,完全映射到内存中,然后在某个入口点执行。

幸运的是,所有这些在 Windows 中都已经处理好了。通过把 C++ 项目改为编译为动态库,你可以创建称为动态链接库(DLL)的自包含、与位置无关的二进制。然后你可以混合使用线程注入或劫持以及 LoadLibrary() API 函数,把 DLL 文件映射到游戏内存中。

打开 GameHackingExamples/Chapter7_CodeInjection 目录中的 main-codeInjection.cpp 和 GameHackingExamples/Chapter7_CodeInjection_DLL 下的 dllmain.cpp,在阅读本节时跟着一些示例代码。在 main-codeInjection.cpp 中,特别注意 LoadDLL()函数。