FPS游戏:实现D3DHook劫持透视
FPS游戏可以说一直都比较热门,典型的代表有反恐精英,穿越火线,绝地求生等,基本上只要是FPS游戏都会有透视挂的存在,而透视挂还分为很多种类型,常见的有D3D透视,方框透视,还有一些比较高端的显卡透视,本教程将学习D3D透视的实现原理,并通过DLL注入的方式实现透视。
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则弹出一个消息框,其他进程直接跳过,即可实现指定进程注入。
|
调用代码如下,注意必须将上方编译好的hook.dll与下方工程放到同一个目录下,通过LoadLibrary函数获取到模块句柄,然后通过GetProcAddress获取到导出函数地址,并通过函数指针调用。
|
计算 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); |
上方的代码就是你在VS中看到的代码片段,该代码片段就是调用 DrawIndexedPrimitive
函数的初始化工作,可以明显的看出压栈了6条数据,最后调用了 call eax
我们直接在单步【F9】走到00D01875地址处并按下【F11】进入到CALL的内部,可看到以下代码片段,我们需要记下片段中的 6185CD20
这个地址。
6185CD20 8B FF mov edi,edi |
上方的起始地址 6185CD20 经常会变化,所以我们需要找到当前 d3d9.dll 模块的基址,通过X64DBG获取到的基址是61800000
通过当前地址减去模块基址 6185CD20 - 61800000
得到相对偏移地址5CD20
,此时我们就可以通过 d3d9.dll + 5CD20 来动态的计算出这个变化的地址,编程实现的代码片段如下:
|
将这个DLL注入到游戏,即可获取到模块基址,此处编码问题显示有问题,不过已经可以获取到了。
劫持 DrawIndexedPrimitive 函数
劫持 DrawIndexedPrimitive 函数就可以感知绘图操作,其实这里就是 API Hook 首先我们使用 VirtualProtect()
函数将我们需要填充的内存设置为可读写可执行权限,接着直接使用 jmp (远跳转)
指令替换掉系统领空中的 DrawIndexedPrimitive 函数的前5个字节,然后让其跳转到我们的 hook.dll
模块中的 MyDrawIndexedPrimitive
执行我们自己的绘图过程,执行完毕以后直接通过 Transfer_DrawIndexedPrimitive
中转函数跳转回程序领空中,即可完成 D3D的函数劫持。
我们需要Hook该函数,并跳转到我们自己的函数中,为了保证调用堆栈的平衡,我们需要确保自己的函数参数应和系统函数参数相等,如下是DrawIndexedPrimitive函数的原型定义。
STDMETHOD(DrawIndexedPrimitive)( |
从上方的定义上,可以看出一共传递了6个参数,这里需要注意,由于该函数是类函数,在调用时需要传递自身指针(pdevice ->DrawIndexedPrimitive()),所以我们还需要加上一个自身指针,完整声明应该如下:
pdevice = LPDIRECT3DDEVICE9 pDevice |
上方我们既然知道了声明方式,那么我们就可以制作自己的中转函数Transfer_DrawIndexedPrimitive
以及自己的MyDrawIndexedPrimitive
函数了,代码片段如下,需要注意调用约定:
__declspec(naked) HRESULT __stdcall Transfer_DrawIndexedPrimitive(LPDIRECT3DDEVICE9 m_pDevice, |
接着公布下Hook函数的代码,下方我们通过内联汇编进行了跳转的链接,构成了一个完整的Hook链。
bool HookDrawIndexedPrimitive() |
最终完整代码如下所示:
|
将代码编译为 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) |
添加虚拟键位: 创建并添加虚拟键位,按下上光标键模型序号加2,按下下光标键模型序号减2,进入游戏以后按下上光标键,观察游戏的反应,如果人物消失了,就是我们要找的人物ID号。
WNDPROC Global_OldProc = NULL; |
完整版:
|
关闭Z轴缓冲: 通过 GetStreamSource
函数获取到模型的来源,通过判断来源来禁用相应模型的Z轴缓冲,实现透视。
|
如下截图:我直接禁用了全部的模型Z轴,实现了地图全透的效果
老实说,这款游戏我并没有找到人物的ID(一般也不玩CS),利用上面的方法排查就能找到,找到后替换上方的敌人ID即可完成针对人物的透视,这里懒得试了。
教程课件:https://cdn.lyshark.com/courseware/hookZ.zip
写教程不易,转载请加出处,谢谢 !!