6.2 植物大战僵尸:实现自动收集阳光

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

通过阳光增加的值为切入点,找到自动收集阳光的关键判断并实现自动收集阳光,首先我们猜测当阳光出现后,我们是否会去点击,这个过程必然是由一个判断和一个时钟周期事件来控制的,那么当我们点击下落的阳光以后,则该判断条件实现,会执行收集阳光的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跳转,经过测试这个地方具体控制阳光是否增加,在向上找就到段首了,此处代码中并没有出现自动收集阳光的关键跳转,因此推断这里应该是一个控制阳光是否增加的子过程(子过程:过程中调用的过程,称为子过程),所以我们继续回朔到上一层。

为了能够回朔到上一层子过程中,我们需要取消阳光递增处00430A11的断点,并在段尾00430AB4处下一个F2断点防止程序跑起来,接着读者需要回到游戏中并等待阳光的出现,当阳光出现后,此时x64dbg就会断在00430AB4断点处,断下后直接取消00430AB4处的断点,执行到Ret处即可返回到上一层。

如下图当读者出CALL后,可以看到我们正是在call <plantsvszombies.sub_4309D0> 这里出来的,而上方就有一个jne plantsvszombies.4313FD关键跳,此处的关键跳转也并不是控制是否回收阳光的关键跳转,而此处的代码量比较少,因此判断此处还是一个子过程,我们继续回溯到上一层。

此时读者需要取消004313F4处的内存断点,此处需要注意,当读者来到004314BA时,在走一步则可以走出这一层CALL调用;

我们直接单步F8执行走出这个CALL,出CALL以后会看到call <plantsvszombies.sub_430E40>的关键调用,我们正是从这个子过程里出来的,接着向上找跳转会看到有一个jne plantsvszombies.431599此处如果将其改为jmp的话即可实现自动收集阳光,也就是说如果jne跳转实现则执行收集阳光,否则继续执行阳光下落的过场动画。

如果我们在关键跳jne plantsvszombies.4313FD处下断点时,会发现当阳光出现后程序会被无限的断下,这说明是有一个定时器线程在不断的执行判断代码,每次都会判断你是否点击了阳光,所以x64dbg才会被一直断下,如果想要实现自动收集阳光则只需要将0043158F处的机器码7508修改为EB08即可;

既然知道了流程那么修改将变得很容易实现了,首先我们需要实现一个ReadByteSet函数该函数用于读取特定位置的机器码,并存储起来其目的是当用户需要恢复时可以快速回退,接着封装一个WriteByteSet函数则用于专门修改特定位置处的机器码;

#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;
}

// 读取内存字节集
byte *ReadByteSet(DWORD Pid, DWORD Base, DWORD Size)
{
HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, 0, Pid);
if (handle == NULL)
{
return 0;
}

byte *buf = new byte[Size];

BOOL ref = ReadProcessMemory(handle, (LPVOID)Base, buf, Size, NULL);
if (ref == TRUE)
{
printf("[*] 读取数据成功 \n");
}
else
{
printf("[*] 读取数据失败 \n");
}
return buf;
}

// 写内存字节集
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);

return WriteProcessMemory(handle, (LPVOID)Base, Buff, Size, NULL);
}

int main(int argc, char *argv[])
{
// 得到进程PID
DWORD pid = GetPidByName("PlantsVsZombies.exe");
printf("[*] 进程PID = %d \n", pid);

// 读取原始字节集
byte *Buff = new byte[2];
Buff = ReadByteSet(pid, 0x0043158F, 2);

for (int i = 0; i < 2; i++)
{
printf("%02X ", Buff[i]);
}

// 写出替换自动收集功能
unsigned char shell[] = { 0xEB };

BOOL ref = WriteByteSet(pid, 0x0043158F, shell, 1);
if (ref == TRUE)
{
printf("[*] 替换数据成功 \n");

for (size_t i = 0; i < sizeof(shell); i++)
{
printf("%x \n", shell[i]);
}
}
else
{
printf("[*] 替换数据失败 \n");
}

system("pause");
return 0;
}

当这段代码被运行后则游戏内的参数将被更改,此时当出现阳光时会自动实现收集功能,效果图如下所示;