6.7 植物大战僵尸:寻找召唤僵尸CALL

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

在前面的章节中笔者教大家找到了有利于玩家通关的一些关键技巧,本章我们将继续寻找僵尸召唤出现的CALL调用,通过手动调用此函数,我们可实现批量召唤更多的僵尸队伍的目的,首先我们还是先来看一下僵尸召唤CALl是如何被遍历出来的。

僵尸CALL的遍历技巧:

  • 首先打开CE > 进入游戏开始新的游戏 > 直接搜索未知初始化数据
  • 等待出现第一个僵尸 > 搜索增加的数值 > 回到游戏中
  • 等待出现第二个僵尸 > 搜索增加的数值 > 然后杀死一个僵尸 > 搜索减少的数值
  • 选择介于两者之间的 > 输入0-10这个范围 > 进一步筛查 > 最终即可找到

当读者根据上述方法排查后,最终会看到如下两个地址,分别是绿色基址0x06a7bd8以及动态地址0x134ef070如下图所示;

我们分别查找是什么改写了这个内存地址,首先时第一个内存地址0x134ef070僵尸出现后会弹出如下图所示的提示信息;

  • 0041DE07 - 01 46 10 - add [esi+10],eax <<

接着是第二个0x06a7bd8内存地址,同样查找是什么改写了这个内存地址,当僵尸出现后会出现后,如下图所示;

  • 00471B57 - 83 46 0C 01 - add dword ptr [esi+0C],01 <<

此时我们关闭CE修改器,打开x64dbg并附加到该进程上,读者分别在0x041DE070x0471B57两个内存地址上设置内存断点,并等待僵尸出现,当出现后会被断在0x041DE07内存地址处,观察发现此处的地址并没有过多的参数传递,判断此处并非召唤CALL地址。

我们直接RETN返回到上一层,会看到如下代码,直接在PUSH地址上下一个F2断点,然后运行游戏,回到游戏等待出现新的僵尸,读者会看到如下图所示的调用CALL,此处的CALL正是僵尸召唤的关键调用位置;

经过笔者的分析,上面的参数经过不同程度的修改确实可以控制僵尸的出现位置,和僵尸的类型,但这里传递的参数还是过多,而且很多参数我们都用不到,那么我们直接出这个CALL,出CALL后读者会看到如下图所示的内容,

上图中mov eax,ediEDI寄存器0x134EEFD0是一个动态地址,因为他是僵尸对象,所以每次程序运行都会发生变化,如果想用代码注入器注入代码的话,则需要找到EDI的基地址,接下来我们将使用CE搜索EDI的基址和偏移。

直接使用CE搜索0x134EEFD0这个内存地址,并得到一级偏移768下一个搜索地址为0x2839f30

  • 004526E0 - 8B B7 68070000 - mov esi,[edi+00000768] <<

继续搜索0x02839f30得到基地址,最终地址为PlantsVsZombies.exe+2A9F38

  • 00512665 - A1 389F6A00 - mov eax,[006A9F38] <<

将上述代码总结起来则可实现无限召唤僵尸的功能,实现代码片段如下所示;

#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 SummonZombies(int x, int y)
{
DWORD addr = 0x40DDC0;

__asm
{
pushad
mov edi, dword ptr ds : [0x6a9f38]
mov edi, dword ptr ds : [edi+0x768]
push x
push y
mov eax,edi
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");

void(*pf)(int, int) = &SummonZombies;

// 指定位置出现
pf(1, 2);

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

system("pause");
return 0;
}

运行上述代码,则读者可自定义传递需要在哪个格子内出现僵尸,并输出如下所示的召唤效果图;