植物大战僵尸:逆向分析阳光
lyshark 人气:0植物大战僵尸这款游戏可以说是很多90后的回忆了,基本上只要是90后或多或少的都接触过,而玩游戏与制作辅助是两个概念,今天我将给大家分享一些游戏辅助方面的制作技巧,之所以使用植物大战僵尸这款游戏是因为游戏简单容易分析,且不需要考虑驱动保护版权等相应的问题,这里我会把我的分析思路分享出来,来供大家参考。
简单实现无限阳光
本次实验内容:通过逆向分析植物阳光数量的动态地址找到阳光的基址与偏移,从而实现每次启动游戏都能够使用基址加偏移的方式定位阳光数据,最后我们将通过使用C语言编写通用辅助实现简单的无限阳光外挂,在教程开始之前我们先来说一下为什么会有动态地址与基址的概念!
大部分编程语言都会有局部变量和全局变量,相对于局部变量来说是在游戏运行后动态分配的默认由堆栈存储,而全局变量则是我们所说的基址其默认存储在全局数据区,全局数据区里面的数据则是在编译的时候就写入到程序里了,所以不会变化,而游戏的开发都会使用面向对象技术,我们可以推测游戏中的阳光很可能就是类中的一个数据成员,而数据成员的地址就是通过new动态分配的,如下代码:
#include <stdio.h>
class SunClass{
public:
int SunTime;
int SunValue;
int SunAttr;
};
int main()
{
SunClass *Sun=new SunClass;
Sun->SunValue=100;
printf("SunValue: %d\n",Sun->SunValue);
return 0;
}
如上代码定义了SunClass
类,在主函数中我们为Sun实例指针动态分配了内存,分配的内存存储在栈中,而栈地址每次都会发生变化,所以分配的内存地址是不固定的,从而导致阳光的地址是动态的
好!现在我们就进入正题,开始挖掘游戏数据,先从最简单的阳光地址找起来吧,首先你需要运行游戏并附加植物大战僵尸进程,然后我们开启新的游戏,首次扫描我们先来遍历4字节的50,也就是搜索当前阳光的数量,当然你也可以尝试搜索金钱数量等,道理都是一样的,这里就拿阳光的搜索方法作为演示目标。
接着我们需要让阳光发生变化,这样才可以让我们继续更加精确的确定这个局部变量在内存中的地址是多少,此处我手动种植了一颗向日葵则阳光变为了0,我们就输入0然后再次扫描,由于这款游戏比较简单,基本上经过两次筛选就能定位到阳光的内存地址了,在遍历一些大型游戏的时候,读者应该有耐心,经过多次筛查直到最终找到正确的(动态)内存地址为止。
观察上图13C66448
地址,会发现CE显示该地址是一个灰色地址,在CE中灰色就表示是动态地址而绿色则表示基址,此处的动态地址则相当于我们上方代码中给一个类动态new开辟的内存空间的首地址,由于该地址是系统为我们动态开辟的,所以每次重启游戏该地址都会发生变化,为了能够制作外挂我们必须要找到阳光的基址。
我们继续将地址栏中的地址双击加入到最底部的地址栏,然后在地址上右键,选择查找改写地址
当我们选择查找改写地址的时候,其实CE就为我们在这个地址上下了硬件写入断点,这个下断点的功能我们同样可以使用X64dbg来完成,此时回到游戏等待阳光出现并点击阳光,则此时会出现以下汇编指令。
上图中我们可以得知add [eax+5560],ecx
这条指令是加法运算,最右侧ECX里面就是我们当前需要增加的阳光数,将ECX中的阳光数赋值给[eax+5560]
这个内存地址,那么我们的阳光就会增加,此时我们需要知道EAX寄存器指向的地址是多少,CE中已经为我们分析出了EAX寄存器当前值是13C60EE8
我们此时需要记下它的一级偏移5560
,然后去搜索13C60EE8
这个内存地址。
上图搜索结果可以看到有非常多的数据,那我们该如何判断应该选择那一个呢?这里就是一个技巧的问题了,我们需要尽量选择地址不同的,比如标红处的位置是我们重点关注的对象,其中13C60EE8这个内存地址就相当于我们SunClass
类实例化的基地址,而5560则是阳光在类中的偏移地址,此处我们需要分析谁给EAX赋值了,直接在00FE82E8
右键,查找访问地址,然后会看到以下截图内容:
此处会出现一大堆指令,这里也需要一个遍历技巧,我们可以排除CMP之类的对比指令,因为我们是增加阳光所以不可能出现对比的代码,此外我们需要关注操作数左侧是EAX的,因为我们要找的是谁给EAX赋值的,我们选择mov eax,[ecx+00000768]
这条汇编指令,然后发现二级偏移是768
,我们继续查找谁给ECX赋值的,这里直接记下ECX寄存器中的地址00FE7B80
继续搜索十六进制数00FE7B80
如下搜索结果可以看到有绿色的地址,这些绿色的地址都属于全局变量,到此说明我们已经找到了这个阳光的基地址了,这里我们可以随意选择绿色的地址作为基址使用,此处我选择的是006A9EC0
来当作基址使用,前面找到的地址每次启动游戏都会发生变化,而这个基址是永远不会变化的。
最后我们通过查找到的基址与偏移相加的形式,就可以定位到动态地址了,具体公式应该是阳光= [[[006a9ec0]+768]+5560]
,我们可以直接在CE中添加这个指针,用于进行测试,如下图所示:
最后我们再来总结一下查找思路,其基址查找过程可以描述为以下流程,如果用正向的思路来理解的话应该从后向前来看,会发现正向思路来看会非常的清晰,而我们找基址则是从逆向的角度来分析,也就是从前向后来理解这个过程。
已知阳光的动态地址ECX的值就是增加的阳光 将增加值ECX赋值给 [eax+5560] 我们就得到了阳光
00430A11 - 01 88 60550000 - add [eax+00005560],ecx <<
我们需要继续找出EAX是多少? 由第二条汇编指令可知EAX的值来自于[ecx+768]这个地址
0045B6FD - 8B 81 68070000 - mov eax,[ecx+00000768] <<
最后我们继续跟随查找ECX里面存储的数据得到 [006A9EC0] 该数据明显属于全局数据区
00467B00 - 8B 0D C09E6A00 - mov ecx,[006A9EC0] <<
最后总结出定位静态基址公式 【阳光= [[[006a9ec0]+768]+5560]】
通过编程的方式读取并修改我们的阳光数量,如下这样一段代码,它可以实现读取动态地址并修改阳光数量。
#include <iostream>
#include <Windows.h>
int GetDyAddr(int Pid,int Base, int Offset[], int len)
{
int temp;
HANDLE Process;
Process = OpenProcess(PROCESS_ALL_ACCESS, false, Pid);
ReadProcessMemory(Process, (LPVOID)Base, &temp, 4, NULL);
for (int i = 0; i < len; i++)
{
if (i == len - 1)
temp += Offset[i];
else
ReadProcessMemory(Process, (LPVOID)(temp + Offset[i]), &temp, 4, NULL);
}
return temp;
}
int main()
{
int base;
int offset[3];
int PID = 5772;
base = 0x006a9ec0;
offset[0] = 0x768;
offset[1] = 0x5560;
int addr = GetDyAddr(PID, base, offset, 2);
printf("进程地址:%x\n", addr);
HANDLE Process = OpenProcess(PROCESS_ALL_ACCESS, false, PID);
WriteProcessMemory(Process, (LPVOID)addr,&PID,4,0);
}
实现自动收集阳光
本次实验内容:通过阳光增加的值为切入点,找到自动收集阳光的关键判断并实现自动收集阳光,首先我们猜测当阳光出现后,我们是否会去点击,这个过程必然是由一个判断和一个时钟周期事件来控制的,那么当我们点击下落的阳光以后,则该判断条件实现,会执行收集阳光的CALL,否则的话继续执行阳光下落的过场动画,这正是正向开发的一种开发手段,此时我们也仅仅是猜测,接下来我们将去验证这个大胆的想法。
为了找到阳光自动收集的关键跳转,我们需要以阳光增加作为切入点,为啥以它作为切入点呢?我们可以这样思考,当我们点击阳光后阳光增加了,说明已经完成了判断,下一步就是写入变量从而增加阳光,那么我们先来找到阳光的动态地址,并在该动态地址上按下F6键查找写入,然后回到游戏等待阳光出现并点击阳光,此时CE会出现以下代码,我们只需要记下00430A11
这个内存地址,然后直接关闭CE。
接着打开X64dbg附加到游戏进程,附加完成后,游戏会被X64dbg暂停运行,此时我们直接按下F12让游戏运行起来,然后按下Ctrl + G
输入00430A11
跳转到刚才找到的代码位置,过去以后直接F2下一个断点。
此时我们需要逆向思考一个问题add dword ptr ds:[eax+0x5560],ecx
这条指令是在我们阳光被点击后执行的,也就是说我们已经点击了阳光现在开始赋值了,那判断阳光是否被回收肯定是在这条指令之前出现,所以我们向上找,观察代码我们不难看出执行add dword ptr ds:[eax+0x5560],ecx
指令之前有一个无条件跳转jmp 0x00430A0E
跳过来的。
继续向上查找跳转来源,可知在jmp跳转之前有一个je 0x004309EF
跳转,经过测试这个地方具体控制阳光是否增加,在向上找就到段首了,此处代码中并没有出现自动收集阳光的关键跳转,因此推断这里应该是一个控制阳光是否增加的子过程(子过程:过程中调用的过程,称为子过程),所以我们继续回朔到上一层。
为了能够回朔到上一层,我们需要取消阳光递增处的断点,并在段尾00430AB3
处下一个F2断点防止程序跑飞,回到游戏等待阳光的出现,然后X64dbg就会断下,断下后直接取消00430AB3
处的断点,执行到Ret处即可返回到上一层。
返回到上一层以后,可以看到我们正是在call <plantsvszombies.sub_4309D0>
这里出来的,而上方就有一个jne plantsvszombies.4313FD
关键跳,此处的关键跳转也并不是控制是否回收阳光的关键跳转,而此处的代码量比较少,因此判断此处还是一个子过程,我们继续回溯到上一层。
我们直接单步F8运行到返回,并出这个CALL,出CALL以后会看到call <plantsvszombies.sub_430E40>
没错!我们正是从这个子过程里出来的,接着向上找跳转会看到有一个jne plantsvszombies.431599
此处如果将其改为jmp
的话即可实现自动收集阳光,也就是说如果jne跳转实现则执行收集阳光,否则继续执行阳光下落的过场动画。
注意:如果我们在关键跳jne plantsvszombies.4313FD
处下断点时,会发现当阳光出现后程序会被无限的断下,这说明是有一个定时器线程在不断的执行判断代码,每次都会判断你是否点击了阳光,所以X64dbg才会被一直断下。
知道了修改流程,那我们就通过编程的方式来实现修改程序的硬编码,首先我们可以通过以下代码完成字节集的读取。
#include <stdio.h>
#include <Windows.h>
byte *ReadByteSet(DWORD Pid, DWORD Base, DWORD Size)
{
HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, 0, Pid);
byte *buf = new byte[Size];
ReadProcessMemory(handle, (LPVOID)Base, buf, Size, NULL);
return buf;
}
int main()
{
byte *Buff = new byte[10];
Buff = ReadByteSet(2232, 0x00401000, 10);
for (int i = 0; i < 10; i++)
printf("%02X ", Buff[i]);
return 0;
}
既然有读取内存字节集,那么就有写入字节集,如下代码就是一种字节集写入的实现方式。
#include <stdio.h>
#include <Windows.h>
BOOL WriteByteSet(DWORD Pid, DWORD Base, unsigned char *ShellCode, DWORD Size)
{
BYTE *Buff = new BYTE[Size];
memset(Buff, *ShellCode, Size);
HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, 0, Pid);
BOOL Ret = WriteProcessMemory(handle, (LPVOID)Base, Buff, Size, NULL);
if (Ret)
return TRUE;
else
return FALSE;
}
int main()
{
unsigned char shell[] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };
BOOL temp = WriteByteSet(3772, 0x00401010, shell, 8);
return 0;
}
想要实现阳光自动收集,只需要将0x0043158F
机器码0x75 0x08
修改为0xEB 0x08
即可实现效果。
unsigned char shell[] = { 0xEB };
BOOL temp = WriteByteSet(9744, 0x0043158F, shell, 1);
寻找葵花生产速度
本次实验内容:通过CE修改器遍历出控制太阳花吐出阳光的时间变量,太阳花吐出阳光是由一个定时器控制的,首先我们找到第一个太阳花的基址与偏移,然后找出第二个太阳花的动态地址,并通过公式计算得到太阳花结构长度的相对偏移,最后我们通过C语言编程实现,遍历并修改所有图中的太阳花吐出阳光的时间,最终实现全图吐阳光。
从本次实验开始将接触到关于分析定时器的相关技巧,一般的定时器分为递增定时器与递减定时器,不过大多数游戏都会使用递减定时器,因为递减定时器更好编程判断,本游戏中的太阳花生产速度使用的就是递减定时器,太阳花生产阳光一定是一个周期性的事件,我们只要找到该定时器并改写它的时间即可实现无限吐阳光,如下是太阳花定时器的遍历技巧:
首先种下第一个太阳花 -> 然后CE马上搜索 -> 未知的初始值
回到游戏短暂等待(时钟发生变化) -> 然后马上切回CE -> 搜索减少的数值 -> 掉一点搜一点
如果中途太阳花吐出了阳光 -> 则需要搜索增加的数值(1次) -> 然后再搜索减少的数值
最终找到一个动态地址(范围:0-5000) -> 锁定该变量范围在1至10即可 -> 实现无限出阳光
修改太阳花时钟有两种方式,第一种找到基址与偏移然后分别修改每一个定时器的时钟,第二种方式则是找到汇编跳转并进行改写,第一种方式要找植物相对偏移,首先我们先来猜测以下游戏作者会用什么方式存储不同植物的栏位。
如下图: 我们可先来猜测,游戏作者会使用二维结构体来存储植物位置,通过结构体链表将不同植物进行连接,当我们铲除植物的时候,只需要在链表中摘除相应节点,而太阳花的的地址一定是连续存储在内存中的线性空间,此游戏的矩阵可能就是5*9
这么一个范围,假设在横坐标X轴如果两个植物之间的相对偏移是14C(14C就是太阳花结构体的实际长度),那么我们找到第一个植物的基址与偏移,每次相加14C的偏移量,则可遍历到下一个植物的内存地址,同理如果相减14C则就可遍历出上一个植物的内存地址,而纵坐标Y可能就是由一个1C偏移来控制的,此时我们也仅仅只是猜测。
如果我们按照上图中的方式进行推理,其计算每一个阳光时钟公式就可总结为如下,但真的是这样吗?
X坐标下的第1个植物:基地址 + 偏移1 + 偏移2 + 768
X坐标下的第2个植物:基地址 + 偏移1 + 偏移2 + 768 + 14C
X坐标下的第3个植物:基地址 + 偏移1 + 偏移2 + 768 + 14C + 14C
Y坐标下的第1个植物:基地址 + 偏移1 + 偏移2 + 768 + 1C
Y坐标下的第2个植物:基地址 + 偏移1 + 偏移2 + 768 + 1C + 14C
其实并不是!经过我对具体坐标的分析,在本游戏中太阳花与太阳花之间,可能使用了一维结构体来存储的植物与植物之间的属性,每次相加偏移都会遍历到下一个植物的属性上面,也就是说无论太阳花种植到在什么位置,只要相加偏移就可以遍历到下一个植物的冷却数据,而需要遍历的次数则取决于太阳花的种植数量。
首先我们种植一颗太阳花,并通过上方的遍历技巧找到当前第一个植物的动态地址,排查到最后可发现剩余11条结果,此时我们可猜测这个定时器应该在0-10000之间,应该不会大于这个参数,如下图我找到了13D65160
这个地址,将该地址锁定为10就可以实现第一个太阳无限吐阳光。
接着我们在第一个太阳花的旁边种植第二个太阳花,然后还是使用前面的遍历技巧找到第二个太阳花的动态地址13D652AC
,找到以后我们可以猜测第一个与第二个在内存中的布局应该是连续的,所以我们可以使用13D652AC - 13D65160 = 14C
此处得到的14C其实就是太阳花结构的实际长度,也可以说是两个太阳花之间的偏移值。
既然知道了太阳花之间的相对偏移,那么我们下一步就是来找一个固定的地址,我们在第一个太阳花地址上,右键选择查找改写地址,然后可看到0045FA48 - 83 47 58 FF - add dword ptr [edi+58],-01 <<
这条汇编指令,此汇编指令乍一看是一条加法指令,但其相加的操作数是-1也就是相减,此处就是太阳花的定时器,其每次减1直到为0则吐出阳光,这里我们就可知该定时器是一个递减定时器,我们只需要记下偏移为58
下一个地址是13D65108
即可。
回到CE我们继续搜索十六进制地址13D65108
然后找到偏移为AC
下一个地址为13D08948
继续搜索13D08948
得到下一个偏移数据为768
,继续搜索00FE7B80
最终经过我们的搜索得到了一个绿色的基址00599F75 - A1 389F6A00 - mov eax,[006A9F38] <<
最后使用CE添加这个基地址与偏移数据来验证一下,公式为 [[[006A9F38+768]+AC]+58]]
此时我们就可以定位到第一个太阳花的动态地址了。
根据上面的理论,我们知道太阳花的结构体大小为14C
,那么我们在第一个太阳花动态地址的基础上加上14C就可以得到第二个太阳花的动态地址。
既然找到了基址与偏移,接下来就是通过C语言编程实现修改全图太阳花的冷却时间,此处贴出我实现的代码.
int main()
{
int base;
int offset[4];
int PID = 1292;
base = 0x006a9f38;
offset[0] = 0x768;
offset[1] = 0xac;
offset[2] = 0x58;
int addr = GetDyAddr(PID, base, offset, 3);
printf("阳光吐出动态地址:%x\n", addr);
HANDLE Process = OpenProcess(PROCESS_ALL_ACCESS, false, PID);
int SunOffset = 0;
int SunNum = 10;
while (TRUE)
{
for (int i = 0; i < 5; i++)
{
WriteProcessMemory(Process, (LPVOID)(addr + SunOffset), &SunNum, 4, NULL);
SunOffset = SunOffset + 0x14c;
printf("搞事 fuck ok\n");
}
SunOffset = 0;
Sleep(1000);
}
return 0;
}
当我们进入游戏种植好太阳花以后,我们开启这个辅助,即可实现让前四个太阳花吐出阳光,最后种植的太阳花则不修改,为了防止程序出现假死我通过sleep函数让太阳花每一秒吐出一个阳光,这样修改的话会很有节奏感。
上述方法,虽然可以修改达到无线吐阳光的作用,但是这种修改方式,显然是不太合理,如果图中有10个太阳花,那么我们则只能循环十次,这种效率还是太低,其实我们可以通过直接修改硬编码的方式来实现一劳永逸的效果,之所以是一劳永逸,是因为所有太阳花的吐阳光判断都是共用一个判断函数执行的,阳光的递减时钟都会走一个地方add dword ptr [edi+58],-01
我们只需要定位到这里,然后分析出阳光产生的关键键跳转并改掉其硬编码即可。
上图是经过测试后备注的一些细节,我们只需要将图中的0045FA7D
处的指令集,替换为nop即可实现全图的植物无线吐阳光啦,其C语言修改代码如下,代码中使用了上面封装好的的写内存字节集函数。
int main()
{
int PID = 3612;
unsigned char Auto[] = { 0xEB };
unsigned char Suns[] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };
BOOL ret = WriteByteSet(PID, 0x0043158F, Auto, 1);
BOOL ret1 = WriteByteSet(PID, 0x0045FA7D, Suns, 6);
if (ret != 0 & ret1 != 0)
{
printf("您的向日葵已打包,请注意查收\n");
}
return 0;
}
最终配合自动收集阳光,即可实现如下图所示的变态功能,其实这也不算变态,毕竟还有更加变态的,其实这种修改方式并不完美,因为我们的阳光数量可能是一个整数类型,如果不加以控制,当整数变量到达所能承载的最大范围时,则程序会发生整数溢出,轻则阳光变为负数,重则直接崩溃卡死。
文章备份链接:https://blib.cn/post/4ed8.html
原创作品:转载请加出处,您添加出处,是我创作的动力!
加载全部内容