02-d3d11内部绘制

Course: Unity 逆向

🎮 D3D11内部绘制完全指南(挂钩渲染+IMGUI集成)

核心目标:掌握D3D11游戏内部绘制的核心流程,通过虚表Hook挂钩Present函数,集成MGUI实现自定义渲染,解决窗口缩放、消息处理等关键问题,适用于Unity/UE4等基于D3D11的引擎


D3D11是多数游戏(如Unity、UE4)的底层渲染API,内部绘制本质是在游戏自身渲染流程中插入自定义渲染逻辑,核心步骤仅3步:

  1. 挂钩游戏D3D11渲染关键函数(通用挂钩点:Present
  2. 在游戏调用Present(帧渲染提交)前,执行自定义渲染(如UI、绘制)
  3. 自定义渲染可使用D3D11 API或MGUI等第三方库实现
  • 作用:D3D11交换链(SwapChain)的纯虚函数,负责将渲染缓冲区提交到屏幕,是每帧渲染的最后一步
  • 优势:通用性极强,几乎所有D3D11游戏都依赖此函数,是内部绘制的标准挂钩点
  • 特点:属于纯虚函数,通过虚表(vtable) 调用,需通过虚表Hook实现挂钩

  • PresentIDXGISwapChain接口的纯虚函数,无固定地址,需通过虚表索引定位
  • 虚表Hook直接修改类的虚函数指针,不破坏原始代码,兼容性强,适用于所有纯虚函数挂钩
  1. 获取目标对象指针:找到游戏的IDXGISwapChain实例(交换链指针)
  2. 解析虚表地址:C++中,对象首地址即虚表指针((void**)swap_chain
  3. 复制原始虚表:为避免破坏原始逻辑,复制一份虚表用于修改
  4. 替换目标函数:将虚表中Present对应的索引(通常为第8位)替换为自定义函数
  5. 恢复原始虚表:不需要时还原虚表,避免游戏崩溃
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;
    }
};

交换链(IDXGISwapChain)是D3D11渲染的核心对象,Present函数隶属于该对象,获取交换链是Hook的前提,通用步骤如下:

  • 工具:Cheat Engine(CE)
  • 步骤:
    1. 附加游戏进程,查找IDXGISwapChain::Present的虚表索引(通常为第8位)
    2. 通过虚表索引找到函数地址,下断点
    3. 断下后,RCX寄存器即为交换链(IDXGISwapChain*)指针(类成员函数的this指针)
  • 核心思路:通过模块偏移获取,以Unity为例:

    1. 加载游戏模块(如UnityPlayer.dll
    2. 查找交换链的模块内偏移(如0x3A0,通过CE内存访问追踪获取)
    3. 代码实现:
    // 获取模块基址
    HMODULE hModule = GetModuleHandleA("UnityPlayer.dll");
    // 计算交换链地址(模块基址 + 偏移)
    IDXGISwapChain* pSwapChain = (IDXGISwapChain*)((UINT8*)hModule + 0x3A0);
  • 特点:无需依赖引擎特定API,通用所有D3D11游戏,包括Unity、UE4、开源引擎等


  • 编译模式:Release(避免调试信息影响性能)
  • C++标准:C++17(支持后续MGUI特性)
  • 预处理:禁用预处理头,禁用4996警告(兼容标准库函数)
  • 依赖:引入D3D11头文件(d3d11.h)、MGUI库文件
// 全局变量
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;
}
// 原始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);
}
  1. 引入IMGUI库头文件和链接库
  2. 初始化时传入D3D设备和上下文
  3. 创建渲染目标(与游戏交换链兼容)
  4. 在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();
}

  • 仅Hook Present函数,未处理窗口大小改变时的D3D资源重置,导致渲染目标失效
// 原始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;
}
  • IMGUI未处理窗口消息,导致游戏窗口过程被阻塞
// 原始窗口过程函数
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绘制(菜单、血条、瞄准线)
  • 数据可视化(帧率、内存占用)
  • 逆向分析辅助(绘制碰撞盒、调试信息)