前言
最近学习了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
再次搜索:
就搜到基址了,这个固定值的位置是:
[[PlantsVsZombies.exe+355E0C]+0x868]
添加指针来验证:
找对值了
到此就找到召唤僵尸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
到这里为止一直都很顺利,当时我在这里遇到了坑,调试了一下午才发现问题所在,这里跟大家分享一下调的过程
首先是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,发现了问题所在:
这里召唤完僵尸后,会从栈里取个值,就叫他varA好了,第一次取值的时候一定是取到0,然后在这里+1后,跳转走:
跳走之后,会取出刚刚栈里的那个值varA,作为索引去一个地址寻找FFFFFFFF,如果没找到,就再来一遍召唤僵尸并且给varA+=1,然后再次索引找值
下断点后,正常情况下来说varA的值是从0开始,然后基本上很快就跳出这个循环了:
而我Hook了之后栈里获取的值变成了A:
从A开始遍历,这就会循环很多很多次都挑不出,然后游戏连续召唤僵尸,然后就奔溃了
不难发现问题的所在,Hook后,函数调用完,栈的位置不对,压入的两个参数提高了栈顶,但没有给加(add esp,8
)回来,无脑在Hook函数里加了add esp,8
之后发现没用,突然意识到了!!!
Cpp默认是__cdecl
,是调用者来平栈,这个游戏的调用者没有来平栈,那大概率是在函数内平栈了,那就是__stdcall
了,函数需要声明为这个函数调用约定才行!
经过一番修改:
DWORD __stdcall myZombieCall(DWORD type, DWORD line) {
游戏正常运行了,这么简单的问题折腾一下午。。。。
总结
最后说两句,调试了一下午,我做了的那些事(还是自己见识太少思路太少)
当时调试了一下午,我先后对比了CALL内部的执行流程,看有没有啥区别,无果,
当时看召唤了这么多僵尸,比正常情况下多,我以为除了这个地方还有其他地方调用这个CALL,我就把这个地方的CALL地址改了,然后把Hook地址提前了5字节,这样一来,我以为就会正常了,结果还是召唤出好多僵尸,无果。。。
最后才对比召唤CALL调用位置前后的区别,发现从栈里取出来的值不一样,才发现问题所在
本来中途都差点想放弃了,还好坚持下来了,有时候真就是离目标很接近了的时候放弃的想法很大。