FPS游戏可以说一直都比较热门,典型的代表有反恐精英,穿越火线,绝地求生等,基本上只要是FPS游戏都会有透视挂的存在,而透视挂还分为很多种类型,常见的有D3D透视,方框透视,还有一些比较高端的显卡透视,本教程将学习D3D透视的实现原理,并通过DLL注入的方式实现透视。

反恐精英下载地址:链接:https://pan.baidu.com/s/1U4-E9-xNIoHOyLg5aP_l7w 提取码:yupq

DX9 SDK精简版:链接:https://pan.baidu.com/s/1SUufWoizbpZL1ki85J1zbA 提取码:u1ak

Direct3D 透视是一种主流的透视方式,因为现如今大部分游戏都会使用Dx9图形接口,那么我们该如何实现D3D透视?

在D3D中普遍会使用深度缓存区(Depth Buffer)来进行消隐处理,通过使用Z轴深度缓存即可实现将人物被遮挡的部分不被显示出来,而我们的目的就是要让它强制显示出来,D3D的核心功能主要集成在COM组件中,只要Hook其中EndScence(), DrawPrimitive(),DrawIndexedPrimitive()函数就可以感知游戏的绘图操作,然后通过调用SetRenderState()渲染函数,改变其中的渲染参数即可实现不同的透视效果。

为了确保能够正常的编译代码,请自行配置好 Direct3D 9 SDK 和 VS 系列开发环境,过程中使用了 x64dbg,DBGview工具,我这里还是使用CS起源作为演示对象吧,电脑上没别的游戏。

SetWindowHookEx 全局注入

SetWindowHookEx 函数可以将一个Dll强行插入到系统的每个进程里,因为是全局注入,所以该方法可注入到具有保护的游戏中,首先我们需要创建一个Dll工程 hook.cpp 然后将SetHook方法导出,在DllMain中进行了判断,如果窗口句柄为valve001则弹出一个消息框,其他进程直接跳过,即可实现指定进程注入。

#include <windows.h>
HHOOK global_hook; LRESULT CALLBACK MyProc(int nCode, WPARAM wParam, LPARAM lParam)
{
return CallNextHookEx(global_hook, nCode, wParam, lParam);
}
extern "C" __declspec(dllexport) void SetHook()
{
global_hook = SetWindowsHookEx(WH_CBT, MyProc, GetModuleHandle(TEXT("hook.dll")), 0);
}
extern "C" __declspec(dllexport) void UnHook()
{
if (global_hook) UnhookWindowsHookEx(global_hook);
} bool APIENTRY DllMain(HANDLE handle, DWORD dword, LPVOID lpvoid)
{
HWND hwnd = FindWindowW(L"valve001",NULL);
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);
if (GetCurrentProcessId() == pid)
{
MessageBox(hwnd, TEXT("inject"), 0, 0);
}
return true;
}

调用代码如下,注意必须将上方编译好的hook.dll与下方工程放到同一个目录下,通过LoadLibrary函数获取到模块句柄,然后通过GetProcAddress获取到导出函数地址,并通过函数指针调用。

#include <windows.h>

int main()
{
HMODULE hMod = LoadLibrary(TEXT("hook.dll"));
typedef void(*pSetHook)(void);
pSetHook SetHook = (pSetHook)GetProcAddress(hMod, "SetHook");
SetHook();
while (1)
{
Sleep(1000);
}
return 0;
}

计算 DrawIndexedPrimitive 偏移

我们需要找到 DrawIndexedPrimitive 这个渲染函数并 Hook 这个函数,但 DrawIndexedPrimitive 函数与其他普通API函数不同,由于 DirectX 的功能都是以COM组件的形式提供的类函数,所以普通的Hook无法搞它,我这里的思路是,自己编写一个D3D绘图案例,在源码中找到 DrawIndexedPrimitive 函数并设置好断点,通过VS调试单步执行找到函数的所在模块的地址,并与d3d9.dll的基址相减得到相对偏移地址。

#include <windows.h>
#include<tchar.h>
#include<d3d9.h>
#pragma comment( lib, "d3d9.lib") #define null NULL
#define RETURN return LPDIRECT3D9 g_pD3D = NULL;
LPDIRECT3DDEVICE9 g_pd3dDevice = NULL;
LPDIRECT3DVERTEXBUFFER9 g_pVB = NULL; struct CUSTOMVERTEX
{ float x, y, z, rhw;
DWORD color;
};
#define FVF ( D3DFVF_XYZRHW | D3DFVF_DIFFUSE ) HRESULT InitD3D(HWND hWnd)
{
g_pD3D = Direct3DCreate9(D3D_SDK_VERSION);
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory(&d3dpp, sizeof(d3dpp));
d3dpp.Windowed = TRUE;
d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING,&d3dpp, &g_pd3dDevice);
return S_OK;
} HRESULT InitVB()
{
CUSTOMVERTEX v[] =
{
100, 000, 0, 1, 0xffff0000,
300, 50, 0, 1, 0xff00ff00,
500, 400, 0, 1, 0xff0000ff
}; g_pd3dDevice->CreateVertexBuffer(3 * sizeof(v), 0, FVF, D3DPOOL_DEFAULT, &g_pVB, 0); void* vb;
g_pVB->Lock(0, 0, (void**)&vb, 0);
memcpy(vb, v, sizeof(v));
g_pVB->Unlock();
return S_OK;
}
void Render()
{
g_pd3dDevice->Clear(0, 0, D3DCLEAR_TARGET, D3DCOLOR_XRGB(255, 255, 0), 1, 0); g_pd3dDevice->BeginScene();
g_pd3dDevice->SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX));
g_pd3dDevice->SetFVF(FVF);
//g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 10);
g_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 4, 0, 4);
g_pd3dDevice->EndScene(); g_pd3dDevice->Present(0, 0, 0, 0);
} LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
message == WM_CLOSE ? PostQuitMessage(0) : 0;
return DefWindowProc(hWnd, message, wParam, lParam);
}
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE, PTSTR, int)
{
wchar_t cn[] = L"ClassName";
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = cn;
RegisterClass(&wc); HWND hWnd = CreateWindow(cn, 0, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,NULL, NULL, hInstance, NULL);
ShowWindow(hWnd, SW_SHOW); InitD3D(hWnd);
InitVB();
MSG msg;
ZeroMemory(&msg, sizeof(msg));
while (msg.message != WM_QUIT)
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
Render();
}
}
return 0;
}

首先我们直接在VS中运行自己的工程(这样的例子有很多),然后在源代码中找到 DrawIndexedPrimitive并下一个【F9】断点,然后直接运行程序,发现程序断下后直接按下【Alt + 8】切到反汇编窗口。

函数调用:g_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 4, 0, 4);
00D01853 8B F4 mov esi,esp
00D01855 6A 04 push 4
00D01857 6A 00 push 0
00D01859 6A 04 push 4
00D0185B 6A 00 push 0
00D0185D 6A 00 push 0
00D0185F 6A 04 push 4
00D01861 A1 44 91 D0 00 mov eax,dword ptr ds:[00D09144h]
00D01866 8B 08 mov ecx,dword ptr [eax]
00D01868 8B 15 44 91 D0 00 mov edx,dword ptr ds:[0D09144h]
00D0186E 52 push edx
00D0186F 8B 81 48 01 00 00 mov eax,dword ptr [ecx+148h]
00D01875 FF D0 call eax
00D01877 3B F4 cmp esi,esp
00D01879 E8 EF F8 FF FF call __RTC_CheckEsp (0D0116Dh)

上方的代码就是你在VS中看到的代码片段,该代码片段就是调用 DrawIndexedPrimitive 函数的初始化工作,可以明显的看出压栈了6条数据,最后调用了 call eax 我们直接在单步【F9】走到00D01875地址处并按下【F11】进入到CALL的内部,可看到以下代码片段,我们需要记下片段中的 6185CD20 这个地址。

6185CD20 8B FF                mov         edi,edi
6185CD22 55 push ebp
6185CD23 8B EC mov ebp,esp
6185CD25 6A FF push 0FFFFFFFFh
6185CD27 68 C8 49 87 61 push 618749C8h
6185CD2C 64 A1 00 00 00 00 mov eax,dword ptr fs:[00000000h]
6185CD32 50 push eax
6185CD33 83 EC 20 sub esp,20h
6185CD36 53 push ebx
6185CD37 56 push esi
6185CD38 57 push edi
6185CD39 A1 70 62 95 61 mov eax,dword ptr ds:[61956270h]

上方的起始地址 6185CD20 经常会变化,所以我们需要找到当前 d3d9.dll 模块的基址,通过X64DBG获取到的基址是61800000通过当前地址减去模块基址 6185CD20 - 61800000 得到相对偏移地址5CD20,此时我们就可以通过 d3d9.dll + 5CD20 来动态的计算出这个变化的地址,编程实现的代码片段如下:

#include <windows.h>
HHOOK global_hook; LRESULT CALLBACK MyProc(int nCode, WPARAM wParam, LPARAM lParam)
{
return CallNextHookEx(global_hook, nCode, wParam, lParam);
}
extern "C" __declspec(dllexport) void SetHook()
{
global_hook = SetWindowsHookEx(WH_CBT, MyProc, GetModuleHandle(TEXT("hook.dll")), 0);
} ULONG_PTR GetDrawIndexedPrimitiveAddr()
{
HANDLE handle = GetModuleHandle(TEXT("d3d9.dll"));
if (handle == INVALID_HANDLE_VALUE) return NULL;
return(ULONG_PTR)handle + 0x5cd20;
} bool APIENTRY DllMain(HANDLE handle, DWORD dword, LPVOID lpvoid)
{
HWND hwnd = FindWindowW(L"Valve001", NULL);
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);
if (GetCurrentProcessId() == pid)
{
ULONG_PTR temp = GetDrawIndexedPrimitiveAddr();
MessageBox(hwnd, (LPCWSTR)temp,0,0);
}
return true;
}

将这个DLL注入到游戏,即可获取到模块基址,此处编码问题显示有问题,不过已经可以获取到了。

FPS 游戏实现D3D透视-LMLPHP

劫持 DrawIndexedPrimitive 函数

劫持 DrawIndexedPrimitive 函数就可以感知绘图操作,其实这里就是 API Hook 首先我们使用 VirtualProtect() 函数将我们需要填充的内存设置为可读写可执行权限,接着直接使用 jmp (远跳转) 指令替换掉系统领空中的 DrawIndexedPrimitive 函数的前5个字节,然后让其跳转到我们的 hook.dll 模块中的 MyDrawIndexedPrimitive 执行我们自己的绘图过程,执行完毕以后直接通过 Transfer_DrawIndexedPrimitive 中转函数跳转回程序领空中,即可完成 D3D的函数劫持。

我们需要Hook该函数,并跳转到我们自己的函数中,为了保证调用堆栈的平衡,我们需要确保自己的函数参数应和系统函数参数相等,如下是DrawIndexedPrimitive函数的原型定义。

STDMETHOD(DrawIndexedPrimitive)(
THIS_ D3DPRIMITIVETYPE,
INT BaseVertexIndex,
UINT MinVertexIndex,
UINT NumVertices,
UINT startIndex,
UINT primCount
)

从上方的定义上,可以看出一共传递了6个参数,这里需要注意,由于该函数是类函数,在调用时需要传递自身指针(pdevice ->DrawIndexedPrimitive()),所以我们还需要加上一个自身指针,完整声明应该如下:

pdevice =  LPDIRECT3DDEVICE9 pDevice

HRESULT DrawIndexedPrimitive(
[in] LPDIRECT3DDEVICE9 pDevice, // 设备指针
[in] D3DPRIMITIVETYPE Type, // 图元类型
[in] INT BaseVertexIndex, // 起始顶点索引
[in] UINT MinIndex, // 最小顶点索引
[in] UINT NumVertices, // 顶点数量
[in] UINT StartIndex, // 起始索引
[in] UINT PrimitiveCount // 图元数量
)

上方我们既然知道了声明方式,那么我们就可以制作自己的中转函数Transfer_DrawIndexedPrimitive以及自己的MyDrawIndexedPrimitive函数了,代码片段如下,需要注意调用约定:

__declspec(naked) HRESULT __stdcall Transfer_DrawIndexedPrimitive(LPDIRECT3DDEVICE9 m_pDevice,
D3DPRIMITIVETYPE type,
INT BaseVertexIndex,
UINT MinVertexIndex,
UINT NumVertices,
UINT startIndex,
UINT primCount)
{
__asm{
mov edi, edi
push ebp
mov ebp, esp
mov eax, jump
jmp eax
}
} HRESULT __stdcall MyDrawIndexedPrimitive(LPDIRECT3DDEVICE9 m_pDevice,
D3DPRIMITIVETYPE type,
INT BaseVertexIndex,
UINT MinVertexIndex,
UINT NumVertices,
UINT startIndex,
UINT primCount)
{
OutputDebugStringA("执行我自己的函数,中转函数\r\n");
return Transfer_DrawIndexedPrimitive(m_pDevice, type, BaseVertexIndex, MinVertexIndex, NumVertices, startIndex, primCount);
}

接着公布下Hook函数的代码,下方我们通过内联汇编进行了跳转的链接,构成了一个完整的Hook链。

bool HookDrawIndexedPrimitive()
{
ULONG_PTR address = GetDrawIndexedPrimitiveAddr();
DWORD oldProtect = 0;
if (VirtualProtect((LPVOID)address, 5, PAGE_EXECUTE_READWRITE, &oldProtect)) // 设置内存保护方式为可读写
{
DWORD value = (DWORD)MyDrawIndexedPrimitive - address - 5; // 计算出需要跳转字节
jump = address + 5; // 计算下一个跳转字节
__asm
{
mov eax, address
mov byte ptr[eax],0xe9 // 填充为 jmp
add eax,1 // 指针递增
mov ebx,value // 中转
mov dword ptr[eax],ebx // 赋值跳转地址(远跳转)
}
VirtualProtect((LPVOID)address, 5, oldProtect, &oldProtect); // 恢复内存保护方式
}
return true;
}

最终完整代码如下所示:

#include <windows.h>
#include <d3d9.h>
#pragma comment(lib, "d3d9.lib") HHOOK global_hook;
DWORD jump = 0; LRESULT CALLBACK MyProc(int nCode, WPARAM wParam, LPARAM lParam)
{
return CallNextHookEx(global_hook, nCode, wParam, lParam);
}
extern "C" __declspec(dllexport) void SetHook()
{
global_hook = SetWindowsHookEx(WH_CBT, MyProc, GetModuleHandle(TEXT("hook.dll")), 0);
} __declspec(naked) HRESULT __stdcall Transfer_DrawIndexedPrimitive(LPDIRECT3DDEVICE9 m_pDevice,
D3DPRIMITIVETYPE type,
INT BaseVertexIndex,
UINT MinVertexIndex,
UINT NumVertices,
UINT startIndex,
UINT primCount)
{// 中转函数,执行被我们填充后的指令片段,并跳转到原始指令的后面继续执行
__asm{
mov edi, edi
push ebp
mov ebp, esp
mov eax, jump
jmp eax
}
} HRESULT __stdcall MyDrawIndexedPrimitive(LPDIRECT3DDEVICE9 m_pDevice,
D3DPRIMITIVETYPE type,
INT BaseVertexIndex,
UINT MinVertexIndex,
UINT NumVertices,
UINT startIndex,
UINT primCount) {// 在此函数中DIY添加功能(例如:绘制菜单)
OutputDebugStringA("执行我自己的函数,中转函数\r\n");
return Transfer_DrawIndexedPrimitive(m_pDevice, type, BaseVertexIndex, MinVertexIndex, NumVertices, startIndex, primCount);
} ULONG_PTR GetDrawIndexedPrimitiveAddr()
{
HANDLE handle = GetModuleHandle(TEXT("d3d9.dll")); // 获得d3d9.dll模块基址
if (handle == INVALID_HANDLE_VALUE) return NULL;
return(ULONG_PTR)handle + 0x5cd20; // 相加偏移
} bool HookDrawIndexedPrimitive()
{
ULONG_PTR address = GetDrawIndexedPrimitiveAddr();
DWORD oldProtect = 0;
if (VirtualProtect((LPVOID)address, 5, PAGE_EXECUTE_READWRITE, &oldProtect)) // 设置内存保护方式为可读写
{
DWORD value = (DWORD)MyDrawIndexedPrimitive - address - 5; // 计算出需要跳转字节
jump = address + 5; // 计算下一个跳转字节
__asm
{
mov eax, address
mov byte ptr[eax],0xe9 // 填充为 jmp
add eax,1 // 指针递增
mov ebx,value // 中转
mov dword ptr[eax],ebx // 赋值跳转地址(远跳转)
}
VirtualProtect((LPVOID)address, 5, oldProtect, &oldProtect); // 恢复内存保护方式
}
return true;
} bool APIENTRY DllMain(HANDLE handle, DWORD dword, LPVOID lpvoid)
{
HWND hwnd = FindWindowW(L"valve001", NULL);
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);
if (GetCurrentProcessId() == pid)
{
HookDrawIndexedPrimitive();
}
return true;
}

将代码编译为 hook.dll 并使用前面提到过的SetWindowHook方法注入游戏,注入后发现已经成功劫持,并且游戏没有崩溃说明我们的Hook中转正常,如果出现错误多半是代码没有衔接完整。

FPS 游戏实现D3D透视-LMLPHP

我们通过X64DBG附加游戏进程,可以观察到模块已经注入成功了,我们将 d3d9.dll + 5cd20 = 5B50CD20

FPS 游戏实现D3D透视-LMLPHP

X64DBG直接跟一下这个地址,观察我们写入的情况,发现一个远指针(远跳转)

FPS 游戏实现D3D透视-LMLPHP

在 jmp hook.5D391122 地址处继续跟进,既可以看到我们自己的中转函数了。

FPS 游戏实现D3D透视-LMLPHP

找人物模型ID号

简单的模型过滤:

HRESULT __stdcall MyDrawIndexedPrimitive(LPDIRECT3DDEVICE9 m_pDevice, D3DPRIMITIVETYPE type, INT BaseVertexIndex, UINT MinVertexIndex, UINT NumVertices, UINT startIndex, UINT primCount)
{
HRESULT Result = S_OK;
IDirect3DVertexBuffer9 *pStreamData = NULL;
UINT iOffsetInBytes, iStride;
if (m_pDevice->GetStreamSource(0, &pStreamData, &iOffsetInBytes, &iStride) == D3D_OK) pStreamData->Release(); // 得到模型来源
if (iStride == 200) // 得到来源为200的时候,才会渲染
{
Result = Transfer_DrawIndexedPrimitive(m_pDevice, type, BaseVertexIndex, MinVertexIndex, NumVertices, startIndex, primCount);
}
return Result;
}

添加虚拟键位: 创建并添加虚拟键位,按下上光标键模型序号加2,按下下光标键模型序号减2,进入游戏以后按下上光标键,观察游戏的反应,如果人物消失了,就是我们要找的人物ID号。

WNDPROC Global_OldProc = NULL;
DWORD Fvalue = 0; HRESULT __stdcall MyDrawIndexedPrimitive(LPDIRECT3DDEVICE9 m_pDevice, D3DPRIMITIVETYPE type, INT BaseVertexIndex, UINT MinVertexIndex, UINT NumVertices, UINT startIndex, UINT primCount)
{
HRESULT Result = S_FALSE;
IDirect3DVertexBuffer9 *pStreamData = NULL;
UINT iOffsetInBytes, iStride;
if (m_pDevice->GetStreamSource(0, &pStreamData, &iOffsetInBytes, &iStride) == D3D_OK) pStreamData->Release(); // 得到模型来源
if (iStride != Fvalue) // 当来源不等于Fvalue时,就渲染,否则直接去除
{
Result = Transfer_DrawIndexedPrimitive(m_pDevice, type, BaseVertexIndex, MinVertexIndex, NumVertices, startIndex, primCount);
}
return Result;
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if (uMsg == WM_KEYDOWN)
{
if (wParam = VK_UP) // 按下上光标将,我们让模型编号加2
{
Fvalue += 2;
}
if (wParam == VK_DOWN) // 按下下光标键,我们让我们让模型编号减2
{
Fvalue -= 2;
}
}
return CallWindowProc(Global_OldProc, hwnd, uMsg, wParam, lParam); // 全局热键回调函数
}
bool APIENTRY DllMain(HANDLE handle, DWORD dword, LPVOID lpvoid)
{
HWND hwnd = FindWindowW(L"valve001", NULL);
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);
if (GetCurrentProcessId() == pid)
{
HookDrawIndexedPrimitive();
Global_OldProc = (WNDPROC)SetWindowLong(hwnd, GWL_WNDPROC, (LONG)WindowProc); // 注册全局热键
}
return true;
}

关闭Z轴缓冲: 通过 GetStreamSource 函数获取到模型的来源,通过判断来源来禁用相应模型的Z轴缓冲,实现透视。

HRESULT __stdcall MyDrawIndexedPrimitive(LPDIRECT3DDEVICE9 m_pDevice, D3DPRIMITIVETYPE type, INT BaseVertexIndex, UINT MinVertexIndex, UINT NumVertices, UINT startIndex, UINT primCount)
{
IDirect3DVertexBuffer9 *pStreamData = NULL;
UINT iOffsetInBytes, iStride;
if (m_pDevice->GetStreamSource(0, &pStreamData, &iOffsetInBytes, &iStride) == D3D_OK) pStreamData->Release(); // 得到模型来源
if (iStride == 4) // 得到来源为200的时候,才会关闭Z轴(此处为敌人ID)
{
m_pDevice->SetRenderState(D3DRS_ZENABLE, FALSE); // 关闭Z轴缓冲
}
return Transfer_DrawIndexedPrimitive(m_pDevice, type, BaseVertexIndex, MinVertexIndex, NumVertices, startIndex, primCount);
}

如下截图:我直接禁用了全部的模型Z轴,实现了地图全透的效果

FPS 游戏实现D3D透视-LMLPHP

FPS 游戏实现D3D透视-LMLPHP

老实说,这款游戏我并没有找到人物的ID(一般也不玩CS),利用上面的方法排查就能找到,找到后替换上方的敌人ID即可完成针对人物的透视,这里懒得试了。

写教程不易,转载请加出处,谢谢 !!

05-14 21:59