02-d3d11内部绘制
Course: Unity 逆向
🎮 D3D11内部绘制完全指南(挂钩渲染+IMGUI集成)
核心目标:掌握D3D11游戏内部绘制的核心流程,通过虚表Hook挂钩Present函数,集成MGUI实现自定义渲染,解决窗口缩放、消息处理等关键问题,适用于Unity/UE4等基于D3D11的引擎
🏛️ 核心概念:什么是D3D11内部绘制?
定义与核心逻辑
D3D11是多数游戏(如Unity、UE4)的底层渲染API,内部绘制本质是在游戏自身渲染流程中插入自定义渲染逻辑,核心步骤仅3步:
- 挂钩游戏D3D11渲染关键函数(通用挂钩点:
Present) - 在游戏调用
Present(帧渲染提交)前,执行自定义渲染(如UI、绘制) - 自定义渲染可使用D3D11 API或MGUI等第三方库实现
关键挂钩点:Present函数
- 作用:D3D11交换链(SwapChain)的纯虚函数,负责将渲染缓冲区提交到屏幕,是每帧渲染的最后一步
- 优势:通用性极强,几乎所有D3D11游戏都依赖此函数,是内部绘制的标准挂钩点
- 特点:属于纯虚函数,通过虚表(vtable) 调用,需通过虚表Hook实现挂钩
🔧 关键技术:虚表Hook(核心实现)
为什么用虚表Hook?
Present是IDXGISwapChain接口的纯虚函数,无固定地址,需通过虚表索引定位- 虚表Hook直接修改类的虚函数指针,不破坏原始代码,兼容性强,适用于所有纯虚函数挂钩
虚表Hook核心步骤
- 获取目标对象指针:找到游戏的
IDXGISwapChain实例(交换链指针) - 解析虚表地址:C++中,对象首地址即虚表指针(
(void**)swap_chain) - 复制原始虚表:为避免破坏原始逻辑,复制一份虚表用于修改
- 替换目标函数:将虚表中
Present对应的索引(通常为第8位)替换为自定义函数 - 恢复原始虚表:不需要时还原虚表,避免游戏崩溃
虚表Hook类实现(核心代码框架)
class VTableHook {
private:
void** m_pOriginalVTable; // 原始虚表
void** m_pNewVTable; // 自定义虚表
size_t m_vTableSize; // 虚表大小
public:
// 初始化:传入目标对象,解析虚表并复制
bool Init(void* pTargetObj) {
// 1. 获取原始虚表地址
m_pOriginalVTable = *(void**)pTargetObj;
// 2. 计算虚表大小(遍历至空指针结束)
m_vTableSize = CalculateVTableSize(m_pOriginalVTable);
// 3. 申请内存,复制原始虚表
m_pNewVTable = new void*[m_vTableSize];
memcpy(m_pNewVTable, m_pOriginalVTable, m_vTableSize * sizeof(void*));
// 4. 替换对象的虚表指针
*(void**)pTargetObj = m_pNewVTable;
return true;
}
// 绑定:替换虚表中指定索引的函数
template<typename T>
void Bind(size_t idx, T pNewFunc) {
if (idx < m_vTableSize) {
m_pNewVTable[idx] = (void*)pNewFunc;
}
}
// 卸载:恢复原始虚表
void Unhook() {
if (m_pOriginalVTable && m_pNewVTable) {
*(void**)m_pTargetObj = m_pOriginalVTable;
delete[] m_pNewVTable;
}
}
private:
// 计算虚表大小(遍历至空指针)
size_t CalculateVTableSize(void** pVTable) {
size_t size = 0;
while (pVTable[size] != nullptr) {
size++;
}
return size;
}
};📦 关键步骤1:获取D3D11交换链(通用方法)
交换链(IDXGISwapChain)是D3D11渲染的核心对象,Present函数隶属于该对象,获取交换链是Hook的前提,通用步骤如下:
1. 定位Present函数地址
- 工具:Cheat Engine(CE)
- 步骤:
- 附加游戏进程,查找
IDXGISwapChain::Present的虚表索引(通常为第8位) - 通过虚表索引找到函数地址,下断点
- 断下后,RCX寄存器即为交换链(
IDXGISwapChain*)指针(类成员函数的this指针)
- 附加游戏进程,查找
2. 通用交换链获取(适用于所有D3D11游戏)
核心思路:通过模块偏移获取,以Unity为例:
- 加载游戏模块(如
UnityPlayer.dll) - 查找交换链的模块内偏移(如
0x3A0,通过CE内存访问追踪获取) - 代码实现:
// 获取模块基址 HMODULE hModule = GetModuleHandleA("UnityPlayer.dll"); // 计算交换链地址(模块基址 + 偏移) IDXGISwapChain* pSwapChain = (IDXGISwapChain*)((UINT8*)hModule + 0x3A0);- 加载游戏模块(如
特点:无需依赖引擎特定API,通用所有D3D11游戏,包括Unity、UE4、开源引擎等
🛠️ 代码实现:D3D11 Hook与IMGUI集成
1. 项目配置(VS环境)
- 编译模式:Release(避免调试信息影响性能)
- C++标准:C++17(支持后续MGUI特性)
- 预处理:禁用预处理头,禁用4996警告(兼容标准库函数)
- 依赖:引入D3D11头文件(
d3d11.h)、MGUI库文件
2. DX11 Hook核心实现
步骤1:初始化虚表Hook
// 全局变量
VTableHook g_dx11Hook;
ID3D11Device* g_pD3DDevice = nullptr;
ID3D11DeviceContext* g_pD3DContext = nullptr;
bool g_bInit = false;
// 初始化DX11 Hook
bool DX11HookInit(IDXGISwapChain* pSwapChain) {
// 1. 初始化虚表Hook(挂钩交换链)
if (!g_dx11Hook.Init(pSwapChain)) return false;
// 2. 绑定Present函数(索引8)
g_dx11Hook.Bind(8, &Hooked_Present);
// 3. 绑定Reset函数(索引13,解决窗口缩放异常)
g_dx11Hook.Bind(13, &Hooked_Reset);
// 4. 获取D3D设备和上下文(用于渲染)
pSwapChain->GetDevice(__uuidof(ID3D11Device), (void**)&g_pD3DDevice);
g_pD3DDevice->GetImmediateContext(&g_pD3DContext);
// 5. 初始化MGUI
MGUI_Init(g_pD3DDevice, g_pD3DContext);
return true;
}步骤2:Hooked_Present函数(插入自定义渲染)
// 原始Present函数指针
HRESULT(__stdcall* Original_Present)(IDXGISwapChain* pSwapChain, UINT SyncInterval, UINT Flags);
// 挂钩后的Present函数
HRESULT __stdcall Hooked_Present(IDXGISwapChain* pSwapChain, UINT SyncInterval, UINT Flags) {
// 1. 首次调用初始化MGUI渲染目标
if (!g_bInit) {
MGUI_CreateRenderTarget(pSwapChain);
g_bInit = true;
}
// 2. 开始MGUI渲染(在游戏渲染前插入)
MGUI_BeginRender();
// 自定义渲染内容(如绘制菜单、文本)
MGUI_DrawText(100, 100, "D3D11内部绘制测试", 255, 0, 0);
MGUI_DrawRect(200, 200, 300, 200, 0, 255, 0, 180);
MGUI_EndRender();
// 3. 调用原始Present,提交游戏渲染
return Original_Present(pSwapChain, SyncInterval, Flags);
}3. IMGUI集成(自定义渲染)
核心步骤
- 引入IMGUI库头文件和链接库
- 初始化时传入D3D设备和上下文
- 创建渲染目标(与游戏交换链兼容)
- 在Present中调用MGUI的BeginRender/EndRender插入自定义绘制
关键代码
// MGUI初始化
void IMGUI_Init(ID3D11Device* pDevice, ID3D11DeviceContext* pContext) {
// 初始化MGUI环境
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.IniFilename = nullptr; // 不保存配置文件
// 初始化D3D渲染器
ImGui_ImplWin32_Init(GetGameWindowHandle()); // 获取游戏窗口句柄
ImGui_ImplDX11_Init(pDevice, pContext);
}
// 创建渲染目标(兼容游戏窗口)
void MGUI_CreateRenderTarget(IDXGISwapChain* pSwapChain) {
ID3D11Texture2D* pBackBuffer = nullptr;
pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&pBackBuffer);
g_pD3DDevice->CreateRenderTargetView(pBackBuffer, nullptr, &g_pRenderTargetView);
pBackBuffer->Release();
}❌ 关键问题解决
1. 窗口缩放/移动异常
问题原因
- 仅Hook Present函数,未处理窗口大小改变时的D3D资源重置,导致渲染目标失效
解决方法:Hook Reset函数
// 原始Reset函数指针
HRESULT(__stdcall* Original_Reset)(IDXGISwapChain* pSwapChain, const DXGI_SWAP_CHAIN_DESC* pDesc);
// 挂钩后的Reset函数
HRESULT __stdcall Hooked_Reset(IDXGISwapChain* pSwapChain, const DXGI_SWAP_CHAIN_DESC* pDesc) {
// 1. 释放旧渲染资源
ImGui_ImplDX11_Shutdown();
g_pRenderTargetView->Release();
// 2. 调用原始Reset重置交换链
HRESULT hr = Original_Reset(pSwapChain, pDesc);
// 3. 重新创建渲染目标和MGUI资源
MGUI_CreateRenderTarget(pSwapChain);
ImGui_ImplDX11_Init(g_pD3DDevice, g_pD3DContext);
return hr;
}2. 窗口无法移动/缩放/关闭
问题原因
- IMGUI未处理窗口消息,导致游戏窗口过程被阻塞
解决方法:Hook窗口过程函数
// 原始窗口过程函数
WNDPROC g_pOriginalWndProc = nullptr;
// 挂钩后的窗口过程
LRESULT CALLBACK Hooked_WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) {
// 1. 将消息传递给MGUI(支持鼠标/键盘交互)
ImGui_ImplWin32_WndProcHandler(hWnd, Msg, wParam, lParam);
// 2. 调用原始窗口过程,保证游戏正常消息处理
return CallWindowProc(g_pOriginalWndProc, hWnd, Msg, wParam, lParam);
}
// 初始化时设置窗口过程Hook
void HookWndProc(HWND hGameWnd) {
g_pOriginalWndProc = (WNDPROC)SetWindowLongPtrA(hGameWnd, GWLP_WNDPROC, (LONG_PTR)Hooked_WndProc);
}🎇 最终效果
- ✅ 成功挂钩D3D11的Present函数,每帧插入自定义渲染
- ✅ MGUI正常绘制文本、矩形等UI元素,与游戏渲染无冲突
- ✅ 窗口缩放/移动/关闭正常,无异常闪烁或崩溃
- ✅ 适用于Unity/UE4等基于D3D11的游戏,通用性强
📝 核心总结
关键技术栈
- 渲染API:D3D11(游戏底层渲染)
- Hook方式:虚表Hook(针对纯虚函数Present/Reset)
- 渲染库:MGUI(简化D3D11渲染,支持中文/UI组件)
- 核心思路:在游戏渲染提交前插入自定义逻辑,不破坏原始流程
适用场景
- 游戏内部UI绘制(菜单、血条、瞄准线)
- 数据可视化(帧率、内存占用)
- 逆向分析辅助(绘制碰撞盒、调试信息)