6.5 植物大战僵尸:寻找任意种植CALL

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

本次实验将介绍如何寻找任意种植CALL调用,所谓的任意种植指的是我们可以通过外部CALL调用的方式实现对植物的任意放置,通常情况下游戏中的格子是无法叠加种植的,但当我们找到了任意种植CALL调用时也就实现了叠加种植的效果,本次实验将通过两种方式分析并实现植物的任意种植效果,并通过注入代码实现通用辅助。

首先我们需要找到植物种植CALL调用,种植CALL调用的遍历技巧如下所示;

  • 打开修改器 > 回到游戏 > 然后回到CE > 扫描未知初始数
  • 回到游戏 > 拿起向日葵(不要种) > 搜索变动的数值 > 回到游戏(不要动) > 搜索未变动的数值
  • 回到游戏 > 放下向日葵 > 拿起豌豆射手 > 搜索变动的数值 > 以此重复进行直到找到为止

当我们使用鼠标点击时会在一个地址写入值,当换个植物则会换一个数值,这样如此反复就会找到CALL的地址,经过上方的排查读者应该能看到两个相近的内存地址,我们此处就选择134E7658这个地址,当我们放下植物时这个值会保持在4294967295范围内,如下图所示;

接着我们在该地址上面点击找出是什么访问了这个地址,回到游戏中我们再次点击植物时会看到如下图所示的输出结果,其中的地址0041239A则是触发植物时所出现的一条汇编指令;

  • 0041239A - 89 41 28 - mov [ecx+28],eax

此时我们关闭CE修改器,打开x64dbg调试器并附加到游戏进程上,运行程序,并通过Ctrl+G跳转到0041239A内存区域内,并在此处下断点,回到游戏拿起植物此处则会断下,当读者通过不断栈回溯最后读者应该能找到0x00410A8C处的跳转地址,当读者在此处下段点时会发现,当我们拿起植物并种下时此处就会被断下,而经过分析如果此处没有跳转则植物会被种下,而一旦跳转实现则植物将不会被种下,也就可以说明此处的jne 0x00410AD9就是植物种植的关键位置,而紧随其后的call 0x0040D120则是关键的种植CALL;

为了验证种植CALL的参数传递,此处我们在第一个格子中种植一个豌豆射手,并观察堆栈传递情况,当种下豌豆射手时参数传递如下图所示;

我们让程序运行起来,并在一个新的格子里种植一个向日葵,此时观察并对比参数传递情况,读者应该能看出两个植物种植所需参数之间的变化;

如上两张图的对比已经非常明显了,我们直接使用代码注入器注入分别将push 5 改成1,2,3,然后用mov eax,2控制在第几列种植,即可实现叠加效果。

push 0xFFFFFFFF
push 5
mov eax,2
push 0
push ebp
call 0x0040D120

为了实现自定义注入代码,此处的push 0x1359D108还需要定位来源,此处读者可自行寻找push ebp中EBP寄存器内的值,这里为了节约篇幅不在演示重复的内容,当读者找到了这段调用代码,则可以编写一段代码实现全屏种植效果,当前格子为4*8的矩阵,那么首先这段代码则可写成如下样子;

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

// 实现全屏种植
void MyASM()
{

for (int x = 0; x < 5; x++)
{
for (int y = 0; y < 9; y++)
{
__asm
{
pushad
push 0xFFFFFFFF
push 5
mov eax, x
push y
push 0x14ED3C68
mov ebx, 0x40D120
call ebx
popad
}
}
}
}

// 注入代码
BOOL InjectCode(DWORD dwProcId, LPVOID mFunc)
{
HANDLE hProcess, hThread;
LPVOID mFuncAddr, ParamAddr;
DWORD NumberOfByte;

// 打开当前进程
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcId);
if (hProcess == NULL)
{
return FALSE;
}

printf("[*] 打开目标进程 \n");

// 分配内存空间
mFuncAddr = VirtualAllocEx(hProcess, NULL, 128, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (mFuncAddr == NULL || mFuncAddr == 0)
{
return FALSE;
}

printf("[*] 分配内存空间 \n");

// 写出数据
if (!WriteProcessMemory(hProcess, mFuncAddr, mFunc, 128, &NumberOfByte))
{
return FALSE;
}

// 创建远程线程
hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)mFuncAddr, ParamAddr, 0, &NumberOfByte);
if (hThread == NULL)
{
return FALSE;
}

printf("[*] 创建远程线程 \n");

// 等待线程执行结束
WaitForSingleObject(hThread, INFINITE);

// 释放内存空间
BOOL virtual_flag = VirtualFreeEx(hProcess, mFuncAddr, 128, MEM_RELEASE);
if (virtual_flag == FALSE)
{
return FALSE;
}

// 关闭并返回
CloseHandle(hThread);
CloseHandle(hProcess);

return TRUE;
}

int main(int argc, char *argv[])
{
// 得到进程PID
DWORD Pid = GetPidByName("PlantsVsZombies.exe");

BOOL ref = InjectCode(Pid, MyASM);
if (ref == TRUE)
{
printf("[+] 代码注入完成 \n");
}

system("pause");
return 0;
}

通过手动替换EBP中的值,此处为push 0x14ED3C68并运行这段代码,则可实现全屏种植的效果,如下图所示;

既然找到了种植CALL的地址00410A94,那我们可以猜测,植物在种下之前是否会判断放入方格中是否有植物呢? 答案是肯定的,当我们在一个空地上种植的时候,我们能够种上说明条件成立,那如果方格中有植物则无法完成种植,条件也就不会成立,由此可猜到这里应该是使用一个条件判断来控制的,下面我们就去寻找这个条件判断的位置。

我们打开x64dbg并通过Ctrl+G跳转到0x410A94并在程序的段首0x40FD30处下断点,依次将过程中的关键判断条件进行断点测试,当读者运行到0x40FE2F当此处的JE指令跳转实现则我们即可实现叠加种植的效果,而如果没有跳则无法实现叠加,此处只需要将跳转改为JMP即可实现叠加;

通过编程的方式实现跳转,只需要将0040FE2F处替换为E9 20 09 00 00 90即可,实现代码如下所示;

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

using namespace std;

// 根据进程名得到进程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;
}

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

// 写出替换
unsigned char AutoSun[6] = { 0xE9, 0x20, 0x09, 0x00, 0x00, 0x90 };

BOOL ref = WriteByteSet(pid, 0x40FE2F, AutoSun, 6);
if (ref == TRUE)
{
printf("[*] 替换已开启 \n");

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

system("pause");
return 0;
}

叠加种植效果如下图所示;