6.6 植物大战僵尸:寻找全屏攻击CALL

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

本次实验目标是实现远程种植,要实现这个功能我们就需要找到植物的种植Call,种植Call什么时候会触发呢?当我们种植植物的时候必定会触发种植Call,那么此时我们只要遍历出植物的种植过程即可,通过CE我们找到拿起植物的关键代码,我们可以猜测植物拿起来这个动作之后应该就是种植了,只要我们能够找到控制拿起植物的代码,那么距离种植Call应该不会太远。

寻找植物种植CALL遍历流程如下:

  • 打开游戏并开启新关卡 > 然后在CE中首次扫描 > 扫描未知初始化数值
  • 回到游戏 > 拿起向日葵(不要种) > CE搜索变动的数值 > 回到游戏(不要动) > 搜索未变动的数值
  • 回到游戏 > 放下向日葵(右键) > 拿起豌豆射手 > CE搜索变动的数值 > 以此循环直到找到为止

通过使用上方查找技巧循环查找游戏数据,最后你可以看到如下图所示的一些内存地址,读者可根据自己的思路逐一排查分析,此处我们要找的内存地址默认值(放下植物后)是4294967295找到后请将这两个地址加入到地址栏中,如下图所示;

为了提高查找精度缩小出错的概率,我们手动拿起不同的植物,豌豆射手向日葵和樱桃炸弹,并观察CE中两个地址的变化,会发现地址栏中的两个地址是从0、1、2有顺序的变化,如下图当拿起向日葵时这个值变为了1,由于此游戏栏位下标是从0编号的,所以当拿起第二个植物时此处的编号则变为了1,如下图所示;

此时我们在0x13585884这个内存地址上右键选择找出是什么访问了这个地址,然后回到游戏手动种植一棵向日葵,此时地址表中会出现两条汇编指令,这两条指令正是我们种植植物时所访问的指令,直接记下这两条指令所对应的地址分别是0x00410AC1以及0x004123AC如下图所示;

接着我们以同样的方法,在0x13585888这个内存地址上查找访问代码,然后再次回到游戏中种植一个植物,此时地址表中会出现三条汇编指令,由于我们并不知道到底哪一条指令附近存在种植Call,所以最稳妥的办法就是直接记下这三条指令0x0410865/0x0410A91/0x041239A的内存地址,后期我们会逐步分析这几条汇编指令。

  • 00410AC1 - 8B 42 24 - mov eax,[edx+24] <<
  • 004123AC - 89 41 24 - mov [ecx+24],eax <<
  • 00410865 - 8B 46 28 - mov eax,[esi+28] <<
  • 00410A91 - 8B 40 28 - mov eax,[eax+28] <<
  • 0041239A - 89 41 28 - mov [ecx+28],eax <<

根据如上两张图我们可以总结出这样的五条汇编指令,其实这里有一个排除技巧,因为这几条指令都是在植物种植以后出现的,又因为种植植物这个动作肯定有参数的传递,而正常情况下参数的传递都会使用堆栈或寄存器传递,观察上图可发现mov eax,[eax+28]这条指令是将内存地址中的数据取出来赋值给寄存器,那么下一步很可能就是通过寄存器传递参数以作为Call的参数使用,而紧随其后的mov [ecx+28],eax则是即将要把EAX的值赋给一个内存地址,很明显不会是调用Call之前的动作,对于调用来说很有可能使用EAX寄存器传递参数,那么读者可以自行分析,将赋值语句中的内存传值直接排除掉。

通过经验排查,这五个地址中最有可能是种植Call的地址有两个,分别是0x0410AC10x0410A91这两个都是将数据从内存取出来并放入寄存器,下一步可能就是压入堆栈并调用Call了,这两处中第一处位置虽然底部有一个CALL调用但是该调用传递参数过少可以直接排除掉,而第二个参数传递很像调用CALL,如上图所示;

读者可自行下断点并根据这个执行流程,通过观察可以很容易的提取出种植CALL的调用次序,需要注意的是push ebp里面的地址是动态地址,而针对这种动态地址的找法在上一节逆向植物的相关内容中已经总结过了,此处就直接跳过,读者可提取出此段代码的关键位置;

00410A8E | 8B50 2C                      | mov edx,dword ptr ds:[eax+2C]              |  
00410A91 | 8B40 28 | mov eax,dword ptr ds:[eax+28] |
00410A94 | 52 | push edx | EDX = FFFFFFFF
00410A95 | 50 | push eax | 植物ID号=1 (向日葵)
00410A96 | 8B4424 20 | mov eax,dword ptr ss:[esp+20] | X坐标=2 (第二个格子)
00410A9A | 57 | push edi | Y坐标=0 (第零个格子)
00410A9B | 55 | push ebp | EBP = 1359D108
00410A9C | E8 7FC6FFFF | call <plantsvszombies.sub_40D120> | call 0x0040D120

最后我们的任务是通过编程实现远程注入,并使用槽位3中的樱桃炸弹作为全屏爆炸的工具,并循环填充这个5*9的内存矩阵,以此则可实现全屏攻击的效果,这段注入代码如下所示;

#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 FullScreenAttack()
{
DWORD addr = 0x0040D120;
for (int x = 0; x < 5; x++)
{
for (int y = 0; y < 9; y++)
{
__asm
{
pushad
mov esi, dword ptr ds : [0x006a9ec0]
mov esi, dword ptr ds : [esi + 0x768]
push 0xfffffff
push 0x2
mov eax, x
push y
push esi
call addr
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 x = 0; x < 10; x++)
{
// 注入代码
BOOL ref = InjectCode(Pid, FullScreenAttack);
if (ref == TRUE)
{
printf("[+] 代码注入完成 \n");
}
}

system("pause");
return 0;
}

运行上述代码程序,则读者可以看到屏幕中所有的格子都开始了全屏爆炸攻击,如下图所示;