《植物大战僵尸》是一款非常经典的塔防类游戏,由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
并附加到该进程上,读者分别在0x041DE07
及0x0471B57
两个内存地址上设置内存断点,并等待僵尸出现,当出现后会被断在0x041DE07
内存地址处,观察发现此处的地址并没有过多的参数传递,判断此处并非召唤CALL地址。
我们直接RETN
返回到上一层,会看到如下代码,直接在PUSH
地址上下一个F2
断点,然后运行游戏,回到游戏等待出现新的僵尸,读者会看到如下图所示的调用CALL,此处的CALL正是僵尸召唤的关键调用位置;
经过笔者的分析,上面的参数经过不同程度的修改确实可以控制僵尸的出现位置,和僵尸的类型,但这里传递的参数还是过多,而且很多参数我们都用不到,那么我们直接出这个CALL,出CALL后读者会看到如下图所示的内容,
上图中mov eax,edi
的EDI
寄存器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>
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[]) { 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; }
|
运行上述代码,则读者可自定义传递需要在哪个格子内出现僵尸,并输出如下所示的召唤效果图;