蓝天资源网
当前位置:蓝天资源网 / 汇编逆向 / 正文

植物大战僵尸Hook僵尸CALL实践

作者:忆笙发布时间:2021-10-12 01:07浏览数量:347次评论数量:0次

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

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

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

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

找到僵尸CALL

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

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第1张

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第2张

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第3张

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第4张

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第5张

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

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第6张

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第7张

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

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

直接拿这个值去CE搜索:

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第8张

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第9张

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第10张

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第11张

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

[[PlantsVsZombies.exe+355E0C]+0x868]

添加指针来验证:

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第12张

找对值了

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

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

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

写代码调用僵尸CALL

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

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, [eax];
                mov ebx, callAddr;
                add eax, 0868h;
                mov eax, [eax];
                call ebx;
        }
}

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第13张

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

Hook僵尸CALL

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

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

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[5];    // 函数入口代码
        BYTE m_newBytes[5];    // Inline代码
        BOOL bRet;
};

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

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(出现问题的地方,本函数运行会导致游戏奔溃):

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, [eax];
                mov ebx, callAddr;
                add eax, 0868h;
                mov eax, [eax];
                call ebx;
                lea ecx, ret;
                mov[ecx], eax;
        }
        // 再重新Hook
        ZombieCallHook.reHook();
        return ret;
}

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

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

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第14张

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第15张

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

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第16张

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第17张

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第18张

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

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

经过一番修改:

DWORD __stdcall myZombieCall(DWORD type, DWORD line) {

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

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

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

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

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

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

植物大战僵尸Hook僵尸CALL实践 nbsp 僵尸 DWORD eax 地址 函数 CALL  第19张

忆笙

忆笙 主页 联系他吧

人间山河远阔,只想与你同行。

欢迎 发表评论: