第二十五章:逆向工程中的图形与渲染:DirectX 与 OpenGL 的 Hook、绘制自定义图形(ESP, 透视)、使用 CE 的“绘制”功能

说到游戏逆向,图形渲染这块儿,绝对是绕不开的硬骨头。你想想看,我们费了半天劲找到了人物坐标、血量地址,怎么把它直观地显示在屏幕上?怎么让敌人“穿墙”可见?这就得靠图形 Hook 了。

我个人习惯把图形逆向分成两派:一派是 DirectX,主要用在 Windows 游戏上;另一派是 OpenGL,跨平台的老将。今天咱们就聊聊怎么“截胡”它们的渲染流程,画出我们自己的东西。

25.1 图形 Hook 的核心思路

说白了,游戏每渲染一帧,都会调用一个核心函数。在 DirectX 里叫 Present,在 OpenGL 里叫 SwapBuffers。我们只要把这个函数“劫持”了,在它执行前后插入自己的绘制代码,就能在游戏画面上叠加任何东西。

为什么会这样?因为游戏引擎并不知道我们插了一脚。它以为自己在正常画图,实际上我们已经在它的画布上“涂鸦”了。

核心原理: Hook 住渲染函数的 EndScene(DX)或 SwapBuffers(GL),在原始调用前后插入自定义绘制逻辑。

25.2 DirectX Hook 实战

DirectX 9 和 11 的 Hook 方式略有不同,但思路一致。我以 DX9 为例,因为它的 vtable 表结构最简单。

每个 DirectX 设备对象都有一个虚函数表(vtable)。Present 函数在 vtable 中的索引通常是 17(DX9)。我们只需要修改这个指针,让它指向我们的函数。

// 伪代码:Hook DirectX 9 的 Present
typedef HRESULT (__stdcall *Present_t)(LPDIRECT3DDEVICE9, CONST RECT*, CONST RECT*, HWND, CONST RGNDATA*);
Present_t originalPresent = nullptr;

HRESULT __stdcall hookedPresent(LPDIRECT3DDEVICE9 device, CONST RECT* src, CONST RECT* dest, HWND hwnd, CONST RGNDATA* data) {
    // 在这里绘制我们的 ESP 框
    DrawESP(device);
    // 调用原始函数,让游戏正常渲染
    return originalPresent(device, src, dest, hwnd, data);
}

// 安装 Hook
void InstallHook() {
    // 获取 vtable 指针
    DWORD* vtable = *(DWORD**)device;
    // 保存原始函数
    originalPresent = (Present_t)vtable[17];
    // 修改 vtable
    vtable[17] = (DWORD)hookedPresent;
}

避坑指南: 我曾经在 Hook DX11 时踩过一个坑——DX11 的 Present 在 vtable 中的索引是 8(不同版本有差异)。一定要用调试器确认索引值,别想当然。

25.3 OpenGL Hook 实战

OpenGL 的 Hook 相对直接一些。它的核心函数是 wglSwapBuffers(Windows 平台)。我们只需要用 Detours 或 MinHook 库来拦截它。

// 伪代码:Hook OpenGL 的 wglSwapBuffers
typedef BOOL (__stdcall *SwapBuffers_t)(HDC);
SwapBuffers_t originalSwapBuffers = nullptr;

BOOL __stdcall hookedSwapBuffers(HDC hdc) {
    // 获取 OpenGL 上下文
    HGLRC glrc = wglGetCurrentContext();
    // 在这里绘制我们的透视辅助线
    DrawGlow(glrc);
    // 调用原始函数
    return originalSwapBuffers(hdc);
}

// 安装 Hook
void InstallHook() {
    HMODULE glLib = GetModuleHandleA("opengl32.dll");
    void* target = GetProcAddress(glLib, "wglSwapBuffers");
    MH_CreateHook(target, hookedSwapBuffers, (void**)&originalSwapBuffers);
    MH_EnableHook(target);
}

嗯,这里要注意:OpenGL 的绘制必须在当前上下文中进行。如果你在 Hook 函数里没有正确获取上下文,绘制会失败。

25.4 绘制自定义图形:ESP 与透视

ESP(Extra Sensory Perception)说白了就是“透视框”。我们把敌人的 3D 坐标转换成屏幕上的 2D 坐标,然后画个框。

坐标转换的核心是 WorldToScreen 函数。它需要用到游戏的视图矩阵(View Matrix)和投影矩阵(Projection Matrix)。

// 伪代码:WorldToScreen 转换
bool WorldToScreen(Vector3 worldPos, Vector2& screenPos, float* viewMatrix, int screenWidth, int screenHeight) {
    // 矩阵乘法,将世界坐标转换到裁剪空间
    float clipX = viewMatrix[0] * worldPos.x + viewMatrix[4] * worldPos.y + viewMatrix[8] * worldPos.z + viewMatrix[12];
    float clipY = viewMatrix[1] * worldPos.x + viewMatrix[5] * worldPos.y + viewMatrix[9] * worldPos.z + viewMatrix[13];
    float clipW = viewMatrix[3] * worldPos.x + viewMatrix[7] * worldPos.y + viewMatrix[11] * worldPos.z + viewMatrix[15];

    if (clipW < 0.001f) return false;  // 在相机后面,不绘制

    // 归一化设备坐标
    float ndcX = clipX / clipW;
    float ndcY = clipY / clipW;

    // 映射到屏幕坐标
    screenPos.x = (ndcX + 1.0f) * 0.5f * screenWidth;
    screenPos.y = (1.0f - ndcY) * 0.5f * screenHeight;
    return true;
}

关键点: 视图矩阵通常每帧都会变化(因为你在移动视角)。你需要动态读取它。我一般会在 Hook 函数里每帧重新获取矩阵地址。

25.5 使用 CE 的“绘制”功能

其实 Cheat Engine 自带了一个“绘制”功能,适合快速验证想法。你不需要写复杂的 Hook 代码,CE 帮你做了底层工作。

在 CE 中,你可以通过“Lua 脚本”或“Auto Assembler”来调用绘制函数。CE 提供了 drawCircledrawBoxdrawText 等 API。

-- CE Lua 脚本示例:在屏幕上绘制敌人位置
function drawESP()
    local playerX = readFloat("playerX")
    local playerY = readFloat("playerY")
    local enemyX = readFloat("enemyX")
    local enemyY = readFloat("enemyY")

    -- 计算屏幕坐标(简化版,实际需要矩阵转换)
    local screenX = enemyX - playerX + 400  -- 假设屏幕中心偏移
    local screenY = enemyY - playerY + 300

    -- 使用 CE 绘制函数
    drawBox(screenX - 10, screenY - 10, 20, 20, 0xFF0000FF)  -- 红色框
    drawText(screenX, screenY, "Enemy", 0xFFFFFFFF)
end

-- 每帧调用
function onFrame()
    drawESP()
end

注意: CE 的绘制功能只能在 CE 附加到进程时使用。如果你要发布成品工具,还是得自己写 Hook。CE 更适合调试和原型验证。

25.6 知识体系总览

下面这张图总结了本章的核心逻辑。你可以看到,从 Hook 渲染函数到绘制自定义图形,是一条清晰的链路。

图形逆向核心流程 Hook 渲染函数 Present / SwapBuffers DirectX 分支 vtable Hook / Detours OpenGL 分支 wglSwapBuffers Hook 获取矩阵数据 View / Projection Matrix 获取 GL 上下文 wglGetCurrentContext WorldToScreen 转换 3D 坐标 → 2D 屏幕坐标 绘制 ESP / 透视框

25.7 常见问题与避坑

问题 原因 解决方案
绘制闪烁 绘制代码在 Present 之后执行,被覆盖 在 Present 返回前绘制,或使用 Overlay 层
坐标偏移 矩阵读取错误或转换公式有误 用 CE 的“查找访问地址”功能验证矩阵值
Hook 后游戏崩溃 vtable 索引错误或函数签名不匹配 用 IDA 或 ReClass 确认函数原型
OpenGL 绘制不显示 未正确设置 GL 投影矩阵 在绘制前调用 glPushMatrix/glPopMatrix

我的经验: 如果你刚开始做图形 Hook,建议先用 CE 的“绘制”功能跑通流程,再切换到代码实现。这样可以避免“矩阵不对还是 Hook 不对”的纠结。

图形逆向这块儿,说白了就是“截胡”游戏的渲染流程。你只要掌握了 Hook 渲染函数、读取矩阵、坐标转换这三板斧,ESP 和透视就是水到渠成的事。别怕踩坑,我当年调试一个 DX11 的 Hook 花了两天,最后发现是 vtable 索引少算了 1。嗯,这些坑踩过了,以后就顺了。

公众号:蓝海资料掘金营,微信 deep3321