selph
selph
Published on 2021-10-10 / 1,038 Visits
0
0

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

前言

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

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

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

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

找到僵尸CALL

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

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

image-20211010093948334

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

image-20211010094157694

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

image-20211010094328033

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

image-20211010094453626

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

image-20211010094633952

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

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

image-20211010094859049

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

image-20211010094950140

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

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

直接拿这个值去CE搜索:

image-20211010095131398

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

image-20211010095310765

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

image-20211010095416309

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

image-20211010095440516

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

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

添加指针来验证:

image-20211010095557104

找对值了

到此就找到召唤僵尸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下:

image-20211010100324232

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

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,发现了问题所在:

image-20211010102035581

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

image-20211010102430512

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

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

image-20211010103431646

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

image-20211010103125523

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

image-20211010103259200

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

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

经过一番修改:

DWORD __stdcall myZombieCall(DWORD type, DWORD line) {

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


总结

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

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

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

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

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


Comment