保卫萝卜PC版内存修改器分析、制作过程
0x00 前言
近期在 52 上看到一位大佬分享了基于 Cheat Engine
的 保卫萝卜破解教程
,欲初探一下逆向知识,并锻炼 Win32
编程技能,故有此篇。
文章尽可能详细记录了各个过程,既是自己学习路上的一点经验记录,又是一篇简单教程,期望能对一些像我一样的入门小白有所帮助。
0x01 游戏内存分析
(这一部分我写得详细些方便小白入门,大佬可跳过)
首先运行游戏,然后打开 CE
- 打开 LuoBo.exe
,点击 Attach debugger to process
附加调试器,然后随便开一个关卡,把游戏暂停。
我们看到游戏中的金币数量是 450 。因此在右侧,填入当前金币数 450
,并使用 First Scan
进行初次扫描。可以看到出现了一大堆地址。
接下来种一个炮塔,让金币发生变化,然后暂停游戏,此时金币值是
350。点击 Next Scan
筛选出变化后的金币值。
看到左侧列表中只有一个内存地址了,我们先右键把它加入地址列表。
因为当前获取到的是金币的动态地址,每次打开游戏,这个值都是变化的,无法制作成修改器。所以下面我们来寻找基址(手动),通过静态的指针实现对这个内存区域的稳定访问。
对金币的内存地址右键-Find Out what accesses this address
,然后继续游戏,打一个怪。这时暂停游戏,可以发现窗口中出现了一些汇编指令。
很明显,这个 add
指令就是用于增加金币的,而且金币的地址是 esi+0x74
。我们往下拉一下,可以看到 esi
的地址。我们把它选中复制下来。
回到 CE
主窗口,点一下 New Scan
,勾选
Hex
复选框,把刚才复制的粘贴进去,点击
First Scan
开始新的扫描。
可以看到,左边的地址中出现了相应结果。好消息是,列表中出现了绿色字样的
Luobo.exe+105E68
,这表明这是个静态地址,至此,金币基址的寻找工作告一段落。
接下来我们手动添加指针,尝试访问金币所在的内存区域,作为验证。
点击右下方 Add Address Manaually
按钮-勾选
Pointer
,在最下方填入 Luobo.exe+105E68
,倒数第二个框填写偏移量
74
,观察到最上方的内存地址中,运算出的值为
364
,恰好为金币数,说明没有问题。(有兴趣可以退掉游戏重开下,发现该指针仍可以访问到金币,而之前的动态地址不行)
按照前文提到的大佬的分析,金币是有校验的。我们需要破坏这个校验,防止程序闪退。第一步还是需要找到校验指令所在内存区域的基址。
在小窗口中选中 add
指令,点击右侧
Show disassembler
查看汇编与内存。
可以发现,金币在增加之后,有如下一段指令:
1 | mov eax,[esi+74] |
在其他金币减少的过程中,这段指令也出现,所以高度怀疑是验证代码。
上述代码实现了这样的功能:
- 把金币的数量(
[esi+74]
)存入eax
- 使用
dec
指令使得eax
减一 - 将
eax
的值存入[esi+0xEC]
可见,检测的机制大致就是比较两个地址上的金币相差是否为1.
所以接下来,只需要对 [esi+0xEC]
查访问就行了。
在主窗口下方,将金币的指针复制一份,把偏移量改成
EC
,右键查访问,选择
Find what accesss the address pointed at by this pointer
,接着再随意打怪或者升级下炮塔,然后暂停游戏。
从图中可以发现有 mov
指令把这个校验值读取到了
ecx
,选中这条指令,与之前相似,在右侧点击“查看汇编”按钮。
汇编有如下指令:
1 | mov ecx,[esi+000000EC] |
这些代码实现的是“将校验值加一之后与金币值比较,如果相等则跳转到
Luobo.exe+245F2
”。(对应操作码:0x74 0x70
)
我们双击 je
所在的那一行指令,将指令改为
jmp
即可实现无条件跳转。(对应操作码:0xEB 0x70
)
至此,金币修改就完全被破解了。随意修改金币,都不会造成游戏闪退。
这里,我们留意记下这条 je
指令的静态地址为
Luobo.exe+24580
,为后续编写内存修改器做准备。
0x02 修改器程序流程构建
总体思路
flowchart LR
START(开始) --> READ[打开进程并读取] --> MODIFY[修改内存] --> END(结束)
具体一点点
和函数名做一个对应。
flowchart TD
START("开始")
END("释放资源<br/>结束程序")
subgraph FIND[" "]
B0[["查找进程"]]
B0 -.- B1
B0 -.- B2
B1["findWindow();"]
B2["getPID();"]
end
subgraph ACCESS[" "]
C0[["访问并读取"]]
C0 -.- C1
C0 -.- C2
C1["openProcess();"]
C2["getAddress();"]
end
subgraph MOD[" "]
D0[["修改内存"]]
D0 -.- D1
D0 -.- D2
D2 --> D3 --> D4 --> D5
D1["modifyJumpCheck();"]
D2[/"金币修改循环<br/>loopContinueFlag=TRUE"\]
D3["modifyCoinNum();"]
D4["Sleep();"]
D5[\"金币修改循环"/]
end
START ==> FIND ==> ACCESS ==> MOD
D5 --> END
0x03 修改器代码实现
(模块化叙述,可能有不准确之处,具体代码见开源工程)
CMakeLists
管理项目的 CMakeLists.txt
:
比较重要的是添加上 UAC 权限的请求。
1 | cmake_minimum_required(VERSION 3.23) |
Headers
普普通通头文件,win32编程的必备。
1 |
查找窗体
先来个全局变量用于储存句柄信息:
1 | HWND hwnd; |
然后开始查找游戏窗体,以便后续获取 PID
:
1 | void findWindow(void) |
上面的代码加入了判断,如果不存在就报错。
获取PID
拿到句柄以后,可以据此来找到 PID
,方便后续访问进程。
还是来个全局变量储存下 PID
:
1 | DWORD pid; |
接下来是函数:
1 | void getPID(void) |
访问进程
接下来根据 PID
去访问进程,先定义个全局变量:
1 | HANDLE hProcess; |
接下来编写对应函数:
1 | void openProcess(void) |
值得注意的是,在调用 OpenProcess
函数时,传入参数中
dwDesiredAccess
项需要添加
PROCESS_QUERY_INFORMATION
,否则虽然在 Windown10
上测试正常,但在 Windows7 上会出现 ErrorCode: 5
错误,即“拒绝访问”。
其它信息可参考官方文档:process access rights
内存地址获取与计算
打开进程后,需要进行内存地址读取计算。
模块基址
前文提到的地址都是 Luobo.exe+XXXXX
形式,但实际使用中,我们并不知道 Luobo.exe
的内存地址,因此第一步就是获取模块基址。
先定义一个结构体用来保存信息:
1 | struct |
获取模块基址的方法很多,这里主要用到
EnumProcessModulesEx
函数,读取出来就是 CE 中的
Luobo.exe
,是一个指针。
1 | ... |
各变量基址
金币基址获取如下。注意,金币的基址本身是一个指针,其值(即金币变量所在的地址)为
Luobo.exe+0x105E68
所指向的内容加上 0x74
。
1 | ... |
je
指令的基址则简单些,直接把模块基址加上偏移量就行。
1 | ... |
注:getAddress();
函数完整代码请见源码。
内存修改
基址也已经到手,现在“万事俱备,只欠东风”。先把金币的校验破坏掉,就可以随意修改金币了。
金币校验修改
先访问下基址,读取看看是不是找到了关键的那条 je
指令。ReadProcessMemory
函数将内存
Address.jumpCheckBase
处的数值读出,并保存到
tempBuf
之中。
1 | void modifyJumpCheck(void) |
接下来是比较,如果 tempBuf
的内容和
originalCode
的内容一样,说明金币校验还没有被修改,这时我们就可以调用
WriteProcessMemory
把 targetCode
的内容写入进去。
1 | ...接上一代码段 |
至此,金币校验就被破坏了。由于该内存区域在游戏运行过程中不会被再次修改,所以这里在游戏开始运行时修改一次即可达成目的,可谓“一劳永逸”。
金币数量修改
接下来就是修改金币数了。先读取一下当前金币数,如果比目标值(666666)小 1000,则把金币修改为目标值。
1 | void modifyCoinNum(void) |
由于金币数实时变动,届时可以把该函数放入循环中,实现不间断的监测和修改。
main 函数
依次调用上述函数即可:
1 | int main() |
0x04 修改器细节优化
截至当前,修改器的基本功能已经完成,接下来是一些细节上的完善,以得到“锦上添花”的效果。
SIGNAL捕获
在用户按下 Ctrl+C
以及尝试关闭程序、关机、注销等时刻,系统会发送不同的信号导致进程终止,此时我们仍有些打开的句柄没有释放,因此我们可以捕获此类信号,添加自定义的处理流程。
我们可以定义一个函数用来关闭句柄,清理内核对象。(后来了解到,实际上整个进程结束后,内核也会回收这些资源,这里就留作记录)
1 | void sweep(void) |
然后声明一下自定义的处理流程,在流程中调用上述函数。
1 | BOOL WINAPI CtrlHandler(DWORD fdwCtrlType) |
声明完以后并不是万事大吉,不要忘记将其注册。注册通常放在
main
函数之中。
1 | SetConsoleCtrlHandler(CtrlHandler, TRUE); |
提示:如需取消注册自定义的处理流程,将上一行代码的 TRUE
改为 FALSE
执行一遍即可。
为程序添加图标
在 CMakeLists.txt
所在目录新建 res
资源文件夹,在文件夹中放入图标 logo.ico
,并新建资源描述文件 logo.rc
,编写以下内容:
1 | IDI_ICON1 ICON DISCARDABLE "logo.ico" |
然后在 CMake 中将其添加到目标中。
1 | add_executable(LuoBo_Mod main.c res/logo.rc) |
日志分级与彩色文字
在调试与发布的工程中,日志分级能带来很大的便利。彩色文字则让不同级别的输出更加明显,界面更加美观。
主要过程就是引入一对 .c/.h
文件,并添加到 CMake
目标,设置好宏参数,然后把 printf
按照所需等级对应替换为
pr_info
、pr_warn
、pr_err
、pr_bug
等。(注意,该方法实现的彩色在 Windows7
系统上并不奏效,故可以通过宏参数控制编译出无色彩版本。)
详细过程可以参靠我之前写过的 这篇文章 ,这里就不详述了。
条件编译控制
CMake 是一个强大的工具,通过更改 CMake 配置文件,我们就可以实现刚才提到的一些条件选项。
首先设置 “彩色版” 和 “无色版” 两个编译目标。
1 | add_executable(LuoBo_Mod_Colorful main.c main.h main.h print.h print.c res/logo.rc) # 彩色版本 |
接下来就可以根据构建的类型(CMAKE_BUILD_TYPE
)配置输出等级,根据构建目标(target
)设置是否启用色彩。
1 | # 根据目标配置颜色类型 |
优化后流程图
添加上细节的优化之后,程序的工作流程如下图。
flowchart TD
START("开始")
SIGNAL>"开始监听信号"]
CHECK(["有错误发生"])
ERROR["错误提示"]
END("释放资源<br/>结束程序")
subgraph CONSOLE[" "]
A0[["设置终端属性"]]
A0 -.- A1
A0 -.- A2
A0 -.- A3
A1["system(title xxx);"]
A2["SetConsoleCtrlHandler();"]
A3["enableColorful();"]
end
subgraph FIND[" "]
B0[["查找进程"]]
B0 -.- B1
B0 -.- B2
B1["findWindow();"]
B2["getPID();"]
end
subgraph ACCESS[" "]
C0[["访问并读取"]]
C0 -.- C1
C0 -.- C2
C1["openProcess();"]
C2["getAddress();"]
end
subgraph MOD[" "]
D0[["修改内存"]]
D0 -.- D1
D0 -.- D2
D2 --> D3 --> D4 --> D5
D1["modifyJumpCheck();"]
D2[/"金币修改循环<br/>loopContinueFlag=TRUE"\]
D3["modifyCoinNum();"]
D4["Sleep();"]
D5[\"金币修改循环"/]
end
START ==> |条件编译控制| CONSOLE ==> FIND ==> ACCESS ==> MOD
CONSOLE --> SIGNAL -.-> |"收到信号"| END
FIND --> CHECK
ACCESS --> CHECK
MOD --> CHECK
CHECK --> ERROR --> END
D5 --> END
0x05 效果展示
放几张效果图(在 Windows Terminal 运行效果最佳)
游戏界面
Debug 彩色版
Release 彩色版
Release 无色版
0xFE 下载地址
项目在 Github 开源:https://github.com/hui-shao/LuoBo_Mod.git
Release 页面中有构建好的二进制文件可下载运行。
对应游戏:【怀旧游戏】保卫萝卜 Beta 绿化版
0xFF 结束
首次尝试逆向分析和内存修改器制作,若有不对之处恳请指正,不尽感激。
首发于个人博客及 52pojie 论坛,转载请注明。