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的基址相減得到相對偏移地址。
首先我們直接在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注入到游戲,即可獲取到模塊基址,此處編碼問題顯示有問題,不過已經可以獲取到了。
劫持 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中轉正常,如果出現錯誤多半是代碼沒有銜接完整。
我們通過X64DBG附加游戲進程,可以觀察到模塊已經注入成功了,我們將 d3d9.dll + 5cd20 = 5B50CD20
X64DBG直接跟一下這個地址,觀察我們寫入的情況,發現一個遠指針(遠跳轉)
在 jmp hook.5D391122 地址處繼續跟進,既可以看到我們自己的中轉函數了。
找人物模型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軸,實現了地圖全透的效果
老實說,這款游戲我並沒有找到人物的ID(一般也不玩CS),利用上面的方法排查就能找到,找到后替換上方的敵人ID即可完成針對人物的透視,這里懶得試了。
寫教程不易,轉載請加出處,謝謝 !!