软件安全实验-游戏外挂设计与防范
软件安全实验-游戏外挂设计与防范
【实验目的】
本实验通过分析Windows XP附带的扫雷游戏,实现自动扫雷的外挂程序。一方面了解内存外挂的相关技术,包括通过CE工具定位关键内存地址、读写进程内存数据、模拟鼠标操作等;另一方面分析防范此类外挂的方法,增强对游戏安全防护机制的理解。
【实验原理】
内存读写。
Windows操作系统为每个进程分配独立的虚拟地址空间,扫雷游戏的雷区数据、地雷数量、行列数、计时器等关键变量都存储在进程的内存空间中。通过Windows
API函数OpenProcess获取目标进程句柄后,可以使用ReadProcessMemory函数读取进程内存中的任意地址数据,从而获取雷区的完整信息。
CheatEngine工具。 CheatEngine(CE)是一款内存扫描工具,通过反复扫描目标进程中变化或固定的数值,可以定位到存储特定数据的内存地址。在本实验中,使用CE分别定位了地雷数量、游戏计时器、雷区行列数以及雷区首地址等关键内存位置。
雷区数据结构。
扫雷游戏的雷区在内存中以二维数组形式存储,每个格子用一个字节(Byte)标识。未点开的格子值为0x0F,未点开的地雷为0x8F,空格为0x40,地雷数量标识为0x41~0x46等。通过读取雷区内存数据,可以判断每个格子是否为地雷。
鼠标模拟。 Windows
API提供了mouse_event和SetCursorPos函数,可以模拟鼠标的移动和点击操作。通过将鼠标移动到扫雷游戏界面中对应格子的位置,模拟左键或右键单击,即可实现自动翻开或标记地雷的操作。
坐标转换。
Spy++记录的鼠标坐标以扫雷游戏窗口左上角为原点,而SetCursorPos以桌面左上角为原点。因此需要使用GetClientRect获取客户区信息,并通过ClientToScreen函数进行坐标系转换,将客户坐标转换为屏幕坐标。
Step 1 使用CheatEngine定位内存地址
(1)加载游戏进程
运行扫雷游戏,打开CheatEngine(CE),在CE界面中点击"选择进程"按钮,找到扫雷游戏的进程并加载。此时CE可以对该进程的内存空间进行扫描和读取操作。
(2)定位地雷数量的内存地址
在CE界面选择数据类型为4字节,扫描类型为"精确数值",数值内容填写扫雷游戏所显示的地雷数量(如10),然后单击"初次扫描"。随后调整扫雷游戏难度等级,使地雷数量发生改变,在CE界面填写新的地雷数量,单击"再次扫描"。不断重复此操作,直至CE左侧找到的地址数降为个位数。
最终定位到的地雷数量内存地址为:0x1005194。
(3)定位游戏计时器的内存地址
单击"新的扫描"按钮,选择数据类型为4字节,扫描类型为"精确数值",数值内容填写0,然后单击初次扫描。点开雷区任意一个格子使游戏开始计时,在CE界面将扫描类型改为"增加的数值",然后单击"再次扫描"。计时增加后重复操作,或计时增加到较高数值后开始新的一局,将扫描类型改为"减少的数值"再次扫描。
最终定位到的游戏计时器内存地址为:0x100579C。
(4)定位雷区行列数的内存地址
单击"新的扫描"按钮,选择数据类型为4字节,扫描类型为"精确数值",数值内容填写雷区的行数(如初级为9),然后单击初次扫描。在扫雷程序中单击"游戏–自定义"设置新的高度和宽度值,在CE界面填写新行或列的数值,单击"再次扫描"。不断重复此操作,直至地址数减少为个位数。
最终定位到的内存地址为:
存储雷区宽度的地址:
0x1005334存储雷区高度的地址:
0x1005338

(5)扫描雷区内存地址范围

单击CE"新的扫描"按钮,选择数据类型为Byte,扫描类型为"未知的初始数值",然后进行初次扫描。左键单击雷区左上角第一个格子,然后将扫描类型改为"变动的数值",再单击"再次扫描"。开启新一局游戏,重复此操作,直至左侧被标注为绿色的地址减少为个位数。
在扫描得到的多组地址中,通过右键"浏览相关内存区域",打开对应的内存空间,通过开始新一局游戏和点击雷区左上角的操作,确定雷区真实的内存起始地址以及各类标识数据。
Step 2 确定内存标识数据
.png)
(1)地雷标识数据
通过浏览相关内存区域并观察不同操作后的数据变化,确定地雷相关的标识数据如下:
未爆炸地雷:
0x8F爆炸地雷:
0x8A起爆点:
0xCC标上旗子:
0x8E标上问号:
0x8D
(2)非地雷标识数据
非地雷格子的标识数据如下:
未点开:
0x0F空格(周围无地雷):
0x40地雷个数(周围有1~8个地雷):
0x41、0x42、0x43、0x44、0x45、0x46……标旗:
0x0E标问号:
0x0D
(3)雷区每行格子数
通过创建新一局游戏,依次点击第1、2、3行最左侧格子的操作,确定雷区每行的起始地址,寻找起始地址与雷区行列之间的对应关系。每行格子的内存偏移为32个字节(即每行占32个Byte),雷区首地址从0x1005340开始,第\(i\)行第\(j\)列的格子在内存数组中的索引为\((i+1) \times 32 + (j+1)\)。
Step 3 使用Spy++确定格子大小
(1)获取窗口信息
打开Spy++,单击"监视–日志消息",勾选"隐藏Spy++选项",拖动"查找程序工具"至扫雷游戏主界面,获取扫雷窗口类(WNDCLASS)的名称以及标题名。

(2)记录鼠标消息坐标
切换至Spy++的"消息"窗口,先清除所有消息,然后勾选"WM_LBUTTONDOWN和WM_LBUTTONUP"两个消息。返回扫雷游戏界面,依次单击左上角、紧邻左上角右方、紧邻左上角下方的三个格子,查看Spy++记录的鼠标单击时的xPos和yPos的值。

(3)计算格子大小
根据Spy++记录的坐标数据,计算格子的平均宽和高度:
左上方第一个格子坐标:\((x=18, y=62)\)
左上方右一个格子坐标:\((x=36, y=61)\)
左上方下一个格子坐标:\((x=20, y=79)\)
水平方向:\(36 - 18 = 18\)像素,取整后约为16像素(考虑点击位置偏移);垂直方向:\(79 - 62 = 17\)像素,取整后约为16像素。对于初级难度(\(9 \times 9\))的进一步验证:
左上方坐标:\((x=19, y=61)\)
右上方坐标:\((x=151, y=60)\)
左下方坐标:\((x=20, y=191)\)
右下方坐标:\((x=148, y=192)\)
水平范围:\(151 - 19 = 132\)像素,每格宽度约为\(132 \div (9-1) \approx 16\)像素;垂直范围:\(191 - 61 = 130\)像素,每格高度约为\(130 \div (9-1) \approx 16\)像素。
最终确定:格子宽度为16像素,格子高度为16像素。
Step 4 编程实现自动扫雷
(1)实现逻辑
自动扫雷程序的实现逻辑如下:
创建WIN32空项目,编写源代码;
通过
FindWindow获取扫雷游戏窗口句柄,使用GetWindowThreadProcessId获取进程ID,再通过OpenProcess获取进程句柄;根据雷区宽度(地址
0x1005334)和高度(地址0x1005338)所在内存地址,使用ReadProcessMemory读取行和列的值;根据雷区首地址
0x1005340,读取完整的雷区数据;将扫雷游戏界面置顶(
SetForegroundWindow),遍历雷区第\(i\)行第\(j\)列的数值,判断是否为地雷:若为0x8F(未爆炸地雷)则模拟右键单击标记旗子;若为0x0F(未点开非地雷)则模拟左键单击翻开;根据扫雷游戏界面的客户坐标系,计算第\(i\)行第\(j\)列的坐标位置(起始坐标\(x=18, y=62\),步长\(16\)像素),通过
ClientToScreen转换为屏幕坐标,然后移动鼠标并执行鼠标操作;重复上述过程,直至雷区数据全部被扫描完成。
(2)关键API函数
本实验使用的核心Windows API函数包括:
读写内存:
1 | |
获取进程句柄:
1 | |
窗口操作:
1 | |
鼠标模拟:
1 | |
其中mouse_event函数的dwFlags参数取值为:MOUSEEVENTF_LEFTDOWN(左键按下)、MOUSEEVENTF_LEFTUP(左键弹起)、MOUSEEVENTF_RIGHTDOWN(右键按下)、MOUSEEVENTF_RIGHTUP(右键弹起)。
Step 5 运行结果
编译并运行自动扫雷程序,程序首先获取扫雷游戏窗口并读取雷区数据,然后将扫雷窗口置顶,逐格遍历雷区。对于每个格子,程序根据内存中读取的标识值判断其类型:若为未爆炸地雷(0x8F),则模拟右键单击标记旗子;若为未点开的非地雷格子(0x0F),则模拟左键单击翻开。程序能够在极短时间内完成整个雷区的扫描和操作,实现了自动扫雷功能。


【思考与总结】
(1)除了使用CE得到各项地址外,还有哪些方法可以得到这些地址?
除了CheatEngine之外,获取内存地址的方法还包括:
**调试器。**使用OllyDbg、x64dbg等调试工具附加到扫雷进程,通过反汇编和断点跟踪,可以找到读写相关内存位置的指令,从而逆向出关键变量的地址。例如在反汇编窗口中搜索访问雷区数据的mov指令,即可定位雷区首地址。
**内存特征码扫描。**通过分析扫雷程序的可执行文件,提取其中的特征码,可以在运行时通过模式匹配定位关键数据结构的地址。
**进程内存转储。**使用工具(如Process Hacker、VMMap等)将扫雷进程的内存空间导出为文件,然后在文件中搜索特定的数据模式,找到雷区等数据结构的位置。
**API Hook。**拦截扫雷程序对Windows
API的调用(如GetDlgItemInt等),可以观察程序读取了哪些内存地址的数据。
(2)除了通过模拟鼠标按键操作之外,还有哪些方法能够实现自动扫雷?
**直接修改内存。**使用WriteProcessMemory直接修改雷区内存数据,将所有未翻开的非地雷格子的值从0x0F改为空格值0x40,游戏会自动显示翻开后的效果,无需模拟鼠标操作。如下图所示,成功修改了游戏的内存区域,代码见附录。

**修改游戏逻辑。**通过修改扫雷程序的代码段(Code Patching),改变游戏的判断逻辑。例如将判断是否为地雷的跳转指令修改为永远不跳转,这样即使点击到地雷也不会爆炸。
**注入DLL。**将自定义的DLL注入到扫雷进程的地址空间中,直接在进程内部调用游戏自身的函数来实现翻格操作,这样可以绕过一些基于外部进程检测的防护机制。
**发送窗口消息。**使用SendMessage或PostMessage向扫雷窗口发送WM_LBUTTONDOWN和WM_LBUTTONUP消息,可以模拟鼠标点击而无需实际移动鼠标光标。
(3)如何防范内存型外挂?
内存型外挂的核心在于通过读写进程内存来获取或篡改游戏数据,防范此类外挂可以从以下几个方面入手:
**数据加密。**将游戏中的关键数据(如雷区信息、角色属性等)在内存中进行加密存储。游戏运行时使用自定义的解密算法在寄存器中临时解密使用,使用完毕后立即重新加密。这样即使外挂程序读取了内存数据,得到的也是加密后的无效数据。可以采用异或加密、RC4流加密等轻量级加密方案。
**内存校验。**在游戏的关键数据区域设置校验和,游戏运行过程中定期对内存数据进行完整性校验。一旦检测到内存被非法修改(如通过CE直接修改),立即终止游戏或触发反作弊机制。
**反调试检测。**检测是否有调试器附加到游戏进程。常用的检测方法包括:调用IsDebuggerPresent函数、检查PEB中的调试标志位、检测硬件断点(检查DR寄存器)、检测调试端口等。
**代码混淆与反逆向。**对游戏的可执行代码进行混淆处理,增加逆向分析的难度。例如增加虚假分支、内联函数消除函数调用特征、使用花指令干扰反汇编器等。
**进程保护。**通过驱动级别的保护(如内核态反作弊驱动),限制外部进程对游戏进程的内存读写权限。例如BattlEye、Easy
Anti-Cheat等商业反作弊系统就采用了驱动级别的进程保护机制,防止外部工具调用ReadProcessMemory和WriteProcessMemory。
**服务器端验证。**对于网络游戏,将关键逻辑放在服务器端验证。客户端仅负责显示和输入,所有游戏状态的变更都由服务器判定和同步。这样即使客户端内存被篡改,也无法影响服务器端的游戏逻辑,从根本上杜绝了内存型外挂的威胁。
【附录-完整源代码】
自动扫雷程序(sweeper.cpp)
1 | |
直接修改内存版本(sweeper_write.cpp)
1 | |