Skip to content

透视/ESP 渲染技术 ​

场景 ​

在竞技 FPS/TPS 游戏中获得视觉优势——透过障碍物看到敌人位置、血量、武器信息。核心在于劫持渲染管线,在绘制循环中注入自定义绘制逻辑或修改深度测试/着色器参数。

输入信号 ​

  • 游戏使用 DX11/DX12/OpenGL/Vulkan 渲染
  • 反作弊扫描 Present hook、检测 overlay 窗口、校验 shader hash
  • 需要在不被 BattlEye/EAC/FaceIt 检测的前提下实现 ESP

D3D11 Hook — Present 劫持 ​

稳定的 Present Hook ​

cpp
#include <d3d11.h>
#include <dxgi.h>

typedef HRESULT(__stdcall* Present_t)(IDXGISwapChain*, UINT, UINT);
Present_t OriginalPresent = nullptr;

// 找到 Present 的虚函数表索引
// IDXGISwapChain vtable (DXGI 1.1-1.6):
// [0] QueryInterface
// [1] AddRef
// [2] Release
// [3] SetPrivateData
// [4] SetPrivateDataInterface
// [5] GetPrivateData
// [6] GetParent
// [7] GetDevice
// [8] Present       ← 需要 hook 的
// [9] GetBuffer
// [10] SetFullscreenState
// [11] GetFullscreenState
// [12] GetDesc
// [13] ResizeBuffers
// [14] ResizeTarget

HRESULT __stdcall HookedPresent(IDXGISwapChain* swapChain,
    UINT syncInterval, UINT flags) {
    
    // 首次调用时初始化渲染资源
    static bool initialized = false;
    if (!initialized) {
        initialized = InitRenderResources(swapChain);
    }
    
    // 在 Present 前执行 ESP 绘制
    if (g_esp_enabled) {
        RenderESP(swapChain);
    }
    
    return OriginalPresent(swapChain, syncInterval, flags);
}

bool InitRenderResources(IDXGISwapChain* swapChain) {
    ID3D11Device* device = nullptr;
    ID3D11DeviceContext* context = nullptr;
    
    HRESULT hr = swapChain->GetDevice(__uuidof(ID3D11Device),
        (void**)&device);
    if (FAILED(hr)) return false;
    
    device->GetImmediateContext(&context);
    
    // 创建字体、画笔、着色器等
    // ...
    return true;
}

Present Hook 安装 (VMT 替换) ​

cpp
// VMT 劫持: 复制原始虚函数表 → 替换 Present 条目
void InstallPresentHook() {
    // 创建 dummy DXGI swapchain 获取 vtable
    IDXGISwapChain* swapChain = CreateDummySwapChain();
    
    // 读取 vtable
    void** vtable = *(void***)swapChain;
    OriginalPresent = (Present_t)vtable[8];
    
    // 写入新 vtable (或直接在原始 vtable 页面修改)
    DWORD oldProtect;
    VirtualProtect(&vtable[8], sizeof(void*), PAGE_READWRITE, &oldProtect);
    vtable[8] = &HookedPresent;
    VirtualProtect(&vtable[8], sizeof(void*), oldProtect, &oldProtect);
    
    // 反检测: EAC 扫描 vtable 是否在 .rdata 段
    // 直接在 .rdata 页面写 (VirtualProtect → RWX) 可以绕过简单扫描
    // 但更高级的检测: 读取 swapchain 并比较函数指针的原始偏移
}

DX12 Present Hook ​

cpp
// DX12 IDXGISwapChain::Present 索引同 DX11
// 但 DX12 需要额外注意命令队列同步

HRESULT __stdcall HookedPresentDX12(IDXGISwapChain3* swapChain,
    UINT syncInterval, UINT flags) {
    
    // DX12: 需要等待 GPU 完成前一帧
    // 否则在绘制时可能出现命令列表冲突
    
    // 获取命令队列
    ID3D12Device* device = nullptr;
    swapChain->GetDevice(IID_PPV_ARGS(&device));
    
    // 执行 ESP 绘制 (需要自己的 command list)
    if (g_esp_enabled) {
        RenderESPDX12(device, swapChain);
    }
    
    return OriginalPresentDX12(swapChain, syncInterval, flags);
}

World-to-Screen 矩阵 ​

视投影矩阵定位 ​

cpp
// W2S 核心: 将 3D 世界坐标投影到 2D 屏幕坐标
// screen_xy = world_xyz * view_matrix * projection_matrix * viewport_matrix

struct Matrix4x4 {
    float m[16];  // row-major
    
    Vector3 Transform(const Vector3& v) const {
        float w = v.x * m[12] + v.y * m[13] + v.z * m[14] + m[15];
        if (w < 0.001f) return Vector3(0, 0, 0);
        
        float inv_w = 1.0f / w;
        float x = (v.x * m[0] + v.y * m[1] + v.z * m[2] + m[3]) * inv_w;
        float y = (v.x * m[4] + v.y * m[5] + v.z * m[6] + m[7]) * inv_w;
        
        // 屏幕坐标: [-1,1] → [0, screen]
        float screen_x = (1.0f + x) * screen_width * 0.5f;
        float screen_y = (1.0f - y) * screen_height * 0.5f;
        
        return Vector3(screen_x, screen_y, w);  // w 用于距离判断
    }
};

// Pattern scan 查找 view-projection 矩阵
// 常见特征: Unreal Engine 4/5
// "ViewProjectionMatrix" 在渲染器中通常是全局变量
// IDA/Ghidra pattern:
// F3 0F 11 ?? ?? ?? ?? ?? F3 0F 11 ?? ?? ?? ?? ?? F3 0F 11 ?? ?? ?? ?? ??
// 对应: movss [r64+off], xmm0 (连续 4 次 → 4x4 矩阵的一行)

// Unity (IL2CPP): 从 Camera.worldToCameraMatrix / projectionMatrix 读取
// 用 Frida 定位:
// var camera = Camera.get_main();
// var vp = camera.projectionMatrix * camera.worldToCameraMatrix;

W2S 函数 Hook ​

cpp
// 一些 FPS 游戏使用特定 W2S 函数:
// WorldToScreen(Matrix*, Vector3*, Vector2*, bool*)
// 直接 hook 该函数可获取所有可见/不可见对象的屏幕坐标

// 特征: WorldToScreen 通常紧邻:
// - 检查 z-buffer 的可视性测试
// - 距离裁剪计算
// - 屏幕边界裁剪

// Hook 后:
bool __fastcall HookedWorldToScreen(void* this_ptr, Matrix4x4* vp,
    Vector3* world, Vector2* screen, bool* on_screen) {
    
    bool result = OriginalWorldToScreen(this_ptr, vp, world, screen, on_screen);
    
    if (result && *on_screen) {
        // 记录所有投影位置到 ESP 缓存
        g_esp_cache.push_back({*world, *screen});
    }
    
    return result;
}

DrawIndexed Object Filtering ​

cpp
// 高级 ESP: Hook DrawIndexed → 按顶点/常量缓冲区过滤 → 修改 shader

typedef void(__stdcall* DrawIndexed_t)(ID3D11DeviceContext*,
    UINT, UINT, INT);
DrawIndexed_t OriginalDrawIndexed = nullptr;

void __stdcall HookedDrawIndexed(ID3D11DeviceContext* context,
    UINT indexCount, UINT startIndex, INT baseVertex) {
    
    // 获取当前输入装配状态
    ID3D11Buffer* vertexBuffer = nullptr;
    UINT stride = 0, offset = 0;
    context->IAGetVertexBuffers(0, 1, &vertexBuffer, &stride, &offset);
    
    // 分析 stride 识别对象类型
    // FPS 游戏常见 stride 值:
    // 32 字节: 简单顶点 (pos + normal + uv)
    // 48 字节: 骨骼动画顶点
    // 56 字节: 带切线顶点
    
    if (g_chams_enabled && IsPlayerModel(stride, indexCount)) {
        // 替换着色器为 Chams 着色器
        context->VSSetShader(g_chamsVertexShader, nullptr, 0);
        context->PSSetShader(g_chamsPixelShader, nullptr, 0);
        
        // 禁用深度测试 (使敌人穿墙可见)
        context->OMSetDepthStencilState(g_depthDisabledState, 0);
        
        OriginalDrawIndexed(context, indexCount, startIndex, baseVertex);
        
        // 恢复原始着色器
        context->VSSetShader(g_originalVS, nullptr, 0);
        context->PSSetShader(g_originalPS, nullptr, 0);
        context->OMSetDepthStencilState(g_originalDepthState, 0);
    } else {
        OriginalDrawIndexed(context, indexCount, startIndex, baseVertex);
    }
}

Chams / Glow 实现 ​

修改深度缓冲区 ​

cpp
// Chams: 使玩家模型在墙后显示不同颜色
// 技术: 绘制两次 — 第一次禁用深度写入使 z-fail → 显示颜色
//        第二次正常绘制

// 深度测试状态:
D3D11_DEPTH_STENCIL_DESC depthDisabledDesc = {};
depthDisabledDesc.DepthEnable = TRUE;
depthDisabledDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;  // 不写入深度
depthDisabledDesc.DepthFunc = D3D11_COMPARISON_LESS;
depthDisabledDesc.StencilEnable = FALSE;

ID3D11DepthStencilState* g_depthNoWriteState = nullptr;
device->CreateDepthStencilState(&depthDisabledDesc, &g_depthNoWriteState);

// 完全禁用深度测试 (穿透效果):
depthDisabledDesc.DepthEnable = FALSE;
device->CreateDepthStencilState(&depthDisabledDesc, &g_depthNoWriteAllState);

修改 Shader 常量 ​

cpp
// Glow/Highlights: 修改特定对象的像素着色器常量
// 通过 Hook PSSetConstantBuffers 或 UpdateSubresource

void OverridePlayerColor(ID3D11DeviceContext* context) {
    // 玩家高亮颜色 (RGBA)
    float glowColor[4] = { 1.0f, 0.0f, 0.0f, 0.5f };  // 半透明红色
    
    // 查找并替换常量缓冲区中的颜色
    ID3D11Buffer* constantBuffers[D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT];
    context->PSGetConstantBuffers(0, 4, constantBuffers);
    
    for (int i = 0; i < 4; ++i) {
        if (constantBuffers[i]) {
            D3D11_BUFFER_DESC desc;
            constantBuffers[i]->GetDesc(&desc);
            
            // 读取当前常量, 尝试替换颜色相关偏移
            uint8_t* mappedData = MapBuffer(constantBuffers[i]);
            // 在 mappedData 中扫描类似的 float 模式...
            // (需要结合静态逆向确定颜色偏移)
        }
    }
}

External Overlay 渲染 ​

cpp
// 外部覆盖窗口 (不注入游戏进程, 检测面小)

HWND CreateTransparentOverlay(int width, int height) {
    WNDCLASSEX wc = {};
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.lpfnWndProc = OverlayWndProc;
    wc.hInstance = GetModuleHandle(NULL);
    wc.lpszClassName = L"OverlayWindow";
    RegisterClassEx(&wc);
    
    HWND overlay = CreateWindowEx(
        WS_EX_TOPMOST | WS_EX_TRANSPARENT | WS_EX_LAYERED |
        WS_EX_TOOLWINDOW,  // 不在任务栏显示
        L"OverlayWindow",
        L"",
        WS_POPUP,
        0, 0, width, height,
        NULL, NULL, wc.hInstance, NULL
    );
    
    // 设置透明色
    SetLayeredWindowAttributes(overlay, RGB(0, 0, 0), 0, LWA_COLORKEY);
    
    // 启用鼠标穿透: SetWindowLong + WS_EX_TRANSPARENT
    // 也可以使用 SetLayeredWindowAttributes with LWA_ALPHA 做半透明
    
    return overlay;
}

// 在覆盖窗口上绘制 ESP:
void RenderOnOverlay(HWND overlay, const ESPData& data) {
    HDC hdc = GetDC(overlay);
    
    // 使用 GDI 或 Direct2D 绘制
    // 设置画笔颜色 (敌人红色, 队友蓝色)
    HPEN redPen = CreatePen(PS_SOLID, 2, RGB(255, 0, 0));
    SelectObject(hdc, redPen);
    
    for (const auto& entity : data.entities) {
        if (entity.is_visible) {
            // 绘制方框
            Rectangle(hdc,
                entity.box_left, entity.box_top,
                entity.box_right, entity.box_bottom
            );
            // 绘制文字
            TextOutA(hdc, entity.label_x, entity.label_y,
                entity.label.c_str(), entity.label.length());
        }
    }
    
    ReleaseDC(overlay, hdc);
}

// 更新覆盖窗口位置追踪游戏窗口:
void UpdateOverlayPosition(HWND overlay, HWND gameWindow) {
    RECT rect;
    GetClientRect(gameWindow, &rect);
    
    POINT upperLeft = { rect.left, rect.top };
    ClientToScreen(gameWindow, &upperLeft);
    
    SetWindowPos(overlay, HWND_TOPMOST,
        upperLeft.x, upperLeft.y,
        rect.right - rect.left,
        rect.bottom - rect.top,
        SWP_SHOWWINDOW);
}

ImGui 集成模式 ​

cpp
// DX11 + ImGui ESP 绘制

// 在 Present hook 内:
HRESULT __stdcall HookedPresent(IDXGISwapChain* swapChain, ...) {
    if (!g_imgui_initialized) {
        ImGui::CreateContext();
        ImGui_ImplDX11_Init(device, context);
        g_imgui_initialized = true;
    }
    
    // 每帧更新
    ImGui_ImplDX11_NewFrame();
    ImGui::NewFrame();
    
    if (g_menu_open) {
        ImGui::Begin("ESP Menu");
        ImGui::Checkbox("Enable ESP", &g_esp_enabled);
        ImGui::ColorEdit3("Enemy Color", g_enemy_color);
        ImGui::SliderFloat("Distance", &g_max_distance, 100, 1000);
        ImGui::Checkbox("Box ESP", &g_box_esp);
        ImGui::Checkbox("Line ESP", &g_line_esp);
        ImGui::Checkbox("Health Bar", &g_health_bar);
        ImGui::End();
    }
    
    // ESP 绘制
    if (g_esp_enabled) {
        for (const auto& player : g_player_cache) {
            Vector2 screen = player.w2s_position;
            
            if (g_box_esp) {
                float box_h = player.height / player.distance * 500;
                float box_w = box_h * 0.6f;
                ImGui::GetBackgroundDrawList()->AddRect(
                    ImVec2(screen.x - box_w/2, screen.y - box_h),
                    ImVec2(screen.x + box_w/2, screen.y),
                    player.is_enemy ?
                        IM_COL32(255, 0, 0, 255) : IM_COL32(0, 255, 0, 255),
                    0.0f, 0, 2.0f
                );
            }
            
            if (g_health_bar) {
                // 绘制血量条
            }
            
            if (g_line_esp) {
                // 屏幕中线到玩家的线
            }
        }
    }
    
    ImGui::Render();
    ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
    
    return OriginalPresent(swapChain, syncInterval, flags);
}

反作弊检测向量与绕过 ​

1. VMT Hook 检测
   EAC/BE 扫描 IDXGISwapChain vtable 是否在可写页面
   绕过: 使用硬件断点或 PageGuard (在 .rdata 页直接写)

2. Present Hook 检测
   检查 Present 函数头前 5 字节是否为 JMP
   绕过: 使用硬件断点的单步 hook (mov eax, target; jmp eax)

3. Overlay 窗口检测
   枚举所有 WS_EX_LAYERED 窗口 → 检查透明度+尺寸
   绕过: 使用 SetWindowsHookEx 注入到游戏进程内绘制
         或: 在游戏窗口的子窗口区域内绘制 (隐藏窗口类名)

4. Shader hash 校验
   BattlEye 定期校验所有已加载 shader 的 hash
   绕过: 在哈希校验前恢复原始 shader

5. 驱动级扫描
   EAC/BE 内核驱动扫描用户态 handle 和线程
   绕过: 使用无窗口的 DXGI output duplication API

6. 帧缓冲检测
   某些反作弊周期性截取帧缓冲进行 AI 分析
   绕过: 只在特定帧渲染 ESP, 跳过检测帧

攻击链 ​

确定渲染 API (DX11/DX12/Vulkan)
→ 创建假 swapchain 获取 vtable → Hook Present
→ 通过 W2S pattern scan 定位视投影矩阵
→ 实现对象分类 (玩家/物品/地图)
→ 实现 ESP 渲染 (box/line/health/weapon)
→ 实现 Chams/Glow (深度测试修改)
→ 反检测: 绕过 VMT 扫描 + overlay 检测
→ Bypass: 硬件断点 hook / driver-less 绘制

MCP 工具映射 ​

攻击链步骤MCP 工具说明
搜索渲染引擎相关函数ghidra_summary_call_focusbehavior="render" 或 "graphics"
特征码定位 W2S/DrawIndexedsearch_pattern视投影矩阵或 DrawIndexed 特征码
函数阅读与反编译ghidra_summary_function_detail含完整 decompile + callees
基础信息查kb_router搜索 DX11 hook / W2S 相关知识
汇编验证rizin_assemble_bytes验证 JMP/VMT 替换指令
导入表扫描rizin_imports确认 d3d11/dxgi/vulkan-1 导入

证据与验证闭环 ​

  • 固定输入样本、SHA256、工具版本和全部参数,先保存未处理 baseline。
  • 每个假设至少绑定一个可观察量:已知明密文对、协议字段、状态转移、时间分布、偏移或重放输出。
  • 用独立脚本重放核心变换,并以断言、输出哈希或逐字段 diff 验证,不以“看起来合理”作为结论。
  • 原始抓包/样本进入 exports/general/,派生文件与原件分离并记录转换链。

GPL-3.0 · 仅供授权环境下的学习与防御性研究使用