6.1 植物大战僵尸:增加自己的阳光

《植物大战僵尸》是一款非常经典的塔防类游戏,由PopCap Games公司开发并先后在多个平台上推出。主要玩法为种植各种攻击性植物,抵御僵尸攻击,该游戏可以说绝大多数九零后都接触或者玩过,本章将通过逆向分析技术对该游戏进行分析,并实现一些游戏之外的功能,以此让用户理解二进制安全技术的应用范围。

根据第二章中CE修改器的使用方法,相信读者应该能理解如何增加自己的阳光了,通过简单的阳光地址排查即可得到阳光的动态地址,本次实验目标,通过逆向分析植物阳光数量的动态地址找到阳光的基址与偏移,从而实现每次启动游戏都能够使用基址加偏移的方式定位阳光数据,最后我们将通过使用C语言编写通用辅助实现自定义阳光功能,在开始之前我们先来说一下为什么会有动态地址与基址的概念!

我们都知道大部分编程语言都会有局部变量全局变量这两种,相对于局部变量来说是在游戏运行后动态分配的默认由堆栈存储,而全局变量则是我们所说的基址其默认存储在全局数据区,全局数据区里面的数据则是在编译的时候就写入到程序里了,所以不会变化,而游戏的开发都会使用面向对象技术,我们可以推测游戏中的阳光很可能就是类中的一个数据成员,而数据成员的地址就是通过new()动态分配的,如下代码:

#include <stdio.h>

class SunClass
{
public:
int SunTime;
int SunValue;
int SunAttr;
};

int main(int argc, char *argv[])
{
SunClass *Sun=new SunClass;
Sun->SunValue=100;
printf("SunValue: %d",Sun->SunValue);
return 0;
}

如上代码定义了SunClass类,在主函数中我们为Sun实例指针动态分配了内存,分配的内存存储在栈中,而栈地址每次都会发生变化,所以分配的内存地址是不固定的,从而导致阳光的地址是动态变化的。

好!现在我们就进入正题,先从最简单的阳光地址找起,首先你需要运行游戏并附加植物大战僵尸进程,然后我们开启新的游戏,首次扫描我们先来遍历4字节的150吧,也就是搜索当前阳光的数量,当然你也可以尝试搜索金钱数量等,道理都是一样的,这里就拿阳光的搜索方法作为演示目标。

接着我们需要让阳光发生变化,这样才可以让我们继续更加精确的确定这个局部变量在内存中的地址是多少,此处我手动种植了一颗豌豆射手则此时的阳光变为了50个,我们就输入50然后再次扫描,由于这款游戏比较简单,基本上经过两次筛选就能定位到阳光的内存地址了,在遍历一些大型游戏的时候,读者应该有耐心,经过多次筛查直到最终找到正确的动态内存地址为止。

观察上图13D1A588地址,会发现CE显示该地址是一个灰色地址,在CE中灰色就表示是动态地址而绿色则表示基址,此处的动态地址则相当于我们上方代码中给一个类动态new开辟的内存空间的首地址,由于该地址是系统为我们动态开辟的,所以每次重启游戏该地址都会发生变化,为了能够固定该地址我们需要寻找到它的基址。

此时读者需要在该地址上方右键,选择查找改写地址当我们选择查找改写地址的时候,其实CE就为我们在这个地址上下了硬件写入断点,此时回到游戏等待阳光出现并点击阳光,则此时会出现以下一条汇编指令。

上图中我们可以得知add [eax+5560],ecx这条指令是加法运算,最右侧ECX里面就是我们当前需要增加的阳光数,将ECX中的阳光数赋值给[eax+5560]这个内存地址,那么我们的阳光就会增加,此时我们需要知道EAX寄存器指向的地址是多少,CE中已经为我们分析出了EAX寄存器当前值是13D15028此时需要记下它的一级偏移5560,然后去搜索13D15028这个内存地址。

根据上图搜索结果可以看到有非常多的数据,那我们该如何判断应该选择那一个呢?这里就是一个技巧的问题了,我们需要尽量选择地址不同的,比如高亮处的位置是我们重点关注的对象,其中0294A689这个内存地址就相当于我们SunClass类实例化的基地址,而5560则是阳光在类中的偏移地址,此处我们需要分析谁给EAX赋值了,直接在0294A689右键,查找访问地址,然后会看到以下截图内容:

此处会出现一大堆指令,这里也需要一个遍历技巧,我们可以排除CMP之类的对比指令,因为我们是增加阳光所以不可能出现对比的代码,此外我们需要关注操作数左侧是EAX的,因为我们要找的是谁给EAX赋值的,我们选择mov eax,[ecx+00000768]这条汇编指令,之所以会选择这一条指令是因为我们的上一条指令是add [eax+5560],ecx而根据词条指令我们需要得到EAX寄存器的值,所以在mov eax,[ecx+00000768]恰巧是对EAX寄存器进行了赋值操作,则选择这一句将会是正确的,然后发现二级偏移是768,继续查找谁给ECX赋值的,这里直接记下ECX寄存器中的地址02949F30,并继续搜索这个内存地址;

如上图所示,继续搜索十六进制数02949F30如下搜索结果可以看到有绿色的地址,这些绿色的地址都属于全局变量,到此说明我们已经找到了这个阳光的基地址了,这里我们可以随意选择绿色的地址作为基址使用,此处我选择的是PlantsVsZombies.exe+2A9EC0也就是006A9EC0来当作基址使用,前面找到的地址每次启动游戏都会发生变化,而这个基址是永远不会变化的。

至此我们通过查找到的基址与偏移相加的形式,就可以定位到动态地址了,具体公式应该是阳光= [[[006a9ec0]+768]+5560],我们可以直接在CE中添加这个指针,用于进行测试,如下图所示:

最后我们再来总结一下查找思路,其基址查找过程可以描述为以下流程,如果用正向的思路来理解的话应该从后向前来看,会发现正向思路来看会非常的清晰,而我们找基址则是从逆向的角度来分析,也就是从前向后来理解这个过程。

已知阳光的动态地址ECX的值就是增加的阳光 将增加值ECX赋值给[eax+5560]我们就得到了阳光

  • add [eax+00005560],ecx

我们需要继续找出EAX是多少? 由第二条汇编指令可知EAX的值来自于[ecx+768]这个地址

  • mov eax,[ecx+00000768]

最后我们继续跟随查找ECX里面存储的数据得到[006A9EC0]该数据明显属于全局数据区

  • mov ecx,[006A9EC0]

最后总结出定位静态基址公式,当前阳光就等于[[[006a9ec0]+768]+5560]获取到。

接下来则是如何通过编程的方式读取并修改我们的阳光数量,如下这样一段代码,它可以实现读取动态地址并修改阳光数量,首先通过GetPidByName()我们可以根据进程名称获取到该程序的进程PID号,其次通过CalculateDynamicMemoryAddresses()这个自定义函数可动态计算处当前阳光的动态地址,最后通过调用WriteProcessMemory向该地址内写出一个数值,并以此实现替换阳光的目的,这段代码功能如下所示;

#include <iostream>
#include <windows.h>
#include <string>
#include <tlhelp32.h>

// 根据进程名得到进程PID
DWORD GetPidByName(const char* name)
{
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32 = { sizeof(PROCESSENTRY32) };
DWORD pid = 0;

if (Process32First(snapshot, &pe32))
{
do
{
if (_stricmp(pe32.szExeFile, name) == 0)
{
pid = pe32.th32ProcessID;
break;
}
} while (Process32Next(snapshot, &pe32));
}
CloseHandle(snapshot);
return pid;
}

// 计算动态内存地址
int CalculateDynamicMemoryAddresses(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;
}

// 设置阳光
BOOL SetSunshine(const char *process, int value)
{
DWORD pid = GetPidByName(process);
printf("[*] 进程PID = %d \n", pid);

INT base;
INT offset[3];
base = 0x006a9ec0;
offset[0] = 0x768;
offset[1] = 0x5560;

int addr = CalculateDynamicMemoryAddresses(pid, base, offset, 2);
printf("[*] 进程动态地址 = 0x%x \n", addr);

HANDLE Process = OpenProcess(PROCESS_ALL_ACCESS, false, pid);

return WriteProcessMemory(Process, (LPVOID)addr, &value, 4, 0);
}

int main(int argc, char *argv[])
{
DWORD Sun = 9999;
BOOL ref = SetSunshine("PlantsVsZombies.exe", Sun);

if (ref == TRUE)
{
printf("[+] 阳光已被调整为 %d \n", Sun);
}
else
{
printf("[-] 增加失败 \n");
}

system("pause");
return 0;
}

至此,当读者尝试运行上述代码时,则会将目标进程内的阳光数量调整为9999如下图所示;