网络游戏逆向分析-2-搜索基础数据(血量)

人物属性是一个游戏的基础数据,所有的游戏架构都得在基础数据的基础上,所以搜索到基础数据是重中之重。

这里首先来分析人物属性的气血值这个东西。

 

 

和单机游戏搜索数据是一样的,同样是采用CE,OD等工具来进行搜索。

搜索血量

首先采用CE,通过数据变化来定位该数据存放的内存的位置。

首先搜索初始血量,1635:

 

 

值有很多,再在被怪物攻击血量减少后再次搜索:

现在是1433,再次搜索:

 

 

结果就只剩下2个了。

采用内置的游戏技能,按Z加血再来分析:

 

 

然后通过仔细观察分析,第一个地址的值是再人物血量减少后直接扣除,而第二个值确实缓慢扣除,但是最后的结果都是一样的,所以肯定第一个地址更符合我们的需求。

但是,这个地址肯定不是一个全局变量的地址,关于全局变量前面的PE有提过,就是一个固定的地址,这个地址可能是一个堆地址,在程序重新使用后就会变的一个地址,对我们来说意义不大,但是可以通过它来找到全局变量的地址。

如何找到全局变量的地址

可以根据找到的这个临时地址来通过打一个访问或者写入的断点来寻找对应的值。

这里采用打一个访问断点来寻找。

采用xdbg来调试该进程,访问该内存地址来观察:

首先给前面找到的地址下一个断点,注意尽量不要采用内存断点,因为内存是按页来处理的,有可能一个断点会导致多个断点出现,尽量下硬件断点:

 

 

下一个硬件访问的四字节断点。

然后再让程序跑起来:

 

 

这里很明显地停止到了刚刚硬件断点的位置,硬件断点和内存断点的区别就是,硬件断点是在执行完内容后才停下来,而内存断点就是在断点的位置停下来不执行,所以这里停下来的位置应该是当前EIP的上一条指令:

 

 

也就是这一条指令:

mov edi,dword ptr ds:[esi+1C]

这里的esi+1C的值=5F499D74 //先将其记录下来

然后把原来的硬件断点给它取消掉,再给该mov edi,dword ptr ds:[esi+1c]按F2下一个int3断点。

但是可以发现运行后是一直停在断点处,于是就可以下一个条件断点,条件是该指令的esi+1C的值等于前面我们找到的地址值5F499D74:

 

 

然后选择编写:

 

 

由于中途我的游戏崩了,所以数据不对要重新来,但是重新一个一个截图又太麻烦了,我就不把前面的截图修改了,大家自行修改吧

为了防止游戏不崩溃,大家再使用完断点后最后把游戏继续运行下去,不然可能会崩溃,比如:判断你掉线了什么之内的。

 

 

针对这条指令往上找,可以很明显的看到前面有一条指令:

mov esi,ecx //改变了esi的值

于是这里再给该指令下一条条件断点,为的是esi+1C的值还是前面我们找到的血量的地址的值:

 

 

然后这里也可以停止到该断点处,说明没有任何问题,然后继续往上面看:

 

 

这里很明显地可以看出来,是一个标准的函数的结构,先提升栈空间然后再处理,而且前面还有int3,一个正常的函数段里面是不会出现int3这种东西的,int3意味着是一个断点了。还可以通过给这个int3下面的第一条指令下断点,查看到该指令的时候堆栈是什么样子的是不是堆栈的下一个内容是一个返回地址。(因为call一个函数时,会把下一条执行的地址压入到堆栈)。

通过堆栈的返回地址得到函数的地址,然后来观察这个ecx是怎么来的:

 

 

 

很明显就是这一段内容:

 

 

这段代码,在调用函数之前修改了ecx的值:

lea ecx,dword ptr ds:[esi+2A0]

这里的意思就是ecx = esi+2A0

添加一个条件断点来判断是否这条指令是否是我们要找的:

 

 

发现是可以断下来的,那就没有任何问题。

然后继续往上找esi的值:(找了很久才有的结果)

 

 

同样的给这个函数体的最上面打一个条件断点,因为这里的Mov esi,ecx之前没有修改过ecx的值,所以肯定是在前面。是可以断下来的说明这里依然没有问题。

 

 

访问返回地址的值:

 

 

这里肯定就是这条指令的问题了,同样的加一个条件断点来判断找的地址是否正确,经过我的演算是正确的。

然后继续往上探索esi的值:

 

 

这里表明了esi来源与ecx,继续用前面的办法给该函数体的第一条指令下条件断点:

 

 

断点成功,说明是找对了地方的。然后根据返回值又跳出来到上一个函数:

 

 

一看就是这里:这里又是ecx和esi相互捣腾。然后继续往上找esi的值:

 

 

又往上找了很久才找到esi相关的:mov esi,ecx。

同样的给这个函数体打条件断点判断是不是,如果是就根据栈空间返回到调用函数体的代码里面继续找。

这里我直接跳转到调用函数体的代码里面:

 

 

这里需要注意的是,由于前面有一个ret的返回指令,所以这个整个函数代码体肯定不能直接往上找,只能在ret和call之间找,因为这里就是该函数体了。

所以这里直接给ret的下一条指令打条件断点看是否符合,然后再通过栈回到上一个。

 

 

这里得到的mov ecx,ebx再进行条件断点测试,这里我测试是正确的。然后往上找ebx的内容。

mov ebx,dword ptr ds:[ebx+eax]

对这里的[ebx+eax]添加条件断点来判断是否找对位置,这里我验证出来是找对了的。

这里稍微有一点区别,因为是涉及了两个寄存器ebx和eax,通过多次运行发现每次运行到这里的时候ebx和eax的值都是固定不变的。

00ED3604 | 8B1C03                   | mov ebx,dword ptr ds:[ebx+eax]          
//| eax == 709A45C0 ebx == 10

这个可以猜得到,eax是一个地址,然后ebx是一个偏移地址。

然后eax又来自于mov eax,dword ptr ds:[esi+c8],通过观察发现esi是不变的是一个定值。

然后继续找这个esi:

 

 

直到这个函数快结束的时候才找到:

00ED349E | 8BF1                     | mov esi,ecx                             |

同样添加条件断点来验证。验证OK。

通过栈返回调用函数来继续找ecx的值:

 

 

一来就找到ecx的变化了,通过条件断点验证是正确的,继续往上找esi:

 

 

直到最上面才看到esi和ecx,又得到了是ecx给esi

mov esi,ecx

然后通过函数返回地址再找到上一层:

 

 

mov ecx,esi //ecx==esi

经过验证后是正确的,然后继续往上找esi对应的值:

 

 

 

 

添加验证后,又是ecx,然后得往上再返回函数再继续找:

 

 

ecx,又来源与esi。

反正就是这样一直找一直找:

直到最后的值是一个立即数:

 

 

 

这个就是一个基址了,这个在PE文件的时候有讲过,重定位表中会有一些写死了的地址也就是基址,就是游戏重启了也不会变,除非游戏更新了。

总结

通过CE工具来筛选,筛选到一个临时的变量存储空间,然后依据该内容给调试器打断点,最好是硬件写入断点,然后再根据该断点的内容,来通过各自寄存器往上找,直到找到一个固定值,一个立即数,一个不会变的值,然后再把偏移值+上作为一个公式来访问。

因为PE的原因,这个值是不会变的,除非是比较新的采用了随机基址。