0x01 前言
之前看到隔壁发了一篇关于植物大战僵尸的文章,正好自己也深入研究了一下,写一篇文章记录一下吧,如果有错误欢迎各位师傅指正。
需要用到的工具:CE(Cehat Engine)、OD
需要的知识基础:CE、OD的使用,汇编语言,C语言的基础。
0x02 修改阳光
在植物大战僵尸中,阳光非常重要,我们要是有足够的阳光,那么不就可以无限通关了。
我们知道每个进程都有一个独立的地址空间,在保护模式中,处理器为每个进程分配总共位4GB的内存(虚拟内存)。那么在植物大战僵尸运行的时候,阳光作为一个变量也被存储进了这段地址空间中,如何找到阳光的这个地址呢,我们可以通过CE把不断变动的数值进行筛选,从而找到这个内存地址。
首先通过CE打开植物大战僵尸进程。
在数值处,输入当前游戏阳光的数值点击首次扫描。
首次扫描找到的地址很多,我们在游戏中变化一下阳光的数值,然后再对新的
数值进行再次扫描,这样就可以找到阳光值了。
可以修改阳光的值,看到在游戏中,阳光值已经修改成我们想要的数值了。
但是在重启游戏后这个地址就失效了,这里我们就需要去寻找基址,来进行修改。
0x03 什么是基址,如何寻找基址
3.1 什么是基址
什么是基址,这个问题需要从内存寻址方式来说。
16位的cpu工作在实模式下,运行的是DOS系统。
我们来看下这张图,在实模式中,16位cpu有20根地址总线,他的内存大小是220,1MB大小,但是16位的cpu寄存器只有16位,最大是216,64kb大小,这样一个寄存器无法寻址到所有地址,所以就采用了分段寻址,cpu把内存分成若干个最大为64kb 的逻辑段,这样每个段就可以用“段基地址+端内偏移地址来表示”,但是要想访问到1MB大小的物理地址,还需要段寄存器(存放了段基地址)×10H(十六进制,其实就是向左移了4位)+偏移量,计算出来一个20位的数,这样就可以访问到所有的物理地址了。
这样的话程序是直接访问物理地址的,所以必须在一个程序运行完成后再运行另一个程序,这也就是为什么DOS系统是单线程模式,但是他可以通过中断机制来中断正在运行的程序来运行其他程序。
后来发展到到80286 cpu就有了保护模式,到80386 cpu就增长到了32位,32位的cpu支持三种工作模式:实模式、保护模式、系统管理模式。
在32位cpu中不在使用DOS操作系统而是Windows图形化界面了,这样同时运行的程序数量就大大增加了,在保护模式中,每个程序运行的时候会分配一个4GB大小的虚拟内存,然后通过内存映射(逻辑地址--线性地址--物理地址)的方式找到物理地址,这样就可以实现多程序同时运行,而且每个程序运行所分配的内存空间都是被保护的,其他程序是不能访问的,不再像16位cpu那样只能一次运行一个程序。
在发展到保护模式后,加之cpu 已经是32位的了,寄存器的大小是32位的,寻址可达4GB,所以一般用一个寄存器就可以寻到所有的地址了,即使一个寄存器已经可以寻到所有的地址了,但因分段寻址的方式可以大大提高编程管理者的效率,所以还是保留了下来,这时段寄存器已经不再存放段基地址而是段选择子(段选择符),每一个段选择子都指向一个段描述符(段描述符里包含了该内存段的基地址)。
这样一般程序要找到内存所存储的数据或者下一个指令,都要通过段基地址+偏移地址的形式来确定所需要的东西所在内存的位置,以便读取。
那么这个最一开始的段基地址就是所谓的基址,也就是定义段在地址空间中的起始地址,因为这个地址是不会变的,所以我们找到这个地址,找到固定的偏移量,就可找到阳光值的地址。
3.2 怎么去寻找基址
了解完什么是基址,那我们应该怎么去寻找基址呢。
我们先来看一下,阳光值有可能存放在什么位置。
当编译一个C/C++程序时,计算机的内存被分成了4个区域,一个包括程序的代码,一个包括所有的全局变量,一个是栈(Stack),还有一个是堆(Heap)。
代码区就不用说了,是放程序的二进制代码的,阳光地址肯定不会放在这里。
3.2.1 栈内存
栈内存,是系统自动分配的一段内存,使用结束后系统会自动释放。
在OD中我们可以看到右下角的堆栈窗口,这段空间就是程序在运行的时候系统自动分配的内存空间,有栈顶和栈底,遵循先入后出的原则,在程序进行函数调用或者其他操作的时候都会在这段空间内进行压栈和出栈,并且栈顶会不断的变化。
那么阳光地址有没有可能是栈内存里的一个地址呢,不太可能,因为他虽然是在游戏重启后改变了,但是在游戏运行的过程中,一直是不变的。
3.2.2 堆内存
堆内存,是程序动态申请的一段内存,需要程序员去申请,使用完毕后手动释放。C语言中用malloc来申请,free来释放,C++中用new来申请,delete释放。
我们想一下,打开游戏和进入冒险模式开始游戏,其实在内存中占用的空间是不一样的,因为我们点击冒险模式进入游戏,程序会动态申请一段空闲的堆空间来使用,游戏需要把地图、植物、僵尸等等重要的信息加载进来,而打开游戏是不需要加载这些的。这样基本就可以断定阳光是存放在这段内存中的了。
3.2.3 全局变量
继续看最后一个全局变量。
全局变量,是指所有函数都可以通过一个固定的地址访问的一个变量。
那么全局变量和基址又有什么关系呢?
来看一下这个图,在一个游戏中结构体的设计一般都是比较复杂的,有可能是一个结构体里面指向另一个结构体,另一个结构体再指向一个结构体等等。
结构体是在程序的运行过程中动态申请的,比如我要们要找的阳光,他所在的这个结构体肯定是我点击开始游戏之后才申请的。
并且结构体里存储了很多信息,阳光、冷却 、金钱、僵尸等等,很多函数都有可能访问这个结构体,所以我们必须有一个指针去指向这个结构体,通过偏移,最后找到我们要找的那个变量,这个就是图里的这个全局变量,也就是基址。
所以我们要向找到基址,修改阳光,需要从一开始的全局变量,找到调用的结构体,通过偏移量,找到另一个调用的结构体,再通过偏移量修改阳光。
那么我们不知道第一个全局变量的位置,但是我们知道阳光的位置,所以,可以通过阳光的位置逆向到全局变量的位置。
刚才我们已经在CE中找到了阳光的地址,这里点击右键—找出是什么写入这个地址。
这里CE会提示附加进程,如果你在OD中附加了进程需要在OD中取消附加,要不然CE会报错。
进程附加成功后会弹出一个窗口,这里回到游戏,对阳光的增加和减少进行一个操作。
我收取了两次阳光,种植了一颗植物(减少了一次阳光),我们可以看到CE已经阳光增加和减少的汇编代码了,前边有个计数,可以很清楚的知道那条是增加阳光的指令,哪条是减少阳光的指令,双击打开增加阳光的指令分析一下。
这里是一个add操作,把ecx的值和[eax+0x5560]里面的值相加,再存到[eax+0x5560]里面,因为我们知道这个一个增加阳光的操作,不用看其他的指令,光看这一条大体可以推断出ecx就是我们拾取的阳光的值,[eax+0x5560]是我们现有阳光的值,这样相加得到拾取阳光后的数值。
前面我们也说过了阳光数值是在结构体2中存储了,通过结构体2的首地址+偏移量来进行访问的,那么这个[eax+0x5560],其中eax很有可能就是我们结构体2的段基地址,5569就是偏移量,那么这里找到eax的值为0x07C9E148,通过eax的值继续往下跟进。
在CE中勾选十六进制,然后输入刚才我们找到的eax的值继续扫描,第一次扫描我们会发现扫描出来的数值很多,这样我们进入游戏操作一下,再次扫描,慢慢筛选我们想要的地址。
这时筛选到一定数量的时候已经很难再往下筛选了,我们来看一下找到的地址,这里有个小技巧,如果我们最终筛选出来的值有几十个的话,我们不用再一个一个的去寻找,找一下比较靠前而且不连续的地址。这里我们来看一下第三个0x00912E98这个地址。
这里选择找出是什么访问这个地址了。
找到之后我们发现里面的内容很多,而且都是在变动的,没关系,我们进游戏再对阳光减少进行一次操作。
操作完成后返回CE,右下角点击停止,观察发现有很多只执行了一次操作的指令,在没有对阳光进行减少之前是没有的,那么基本可以判定这个就是我们要找的,随便选择一条点进去看看。
这里是通过一个cmp指令进行比较,而且又发现了一个偏移,基本可以确定[esi+0x768]这个地址就是结构体1的结构体指针的地址,esi就是结构体1的段基地址,768就是偏移量,继续找是谁访问了esi:0x00912730这个地址。
还是用同样的方法在CE里面进行查找,通过筛选发现地址有很多,有几百个,而且不管怎么在游戏中操作地址都不会减少了,看一下已经筛选出的地址。
发现有4个绿色的地址,其实这个就是要找的基址了。
点开看看,他是用PlantsVsZombies.exe+2A9ECO来表示的,这个PlantsVsZombies.exe就是植物大战僵尸的主模块。
我们来验证一下这个是不是要找的基址。
在右边找到手动添加地址,把两个偏移量添加进去。
可以看到这个地址和之前找到的阳光的地址是一样的,而且显示的数值也是游戏中阳光的数值,那我们把游戏重启一下看看。
可以看到游戏重启后之前找到的阳光地址已经不知道存放的是什么内容了,而通过基址+偏移量找到的地址还是阳光的数值,这样就找到了阳光的基址。
基址找到了,那就可以无限阳光了,好像这还不够,有些小伙伴可能不想只用修改器去修改游戏参数,那下面来看下在汇编中可以怎么修改阳光的逻辑呢。
0x04 使用汇编修改游戏逻辑
4.1 取消后台暂停
不知道大家有没有发现,这个游戏有个功能,就是我只要一退出或者点击其他的窗口他就会自动暂停,这个挺烦人的,那在修改阳光代码之前先把他的这个功能给他改掉吧。
要想改掉这个功能,首先得找到这个暂停功能的函数调用在哪,找到暂停功能函数,才能对他进行操作。
可以使用OD中的中文搜索插件,游戏暂停这个功能有三处中文字符,分别是”游戏暂停、点击返回游戏、返回游戏“,那首先打开OD,把植物大战僵尸的进程附加进去,这里需要注意的是,我们需要用CE重新打开一下植物大战僵尸的进程,要不然OD无法附加。
左上角点击文件—附加,附加进程,因为植物大战僵尸在运行的过程中会调用很多系统的程序,所以点击上方的e模块,选择程序的主模块进行分析。
第一个就是程序的主模块,点进去。
右击—分析—分析代码,或者按Ctrl+A。
右击—中文搜索引擎—智能搜索 这里OD会把程序所有的字符串都列出来,Ctrl+F,搜索一下“点击返回游戏”。
找到游戏暂停的函数,双击进入。
简单看一下函数的主要功能,其实就是一个游戏暂停,然后返回游戏的功能。
这里想要不让程序运行这个函数,最简单粗暴的方式就是直接在程序开始的地方写一个retu,程序在调用函数的时候会用call指令,而call指令会把他下一跳的地址压入堆栈,retu指令一般用在函数调用的结尾,直接返回之前call指令压入堆栈中的那个地址。需要先看一下函数尾部的retu是外平栈还是内平栈,如果是内平栈我们还需要进行平栈操作,这个函数是外平栈,直接在函数头加retu就好了。
这样程序在调用这个函数的之后通过call指令进来,直接指令retu指令,返回到下一跳的地址,就不会再运行暂停函数了,在OD中运行程序,返回程序观察一下。
如果在OD中点击运行程序还是不能运行的话需要找到OD上的t模块,在这里看到程序被挂起了,右击选择Resume All Threads恢复程序,这时再回到游戏就发现游戏的暂停功能已经被我们取消掉了。
4.2 在汇编中修改阳光的几种方式
我们回到正题,怎么样通过修改汇编代码去更改阳光的数值呢,这里提供了三种方式:直接修改、补码和作废JCC。
首先通过CE找到程序修改阳光的汇编代码处,一开始找基址的时候我都保存下来了,直接拿来用就好了。
直接在OD中按Ctrl+G,跳转到0x00430A11处,简单分析一下。
add dword ptr ds:[eax+0x5560],ecx // eac和[eax+0x5560]相加,结果放到[eax+0x5560]里面
mov ecx,dword ptr ds:[eax+0x5560] // 把[eax+0x5560]里的值赋给ecx
cmp ecx,0x2706 // ecx中的值和0x2706进行比较
jle 00430A9D // 如果小于等于就跳转
来分析一下这几段汇编代码,ecx里面存放的值大概就是我们拾取的阳光值,把拾取阳光值和之前的阳光值相加放入[eax+0x5560]这个存放阳光值的地址里面,阳光值和0x2706进行比较,如果大于就往下走,如果小于等于就跳转,这几行代码大概就是这样的一个逻辑。
4.2.1 直接修改
首先第一种方法,直接修改,既然ecx是存放拾取的阳光的数值,那直接把ecx改成一个特别大的值就好了,来试一下。
这里发现一个问题,在我们把ecx改成0x1000的时候,下一行的汇编代码被覆盖掉了,这里是因为计算机只识别0和1,是不识别汇编代码的,所以一般会把汇编转换成16进制,然后再转换成0和1让计算机识别,这串16进制的数字就是硬编码。在OD窗口中,汇编代码左边的那串16进制数字就是硬编码,在改变汇编代码的时候,改变之后的汇编翻译成的硬编码比之前的长就会向下覆盖,所以下一行的代码被覆盖掉了。
可以向上看一行,
mov eax,dword ptr ds:[esi+0x4]
,这里是把[esi+0x4]这个地址里面的值赋值给eax,在OD观察,eax是个很大的值,所以可以把ecx直接改成eax,这样汇编代码的长度和之前是一样的,下一行的代码也不会被覆盖掉,修改完成之后进入游戏看一下,在拾取阳光的时候,阳光直接会变成9990。
4.2.2 补码
介绍完直接修改的方法,来看第二种方法补码。
既然直接把ecx的值改成0x1000会因为长度不够把下一行代码覆盖掉,那可以找一段空白的空间,把修改阳光的代码写进去,然后通过jmp指令跳转一下。
Ctrl+B选一段全是0000的空间跳过去,把
mov dword ptr ds:[eax+0x5560],0x1000
,记住这条指令的地址,把原来的指令修改成
jmp 修改后指令的地址
在补码处加一条jmp跳转到修改阳光汇编的下一跳的地址,也就是0x00430A17
这样程序在运行的时候逻辑就是跳转到补码处,然后执行完修改阳光的操作在跳转回去继续往下走,不会影响程序正常的逻辑。
这里回到游戏试验一下,收取阳光的时候也是会变成4096,因为0x1000换成十进制就是4096。
4.2.3 作废JCC
说完了前面的两种方法,其实还有一种就是作废JCC。
前面分析了这条JCC指令的意思是如果小于就跳转,正常情况下程序都是跳转执行的,看下一条指令,是把0x2706赋值给阳光值所在的这个地址,那直接不让程序跳转,也就是作废这条JCC指令,这样程序就直接向下运行,执行赋值操作,阳光值岂不是就变成了0x2706,0x2706转换成十进制就是9990,这是阳光的最大值。
这里可是使用用NOP填充的方法来作废JCC。
这样就没有了JCC指令,程序就不会跳转,直接执行赋值操作,同样,在收取阳光的时候达到阳光最大值。
4.3 寻找冷却功能
既然阳光都已经搞定了,但是植物种植还有冷却,空有这么多阳光用不了啊。
最后来分析一下冷却功能在游戏中是怎么实现的。
4.3.1 冷却功能在游戏中是怎么实现的
我们想一下,当我们写一个程序实现冷却的功能的时候,是什么逻辑,要么一个数倒计时最后到0,要么0慢慢的加,加到一个值,然后冷却就好了。
那么就需要找到这个数值,改成程序想要的一个数值,从而达到无冷却。
那怎么找到这个数值呢,显然一开始的方法不行了,因为他是不断变化的,而且也不知道数值是怎么变化的。
可以先通过CE进行查找,这里可以选择未知的初始值,一开始不知道可以使用变动的数值来慢慢的进行筛选,这里我知道植物冷却是从0到一个很大的数字,那我们来找一下。
第一次查找会发现CE找到的内容非常多,这里回到游戏,种植一颗植物,让他成为冷却状态,因为已经知道了他是一个从0到大的数字,在CE中选择增加的数值,再次扫描,然后再进游戏让他增大,再扫描,这样重复几次。
这里找到了存放植物冷却的地址。
通过观察发现数值从0到750,植物就冷却好了,那把数值改成750后就可以实现植物无冷却了,但是这里还有一个问题,那么就是只能实现当前栏位的植物冷却,那怎么把其他栏位的植物也改成无冷却呢。
4.3.2 通过修改汇编代码实现植物种植无CD
找到刚才存放植物冷却的地址,右键找出是什么写入了这个地址,在游戏中操作一下。
找到汇编代码的位置。
在OD中附加进程,跳转到0x0048728C这个位置。
简单分析汇编代码
add dword ptr ds:[edi+0x24],0x1 //把[edi+0x24]的值和0x1相加,放[edi+0x24]中
mov eax,dword ptr ds:[edi+0x24] //把[edi+0x24]的值赋给eax
cmp eax,dword ptr ds:[edi+0x28] //eax和[edi+0x28]中的值进行比较
jle 0x004872AC //小于等于则跳转
通过分析汇编代码我们猜测[edi+0x24]中存放的就是不断变大的冷却的数值,[edi+0x28]中存放的就是冷却好的数值,这样如果冷却的数值达到了程序所要的这个冷却数值,jle这个跳转就不会实现,如果没有达到就跳转继续循环,那我们猜测jle跳转后边的指令就是植物冷却的代码。
这样可以使用一开始说到的作废JCC的方式,让程序直接往下走。
回到游戏发现,所有的植物都已经没有了冷却了。
那么除了这种方式还可以通过修改JCC指令的逻辑来实现植物无冷却。
这里jle指令的意思是小于等于则跳转,那可以把jle改成jg,大于则跳转,这样程序在运行的时候是不会跳转的,继续往下执行,也可以实现所有植物无冷却。
0x05 总结
最后总结一下,这是一个简单的游戏逆向思路,通过CE修改植物大战僵尸的阳光数值了解了什么是基址,变量可能存储的三个位置,分别都有什么区别,通过逆向思维的方式找到基址。
我们不止步于通过修改器修改游戏参数,接着讲了怎么在游戏汇编中通过汇编代码修改游戏逻辑,列举了3种常见的修改程序逻辑的方法(直接修改、补码、作废jcc,如果了解jcc指令的话还可以通过修改jcc的逻辑来实现),当然肯定还有其他的很多方法可以实现。最终通过CE和OD合实现了我们的目的,修改了游戏的阳光和植物种植冷却。
后面还有自动化脚本的实现等等,可能后期我还会写一篇文章专门讲一讲,这篇就到这吧。