保卫萝卜PC版内存修改器分析、制作详细过程

文章正文
发布时间:2025-03-25 07:31

001.png (235.25 KB, 下载次数: 1)

下载附件

2022-8-25 23:17 上传

近期看到 @空竹 大佬分享了基于 Cheat Engine 的 保卫萝卜破解教程 ,欲初探一下逆向知识,并锻炼 Win32 编程技能,故有此篇。

文章尽可能详细记录了各个过程,既是自己学习路上的一点经验记录,又是一篇简单教程,期望能对一些像我一样的入门小白有所帮助。

0x01 游戏内存分析

(这一部分我写得详细些方便小白入门)

首先运行游戏,然后打开 CE - 打开 LuoBo.exe ,点击 Attach debugger to process 附加调试器,然后随便开一个关卡,把游戏暂停。

我们看到游戏中的金币数量是 450 。因此在右侧,填入当前金币数  450 ,并使用 First Scan 进行初次扫描。可以看到出现了一大堆地址。

2022-08-24_151540.png (426.57 KB, 下载次数: 1)

下载附件

2022-8-25 23:18 上传

接下来种一个炮塔,让金币发生变化,然后暂停游戏,此时金币值是 350。点击 Next Scan 筛选出变化后的金币值。

看到左侧列表中只有一个内存地址了,我们先右键把它加入地址列表。

2022-08-24_152159.png (412.18 KB, 下载次数: 1)

下载附件

2022-8-25 23:18 上传

因为当前获取到的是金币的动态地址,每次打开游戏,这个值都是变化的,无法制作成修改器。所以下面我们来寻找基址(手动),通过静态的指针实现对这个内存区域的稳定访问。

对金币的内存地址右键-Find Out what accesses this address,然后继续游戏,打一个怪。这时暂停游戏,可以发现窗口中出现了一些汇编指令。

2022-08-24_153209.png (378.98 KB, 下载次数: 0)

下载附件

2022-8-25 23:19 上传

很明显,这个 add 指令就是用于增加金币的,而且金币的地址是 esi+0x74 。我们往下拉一下,可以看到 esi 的地址。我们把它选中复制下来。

2022-08-24_153311.png (374.63 KB, 下载次数: 0)

下载附件

2022-8-25 23:19 上传

回到 CE 主窗口,点一下 New Scan ,勾选 Hex 复选框,把刚才复制的粘贴进去,点击 First Scan 开始新的扫描。

可以看到,左边的地址中出现了相应结果。好消息是,列表中出现了绿色字样的 Luobo.exe+105E68 ,这表明这是个静态地址,至此,金币基址的寻找工作告一段落。

2022-08-24_153924.png (384.01 KB, 下载次数: 0)

下载附件

2022-8-25 23:19 上传

接下来我们手动添加指针,尝试访问金币所在的内存区域,作为验证。

点击右下方 Add Address Manaually 按钮-勾选 Pointer ,在最下方填入 Luobo.exe+105E68 ,倒数第二个框填写偏移量 74,观察到最上方的内存地址中,运算出的值为 364,恰好为金币数,说明没有问题。(有兴趣可以退掉游戏重开下,发现该指针仍可以访问到金币,而之前的动态地址不行)

2022-08-24_155240.png (219.8 KB, 下载次数: 0)

下载附件

2022-8-25 23:19 上传

按照前文提到的大佬的分析,金币是有校验的。我们需要破坏这个校验,防止程序闪退。第一步还是需要找到校验指令所在内存区域的基址。

在小窗口中选中 add 指令,点击右侧 Show disassembler 查看汇编与内存。

2022-08-24_172248.png (186.54 KB, 下载次数: 1)

下载附件

2022-8-25 23:20 上传

可以发现,金币在增加之后,有如下一段指令:

mov eax,[esi+74] dec eax mov [esi+000000EC],eax

在其他金币减少的过程中,这段指令也出现,所以高度怀疑是验证代码。

上述代码实现了这样的功能:

把金币的数量([esi+74])存入 eax

使用 dec 指令使得 eax 减一

将 eax 的值存入 [esi+0xEC]

可见,检测的机制大致就是比较两个地址上的金币相差是否为1.

所以接下来,只需要对 [esi+0xEC] 查访问就行了。

在主窗口下方,将金币的指针复制一份,把偏移量改成 EC,右键查访问,选择 Find what accesss the address pointed at by this pointer ,接着再随意打怪或者升级下炮塔,然后暂停游戏。

2022-08-24_175311.png (464.23 KB, 下载次数: 0)

下载附件

2022-8-25 23:20 上传

2022-08-24_175445.png (375.81 KB, 下载次数: 1)

下载附件

2022-8-25 23:20 上传

从图中可以发现有 mov 指令把这个校验值读取到了 ecx,选中这条指令,与之前相似,在右侧点击“查看汇编”按钮。

2022-08-24_175756.png (325.95 KB, 下载次数: 0)

下载附件

2022-8-25 23:21 上传

汇编有如下指令:

mov ecx,[esi+000000EC] mov eax,00000001 add ecx,eax cmp ecx,[esi+74] je Luobo.exe+245F2

这些代码实现的是“将校验值加一之后与金币值比较,如果相等则跳转到 Luobo.exe+245F2”。(对应操作码:0x74 0x70)

我们双击 je 所在的那一行指令,将指令改为 jmp 即可实现无条件跳转。(对应操作码:0xEB 0x70)

2022-08-24_180254.png (324.95 KB, 下载次数: 1)

下载附件

2022-8-25 23:21 上传

至此,金币修改就完全被破解了。随意修改金币,都不会造成游戏闪退。

这里,我们留意记下这条 je 指令的静态地址为 Luobo.exe+24580 ,为后续编写内存修改器做准备。

0x02 修改器程序流程构建 总体思路

mermaid-1.svg.png (24.66 KB, 下载次数: 0)

下载附件

2022-8-25 23:22 上传

具体一点点

和函数名做一个对应。

mermaid-2.svg.png (98.46 KB, 下载次数: 0)

下载附件

2022-8-25 23:22 上传

0x03 修改器代码实现

(模块化叙述,可能有不准确之处,具体代码见开源工程)

CMakeLists

管理项目的 CMakeLists.txt:

比较重要的是添加上 UAC 权限的请求。

cmake_minimum_required(VERSION 3.23) project(LuoBo_Mod C) add_executable(LuoBo_Mod main.c) # 添加UAC请求管理员权限 set_target_properties(LuoBo_Mod PROPERTIES LINK_FLAGS " /MANIFESTUAC:\"level='requireAdministrator' uiAccess='false'\" ") Headers

普普通通头文件,win32编程的必备。

#include <windows.h> #include <psapi.h> #include "print.h" 查找窗体

先来个全局变量用于储存句柄信息:

HWND hwnd;

然后开始查找游戏窗体,以便后续获取 PID:

void findWindow(void) {     hwnd = FindWindow(NULL, "保卫萝卜Beta");     if (hwnd == NULL)     {         printf("无法获取窗口句柄,请检查进程是否存在!  Code: %lu\n", GetLastError());         exit(0);     } }

上面的代码加入了判断,如果不存在就报错。

获取PID

拿到句柄以后,可以据此来找到 PID ,方便后续访问进程。

还是来个全局变量储存下 PID:

DWORD pid;

接下来是函数:

void getPID(void) {     GetWindowThreadProcessId(hwnd, &pid); // 获取 pid     if (pid == 0)     {         printf("无法获取PID!  Code: %lu\n", GetLastError());         exit(0);     }     printf("PID: %lu\n", pid); } 访问进程

接下来根据 PID 去访问进程,先定义个全局变量:

HANDLE hProcess;

接下来编写对应函数:

void openProcess(void) {     hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION | PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, pid);     if (hProcess == NULL)     {         printf("无法获取句柄ID!  Code: %lu\n", GetLastError());         exit(0);     }     printf("hProcess ID: %lu\n", (unsigned long) hProcess); }

值得注意的是,在调用 OpenProcess 函数时,传入参数中 dwDesiredAccess 项需要添加 PROCESS_QUERY_INFORMATION ,否则虽然在 Windown10 上测试正常,但在 Windows7 上会出现 ErrorCode: 5 错误,即“拒绝访问”。

其它信息可参考官方文档:process access rights

内存地址获取与计算

打开进程后,需要进行内存地址读取计算。

模块基址

前文提到的地址都是 Luobo.exe+XXXXX 形式,但实际使用中,我们并不知道 Luobo.exe 的内存地址,因此第一步就是获取模块基址。

先定义一个结构体用来保存信息:

struct {     DWORD moduleBase;     DWORD module;     DWORD coinNumBase;     DWORD coinCheckBase;     DWORD jumpCheckBase; } Address;

获取模块基址的方法很多,这里主要用到 EnumProcessModulesEx 函数,读取出来就是 CE 中的 Luobo.exe,是一个指针。

... HMODULE hModule[128] = {0}; DWORD dwRet = 0; int bRet = EnumProcessModulesEx(hProcess, (HMODULE *) (hModule), sizeof(hModule), &dwRet, LIST_MODULES_ALL); if (bRet == 0) {     printf("EnumProcessModules Failed.  Code: %lu\n", GetLastError());     exit(0); } Address.moduleBase = (DWORD) hModule[0];  // 数组首元素即基地址 ... 各变量基址

金币基址获取如下。注意,金币的基址本身是一个指针,其值(即金币变量所在的地址)为 Luobo.exe+0x105E68 所指向的内容加上 0x74。

... ReadProcessMemory(hProcess, (LPCVOID) (Address.moduleBase + 0x105E68), &Address.coinNumBase, 4, NULL); Address.coinNumBase += 0x74; printf("金币基址: 0x%p\n", (void *) Address.coinNumBase); ...

je 指令的基址则简单些,直接把模块基址加上偏移量就行。

... Address.jumpCheckBase = Address.moduleBase + 0x24580; printf("跳转校验基址: 0x%p\n", (void *) Address.jumpCheckBase); ...

注:getAddress(); 函数完整代码请见源码。

内存修改

基址也已经到手,现在“万事俱备,只欠东风”。先把金币的校验破坏掉,就可以随意修改金币了。

金币校验修改

先访问下基址,读取看看是不是找到了关键的那条 je 指令。ReadProcessMemory 函数将内存 Address.jumpCheckBase 处的数值读出,并保存到 tempBuf 之中。

void modifyJumpCheck(void) {     static const BYTE originalCode[] = {0x74, 0x70};  // je 未修改时操作码     static const BYTE targetCode[] = {0xEB, 0x70};  // jmp 修改后的操作码     BYTE tempBuf[2] = {0};     ReadProcessMemory(hProcess, (LPCVOID) Address.jumpCheckBase, tempBuf, sizeof(tempBuf), NULL);     printf("Previous Code: %x %x\n", tempBuf[0], tempBuf[1]); ...未完

接下来是比较,如果 tempBuf 的内容和 originalCode 的内容一样,说明金币校验还没有被修改,这时我们就可以调用 WriteProcessMemory 把 targetCode 的内容写入进去。

...接上一代码段         if (tempBuf[0] == originalCode[0] && tempBuf[1] == originalCode[1])     {         printf("CoinNumCheck patch point found. Trying to patch.\n");         WriteProcessMemory(hProcess, (LPVOID) Address.jumpCheckBase, targetCode, sizeof(targetCode), NULL);     } else if (tempBuf[0] == targetCode[0] && tempBuf[1] == targetCode[1])     {         printf("CoinNumCheck has already been patched.\n");     } else     {         printf("Unknown CoinNumCheck patch state.\n");     } }

至此,金币校验就被破坏了。由于该内存区域在游戏运行过程中不会被再次修改,所以这里在游戏开始运行时修改一次即可达成目的,可谓“一劳永逸”。

金币数量修改

接下来就是修改金币数了。先读取一下当前金币数,如果比目标值(666666)小 1000,则把金币修改为目标值。

void modifyCoinNum(void) {     DWORD coinNum_previous = 0;     ReadProcessMemory(hProcess, (LPCVOID) Address.coinNumBase, &coinNum_previous, 4, NULL);     printf("Previous Coin Num: %lu\n", coinNum_previous);     DWORD coinNum_target = 666666;     if (coinNum_previous < coinNum_target - 1000)     {         WriteProcessMemory(hProcess, (LPVOID) Address.coinNumBase, &coinNum_target, 4, NULL);         printf("Set Coin Num to: %lu\n", coinNum_target);     } }

由于金币数实时变动,届时可以把该函数放入循环中,实现不间断的监测和修改。

main 函数

依次调用上述函数即可:

int main() {     findWindow();     getPID();     openProcess();     getAddress();     modifyJumpCheck();     while (1)     {         modifyCoinNum();         Sleep(5 * 1000);     }     return 0; } 0x04 修改器细节优化

截至当前,修改器的基本功能已经完成,接下来是一些细节上的完善,以得到“锦上添花”的效果。

SIGNAL捕获

在用户按下 Ctrl+C 以及尝试关闭程序、关机、注销等时刻,系统会发送不同的信号导致进程终止,此时我们仍有些打开的句柄没有释放,因此我们可以捕获此类信号,添加自定义的处理流程。

我们可以定义一个函数用来关闭句柄,清理内核对象。(后来了解到,实际上整个进程结束后,内核也会回收这些资源,这里就留作记录)

void sweep(void) {     if (hProcess != NULL)     {         CloseHandle(hProcess);         hProcess = NULL;     } }

然后声明一下自定义的处理流程,在流程中调用上述函数。

BOOL WINAPI CtrlHandler(DWORD fdwCtrlType) {     switch (fdwCtrlType)     {         // Handle the CTRL-C signal.         case CTRL_C_EVENT:             pr_warn("Ctrl-C event\n\n");             Beep(750, 300);             loopContinueFlag = FALSE;             return TRUE;             // CTRL-CLOSE: confirm that the user wants to exit.         case CTRL_CLOSE_EVENT:             Beep(600, 200);             pr_warn("Ctrl-Close event\n\n");             sweep();             _exit(0);             // Pass other signals to the next handler.         case CTRL_BREAK_EVENT:             Beep(900, 200);             pr_warn("Ctrl-Break event\n\n");             sweep();             _exit(0);         case CTRL_LOGOFF_EVENT:             Beep(1000, 200);             pr_warn("Ctrl-Logoff event\n\n");             sweep();             _exit(0);         case CTRL_SHUTDOWN_EVENT:             Beep(750, 500);             pr_warn("Ctrl-Shutdown event\n\n");             sweep();             _exit(0);         default:             return FALSE;     } }

声明完以后并不是万事大吉,不要忘记将其注册。注册通常放在 main 函数之中。

SetConsoleCtrlHandler(CtrlHandler, TRUE);

提示:如需取消注册自定义的处理流程,将上一行代码的 TRUE 改为 FALSE 执行一遍即可。

为程序添加图标

在 CMakeLists.txt 所在目录新建 res 资源文件夹,在文件夹中放入图标 logo.ico ,并新建资源描述文件 logo.rc,编写以下内容:

IDI_ICON1 ICON DISCARDABLE "logo.ico"

然后在 CMake 中将其添加到目标中。

add_executable(LuoBo_Mod main.c res/logo.rc) 日志分级与彩色文字

在调试与发布的工程中,日志分级能带来很大的便利。彩色文字则让不同级别的输出更加明显,界面更加美观。

主要过程就是引入一对 .c/.h 文件,并添加到 CMake 目标,设置好宏参数,然后把 printf 按照所需等级对应替换为 pr_info、pr_warn、pr_err、pr_bug 等。(注意,该方法实现的彩色在 Windows7 系统上并不奏效,故可以通过宏参数控制编译出无色彩版本。)

详细过程可以参靠我之前写过的 这篇文章 ,这里就不详述了。

条件编译控制

CMake 是一个强大的工具,通过更改 CMake 配置文件,我们就可以实现刚才提到的一些条件选项。

首先设置 “彩色版” 和 “无色版” 两个编译目标。

add_executable(LuoBo_Mod_Colorful main.c main.h main.h print.h print.c res/logo.rc)  # 彩色版本 add_executable(LuoBo_Mod_NoColor main.c main.h main.h print.h print.c res/logo.rc)   # 无色版本

接下来就可以根据构建的类型(CMAKE_BUILD_TYPE)配置输出等级,根据构建目标(target)设置是否启用色彩。

# 根据目标配置颜色类型 target_compile_definitions(LuoBo_Mod_Colorful PRIVATE PRINT_COLORFUL=1) target_compile_definitions(LuoBo_Mod_NoColor PRIVATE PRINT_COLORFUL=0) # 根据编译类型选择日志等级 if (CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Debug"))     add_compile_definitions(PRINT_LEVEL=LEVEL_DEBUG) else ()     add_compile_definitions(PRINT_LEVEL=LEVEL_INFO) endif () 优化后流程图

添加上细节的优化之后,程序的工作流程如下图。

mermaid-3.svg.png (101.89 KB, 下载次数: 0)

下载附件

2022-8-25 23:27 上传

0x05 效果展示

放几张效果图(在 Windows Terminal 运行效果最佳)

游戏界面

2022-08-25_183719.png (1.59 MB, 下载次数: 0)

下载附件

2022-8-25 23:27 上传

Debug 彩色版

2022-08-25_184058.png (120.09 KB, 下载次数: 0)

下载附件

2022-8-25 23:28 上传

Release 彩色版

2022-08-25_184502.png (74.04 KB, 下载次数: 0)

下载附件

2022-8-25 23:28 上传

Release 无色版

2022-08-25_184548.png (59.36 KB, 下载次数: 1)

下载附件

2022-8-25 23:28 上传

0xFE 下载地址

项目在 Github 开源:https://github.com/hui-shao/LuoBo_Mod.git

对应游戏在我们 52 上就能找到:【怀旧游戏】保卫萝卜 Beta 绿化版

0xFF 结束

首次尝试逆向分析和内存修改器制作,若有不对之处恳请指正,不尽感激。

首发于 个人博客 及 52pojie 论坛,转载请注明。

 

免费评分 参与人数 62威望 +2 吾爱币 +156 热心值 +55 理由
xiuerjiayou     + 1   + 1   好长,看的有点迷。。  
wrsndm       + 1   我很赞同!  
silencewd     + 1   + 1   我很赞同!  
agthe     + 1   + 1   谢谢@Thanks!  
jiangzhikuan     + 1   + 1   用心讨论,共获提升!  
yuxs520     + 1   + 1   对新手很有帮助  
SPXJ1234     + 1   + 1   我很赞同!  
Z146459     + 1     我很赞同!  
Mist_G     + 1   + 1   我很赞同!  
HellClown     + 1   + 1   谢谢@Thanks!  
ceniria     + 1     我很赞同!  
bullshit     + 1   + 1   谢谢@Thanks!  
蜗牛也很牛     + 1   + 1   我很赞同!  
lien426     + 1   + 1   卧槽喂到嘴里了!!!  
RickSanchez     + 1   + 1   我很赞同!  
xcpg     + 1     我很赞同!  
luckydat     + 1     谢谢@Thanks!  
大方     + 1   + 1   感谢楼主的帮助  
soyadokio     + 1   + 1   用心讨论,共获提升!  
qact     + 1   + 1   大佬,有木有用手机GG修改器的教程  
pxm2525     + 1     热心回复!  
jyz0506     + 1     用心讨论,共获提升!  
jjjcc     + 1   + 1   谢谢@Thanks!  
散场     + 1   + 1   谢谢@Thanks!  
sinxxl     + 1   + 1   谢谢@Thanks!  
coco007     + 1   + 1   感谢发布原创作品,吾爱破解论坛因你更精彩!  
victos     + 1   + 1   谢谢@Thanks!  
YycAway     + 1   + 1   谢谢@Thanks!  
Hmily   + 2   + 100   + 1   感谢发布原创作品,吾爱破解论坛因你更精彩!  
lsxon     + 1   + 1   谢谢@Thanks!  
zhiyuckt     + 1   + 1   我很赞同!  
vanity123       + 1   我很赞同!  
ddddhm     + 1   + 1   我很赞同!  
guoruihotel     + 1   + 1   谢谢@Thanks!  
LonelyCrow     + 1   + 1   用心讨论,共获提升!  
晏南风     + 1   + 1   我很赞同!  
niceyang521     + 1   + 1   谢谢@Thanks!  
pla92u       + 1   我很赞同!  
笙若     + 1   + 1   谢谢@Thanks!  
捡尽寒枝不肯栖     + 1   + 1   我很赞同!  
decdeva     + 1   + 1   欢迎分析讨论交流,吾爱破解论坛有你更精彩!  
RippleSky       + 1   我很赞同!  
固相膜     + 1   + 1   别去修改后去耍 您会失去游戏的快乐  
zqtzheng     + 1   + 1   手机版的保卫萝卜,在没root的情况下,可以修改吗  
wdfdiablo     + 1   + 1   我很赞同!  
福仔     + 2   + 1   用心讨论,共获提升!  
xbb       + 1   用心讨论,共获提升!  
の叶子     + 1   + 1   热心回复!  
De蓝     + 1   + 1   热心回复!  
YSJohnson     + 1   + 1   用心讨论,共获提升!  
腿毛利小五郎     + 1   + 1   热心回复!  
嘤嘤樱八重樱QvQ     + 1   + 1   用心讨论,共获提升!  
思念是一种病丶       + 1   热心回复!  
XTING     + 1   + 1   用心讨论,共获提升!  
LN9712     + 1     用心讨论,共获提升!  
rugeng86     + 1   + 1   我很赞同!  
kickbirds     + 2   + 1   谢谢@Thanks!  
wanfon     + 1   + 1   热心回复!  
空竹     + 1   + 1   抛砖引玉  
a210546     + 1   + 1   热心回复!  
sam喵喵       + 1   带源码必须要赞  
1lifedo1thing     + 1   + 1   用心讨论,共获提升!  

查看全部评分