植物大战僵尸:代码实现无限阳光

本次实验内容:通过逆向分析植物阳光数量的动态地址找到阳光的基址与偏移,从而实现每次启动游戏都能够使用基址加偏移的方式定位阳光数据,最后我们将通过使用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
",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
", addr);
HANDLE Process = OpenProcess(PROCESS_ALL_ACCESS, false, PID);
WriteProcessMemory(Process, (LPVOID)addr,&PID,4,0);
}