kn0sky 发表于 2021-10-10 10:51

植物大战僵尸Hook僵尸CALL--实践&踩坑

## 前言

最近学习了Hook技术,就想找个东西拿来练练实战一下,于是看见了文件夹里的植物大战僵尸,emmm,好,就你了

本来是只是想自己练练,没想写下来的,但无奈实践过程中遇到了坑,害得我调试了一下午,才发现原来是这么基础的问题,害,还是自己基础知识的意识不到位

这里本文将记录一下整个操作的过程,以及代码编写,以及我遇到的坑(没注意函数调用约定....)

> 这里的目标是:找到召唤僵尸CALL,并且Hook召唤僵尸CALL让僵尸仅出现在第二行

## 找到僵尸CALL

找僵尸CALL过程很简单,先说思路:一局游戏最后是否胜利,在于判定僵尸有没有打完,游戏里肯定有个地方在记录当前出现的僵尸数量,而这个僵尸的数量是在什么时候增加的呢,那必然是在召唤僵尸的时候增加喽(就好像数量是类里的静态变量,僵尸是类的实例,共同访问同一个变量),找到僵尸数量增加的地方,很可能就是僵尸生成的call内部。

接下来开始实操,通过CE搜索当局游戏僵尸数量,找到记录僵尸数量的地址:



这里找到两个地址,一个是全局地址,一个应该是某个类里面的地址,假如这个数量就是某个类的静态变量,那很可能第二个地址的值就是在召唤僵尸call的时候被修改:



点击反汇编,进入反汇编界面,直接在当前指令处下断点,然后观察调用堆栈:



从上往下看,第一个函数从参数来看,很有嫌疑,双击进去,再次下断点:



这里函数调用前push了两个参数和一个值到eax里,刚刚下断点看到的那个调用堆栈的函数的两个参数是0,2,游戏运行起来后,僵尸出现在了第三行:



可以猜测,这里第二个参数就是僵尸出现的行数,召唤僵尸必要的信息除了行数,就是僵尸的种类了(调用call是一次只加1个僵尸数量,所以每次调用只召唤一个僵尸,所以需要召唤数量的参数)

等了一会,断点断下来了:



这里将栈里的两个0都改成1看看:



在第1行(最上面是第0行)出现了种类为1的僵尸(旗子僵尸),猜想正确

然后接下来的问题是给eax的值:`27B3F3E8`是哪来的?

直接拿这个值去CE搜索:



搜出来数量不多,一个一个看吧,挨个点击右键,是什么访问了这个地址(因为经过多次断下观察,这是个固定的值)其中会找到一个可疑的偏移:



先不管这个代码在干嘛,这里最要紧的是知道这个值是从哪得到的,记下偏移`0x868`:



取出里面的base:`026B9E80`再次搜索:



就搜到基址了,这个固定值的位置是:

```
[+0x868]
```

添加指针来验证:



找对值了

到此就找到召唤僵尸CALL了,整理一下相关信息:

```
召唤僵尸CALL地址:PlantsVsZombies.exe+19A60
参数1:僵尸类型
参数2:僵尸出现位置
eax应该是个对象首地址:[+0x868]
```

接下来开始编写代码调用一下看看

## 写代码调用僵尸CALL

这里用DLL注入进去比较方便,功能代码如下:

```cpp
void CPvZHelper::OnBnClickedButton_callOneZombie()
{
      // 获取模块地址
      HANDLE hModule = GetModuleHandleW(L"PlantsVsZombies.exe");
      // 获取call地址
      DWORD callAddr = (DWORD)hModule + 0x19A60;
      // 获取模块+偏移地址(基址)
      DWORD moduleBase = (DWORD)hModule + 0x355F6C;
      // 设置两个参数,僵尸位置,僵尸类型
      srand((int)time(NULL));
      DWORD para1ZombiePos = RANDOM(5);
      DWORD para2ZombieType = 0;      // 普通僵尸
      // 将参数入栈,将固定值给eax,调用call
      __asm {
                mov eax, para1ZombiePos;
                mov ebx, para2ZombieType;
                push eax;
                push ebx;
                mov eax, moduleBase;
                mov eax, ;
                mov ebx, callAddr;
                add eax, 0868h;
                mov eax, ;
                call ebx;
      }
}
```

测试一下,狂点按钮10下:



出现了好多僵尸,测试成功!

## Hook僵尸CALL

到这里为止一直都很顺利,当时我在这里遇到了坑,调试了一下午才发现问题所在,这里跟大家分享一下调的过程

首先是5字节的InlineHook,套路是固定的,网上找即可,这里就不多啰嗦了,这里介绍一下Hook类的函数功能:

```cpp
class CLHook
{
public:
      CLHook();      // 构造函数
      ~CLHook();      // 析构函数
      BOOL Hook(PROC funcAddr,PROC hookFuncAddr);      // Hook,第一次Hook把原本字节码都记录下来,下次再Hook就用reHook函数了
      VOID unHook();      // 取消Hook
      BOOL reHook();      // 重新Hook
private:
      PROC m_pfnOrig;       // 函数地址
      BYTE m_oldBytes;    // 函数入口代码
      BYTE m_newBytes;    // Inline代码
      BOOL bRet;
};
```

接下来是界面复选框点击函数的功能:

```cpp
void CPvZHelper::OnBnClickedCheck_lockZombiePos()
{
      // 因为是使用复选框控件来进行操作的,所以需要开启一下这个UpdateData,是从界面上取数据的
      UpdateData(TRUE);
      // 获取模块地址
      HANDLE hModule = GetModuleHandleW(L"PlantsVsZombies.exe");
      // 获取CALL地址
      DWORD callAddr = (DWORD)hModule + 0x19A60;
      // 获取我们自己的CALL的地址
      DWORD callAddrHook = (DWORD)myZombieCall;
      if (m_lockZombiePos) {
                ZombieCallHook.Hook((PROC)callAddr, (PROC)callAddrHook);
      }
      else {
                ZombieCallHook.unHook();
      }
      UpdateData(FALSE);
}
```

然后是我们自己的CALL(出现问题的地方,本函数运行会导致游戏奔溃):

```cpp
DWORD myZombieCall(DWORD type, DWORD line) {
      //为了正常调用僵尸CALL,把修改掉的内容改回来
      ZombieCallHook.unHook();
      // 获取地址
      HANDLE hModule = GetModuleHandleW(L"PlantsVsZombies.exe");
      DWORD callAddr = (DWORD)hModule + 0x19A60;
      DWORD moduleBase = (DWORD)hModule + 0x355F6C;
      // 设置参数
      DWORD para1ZombiePos = 1;
      DWORD para2ZombieType = type;
      DWORD ret = 0;
      // 调用CALL
      __asm {
                mov eax, para1ZombiePos;
                mov ebx, para2ZombieType;
                push eax;
                push ebx;
                mov eax, moduleBase;
                mov eax, ;
                mov ebx, callAddr;
                add eax, 0868h;
                mov eax, ;
                call ebx;
                lea ecx, ret;
                mov, eax;
      }
      // 再重新Hook
      ZombieCallHook.reHook();
      return ret;
}
```

我们自己的函数跟调用僵尸CALL召僵尸的函数功能差不多一样,区别在于功能开始前后的unHook和reHook,这些问题都不大,看起来没啥问题,就注入DLL去运行,游戏很快就奔溃了,崩溃之前,超高频率在召唤僵尸(奇怪)

我专门对比了一下Hook前后的僵尸CALL执行流程,看起来没啥区别,但就是无限崩溃(崩溃界面就不截图了哈),啥情况啊!!!这小单机游戏还有保护不成?

经过一下午的琢磨,抄起我的ida,发现了问题所在:



这里召唤完僵尸后,会从栈里取个值,就叫他varA好了,第一次取值的时候一定是取到0,然后在这里+1后,跳转走:



跳走之后,会取出刚刚栈里的那个值varA,作为索引去一个地址寻找FFFFFFFF,如果没找到,就再来一遍召唤僵尸并且给varA+=1,然后再次索引找值

下断点后,正常情况下来说varA的值是从0开始,然后基本上很快就跳出这个循环了:



而我Hook了之后栈里获取的值变成了A:



从A开始遍历,这就会循环很多很多次都挑不出,然后游戏连续召唤僵尸,然后就奔溃了



不难发现问题的所在,Hook后,函数调用完,栈的位置不对,压入的两个参数提高了栈顶,但没有给加(`add esp,8`)回来,无脑在Hook函数里加了`add esp,8`之后发现没用,突然意识到了!!!

Cpp默认是`__cdecl`,是调用者来平栈,这个游戏的调用者没有来平栈,那大概率是在函数内平栈了,那就是`__stdcall`了,函数需要声明为这个函数调用约定才行!

经过一番修改:

```cpp
DWORD __stdcall myZombieCall(DWORD type, DWORD line) {
```

游戏正常运行了,这么简单的问题折腾一下午。。。。

---

## 总结

最后说两句,调试了一下午,我做了的那些事(还是自己见识太少思路太少)

当时调试了一下午,我先后对比了CALL内部的执行流程,看有没有啥区别,无果,

当时看召唤了这么多僵尸,比正常情况下多,我以为除了这个地方还有其他地方调用这个CALL,我就把这个地方的CALL地址改了,然后把Hook地址提前了5字节,这样一来,我以为就会正常了,结果还是召唤出好多僵尸,无果。。。

最后才对比召唤CALL调用位置前后的区别,发现从栈里取出来的值不一样,才发现问题所在

本来中途都差点想放弃了,还好坚持下来了,有时候真就是离目标很接近了的时候放弃的想法很大。

IlikeMaoshu 发表于 2023-7-18 00:33

为了你这个帖子买了激活码,请教大佬,我按照以上做到了能够召唤出boss(在CE内修改esp内地址里的数据) 但用注入器注入后导致游戏崩溃想请教为何会这样…… 该问题长久有效……

阳光好青年 发表于 2021-10-11 14:37

这个有个项目。
通过逆向工程和代码注入给植物大战僵尸新增了一个局域网对战模式。
https://github.com/czs108/Plants-vs.-Zombies-Online-Battle

cjc3528 发表于 2021-10-11 12:18

好NB的教程啊,谢谢分享。学习了

ZAnan 发表于 2021-10-11 13:55

厉害 看着费脑子啊

17315044449 发表于 2021-10-11 15:50

大佬还有个判定,就是那个摇奖的,阳光数达成也判定胜利,这算是变相的全屏秒杀么?

bjxiaoyao 发表于 2021-10-11 16:19

图文并茂,内容精彩,感谢分享。

h149824203 发表于 2021-10-11 16:33

看起来好复杂,膜拜大佬。。

lj2017 发表于 2021-10-11 17:15

感谢分享{:1_921:}

zz1181 发表于 2021-10-11 19:41

好厉害,膜拜

majinwei86 发表于 2021-10-11 21:02

这是真的NB,多支持下!
页: [1] 2 3 4 5
查看完整版本: 植物大战僵尸Hook僵尸CALL--实践&踩坑