第二十五章:逆向工程中的图形与渲染: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 提供了 drawCircle、drawBox、drawText 等 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 渲染函数到绘制自定义图形,是一条清晰的链路。
25.7 常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 绘制闪烁 | 绘制代码在 Present 之后执行,被覆盖 | 在 Present 返回前绘制,或使用 Overlay 层 |
| 坐标偏移 | 矩阵读取错误或转换公式有误 | 用 CE 的“查找访问地址”功能验证矩阵值 |
| Hook 后游戏崩溃 | vtable 索引错误或函数签名不匹配 | 用 IDA 或 ReClass 确认函数原型 |
| OpenGL 绘制不显示 | 未正确设置 GL 投影矩阵 | 在绘制前调用 glPushMatrix/glPopMatrix |
我的经验: 如果你刚开始做图形 Hook,建议先用 CE 的“绘制”功能跑通流程,再切换到代码实现。这样可以避免“矩阵不对还是 Hook 不对”的纠结。
图形逆向这块儿,说白了就是“截胡”游戏的渲染流程。你只要掌握了 Hook 渲染函数、读取矩阵、坐标转换这三板斧,ESP 和透视就是水到渠成的事。别怕踩坑,我当年调试一个 DX11 的 Hook 花了两天,最后发现是 vtable 索引少算了 1。嗯,这些坑踩过了,以后就顺了。