第一次写逆向调试的文章,不足之处,敬请关照! 能力有限,大佬勿喷!
新人学做修改器拿什么游戏练手?可能大部分人会想到《植物大战僵尸》和《血战上海滩》,是的,的确是这样,这两款游戏不仅家喻户晓,而且这两款游戏都属于无壳、无混淆、无加密的“三无”游戏,非常适合新人练习。不过目前网上的关于植物大战僵尸的通过逆向调试来做修改器的文章有很多,所以今天我们来讲讲血战上海滩。另外植物大战僵尸的游戏属性远远多于血战上海滩的游戏属性,且属性间的关系机制较为复杂,对新人来说并不友好,所以我个人推荐新人学逆向修改先从血战上海滩开始练习。
如果新人学做修改器,并且程序没有加密、壳等妨碍新人学习的特殊手段的话使用CheatEngine就可以满足需求,本文就以CE为例。
知识储备
你必须知道的术语
以下知识基于拓展阅读的参考资料讲述的内容并用自己的话来整理的。
(1)断点(Breakpoint)
在指定位置暂停程序执行,允许开发者检查程序状态。断点分为多种类型,例如:普通断点、条件断点、硬件断点、内存断点等。
(2)步进(Step Over)
逐行执行代码,但不会进入被调用的函数内部。例如,执行printf() 时,直接完成整个函数调用而不进入其内部实现。
(3)步入(Step Into)
当执行到函数调用时,进入该函数内部逐行调试,适用于需要分析子函数逻辑的场景。
(4)栈(Stack)
栈是一种后进先出(LIFO)的线性数据结构,其所有操作仅允许在栈顶(Top)进行,而栈底(Bottom)固定不动。栈的物理模型可类比生活中的叠盘子:每次取放只能从顶部操作,最后放入的盘子会最先被取出。
(5)寄存器(Register)
寄存器是计算机中央处理器(CPU)内部的核心组件,用于高速暂存指令、数据和地址。寄存器类就好比商场地下超市的存包柜,存取方便(通过操作指令或地址),但容量有限。其特点可概括为临时性、高速性、容量有限性。
(6)游戏基址(Game Base Address)
程序加载到内存时的起始地址,是定位游戏数据的参考原点。在现代操作系统中,由于ASLR(地址空间布局随机化)的存在,基址往往动态变化。静态基址(如Unity引擎的"GameAssembly.dll")可用于跨版本寻址。例如修改角色血量时,需要通过"基址+固定偏移"的组合来定位内存单元。
(7)偏移(Offset)
相对于基址的位移量,用于精确导航到目标数据。根据内存层级结构,偏移可呈现多级扩展形态。例如0x114514+0x666+0x888,其中每个+号后的数值均代表不同层级的偏移值,就如同玩藏宝图寻宝游戏,根据藏宝图上特定的地点向东走10步再向南走50步…逐步确定宝藏的位置。
(8)指针(Pointer)
存储内存地址的特殊变量,本质是"地址的地址"。如果说内存是一幢写字楼,那么指针变量存储的是某个房间的门牌号。
你必须知道的Win32寄存器
通用寄存器
EAX (累加器)
主要用途:乘除法运算、I/O操作、函数返回值存储(Win32 API的返回值默认存放在EAX)
可分割为:AX(16位)、AH/AL(8位高位/低位)
特殊功能:在字符串操作指令(如LODS)中自动使用
EBX (基址寄存器)
主要用途:内存数据指针,常用于基址寻址
可分割为:BX(16位)、BH/BL(8位高位/低位)
ECX (计数器)
主要用途:循环计数器(LOOP指令)、字符串操作中的重复次数
可分割为:CX(16位)、CH/CL(8位高位/低位)
特殊示例:REP MOVSB指令中控制复制次数
EDX (数据寄存器)
主要用途:乘除法辅助运算、I/O端口地址存储
可分割为:DX(16位)、DH/DL(8位高位/低位)
ESI(源变址寄存器)
主要用途:字符串操作中的源指针,内存数据指针
特殊指令:与LODS/MOVSB等指令配合使用
EDI (目标变址寄存器)
主要用途:字符串操作中的目标指针
特殊指令:与STOS/MOVSB等指令配合使用
指针寄存器
ESP (堆栈指针)
核心功能:始终指向栈顶地址,不可用于算术运算
相关指令:PUSH/POP/CALL/RET直接操作ESP
EBP (基址指针)
核心功能:访问栈帧中的参数和局部变量
特殊用途:通过EBP+偏移量访问函数参数(如EBP+8为第一个参数)
控制寄存器
EIP (指令指针)
核心功能:存储下一条待执行指令的地址
不可直接修改,仅通过JMP/CALL/RET等指令间接改变
EFLAGS (标志寄存器)
关键标志位:
CF(进位标志):无符号运算溢出时置1
ZF(零标志):结果为0时置1
SF(符号标志):结果为负数时置1
OF(溢出标志):有符号运算溢出时置1
DF(方向标志):控制字符串操作方向(STD/CLD修改)
你必须知道的汇编命令
数据传输类
MOV
将数据从源操作数复制到目标操作数(寄存器/内存)。
示例:
mov ecx, 0x114514
功能:将数值0x114514存入ECX寄存器。
PUSH与POP
将寄存器/内存中的值压入/弹出栈顶。
示例:
push ebx:将EBX的值压入栈顶。
pop eax:将栈顶的值弹出到EAX。
LEA
计算源操作数的有效地址(偏移量),并将结果存入目标寄存器。
示例:
lea eax, [ebx + ecx + 0x666]
功能:将EBX + ECX + 0x666的地址计算结果存入EAX。
算术运算类
ADD
32位加法,如ADD EAX, ECX或ADC EDX, 0(用于多精度运算)。
SUB
32位减法,例如SUB EAX, EDX,支持32位操作数。
MUL
无符号乘法,32位模式下结果存在EDX:EAX中,如MUL ECX。
DIV
无符号/有符号除法,32位被除数由EDX:EAX提供,商存入EAX,余数存入EDX。
INC/DEC
32位寄存器或内存操作数自增/自减,如INC DWORD PTR [ESI]。
控制转移类
JMP
无条件跳转到目标地址。
CALL
调用子程序,自动将返回地址压入栈。
空操作
NOP
空操作指令,用于程序优化、时序控制等场景。
你必须知道的CheatEngine用法
1. 附加调试进程
启动目标游戏和CheatEngine,点击“文件”→“打开进程”,选择目标游戏进程进行调试。
2. 搜索动态地址
首次输入当前数值(如金钱100),点击“首次扫描”。
回到游戏继续打怪赚钱,当金钱数值改变后,输入新数值并点击“再次扫描”。
重复直至仅剩一个地址。
3. 修改/锁定地址值
●双击地址将其加入修改列表,输入新值后确认。
●勾选“激活”列(显示“X”)以锁定数值。
4. 下断点
●内存断点:右击内存数据区域,选择“数据断点”→设置触发条件(写入/访问)。
●硬件断点:在汇编代码窗口选中目标行,按F5键设置断点。
你必须知道的《血战上海滩》作弊代码和程序附加命令
按下键盘左上角“~” 键(ESC下方),屏幕左上角将出现>命令输入符输入完整指令。注意保持全英文字符输入法,输入作弊码后必须按下回车键 激活生效反馈。
作弊代码
作弊命令 |
功能 |
备注 |
god_on |
开启无敌 |
免疫所有敌军伤害 |
god_off |
取消无敌 |
受到敌人攻击后会受到伤害 |
haveallweapon |
解锁所有武器 |
但不包括马克沁重机枪 |
addammono |
为当前所持有武器填满弹药 |
ammonolimit |
所有武器弹药无限 |
exit |
立即结束 |
输入并回车后可强制结束游戏进程 |
其他命令
我用IDA对游戏主程序shanghai.exe进行静态分析时,发现有两个附件命令与这篇文章有关:
1、-windows——使用窗口模式运行游戏。
2、-nousepck——不使用PCK包文件加载游戏资源。
第一个命令用于窗口运行游戏,避免操作不当导致桌面假死带来的不必要的麻烦,第二个命令就是从游戏根目录下加载资源,以省去解压缩游戏数据到内存的步骤从而提高读取速度。
如果你理解了上面的一节的话,那么恭喜你,可以跟着我学习了。
寻址与逆向
一、寻找基址和弹药量地址
基址作为程序模块加载的固定起点,配合静态偏移量能构成"基址+偏移"的寻址路径,即便程序重启后动态内存地址更新,通过固定基址仍能准确定位目标数值。这样就可以稳定、有效地定位其准确地址。我在某源码分享网站中已经找到了血战上海滩的基址,为0x005DCFE4,不过基址在代码的预览部分露出来了,武器的偏移量并没有露出来,需要下载,并且下载需要付费,所以没有继续搞。
那么基址0x005DCFE4是怎么来的呢?我们现在开始分析。
首先启动游戏,建议给shanghai.exe建一个快捷方式,同时附上命令“-windows”在窗口模式下进行游戏,以便切换窗口。
启动游戏后,打开CE,选择附加游戏进程
接下来我们就通过搜索到的手枪弹药的动态地址来寻找游戏基址。
开个新局,当前手枪的弹药为10。
切到CE,发起新的搜索,搜索10。
搜到了5400多条结果,你可能会犯糊涂,到底哪个是手枪弹药的地址?不要担心,现在继续搜索。
回到游戏,打出去几个枪子儿
此时弹夹剩7发子弹,现在回到CE,搜索7,点击“再次扫描”,搜索到的地址数量会越来越少,直至剩下一个,我比较幸运,搜了两次就找到了。
当时的手枪弹药的动态地址为0x18A4991C,我们先双击添加到内存修改列表中,并备注“手枪弹夹弹药动态地址”。
知道手枪弹夹弹药动态地址了,不妨来试一下修改弹药吧,如图所示,双击数值7,弹出数组修改窗口,输入你要修改的数值,我们就填50吧。
点击“确定”,选中这个地址按空格键就可以锁定手枪弹夹弹药量。回到游戏,瞧,弹药量变了吧,并且打出子弹后,弹药量会立马复原。
这是你成功的第一步,后面的路途还很遥远。OK,别急着继续,我们先分析一下,手枪开火,弹药会减少,弹药减少,就一定有相应的汇编操作码来完成操作,弹药减少,相应的内存地址的数据也会被改写,那么接下来我们来寻找是哪一行汇编语句改写了弹药地址里的数值。寻找相应的汇编操作码有两种方法,一个方法是通过CE的“找出是什么改写了这个地址”功能来定位相应的语句,另一个方法是通过内存断点来定位相应的操作码,先解锁地址数值,下面一一介绍。
方法一:通过“找出是什么改写了这个地址”功能来定位(推荐)
选中刚才的手枪弹夹弹药的地址,按F6或者右键菜单选择“找出是什么改写了这个地址”,调出操作码搜索框
如果弹出了这个对话框,不用理会,直接选“是”。
这时把这个窗口放到一边别管它,回到游戏。我又打出了两发子弹,这时操作码搜索框显示出了开火时的操作码,计数为2。
选中此行,点击“显示反汇编程序”,即可来到汇编窗口并定位到该操作码。
定位到了shanghai.exe+2F37B处。
方法二:通过内存断点来定位(备用)
如图所示,选中手枪弹药地址,点击“查看内存”,进入汇编窗口。
CE会帮我们定位到手枪弹药地址0x18A4991C处。
仔细观察一下,现在这个地址的十六进制字节值是不是当前弹药余量的十进制值呢?
右击0x30这个值,如图所示,选择“写入时中断”
这时0x30数值会变为深绿色,回到游戏,开火射出子弹,这时游戏假死,断点触发成功,回到CE的汇编窗口,这时中断在shanghai.exe+2F37D处。
你会突然发现,咦?两个方法为什么定位的地方差了一行?这里就要讲一讲CE的“找出是什么改写了这个地址”与内存断点工作机制的异同了。“找出是什么改写了这个地址”是在指令执行前设置,当指定地址被改写时,它能记录改写情况,定位到改写操作发生时的汇编指令处,比如在打出子弹前设置,打出后定位到开火操作码位置。而内存断点是在指令执行后起作用,像对弹药地址设置“写入时中断”,需等游戏执行写入操作(子弹射出)导致游戏假死断点触发,定位位置会和前者有差异 。综述,如果是用内存断点定位操作码,就要关注其上一行的操作码。定位到相关操作码了,接下来就可以分析0x18A4991C这个地址是如何被操作的了。分析前要对开火的操作码下断点,以确定是不是开火操作。如图,我们选中shanghai.exe+2F37B行,就在此处下断点,按F5键或点击左上方的“切换断点”,这时此行变绿,未选中时变红。
回到游戏,再次开火,游戏假死,CE中断。
观察EDI和ECX寄存器,正是手枪弹药地址和手枪弹药余量,ECX寄存器操作着弹药余量。
再来看这一行的操作码:
mov [edi],ecx
同学们应该明白了,就是将当前ECX寄存器的值放入当前EDI寄存器的值所对应的地址中,继续向上跟踪,这时看到了shanghai.exe+2F377处的操作码为dec ecx。
这说明了这里是开火时打出一发子弹,使用DEC指令来自减1,既然到这里了,不妨来整活儿吧,改一下汇编代码,来体验一把子弹杀敌后飞回枪膛的快感吧!
如图所示,双击此行操作码,把“dec ecx”改为“inc ecx”
点击“确定”,暂时去掉刚才的断点,按F9继续运行,回到游戏,走你!看,是不是很带劲儿!?
当然NOP也是可以的,实现了弹药锁定的效果。
接下来继续分析,主要追踪EDI寄存器里的0x18A4991C,一点点的向上翻看代码,注意操作EDI寄存器的行,从相关行或者临近行下断,以保证EDI寄存器里的值为0x18A4991C
经过了一系列追踪,发现了这两行
shanghai.exe+2F2CE C2 0400 ret 0004
shanghai.exe+2F2D1 83 83 FB 02 cmp ebx,02
如果在shanghai.exe+2F2D1处下断,CE断了下来,但是上一行就没有,这是为什么呢?众所周知,程序是按顺序执行的,除非加了某些条件,这说明先前的操作码可能通过JMP、JE、JZ、JNZ、CALL等控制转移类指令跳转到了这一行。这时就要向上找是谁调用了shanghai.exe+2F2D1这一行,继续上翻代码,在shanghai.exe+2F25F处看到了“jg shanghai.exe+2F2D1”,为了验证这个说法,给shanghai.exe+2F25F和shanghai.exe+2F2D1都下断点,再次开火,断在了shanghai.exe+2F25F,EDI寄存器依然为0x18A4991C
然后按F7逐步执行语句,这时它的下一句就是shanghai.exe+2F2D1,说法成立。
突然,发现了一行特别的操作码
shanghai.exe+2F253 8D 8C 31 EC000000 lea edi,[ecx+esi+000000EC]
刚才也介绍过了,ESI是源变址寄存器,那就可以确定手枪的弹药地址只有一级偏移0xEC,既然是相加操作,那么先前ESI里的值就是0x18A4991C-0xEC所得值0x18A49830喽。到了这里,我们就可以试一下搜索是否有基址的指针指向0x18A49830,回到CE主窗口,发起新的扫描,搜索目标为18A49830,记得勾选十六进制,否则不会搜到有关内容。
果不其然搜到了一条基址,正是0x5DCFE4!(刚才说到32位程序一般在0x04XXXXX处加载,所以shanghai.exe+1DCFE4就是0x5DCFE4)
现在已经知道了手枪地址的偏移为0xEC,知道了基址为0x5DCFE4,那么我们可以用基址来添加静态地址了。
如图所示,在CE主窗口点击“手动添加地址”,填入基址值,并勾选指针,添加好描述,确定即可。
这时候两个地址项的指向目标的数值就一样了。
现在退出游戏重新开局,通过基址添加的静态地址是有效的。
接下来该寻找步枪弹药地址了,按理说程序会根据武器编号来判断要把什么地址移入EDI寄存器中,这个时候就有可能得到步枪的弹药地址的偏移了,但是这种方法效率低下,直接搜索其动态地址,计算一下动态地址与指针地址之差,就可以知道步枪弹药地址的偏移了。如图,现在的冲锋枪弹药地址为0x0A9604AC,基址所指向的指针地址为0x0A960390,两者之差为0x11C,因此冲锋枪的弹药基址偏移为0x11C。
其他武器也是如此,不再赘述,所以我把七把武器的弹药基址偏移给大家总结下来了:
武器弹药地址 |
偏移 |
手枪 |
+0xEC |
步枪 |
+0xFC |
手榴弹 |
+0x10C |
冲锋枪 |
+0x11C |
马克沁 |
+0x12C |
巴祖卡 |
+0x13C |
轻机枪 |
+0x14C |
寻找武器激活状态地址
大家知道,只有通过射击来拾取敌人掉落或平民赠与的武器,或者是通过作弊代码才能激活武器,众多游戏逆向案例表明,一件道具的激活与否通常会用0x00和0x01来表示。不过,值为0或者1的地址会成千上万个,武器一旦激活则无法取消,所以通过动态搜索是绝对不可能的事情,所以要另想办法。
来想一想,如果开火后会必然命中目标,目标可能是敌人、周围建筑物、枪械、弹夹、血包,命中不同目标,则执行的函数也就不同,所以接下来我们就尝试在开火射击时下断点,然后走出此函数,看一下是否有多个CALL操作指令。
由于已经重新开局,所以基址指针指向的地址有改变,先留意一下。现在是0x0A960390。
如图,我们在第一节中已知开火/命中目标的行为shanghai.exe+2F37B,再次从这里下断点。
这时侯,游戏中出现了一把步枪
开火后,CE断住,按Shift+F8走出,观察一下后面的操作,确实出现了很多CALL指令。
给下面的几个CALL指令都下上断点,看看开火命中枪械时候哪个CALL会断住(记得取消包括开火在内的其他断点)。
没想到,居然在shanghai.exe+2EA25处断住了,操作码为:
shanghai.exe+2EA25 - E8 D6180000 - call shanghai.exe+30300
那好,现在就到shanghai.exe+30300去看看。按F7步入,有CALL的可以按F8暂时步过以加快排查效率。
在走到shanghai.exe+3037F处时,有这么一行操作码:
shanghai.exe+3037F - C6 84 1A F8000000 01 - mov byte ptr [edx+ebx+000000F8],01
用人话来讲,就是把十进制01也就是十六进制的0x01传入到当前EDX寄存器的值+当前EBX寄存器的值+0xF8所得地址,操作单位为字节。
这不是重点,重点是EBX寄存器,此时寄存器的值正是基址指针所指向的值!
这样一来计算一下:
edx+ebx+000000F8=0x10+0x0A960390+0xF8=0x0A960498
所以步枪激活状态的地址为0x0A960498
现在来尝试一下用基址添加到CE地址列表,勾选指针,并在偏移输入框内输入“0x10+0xF8”,指向的值也是0x0A960498。
接下来验证一下地址是否正确,将它改成0。
回到游戏后,切到其他武器再切回步枪就切不动了,再从CE里改回1,又可以切到步枪了,说明刚才的操作码正是激活武器的关键操作码,后面的武器我们只需在shanghai.exe+3037F处下断点,获取EDX寄存器里的偏移值即可(强制切换马克沁重机枪也会断下来)。经过追踪,得出以下结论:
武器激活状态地址 |
偏移 |
步枪 |
+0x10+0xF8 |
手榴弹 |
+0x20+0xF8 |
冲锋枪 |
+0x30+0xF8 |
马克沁重机枪 |
+0x40+0xF8 |
巴祖卡 |
+0x50+0xF8 |
轻机枪 |
+0x60+0xF8 |
三、其他属性地址搜索
积分、杀敌数、误伤平民数等属性也是按照此方法搜索寻找,计算地址之差得到偏移,已经帮你找到部分基址偏移,方便后续使用。
属性地址 |
偏移 |
备注 |
当前武器 |
+0xE8 |
取值范围必须是0x01~0x06,否则游戏会崩溃,分别代表手枪、步枪、手榴弹、冲锋枪、马克沁、巴祖卡、轻机枪 |
击毙敌人 |
+0x188 |
误伤平民 |
+0x1B0 |
得分 |
+0x194 |
奖励分 |
+0x198 |
总得分 |
+0x19C |
改不改都无所谓,游戏的总得分的结算机制就是得分+奖励分 |
修改器制作思路
知道了游戏基址和一些属性的偏移,就可以制作修改器了,这次继续使用C#来编写程序。根据自己学到的知识以及AI给出的建议,简单列举一下思路和代码。
步骤1:列举常量和变量
创建好C#项目后,新建两个个C#代码文件,一个记录常量,另一个记录变量,我们将基址、各属性偏移放入常量中,将弹药放入变量中。
步骤2:引入用于内存管理的Wn32 API
但凡涉及内存修改,就要通过Windows的应用接口来实现,这时候不得不提到一个重要的系统文件——kernel32.dll,内存管理是这个文件的主要功能之一,负责分配、释放和管理应用程序的内存资源,包括虚拟内存和物理内存的调度。本修改器涉及到的调用有OpenProcess、ReadProcessMemory、 WriteProcessMemory、VirtualProtect、CloseHandle五个函数,分别用于附加进程、跨进程读内存、跨进程写内存、释放句柄,当然,调用这些函数的程序一定要以管理员身份运行,随后再创建一个C#代码文件用于内存管理操作。
关键代码(API声明略):
/// <summary>
/// <逻辑型> 将内存管理器附加到指定进程
/// <param name="process">(进程 欲附加的目标进程对象)</param>
/// <returns><para>成功返回真,否则返回假</para></returns>
/// <remarks>
/// <para>需要管理员权限才能获取完整进程访问权限</para>
/// </remarks>
/// </summary>
public bool Attach(Process process)
{
// 定义完全访问权限标志(包含读写、操作等权限)
const int PROCESS_ALL_ACCESS = 0x1F0FFF;
// 调用OpenProcess API获取进程句柄
_processHandle = OpenProcess(PROCESS_ALL_ACCESS, false, process.Id);
// 验证句柄有效性
return _processHandle != IntPtr.Zero;
}
/// <summary>
/// <整数型> 从指定内存地址取32位整数
/// <param name="address"><para>(内存指针 欲读取的内存地址)</para></param>
/// <returns><para>返回读取到的整数值</para></returns>
/// <exception cref="Win32Exception">当读取操作失败时抛出</exception>
/// </summary>
public int ReadInt(IntPtr address)
{
// 分配4字节缓冲区(32位)
byte[] buffer = new byte[4];
// 调用底层API读取内存
ReadProcessMemory(_processHandle, address, buffer, 4, out _);
// 将字节数组转换为int类型
return BitConverter.ToInt32(buffer, 0);
}
/// <summary>
/// 向指定内存地址写入32位整数
/// <param name="address">(内存指针 目标内存地址, </param>
/// <param name="value">整数型 欲写入的整数值)</param>
/// <exception cref="Win32Exception"><para>当写入操作失败时抛出</para></exception>
/// </summary>
public void WriteInt(IntPtr address, int value)
{
// 将int转换为字节数组(小端序)
byte[] buffer = BitConverter.GetBytes(value);
// 调用底层API写入内存
WriteProcessMemory(_processHandle, address, buffer, 4, out _);
}
/// <summary>
/// 向指定内存地址写入单个字节
/// <param name="address">(内存指针 目标内存地址, </param>
/// <param name="value">整数型 要写入的字节值(0-255))</param>
/// <exception cref="ArgumentException"><para>当输入值超过字节范围时抛出<para></exception>
/// </summary>
public void WriteByte(IntPtr address, byte value)
{
// 创建单字节数组
byte[] buffer = { value };
// 调用底层API写入内存(不检查返回值)
WriteProcessMemory(_processHandle, address, buffer, 1, out _);
}
/// <summary>
/// <内存指针> 解析多级指针链获取最终内存地址
/// <param name="baseAddress">(整数型 基地址, </param>
/// <param name="offsets">整数型数组 偏移量数组)</param>
/// <returns><para>最终解析出的内存地址</para></returns>
/// </summary>
public IntPtr ResolvePointerChain(int baseAddress, int[] offsets)
{
// 初始化当前地址为基地址
IntPtr current = (IntPtr)baseAddress;
// 逐级解析指针链
foreach (int offset in offsets)
{
// 读取当前指针值
current = (IntPtr)ReadInt(current);
// 应用偏移量
current += offset;
}
return current;
}
uint originalProtect;
/// <summary>
/// 内存写保护
/// <param name="currentWeapon">(内存指针 现行武器, </param>
/// <param name="enable">逻辑型 是否写保护)</param>
/// </summary>
public void ToggleMemoryProtection(IntPtr currentWeapon, bool enable)
{
if (enable)
{
// 设置为PAGE_READONLY
VirtualProtect(currentWeapon, 4, 0x02, out originalProtect);
}
else
{
// 恢复原始权限
VirtualProtect(currentWeapon, 4, originalProtect, out _);
}
}
/// <summary>
/// 断开当前附加的进程并释放资源
/// <remarks>
/// <para>必须调用此方法释放系统句柄,避免资源泄漏</para>
/// </remarks>
/// </summary>
public void Detach()
{
if (_processHandle != IntPtr.Zero)
{
// 关闭进程句柄
CloseHandle(_processHandle);
// 重置句柄标识
_processHandle = IntPtr.Zero;
}
}
步骤3:创建键盘钩子
没有快捷键的修改器不是好修改器,注册键盘钩子实现快捷键作弊游戏是必不可少的,这里主要涉及user32.dll的一些接口,如SetWindowsHookEx和UnhookWindowsHookEx函数用于装卸系统钩子,CallNextHookEx和GetKeyState用于传递消息和获取按键状态。
关键代码(API声明略):
/// <summary>
/// 安装全局钩子
/// </summary>
private void InstallHook()
{
using (var curProcess = System.Diagnostics.Process.GetCurrentProcess())
using (var curModule = curProcess.MainModule)
{
_hookId = SetWindowsHookEx(WH_KEYBOARD_LL, _proc,
GetModuleHandle(curModule.ModuleName), 0);
}
}
/// <summary>
/// 钩子回调处理
/// </summary>
/// <param name="nCode"></param>
/// <param name="wParam"></param>
/// <param name="lParam"></param>
/// <returns></returns>
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
{
int vkCode = Marshal.ReadInt32(lParam);
bool shiftPressed = (GetKeyState((int)Keys.ShiftKey) & 0x8000) != 0;
// 检查功能键F1-F7 + Shift组合
if (shiftPressed && vkCode >= (int)Keys.F1 && vkCode <= (int)Keys.F7)
{
int functionNumber = vkCode - (int)Keys.F1 + 1;
HotkeyPressed?.Invoke(functionNumber);
}
}
return CallNextHookEx(_hookId, nCode, wParam, lParam);
}
public void Dispose()
{
if (_hookId != IntPtr.Zero)
{
UnhookWindowsHookEx(_hookId);
_hookId = IntPtr.Zero;
}
}
步骤4:读写内存,实现作弊
如果把前三步比喻成大楼的基建阶段,那么接下来就是大楼室内的装修拆改了。可是,大楼里这么多户人家,要装修哪一户呢?那肯定要先找到是谁家,也就是找到并附加游戏进程,那就要创建一个timmer用于实时检测游戏进程一直是存在的有效的。
找到装修户后,要拆改哪里?这时候就该操作内存地址了,知道了游戏基址和各属性偏移,那就来获取指向的动态地址和值,然后修改后重新写入就OK了。
因为篇幅关系,这次就以获得马克沁重机枪为例吧。首先获取当前武器地址的值,记录一下,然后,将其改为马克沁的代码:0x04,然后将马克沁弹药地址的值改为0xF0(240发子弹),取消拥有时,把拥有马克沁前的武器代码再写回当前武器地址就好了。
/// <summary>
/// 拥有马克沁重机枪
/// <param name="enable">(逻辑型 是否拥有)</param>
/// </summary>
private void HaveMaximLock(bool enable)
{
// 取当前武器地址
IntPtr currentWeapon = _memMgr.ResolvePointerChain(
GloConst.Trainer.baseAddress,
new[] { GloConst.Trainer.offsetCurrentWeapon });
// 马克沁弹药
IntPtr addr = _memMgr.ResolvePointerChain(
GloConst.Trainer.baseAddress,
new[] { GloConst.Trainer.AmmoAddress.offsetWeapon04 }) + GloConst.Trainer.AmmoAddress.offsetWeapon00;
// 如果想拥有就写入数据,否则恢复之前的武器
if (enable)
{
//取当前武器并记录
GloVar.Trainer.weaponBeforemod = _memMgr.ReadInt(currentWeapon);
_memMgr.WriteInt(currentWeapon, 0x4);
_memMgr.WriteInt(addr, 240);
SoundPlay.PlaySound(1); //播放提示音
}
else
{
_memMgr.WriteInt(currentWeapon, GloVar.Trainer.weaponBeforemod);
SoundPlay.PlaySound(2); //播放提示音
}
// 开启/关闭内存写保护,以防游戏时按下其他键造成主视角枪械混乱
_memMgr.ToggleMemoryProtection(currentWeapon, enable);
}
这是修改器的最终效果:
遗憾与不足
1、爆头率、命中率、血量是以小数型数据存储的,经过追溯,似乎基址它们的并不是0x5DCFE4,求教大佬这是为什么。
2、强制修改马克沁的时候,主角视角会同时出现改之前武器和马克沁,原因未知。
结语
通过《血战上海滩》修改器的实践,我们不难发现逆向工程的乐趣与挑战并存。想要在游戏修改领域更进一步,新人需要筑牢三块基石:
-
汇编语言基础是核心
从操作码分析到寄存器追踪,几乎每一步都与汇编指令紧密相关。例如理解 MOV [EDI], ECX 是如何将弹药量写入内存,或是通过 DEC ECX 捕捉弹药递减逻辑。对跳转指令、栈操作的透彻掌握,能让你在调试时快速理清代码执行流程。建议从8086指令集入手,逐步熟悉x86架构的寻址模式和常见优化策略。
-
Windows API内存操作是关键工具
无论是动态读取基址还是实时修改属性值,都离不开底层API的调用。尝试用C/C++或C#封装这些API,将CE的手动操作转化为自动化脚本,是进阶的必经之路。
-
多逆向分析经典案例
从《植物大战僵尸》的全局阳光值到《血战上海滩》的多级指针结构,每个游戏的逆向过程都是独特的学习样本,用七个字来概括就是”多学、多练、多观察“。
修改器的本质是对程序逻辑的深度对话。当你学会用调试器"提问"(下断点)、用汇编指令"倾听"(跟踪执行流)、用内存工具"改写"(数据注入)时,便是真正掌握了逆向工程的密码。
最后,保持好奇心与耐心。我在尝试修改当前武器地址中的武器代码值的时候,手滑写了0x07,游戏突然报错并崩溃,还显示了汇编错误行,这让我一下子就找到了武器切换机制,每一次意外都会让你的分析效率提高一个台阶。再有逆向工程没有捷径——每一次异常崩溃的调试、每一个偏移值的反复验证,都会化作你技术成长的基石。愿各位在破解与创造的过程中,找到属于自己的“上海滩”。
拓展阅读
小白也能通过CE进行数据修改 FPS游戏逆向安全教程 从入门到精通(来源:bilibili)
https://www.bilibili.com/video/av113548231577357
Cheat Engine 教程( 1 - 9 通关 ) (来源:CSDN)
https://blog.csdn.net/freeking101/article/details/101107489
汇编语言常用语句一览(来源:百度文库)
https://wenku.baidu.com/view/2f0a3ecce0bd960590c69ec3d5bbfd0a7856d596?pcf=2&bfetype=new&_wkts_=1744694607287
32位CPU寄存器常用知识汇总(来源:CSDN)
https://blog.csdn.net/weixin_46222091/article/details/109205298
特别赠送
本文有一份特殊的礼物,就是写这篇文章所编写的修改器源代码,此修改器集成了追加生命、弹药锁定、追加杀敌数等八项属性,同时还集成了INI和PCK工具,这里感谢版主@苏紫方璇 提供了INI解密思路,关于PCK文件的解包的讨论请点击这里。
其实我之前制作的PCK Viewer有了迭代版本,支持打包PCK,原本想着要发布的,然而不曾想这段时间比较忙,代码也没整理和注释,所以我把打包的代码移植到修改器上了。
说一下关于源码的问题:
1、修改器依赖于.NET Framework V4.5运行,原则上Visual Studio 2015就可以编译,但是本人是在Visual Studio 2022中编译的,因此建议在Visual Studio 2022中编译。
2、因PCK工具依赖Zlib,所以修改器引用了zlib.NET源码库,这部分源码略,没有加入到项目中,编译前请自行到官网下载并添加到项目中,否则不能通过编译。附件也有备份下载。
Zlib官网:https://www.zlib.net/
3、弹药锁定也是通过计时器周期事件来实现的,并没有通过修改操作码的方式来实现,目的是防止杀软误报,时钟周期事件完全满足修改效果。
4、修改器源码仅供学习,切勿商业用途。
zlib.NET备份下载: