6.4 植物大战僵尸:寻找阳光掉落CALL

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

本次实验将接触到Call调用这个概念,Call相当于你在编程时所编写的函数,而高级语言中的函数最终也是会被编译器转换为汇编格式的Call调用,这些关键Call普遍都会存在各种参数,关于Call的作用,打个比方有些网游外挂可以实现自动寻路,自动吃药,自动打怪,甚至是全屏秒杀,这些功能是通过修改数值也无法做到的,Call就可做到。

其实关键Call就是作者开发过程中写的一个个处理不同事件的独立的处理函数,这些函数包括了各种独立的游戏功能,而我们可以在远程进程中开辟线程,并通过汇编形式动态的调用这些关键Call,从而实现一些变态功能。

我们通过查找阳光的掉落的定时器,并通过定时器变量顺藤摸瓜找到生成阳光的关键Call,通过给此Call传递不同参数实现掉落阳光,钻石,零秒通关等,阳光遍历技巧如下:

  • 进入游戏等待阳光出现 > 当阳光出现后 > 马上搜索未知初始数值
  • 返回游戏等待时钟发生变化 > 马上切回CE > 搜索减少的数值 > 阳光下落一点搜一点
  • 经过上方步骤反复排查 > 最终能找到一个值范围(0-700) > 锁定1即可实现无限掉落

关于阳光掉落基址与偏移的找法,在文章开头就已经分享了查找的技巧,此处直接给出控制阳光掉落公式 [[[006A9F38 + 768] + 5538]]

  • 00413B7C - 83 86 38550000 FF - add dword ptr [esi+00005538],-01
  • 004524F4 - 8B 86 68070000 - mov eax,[esi+00000768]
  • 00599F75 - A1 389F6A00 - mov eax,[PlantsVsZombies.exe+2A9F38]

读者可自行将这段基址与偏移添加到CE的地址栏中,当添加好以后,读者应该能看到如下图所示的提示信息;

我们要找关键Call只需要等待阳光出现,当阳光出现后,CE会检测到一条数据 add dword ptr [esi+00005538],-01 说明此时定时器开始工作了,我们只需要记下这个内存地址00413B7C,然后关闭CE,并打开x64dbg附加到游戏。

此时我们关闭CE修改器,并打开x64dbg附加到游戏进程,然后按下Ctrl+G定位到00413B7C 此时我们需要关注add dword ptr ds:[esi+0x5538],0xFFFFFFFF这条计时器指令下方,就是一个大跳转该跳转跳向了结束,那么我们可以猜测jne 0x413BF1跳过的内容很有可能就是跳过了阳光的生成过程,也就是可能跳过了阳光生成Call。

玩过此游戏的一定知道,游戏屏幕中不止可以掉落出阳光,还可以掉出其他的东西,比如钻石,金钱,奖牌等,那么我们有理由相信,该游戏中调用的Call应该有很多参数传递,比如掉落属性,掉落坐标,掉落类型等,而我们已经找到了阳光计时器每次递减的汇编代码,故猜测调用Call应该就在附近,向下查找发现有很多Call调用,但有一个比较特别的call 0x40CB10,之所以说它特别是因为该Call在调用之前,通过Push传递了多了参数,此处很可疑,我们需要具体分析。

此时我们在00413BE4处下断点,然后回到游戏等待阳光的掉落,当阳光掉落时x64dbg会断下,此时我们将EAX寄存器内的4修改为2,并运行运行程序,观察会出现什么情况;

当读者运行后,回到游戏中会发现本应该出现阳光的地方居然出现了一个金币,而根据测试金币所代表的数字应该就是2了;

由上方的分析结果可知,当我们给EAX传入不同的值则代表不同的物件,如下是本人经过不断尝试后得出的一些物件的具体下标值;

// 第一个push参数传递
push 0 普通掉落
push 2-3 其他掉落方式
push 4 自动收集阳光
push 6 右侧滑出掉落

// 第二个push参数传递
eax=2 金钱
eax=3 钻石
eax=4 普通阳光
eax=5 小的阳光
eax=6 大的阳光
eax=8 自动通关神器

// 猜测该子过程的实现流程
sub_00413BE4(push 0= 物品飞出状态,push eax= 掉落物品,push 0x3c,push edi= 对象状态,ecx= 游戏基地址)
{
return 0;
}

到此,我们还没有办法完成外部注入,因为据我观察程序中的edi寄存器与ecx寄存器中的数据是动态的,每次游戏重新运行都会发生变化,如果想要在外部调用这个Call函数,我们需要找到这两个寄存器的基址,或者说找到他们的来源。

先来分析第一个地址如何定位,第一个动态地址是EDI寄存器,这个寄存器每次存储一个整数,此处无法直接找基址,我采取的方式是从后向前逆推代码,观察那些指令改写了该寄存器,然后将这些改写寄存器的指令拼凑起来。

这些调用分别是上图中的,标签赋值1与标签赋值3,另外加一个call 0x005AF400,程序中正是通过这种方式计算出动态数据的,我们直接将其提取出来,提取后代码如下:

00413B85 | B8 26020000                  | mov eax,226                             | 赋值1
00413B8A | E8 71B81900 | call <0x005AF400> | 这个CALL影响EAX寄存器
00413B96 | 8BF8 | mov edi,eax | 赋值2
00413BA8 | 83C7 64 | add edi,64 | 赋值3

再来分析第二个也就是图中的call 0x0040CB10,第二个相对于第一个来说就好找许多了,因为它是一个动态地址,我们再次回到游戏并等待游戏中阳光的掉落,当阳光掉落后则会断在call 0x0040CB10位置处,我们此时需要记下ECX寄存器中的值,此处的地址是1359D108如下图所示;

接着我们在x64dbg中选择脱离进程(注意不可结束),接着我们打开CE修改器,然后我们来搜索十六进制数据1359D108搜索后可得到如下图所示的信息,此处可知一级偏移值是0x768,接着搜索02729F30内存地址;

  • 004524F4 - 8B 86 68070000 - mov eax,[esi+00000768]

当读者搜索02729F30后会找到0x6A9EC0这个内存地址,该地址也就是我们所需要找的基地址了,总结起来寻址公式为[6A9EC0+768],从这里也可以看出这应该是一个通用对象地址。

  • 00467B00 - 8B 0D C09E6A00 - mov ecx,[006A9EC0]

当读者找到了这些地址以后,相信你应该写出一段具有实际功能的注入代码了,笔者实现的代码流程如下所示;

pushad
mov eax,226
call 0x005AF400
mov edi,eax
add edi,64

mov esi,dword ptr ds:[ 006a9ec0 ]
mov esi,dword ptr ds:[ esi + 768 ]

push 0
push 4
push 3c
push edi
mov ecx,esi
call 0x40cb10
popad

至此我们需要测试一下这段反汇编代码的可用性,读者可以找任意一款反汇编代码注入软件,并点击注入按钮测试效果,但为了能够写出自己的辅助,我们还是需要实现一个简单的注入器,并能够通过远程的方式调用阳光生成Call,使用C语言编写如下代码即可实现注入效果,需要注意调用约定。

#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 AddSun()
{
__asm
{
pushad
mov eax, 226
mov ebx, 0x005AF400
call ebx
mov edi, eax
add edi, 64
mov esi, dword ptr ds : [0x006a9ec0]
mov esi, dword ptr ds : [esi + 0x768]
push 0h
push 0x6
push 0x3c
push edi
mov ecx, esi
mov edx, 0x40cb10
call edx
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");

// 循环注入代码
for (int i = 0; i < 50; i++)
{
BOOL ref = InjectCode(Pid, AddSun);
if (ref == TRUE)
{
printf("[+] 代码注入完成 => %d \n", i);
}
}

system("pause");
return 0;
}

当读者运行这段代码后,则游戏内会生成50个大阳光,当然读者也可以自定义修改例如掉落钻石或者是其他物件,输出效果图如下所示;