【逆向工程】逆向植物大战僵尸 以及关于基址和多级指针的追根究底

1,176 阅读9分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。 看了几个视频都发现就只会让你一级一级的找偏移而不是告诉你为什么CE不能直接搜索到一个数据的基址,为什么会有多级偏移,为什么通过多级偏移就能找到一个数据 所以这里就主要探究下原理 在这里插入图片描述

因为X64DBG内存断点是真的拉跨,这里还里还是OD了 @TOC

为什么不能直接扫描到阳光数量的基地址?

静态地址(基址) 和 动态地址

话不多说CE开扫,改变阳光数量,得到本次进程阳光的动态地址0x12A473A0

在这里插入图片描述

CE中静态地址(或者说基址)以绿色字体显示,静态地址可以理解为全局变量静态变量在内存空间中的地址,在一个程序编译时,就已经确定其在进程的内存中空间地址了除非更改代码静态地址在每次加载程序中都不会改变,学过PE结构的应该都知道,其值大多数都放在data段中。 12A473A0这个字体是黑色的,在CE中代表动态地址,可以理解成局部变量,是在里面的,每次加载程序地址是会改变,是不确定的,关于栈以及局部变量为什么地址是动态的可以看这篇文章C/C++的反汇编表示详解(1)函数调用,栈平衡,变量与参数的内存布局中的局部变量

所以在游戏逆向中的基址,不是指PE结构中程序载入基地址ImageBase,而是指一个结构或变量的静态地址,其可能是一个全局指针的地址,也有可能是一个全局变量的地址,还有可能是一个全局对象的首地址,只要其结构或变量是的地址是静态的,即可称为一个基址

因为阳光数是一个局部变量,其地址值是动态的,所以无法用CE直接找到其静态地址(基址)

但很明显,如果阳光数为一个局部变量是不可取的,因为其重要性必定会在程序的许多代码段中都会被调用

既然如此那肯定有个全局的数据类型可以找到阳光这个局部变量的值,那这就肯定是一个全局指针啦

多级指针 和 多级偏移

这里我们写个Demo来模拟一下怎么通过一个全局指针来找到局部变量阳光的值

#include <stdio.h>
/*僵尸类 里面有僵尸的各类属性*/
class Zombies{
public:
	int type;
	int hP;
};
/*植物类里面包含有阳光值这个属性*/
class Plant{
public:
	int type;
	int hp;
	int sun;//模拟的阳光值
};
/*一个集合类 里面包含了指向各类的指针*/
class Game{
public:
	Zombies* pZombies;
	Plant* pPlant;
};
/*一个全局执政game 指向一个全局对象Game */
Game* game=new Game;

int main()
{
/*
对象Plant是动态new出来的 但game其地址是确定的
*/
	game->pPlant=new Plant;
	game->pPlant->sun=11223344;
	return 0;
}

我们查看一下反汇编

26:       game->pPlant=new Plant;
004010B8   push        0Ch
/*
调用类的构造函数 在堆中创建一个局部对象
*/
004010BA   call        operator new (00401150) 
004010BF   add         esp,4
004010C2   mov         dword ptr [ebp-4],eax
004010C5   mov         eax,[game (00427d4c)]
004010CA   mov         ecx,dword ptr [ebp-4]
/*
将结构首地址传给全局指针game->pPlant
*/
004010CD   mov         dword ptr [eax+4],ecx
27:       game->pPlant->sun=11223344;
004010D0   mov         edx,dword ptr [game (00427d4c)] //EDX为game对象地址
004010D6   mov         eax,dword ptr [edx+4] //EAX为指针pPlant
004010D9   mov         dword ptr [eax+8],0AB4130h //sun值 

我们用CE搜索一下,看看我们的Demo是否满足不能被直接搜索到基地址的情况,发现上述Demo确实满足模拟的阳光值无法被搜索到 在这里插入图片描述

因为植物对象plant是动态new出来的,是一个局部变量,其值是不确定的,就导致我们无法直接搜索到阳光的基地址。但因为全局指针game的地址在代码编译时就已经确定了,所以我们就可以通过game指针的地址来找到阳光的动态地址

所以我们要找的就是 一个全局的指向结构 的首地址 的指针的地址

上面的有点绕口,可以多读几遍,就如同上面的Demo一样,阳光这个结构可能被多层结构嵌套指向,所以地址值可能有多次偏移,这就有了一级指针二级指针多级指针这个说法,在上述Demo中,game这个全局指针可以理解为一级指针,pPlant可以理解为二级指针,如果再有多级嵌套的话,就会有三级指针等等,而一级指针的地址就是我们要找的阳光的基地址 再次注意这里要找的是 该全局指针的地址 而不是其指向的地址 在这里插入图片描述

通过动态地址 获取 静态地址(基址)

根据上面的推测,我们可以通过静态地址获取到动态地址,那么逆推根据阳光的动态地址获取静态地址(也是可行的)

我们首先获取到包含阳光这个结构的首地址,也就是一个指针类似于Demo中的指针pPlant,但我们没有源代码所以暂时无法确定这是个几级指针

在这里插入图片描述

查看是什么改写了其值

在这里插入图片描述


EDI=0B63FEE0
0041BA76 - mov [edi+00005560],esi

很明显edi即是包含阳光这个结构的首地址,阳光值就包含在这个结构中,edi也就是一个pPlant指针的的地址,我们CE搜索一下其地址,看看其地址是否是个静态地址,若是静态地址,则他就不是pPlant指针,而是一个game一级指针,也就是我们最终要找的那个指针。 在这里插入图片描述 发现都是动态地址,那么他就是个类似pPlant类型的指针,可能是二级或者三级...指针,而不是一级指针game,我们在接着来找这个包含pPlant指针结构的首地址,也就是他上一级的指针

这时候千万要注意,这次是查看 是查看谁访问了这个地址 不是改写了地址 因为其地址在本次运行的时候都已经确定指向哪一个结构了,要不然也无法改写阳光的数值,但要改写阳光的数值就必定需要来访问各级指针来寻找阳光的地址

在这里插入图片描述 发现了有静态地址,那么很可能就代表着这个指针就是一级指针 在这里插入图片描述

找到了全局指针的地址(基地址)后,这里就不多赘述了,主要目的是认识多级指针和多级偏移的概念。修改阳光数值代码如下。至于为什么会有多个基址,则是有多个全局指针都指向了包含阳光值这个结构的地址

	INT sunNumber=你要改写的阳光数;
    DWORD writeNum;
    DWORD addr01;
    DWORD addr02;
    /*获取全局指针(基址)指向的地址 即二级指针的地址*/
    ReadProcessMemory(this->hProcess,(LPCVOID)0x006A9EC0,&addr01,sizeof (addr01),&this->pID);//进程句柄 基地址
    /*根据二级指针指向的地址 获取本次进程阳光的地址*/
    ReadProcessMemory(this->hProcess,(LPCVOID)(addr01+0x768),&addr02,sizeof (addr02),&this->pID);
    /*获取改写阳光数*/
    WriteProcessMemory(this->hProcess,(LPVOID)(addr02+0x5560),&sunNumber,sizeof (sunNumber),&writeNum);

使食人花吃僵尸无CD

要想定位到食人花吃僵尸的CD计数器,就要通过CE的模糊搜索来找到其计数器的值,通过其值来找到计数器的代码

CE模糊搜索到三个数值 在这里插入图片描述 选取其中一个,在该处下内存写入断点 在这里插入图片描述 再次运行游戏,在如下指令断下 在这里插入图片描述

很明显可以看出,[edi+54]就是食人花攻击后的冷却时间计时器

00463245      8B47 54       mov     eax, dword ptr [edi+54]
00463248      85C0          test    eax, eax                         ;  判断计时器是否为 0
0046324A      7E 06         jle     short 00463252                   ;  不为 0 则减 1
0046324C      83C0 FF       add     eax, -1
0046324F      8947 54       mov     dword ptr [edi+54], eax          ;  计时器
00463252  |>  8B0F          mov     ecx, dword ptr [edi]
00463254  |.  E8 E705FFFF   call    00453840

那么可以推断,当每次食人花攻击前,都会判断一下该计时器是否为0,所以我们还要找到食人花攻击的代码段。 最显眼的当然是紧跟计时器代码段下面的一个CALL,在此处下断点发现食人花依旧还是会有攻击冷却。

我们接着往下看,发现了一条Switch语句,在此处下断点调试,发现其功能是绘制各种植物的攻击动作。

既然如此,那其中大概率会有食人花的动作,而其每次攻击前判断计时器是否为零的语句也大概率在其中 其Switch是根据[EDI+24]来判断对应的case语句,我们下断点调试,查看食人花对应的case语句是哪一条,最后会发现其对应的是case 6 这条语句,其中会执行一个CALL 我们跟进去看看

在这里插入图片描述

跟进去会发现该代码段很长,但我们已经知道了[EDI+54]是计时器的值,所以只需要着重找含有[EDI+54]的指令就可以了,其他指令可以先忽略掉

在这里插入图片描述 发现了一条CMP指令来比较计时器[EDI+54]是否等于零,可以将下面的jnz指令直接改为nop来进行判断是不是该条指令造成食人花攻击有冷却时间的。 是食人花攻击无冷却时间代码。

	/* 两条nop指令 */
	WORD shellCoded=0x9090;
	/* 原jle指令 */
    WORD shellCode=0x5F75;
    DWORD writeNum;
    
    if(arg1== Qt::Checked){
    /* 用来修改 */
        WriteProcessMemory(this->hProcess,(LPVOID)(0x461565),&shellCoded,sizeof (shellCoded),&writeNum);
    }
    else{
    /* 用来恢复 */
        WriteProcessMemory(this->hProcess,(LPVOID)(0x461565),&shellCode,sizeof (shellCode),&writeNum);
    }