一、分析思路
当我们种下一棵植物的时候,通常需要等待一段时间才能再种下一棵。在游戏开发中实现这一过程的,往往是使用一个定时器的变量。对于定时器的变量,一般有两种,一种是递减的,即从某个数开始逐渐减到0;另一种是递增的,即从0开始一直加到某个数。
二、分析过程
对于一开始,游戏的植物没有种植,所以计时器肯定是不变的,所以这时我们用CE搜未知初始化的值“Unknow initial value”,发现搜出很多值。
然后,种下一棵向日葵,计时器开始计时,这时计时器开始变化了。但我们不知道它是递增型的还是递减型的,我们只知道它变化了,所以我们搜变化了的值“Changed value”(注意:游戏暂停时,Changed value只能搜索一次),还是有很多值。
此时,由于游戏暂停,向日葵的冷却时间也暂停了,因此可以搜索未变化的值“Unchanged value”,可以多次点击进行搜索。
回到游戏,让计时器走一下,然后继续搜【一次】变化的值,仍有很多结果,这时我们已经暂停游戏了,所以计时器未变化,我们搜未变化的值。
我们看到,结果少了一半,但还是有很多。我们选择直接让计时器走完,然后搜变化的值,还是有很多结果,这时冷却已经完成了,定时器是不会变了,所以我们搜未变化的值,不过在此之前我们先玩一下游戏,排除一下干扰,我们看到,结果也少了很多,我们继续这种思路,玩一下搜未变化的值。我们尝试几次后,发现它也不怎么管用了,能排除的值也越来越少,这时,我们把向日葵种下去,这样定时器就开始走了,接着我们搜改变的值,我们看到,结果又少了很多,我们继续这种搜索,一直搜索,直到结果只有几个为止。
最后,我们搜到六个结果。我们继续原来的办法进行排除时,发现只能排除两个,剩下四个怎么都排除不掉。但是我们不断种植向日葵的时候发现,最后一个地址的数值是比较小的,其它的几个也太大了(当然,不排除这个计时器变量是一个浮点或者双精度)。而且这个地址的值是逐渐增加的。
总之,在这几个地址里,我们最喜欢的就是这个了,不过我们还是得先验证一下。我们重新种下一棵向日葵,发现它的值在不断增加,这时我们直接把它的值改成三千,发现向日葵的冷却一下子就好了,这也就证明了,它是我们要找的计时器的变量。
向日葵冷却地址(动态)0x0E0B0F0C,上限750
要去掉这个时间,改的不是当前的冷却时间,而是要改与之比较(cmp)的上限值。
cmp 当前冷却时间计数,冷却时间上限
在该地址上右键,选择 Find out what accesses to this address(谁访问了这个地址),弹出的窗口里面是空白的
此时玩一下游戏,让向日葵冷却时间走动,在看这个窗口
找到一个inc指令,选中这行,点击右边的“Show disassembler”按钮,显示“Memory Viewer”窗口
在下方不远处有一行指令 cmp eax,[edi+28],可能为当前冷却时间与冷却时间上限比较。
比较之后进行跳转 “jle 植物大战僵尸.exe+B3007”
将 “jle 植物大战僵尸.exe+B3007” 指令 nop 掉进行测试
右键选择“Replace with code that does nothing”(使用空指令替换)
此时,再游戏中种植植物,发现所有的冷却时间都已去除。
另外一种方法:去掉某一种植物的冷却时间,需要对冷却时间上限进行进一步分析。
找冷却时间基址 ★★★★★★
004B2FE5 - 38 5F 49 - cmp [edi+49],bl
004B2FE8 - 74 1D - je 植物大战僵尸.exe+B3007
004B2FEA - FF 47 24 - inc [edi+24] <<
004B2FED - 8B 47 24 - mov eax,[edi+24]
004B2FF0 - 3B 47 28 - cmp eax,[edi+28]
EAX=01089338
EBX=00000000
ECX=183A1018
EDX=00000119
ESI=00000000
EDI=0E0B0EE8
ESP=0012FA30
EBP=0012FAAC
EIP=004B2FED
根据上面找到的向日葵冷却动态地址:0x0E0B0F0C
[edi+24] = 0x0E0B0F0C
EDI=0E0B0EE8
在CE中搜这个EDI值,如下图,发现搜索结果为0
此时,需要借助OD来找,关掉CE,打开OD,附加游戏进程
在OD中,按 Ctrl + G,输入地址 004B2FEA (该地址为指令 inc [edi+24] 的地址值)
下图中004B2FEA - 004B2FF3这段汇编代码,表示植物的冷却时间每次增加一个数值,当这个数值达到冷却时间上限[edi+28]时,植物的冷却完成。
004B2FED . 8B47 24 mov eax, dword ptr [edi+0x24] 植物当前冷却时间
004B2FF0 . 3B47 28 cmp eax, dword ptr [edi+0x28] 植物冷却时间上限
在OD中汇编窗口,向上或向下翻动,找到地址 edi + 24 和 edi + 28处,并下一个断点
此时,运行游戏在断点处断下
此时,EDI的值为 0E0B0EE8,运行OD,以便让游戏正常运行
打开CE,搜索0E0B0EE8,看是否能找到,此时发现也未找到
那么,在OD汇编窗口中继续往上找,找到EDI的来源
根据 mov edi,eax 发现EDI来源于EAX
即向日葵冷却动态地址:0x0E0B0F0C = [edi+24] = [eax+24]
继续跟踪EAX,在mov edi,eax处下个断点并断下,
通过堆栈窗口可以找到调用函数的上一层,继续找EAX的来源
也可以选中 004B2FB0 $ 51 push ecx 行
在信息窗口上“本地调用来自于 00428E54”上右键,选择“转到 CALL 来自 00428E54” 找到调用函数的上一层
通过 lea eax, dword ptr [ebx+eax+0x28]
EAX来源于 ebx+eax+0x28
那么, ebx+eax+0x28 + 0x24 为CD的计数时间, ebx+eax+0x28 + 0x28 为CD上限时间
即向日葵冷却动态地址:
0x0E0B0F0C = [edi+24] = [eax+24]
= [ebx+eax+0x28+24]
这时,我们需要找EBX和EAX两个值的来源:
在汇编窗口中,找到EAX来源
00428E40 |. 8B87 5C010000 mov eax, dword ptr [edi+0x15C]
向日葵冷却动态地址:
0x0E0B0F0C = [edi+24] = [eax+24]
= [ebx+eax+0x28+24]
= [ebx+[edi+0x15C]+0x28+24]
在OD汇编窗口中继续往上找,找EBX的来源:xor ebx, ebx
在函数的开始出有一条EBX清零操作
在0x00428E50 到 0x00428E66这个循环体中,发现EBX每次增加0x50
因此, EBX来源于 n * 0x50 (n >= 0)
此循环共执行 dword ptr [eax+0x24] 次
结合 call 004B2FB0 (访问当前植物冷却时间),可以猜想此循环是遍历植物的冷却时间
即植物对象数组的冷却时间
n = 0向日葵 1樱桃炸弹 2坚果 3寒冰射手……
当前植物卡槽总数为 00428E63 |. 3B70 24 |cmp esi, dword ptr [eax+0x24]
中的 [eax+0x24]
向日葵冷却动态地址:
0x0E0B0F0C = [edi+24] = [eax+24]
= [ebx+eax+0x28+24]
= [ebx+[edi+0x15C]+0x28+24]
= [[edi+0x15C] + n* 0x50 + 0x28 + 24] (n >= 0)
在OD上0x00428E50处下个断点,找到EDI值为0x183A1018,并运行游戏
向日葵冷却动态地址:
0x0E0B0F0C = [edi+24] = [eax+24]
= [ebx+eax+0x28+24]
= [ebx+[edi+0x15C]+0x28+24]
= [[0x183A1018+0x15C] + n* 0x50 + 0x28 + 24] (n >= 0)
验证,在OD的命令窗口输入 dd [0x183A1018+0x15C] + n* 0x50 + 0x28 + 24 并回车
(注意:下图测试 EDI= 17FA6838)
dd [0x17FA6838+0x15C] + n* 0x50 + 0x28 + 24 当前植物冷却时间
dd [0x17FA6838+0x15C] + n* 0x50 + 0x28 + 28 当前植物冷却时间上限
(n=0...当前植物卡槽总数 [eax+0x24])
在CE中搜这个EDI的值0x183A1018,找到大量的地址
图中看到0012开头的地址为堆栈中的地址,因此可以排除
其他,各地址段的地址随机取一个(当发现一个地址找不到基址时,换另外一个地址试)
下面以0x01089BA0为例来分析:
现在CE中搜索0x01089BA0这个地址 ,发现并未找到
在OD中用 dd 01089BA0 命令找到01089BA0 在内存中的位置,并下一个内存访问断点
这是,会找到一个偏移 eax+0x868, EAX = 0x01089338
向日葵冷却动态地址:
0x0E0B0F0C = [edi+24] = [eax+24]
= [ebx+eax+0x28+24]
= [ebx+[edi+0x15C]+0x28+24]
= [[[eax+0x868]+0x15C] + n* 0x50 + 0x28 + 24] (n >= 0)
在CE中搜索EAX=0x01089338,找到4个绿色的地址,都为基址
向日葵冷却动态地址:
0x0E0B0F0C = [edi+24] = [eax+24]
= [ebx+eax+0x28+24]
= [ebx+[edi+0x15C]+0x28+24]
= [[[[0x007794F8]+0x868]+0x15C] + n* 0x50 + 0x28 + 24] (n >= 0)
dd [[[0x007794F8]+0x868]+0x15C] + n* 0x50 + 0x28 + 0x24 植物当前CD时间
dd [[[0x007794F8]+0x868]+0x15C] + n* 0x50 + 0x28 + 0x28 植物CD时间上限
dd [[[0x007794F8]+0x868]+0x15C] + n* 0x50 + 0x28 + 0x48
dd [[[0x007794F8]+0x868]+0x15C] + n* 0x50 + 0x28 + 0x49
简化
dd [[[0x007794F8]+0x868]+0x15C] + n* 0x50 + 0x4C
dd [[[0x007794F8]+0x868]+0x15C] + n* 0x50 + 0x50
在OD中输入 dd [[[0x007794F8]+0x868]+0x15C] + 4C,
查看地址值 0x0E0B0F0C 即为向日葵的当前冷却时间,
查看地址值 0x0E0B0F10 即为向日葵的冷却时间上线,0x2EE = 十进制750
[[[0x007794F8]+0x868]+0x15C] + n* 0x50 + 0x28 + 0x24 为当前冷却时间
[[[0x007794F8]+0x868]+0x15C] + n* 0x50 + 0x28 + 0x28 为冷却时间上限
CE中验证:
注意:
冷却时间需要结合这2个标志位进行,请自行分析:
edi+24 当前CD时间
edi+28 CD时间上限
edi+48 byte 0
edi+49 byte 1 CD计数开始
48 word 00 01 表示 开始CD计时,卡片不可选
48 word 01 00 表示未开始CD计时,卡片可选中
三、代码实现
// 禁用CD
void CFZ_PlantsVsZombiesDlg::OnBnClickedCheckCd()
{
UpdateData(true); // 更新窗口状态至变量
UCHAR buf[2];
if(m_b_cd == true)
{
// 去掉冷却时间
//004B2FE8 - 74 1D - je 植物大战僵尸.exe+B3007
//90 90
buf[0] = 0x90;
buf[1] = 0x90;
}
else
{
// 启用冷却时间
//004B2FE8 - 74 1D - je 植物大战僵尸.exe+B3007
buf[0] = 0x74;
buf[1] = 0x1D;
}
//把buf内数据写入地址 0x004B2FE8
HANDLE hp = GetGameProcessHandle();
DWORD bywrite;
if(hp == NULL)
{
MessageBox("打开进程出错", "提示", MB_OK);
return ;
}
::WriteProcessMemory(hp, (LPVOID)0x004B2FE8, buf, sizeof(buf), &bywrite);
::CloseHandle(hp); // 释放句柄
}
// 获取当前关卡植物卡片总数
DWORD GetPlantsCardCount()
{
HANDLE hp = GetGameProcessHandle();
DWORD buf = 0, byread, bywrite;
// 007794F8 +868 +15C + 24
::ReadProcessMemory(hp, (PVOID)0x007794F8, &buf, sizeof(buf), &byread);
::ReadProcessMemory(hp, (PVOID)(buf + 0x868), &buf, sizeof(buf), &byread);
::ReadProcessMemory(hp, (PVOID)(buf + 0x15C), &buf, sizeof(buf), &byread);
::ReadProcessMemory(hp, (PVOID)(buf + 0x24), &buf, sizeof(buf), &byread);
::CloseHandle(hp); // 释放句柄
return buf;
}
// CD时卡片可选
void CFZ_PlantsVsZombiesDlg::OnBnClickedCheckCdcard()
{
UpdateData(true); // 更新窗口状态至变量
UCHAR data[2];
if(m_b_cdcard == true)
{
data[0] = 0x01;
data[1] = 0x00;
}
else
{
data[0] = 0x00;
data[1] = 0x01;
}
HANDLE hp = GetGameProcessHandle();
// 当前关卡植物卡片总数
DWORD iPlantCardCount = GetPlantsCardCount();
DWORD buf = 0, byread, bywrite;
// dd [[[0x007794F8]+0x868]+0x15C] + n* 0x50 + 0x28 + 0x48
::ReadProcessMemory(hp, (PVOID)0x007794F8, &buf, sizeof(buf), &byread);
::ReadProcessMemory(hp, (PVOID)(buf + 0x868), &buf, sizeof(buf), &byread);
::ReadProcessMemory(hp, (PVOID)(buf + 0x15C), &buf, sizeof(buf), &byread);
UpdateData(true);
for(int i = 0; i < iPlantCardCount; i++)
{
::WriteProcessMemory(hp, (PVOID)(buf + i * 0x50 + 0x28 + 0x48 ), data, sizeof(data), &byread);
}
::CloseHandle(hp); // 释放句柄
}
四、延伸分析
根据上面的分析,可以得到植物卡片在内存的数组数据
第一个卡片的地址:
起始地址: [[[0x007794F8]+0x868]+0x15C] + 0* 0x50 + 0x28 (图中为0DCBB3D8)
当前CD: [[[0x007794F8]+0x868]+0x15C] + 0* 0x50 + 0x28 + 0x24 (图中为0DCBB3FC)
CD上限: [[[0x007794F8]+0x868]+0x15C] + 0* 0x50 + 0x28 + 0x28 (图中为0DCBB400)
48、49: [[[0x007794F8]+0x868]+0x15C] + 0* 0x50 + 0x28 + 0x48 (图中为0DCBB420)
第二个卡片的地址:
起始地址: [[[0x007794F8]+0x868]+0x15C] + 1* 0x50 (图中为0DCBB428)
当前CD: [[[0x007794F8]+0x868]+0x15C] + 1* 0x50 + 0x28 + 0x24 (图中为0DCBB44C)
CD上限: [[[0x007794F8]+0x868]+0x15C] + 1* 0x50 + 0x28 + 0x28 (图中为0DCBB450)
48、49: [[[0x007794F8]+0x868]+0x15C] + 1* 0x50 + 0x28 + 0x48 (图中为0DCBB470)