通过劫持EndScene实现菜单绘制
在DirectX 9环境中,菜单绘制是图形应用程序中一个常见且重要的功能。相比于使用D3DDrawLib外部绘图库绘制图形的不稳定和高资源占用率来说,内部绘制方法提供了更为流程化和稳定的解决方案。本文将介绍如何通过挂钩IDirect3DDevice9接口中的EndScene函数,来实现一个内部D3D菜单的绘制案例。
EndScene函数在DirectX 9
中扮演着重要角色,它主要用于实现最终的图像渲染及展现功能,这些操作包括清空缓冲区中的图像、设置视口及其他渲染状态、执行顶点和像素着色器,最终在后台缓冲区生成完整的渲染图像并将其呈现到屏幕上,从而完成一次绘制操作。
若要实现在指定进程内部增加菜单的功能,则需要对EndScene
函数进行Hook
挂钩,EndScene是IDirect3DDevice9接口中的第43
个函数,读者可自行打开d3d9.h
头文件信息,并将光标移动到473
行,此处就是我们需要挂钩的函数。
首先我们需要编写一段可以任意地址Hook
的通用代码,此类代码的实现市面上也有很多可供参考的案例,当然若不想自己实现Hook函数的编写,也可以使用MinHook
、Detours
、EasyHook
、Frida
等第三方类库,此处我们就使用一段简单的代码来代替如下代码所示。
挂钩与摘钩
首先定义一个JmpCode
结构体,以32位为例,这个 JmpCode
结构体用于创建一个相对跳转(JMP)指令。它包含一个 jmp
字节(值为0xE9
)和一个 address
字段,该字段存储计算得到的跳转偏移量。构造函数接受源地址和目标地址,并通过调用 setAddress
方法计算并设置跳转偏移量,从而实现函数挂钩或代码重定向的功能。
它包括两个版本,分别适用于32位和64位系统:
32位系统版本:
- 使用一个字节的
jmp
指令(值为0xE9
)和一个32位的地址偏移量。 - 构造函数通过计算目标地址与源地址之间的偏移量来设置跳转地址。
64位系统版本:
- 使用
FF 25 00 00 00 00
的跳转指令前缀和一个64位的绝对地址。 - 构造函数直接设置目标地址。
接着定义了两个函数,其中hook
用于挂钩,而unhook
则用于摘除钩子,以挂钩函数为例,在其内部首先通过调用JmpCode
结构体生成一个相对跳转的汇编指令集,接着通过使用VirtualProtect
将这段内存设置为读写属性,并将机器码拷贝到其中替换,当替换后再次调用VirtualProtect
恢复内存属性,取消Hook的原理类似。
- hook函数:将指定的原始函数替换为自定义函数,并保存原始函数的代码,以便后续取消挂钩。
- unhook函数:恢复原始函数的代码,取消自定义函数的挂钩。
总结起来的代码如下所示,其中包含了所需调用的头文件信息;
|
全局变量配置
接着我们继续来定义所需变量,如下代码所示,代码中定义了一些全局变量和结构,用于实现自绘菜单和Direct3D的函数劫持。具体包括:
- Direct3D相关变量:如
endSceneAddr
(EndScene函数地址)、g_font
(字体对象)、d3dLine
(线条对象)等。 - 键盘输入相关变量:定义了获取按键状态的函数指针
pfnGetAsyncKeyState
。 - 菜单相关变量:包括窗口宽高(
WindowWidth
和WindowHeight
)、当前选中的菜单项(CurrentlySelected
)、菜单显示状态(IsItDisplayed
)、菜单显示位置(MenuOnTheRight
)和各功能项的状态(如功能项A
到功能项G
)。
这些变量和结构为后续的菜单显示、键盘输入处理以及Direct3D渲染劫持提供了基础。
// --------------------------------------------------- |
键盘回调事件
键盘回调部分,我们通过在入口处使用GetProcAddress
动态获取到GetAsyncKeyState
函数状态,该函数可用于监控键盘热键,得到地址后放入到pfnGetAsyncKeyState
全局变量中存储,通过CreateThread
创建一个CheckKey
线程,该线程中用于监控热键是否为,VK_UP、VK_DOWN、VK_LEFT、VK_RIGHT、以及VK_HOME热键,并根据不同的热键做出对选中菜单的调整,其代码如下所示;
// --------------------------------------------------------------------------------- |
动态绘图函数
绘制部分函数如下所示,其中包括绘制线条、方框、空心矩形、单行文本和双重文本。
- DrawLine:函数用于绘制指定宽度和颜色的线条;
- DrawBox:函数绘制一个指定位置、宽度、高度和颜色的方框;
- DrawBorderRectangle:函数绘制一个空心矩形,通过绘制四条边线来实现;
- DrawString:函数在指定位置绘制单行文本,使用指定的字体大小和颜色;
- DrawStringAndString:函数则在指定位置绘制双重文本,包括提示文字和命令选择,分别使用不同的颜色。
这些函数利用了Direct3D
的绘图功能,通过调用Direct3D
的API实现各种图形和文本的绘制,其代码如下所示;
// --------------------------------------------------------------------------------- |
当有了上述函数的封装,那么绘制一个菜单将变得非常容易,如下DrawArea
函数负责绘制一个自定义的菜单界面,包括菜单的边框、表头、功能项及其状态、选择条和底部的显示/隐藏按钮。通过判断IsItDisplayed
变量是否为真来决定是否显示菜单,并根据预设的菜单配置(如宽度、高度、颜色等)动态计算和绘制各个元素的位置和样式。菜单功能项的名称和状态存储在数组中,并通过遍历这些数组来绘制各个功能项及其当前状态。
// --------------------------------------------------------------------------------- |
自定义EndScene方法
EndScene 方法是IDirect3DDevice9
接口中的重要成员,它是每帧渲染循环中的一个关键步骤,通常与BeginScene
和Present
一起使用。该方法用于通知Direct3D
设备当前的绘制操作已经结束,并准备将这些绘制操作提交到渲染管线进行处理。
由于EndScene是一个在每帧渲染时都会调用的方法,因此它成为了许多自定义UI绘制的理想挂钩点。通过挂钩EndScene
,开发者可以在游戏或应用程序的每一帧渲染完毕之前插入自定义的绘制代码,并以此来实现图形功能的扩展,如下代码中我们在每次渲染之前插入DrawArea
来实现自定义菜单的绘制。
// 自定义转向函数 |
初始化与开始绘制
通过调用initHookThread
初始化一个挂钩线程,用于劫持Direct3D
的EndScene
方法。具体操作包括注册一个窗口类并创建窗口,然后初始化Direct3D
设备,并通过GetVtableFunAddr
来获取设备的虚函数表地址,并以此来定位EndScene
函数,对其进行劫持。成功劫持后,释放Direct3D
对象并销毁窗口。
// 获取虚函数地址 |
在主函数中,我们通过GetProcAddress
获取到GetAsyncKeyState
键盘监控函数地址,并创建CheckKey
用于监控热键,通过创建initHookThread
初始化挂钩函数,则此时挂钩已实现。
// --------------------------------------------------------------------------------- |
读者可自行组合上述代码片段,并将其编译为DLL
动态链接库文件,通过使用LyInjector
注入器或使用其他通用注入工具将模块注入到窗体中,如下所示;
C:\> LyInjector InjectDLL --proc lyshark.exe --dll hook.dll |
当模块被注入后,原EndScene
函数将被替换,此时每当进程调用EndScene
绘制函数时都会经过MyEndScene
自定义函数。
在自定义函数中我们增加DrawArea
函数,当执行结束后在让其恢复到EndScene
原函数上继续执行,如下所示;
挂钩后的绘制效果如下图所示,菜单将被显示出来;
警告:本篇文章中所涉及的内容仅用于技术交流与研究之用,仅允许被用于正规用途或学习目的,请读者自觉遵守相关法规,禁止滥用。