记录在逆向PVZ中的一些过程
0x01 前期准备
I. 游戏逆向分类
脚本类
找图, 模拟鼠标键盘操作
内存类
模拟内存数据操作, 调用游戏内部函数
封包类
破解游戏客户端, 模拟客户端发包
本次为内存类的逆向
II. 工具准备
植物大战僵尸
任意调试器
Cheat Engine
0x02 逆向过程
I. 基址寻找
1. 寻找阳光基址
寻找过程比较简单 找到基址后原路返回
最后定址到阳光基址如下
阳光基址 = [[[PlantsVsZombies.exe + 0x355E0C] + 0x868] + 0x5578]
2. 寻找冷却时间的基址
每个植物都有自己的冷却时间
所以要分析结构体的数组
一栏植物结构体大小0x50字节
+0 (首地址)
...
...
+24 冷却时间
基址定位
第n栏的冷却地址 = [[[[PlantsVsZombies.exe + 0x355E0C] + 0x868] + 0x15C] + 索引*0x50 + 0x28 + 0x24]
II. 逻辑修改
1. 阳光逻辑修改
种植植物阳光减少逻辑位置
PlantsVsZombiesNoCool.exe + 0x27696
逻辑如下
这里我的做法是把sub
直接改成了add
2. 取消植物的冷却时间
植物冷却时间增加位置
PlantsVsZombiesNoCool.exe + 0x9CDFC
逻辑如下
修改逻辑即可
这里我直接将跳转给nop掉了 比较粗糙
其它方法可以自行尝试
III. 外部注入
1. 实现秒杀
思路
- 炸弹全屏
- 僵尸血量全空
先看第一种
猜测至少有的种植函数参数: 植物ID 行数x 列数y
那先通过选中植物找到植物ID的地址
找到之后查找访问 在种植的时候开始访问的地方则就是种植函数的位置了
找到的种植函数地址
[PlantsVsZombies.exe + 0x18D70]
函数情况如下
五个参数
EDI,行数,列数,植物ID, -1
还有一个参数EDI未知 去寻找一下他的基址
这个参数的基址如下
EDI = [[PlantsVsZombies.exe+0x355E0C] + 0x868]
最后尝试后得知樱桃炸弹的ID为2
call测试工具测试一下能不能正常执行
可以看到成功生效了
参数
push -1 //固定
push 2 //植物ID
mov eax, 0x4 //行数
push 4 //列数
mov edi, dword ptr ds:[0x755e0c]
mov edi, dword ptr ds:[edi+868]
push edi //EDI
call 418d70
2. 控制僵尸的刷新
思路就是找到刷新僵尸的call 然后顺藤摸瓜
猜测参数会有行数 + ID
找到修改僵尸的call后 发现周围没有类似刷新僵尸的call 就顺着堆栈往回找
最后找到函数位置为
[PlantsVsZombies.exe + 0x19A60]
函数情况如下
三个参数
EDI, 僵尸ID, 行数
EDI找一下基址
EDI = [[PlantsVsZombies.exe + 0x355E0C] + 0x868]
0x03 修改制作
各个基址都找到咯
可以开始着手写修改了
这里就给出关键的代码
如果想更深一步了解 不嫌弃的话可以参考我的项目(不完整の渣代码)
I. 偏移定义
#pragma once
//********************************************
/* 游戏结构 */
//[PlantsVsZombies.exe + 0x355E0C]
#define GAMEFIXEDADDRESS 0x755E0C
//游戏开始结构
#define GAMESTART 0x868
//阳光基址
#define SUNSHINEOFFSET 0x5578
//栏位信息数组
#define FIELDOFFSET 0x15C
//栏位结构体大小
#define SIZEOFFIELD 0x50
//种植植物call
#define PLANTCALL 0x418D70
//刷僵尸call
#define ZOMBIECALL 0x419a60
//********************************************
//指令
#define SWITCH_INIT 1
#define SWITCH_CHANGESUN 2
#define SWITCH_NOCD 3
#define SWITCH_KILL 4
#define SWITCH_ZOMBIE 5
//********************************************
//共享内存名
#define ShareMemName "PVZTrainer"
II. 主程序
int main() {
int UserInput = 0, ScanfRet = 0;
BaseTool baseTool;
while (true) {
ShowMenu();
ScanfRet = scanf_s("%d", &UserInput);
//结束判断
if (ScanfRet == EOF) {
return 0;
}
//分发
switch(UserInput) {
case SWITCH_INIT:
{
if (baseTool._getProcessID() == 0) {
if (baseTool.FindProcessByName("PlantsVsZombies.exe")) {
cout << "进程加载完成" << endl;
DWORD base = GetBaseAddr(baseTool._getProcessID());
}
else {
cout << "进程加载失败" << endl;
break;
}
}
else {
cout << "进程已经加载完毕" << endl;
}
if (baseTool.Inject() == true) {
cout << "注入完成" << endl;
}
else {
cout << "注入失败" << endl;
}
break;
}
case SWITCH_CHANGESUN:
{
baseTool.EditSunshine();
cout << "修改完成" << endl;
break;
}
case SWITCH_NOCD:
{
baseTool.NoCoolDown();
cout << "操作完成" << endl;
break;
}
case SWITCH_KILL:
{
baseTool.KillAll();
break;
}
case SWITCH_ZOMBIE:
{
baseTool.CreateZombie();
break;
}
default:
{
baseTool.Deject();
return 0;
}
}
Sleep(1000);
system("cls");
}
//清理内核对象
return 0;
}
III. 阳光修改
/******************************************************************************************/
//修改阳光
//参数: void
//返回值: void
/******************************************************************************************/
void BaseTool::EditSunshine() {
if (ProcessHandle == NULL) {
cout << "请先初始化!\n" << endl;
return;
}
int num;
cout << "输入修改值:" << endl;
cin >> num;
DWORD Address = 0;
ReadProcessMemory(ProcessHandle, (LPCVOID)GAMEFIXEDADDRESS, (LPVOID) &Address, sizeof(DWORD), NULL);
ReadProcessMemory(ProcessHandle, (LPCVOID)(Address + GAMESTART), (LPVOID) &Address, sizeof(DWORD), NULL);
WriteProcessMemory(ProcessHandle, (LPVOID)(Address + SUNSHINEOFFSET), (LPCVOID) &num, sizeof(DWORD), NULL);
return;
}
IV. 全员无冷却
两种思路
- 开线程刷新栏位数组
- 直接改代码逻辑(像我之前直接把跳转nop掉)
前者需要通过基址经过两级跳转定址 然后开线程不停刷新每个栏位的CD
而后者相对来说更简单 直接通过基址转移到对应机器码 然后进行修改或恢复就可以了
这里我选择写的前者
/******************************************************************************************/
//无冷却功能
//参数: void
//返回值: void
/******************************************************************************************/
void BaseTool::NoCoolDown() {
if (ProcessHandle == NULL) {
cout << "请先初始化!\n" << endl;
return;
}
if (NoCDThread != NULL) {
cout << "之前已经开启 现在帮您关闭" << endl;
TerminateThread(NoCDThread,0);
NoCDThread = NULL;
return;
}
NoCDThread = CreateThread(NULL, 0, NoCDThreadFunc, (LPVOID)ProcessHandle, NULL, NULL);
}
//线程函数
DWORD WINAPI NoCDThreadFunc(LPVOID p) {
while (true) {
DWORD Address = 0;
ReadProcessMemory((HANDLE)p, (LPCVOID)GAMEFIXEDADDRESS, (LPVOID)& Address, sizeof(DWORD), NULL);
ReadProcessMemory((HANDLE)p, (LPCVOID)(Address + GAMESTART), (LPVOID)& Address, sizeof(DWORD), NULL);
if (Address == NULL) {
cout << "请先进入游戏" << endl;
return 0;
}
ReadProcessMemory((HANDLE)p, (LPCVOID)(Address + FIELDOFFSET), (LPVOID)& Address, sizeof(DWORD), NULL);
//这里忘了记录栏位数目的地址 就直接写上限了 不知道栏位没开全会不会报错
for (int i = 0; i < 10; i++) {
DWORD tempAdd = (DWORD)((char*)Address + i * 0x50 + 0x28 + 0x24);
DWORD CoolValue = 0x100000;
WriteProcessMemory((HANDLE)p, (LPVOID)(tempAdd), (LPVOID)& CoolValue, sizeof(DWORD), NULL);
}
}
return 0;
}
V. 全员秒杀
植物ID知道了
写个循环遍历 在每个位置上种上一次爆炸果
主程序
void BaseTool::KillAll() {
*(int*)g_pShareMemory = 4;
}
DLL端
void DllTool::KillAll() {
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 9; j++) {
__asm {
pushad
pushfd
push - 1
push 2
mov eax, i
push j
mov ecx, dword ptr ds : [GAMEFIXEDADDRESS]
mov ecx, dword ptr ds : [ecx + GAMESTART]
push ecx
mov ecx, PLANTCALL
call ecx
popfd
popad
}
}
}
}
VI. 僵尸刷新
按照提示刷僵尸就行了
主程序
void BaseTool::CreateZombie() {
int row = 0, zombieID = 0, num = 0;
cout << "选择要刷的行数:" << endl;
cin >> row;
cout << "选择要刷的僵尸ID:" << endl;
cin >> zombieID;
cout << "选择要刷的数量:" << endl;
cin >> num;
*((int*)(g_pShareMemory)+1) = row;
*((int*)(g_pShareMemory)+2) = zombieID;
*((int*)(g_pShareMemory)+3) = num;
//赋值完毕再通知DLL
*(int*)g_pShareMemory = SWITCH_ZOMBIE;
}
DLL端
void DllTool::CreateZombie() {
int row = 0, zombieID = 0, num = 0;
row = *((int*)(g_pShareMemory)+1);
zombieID = *((int*)(g_pShareMemory)+2);
num = *((int*)(g_pShareMemory)+3);
while (num > 0) {
num--;
__asm {
pushad
pushfd
push row
push zombieID
mov edi, dword ptr ds : [GAMEFIXEDADDRESS]
mov eax, dword ptr ds : [edi + GAMESTART]
mov edi, ZOMBIECALL
call edi
popfd
popad
}
};
}
0x05 添加/过掉检测
添加检测
外置检测 这里可以通过DLL劫持注入代码
保护的思路
从游戏的关键数据下手
用线程扫描相关数据位置 不满足逻辑条件 则判断被修改
从数据完整性下手
这里可以用CRC校验
游戏启动时先计算CRC值
然后开线程和初始CRC进行对比
0x06 内容总结
本次实践涉及知识面
游戏内存基址寻找
跨进程内存修改
远程线程注入DLL
进程通信
之后需要学习更多的注入方式,以及对于这种修改的防护技术。