封包辅助“不得不说的秘密”(3)

2020-11-23 15:24:09 李良人 21

数据逆向之背包二叉树嵌套物品数组


名字很长,嗯!很好。

 

原理扯淡:

 

在上期我们接触了链表结构的逆向,主要是配合封包实现功能而需要的一些必要数据。虽然在前几篇笔记中数据逆向好像是一个配角来的,不过它其实也是有很多独挡一面的情况——就比如 FPS 类的透,其实就是逆向出人物数组获得坐标 再画出来而已,所以要了解数据结构与数据逆向 也是非常必要的。

 

好的,闲扯不多说 我们来进入正题。数据结构说白了就是存储数据的一直方式而已,把一个个的成员按照某种规律科学的存放在内存中。而这个成员可以是任何东西,只要是一个概念上的整体——比如另一个数据结构?

 

这就是数据结构的嵌套,这种情况也非常的常见,特别是在一些RPG游戏类型里面。

 

像下面要说的一个小栗子,就是二叉树嵌套数组,每一个二叉树的成员 都是 或者 都包含一个数组,当然在那之前我们先来认识一下数组与二叉树在内存中的通常形态。 PS:最好有一些C语言基础。

 

(一)数组:

飞郁网络培训有限公司

如图所示,数组的结构非常简单。由一片连续的内存空间组成,每个成员的宽度都必须是一样的,通常还有两个指针 首指针保存着数组第一个成员的地址,尾指针指向最后一个成员 假想的下一个成员,在逆向中通常尾指针就在首指针的旁边,要得到数组成员个数只需要用 尾地址-首地址 除以 成员的宽度。

 

简单并不意味着差劲,相反数组因为结构简单带来的一些优势——速度很快(通过一个数学公式可以很快计算出下一个成员)、易使用,可以说是最常见用得最多的数据结构了,谁会讨厌数组呢?

 

假如我申请了这样一个数组:

double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0};

 

如图:

飞郁网络培训有限公司

balance是我们的数组首地址,那么我们可以通过(balance+n*每个成员的宽度 n是每个成员的 位置-1,又被成为下标 比如:第一个成员1000.0,他的位置是 1,下标是1-1 = 0,而double类型的宽度是8字节,所以代入公式 balance+0*8,就可以得到它的地址并访问它,也因为如此想得到数组中的每一个成员 不需要一次一次的指针寻址,所以它的速度非常的快。

 

在汇编代码中我们通常可以非常直接的看到类似这样的公式:

 

mov edi,[eax+esi*4]

 

一眼就可以看出这是一个数组(从某数组中取出下标为esi的成员),并且eax是数组的首地址。

 

 

(二)二叉树:

飞郁网络培训有限公司

二叉树的结构如图所示,上次我们领悟了链表,再来看它就简单得多了。实际上链表可以看做二叉树的极简形体即——每个成员都只有一个子成员,而二叉树不同其中每个成员都可能挂着2个成员(可能有1一个,可能有2个,也可能没有)。

 

其中第一个成员A被成为根节点,左边的子成员B被称为左子树,右边的子成员C被称为右子树,根节点只有一个,而左右子树可能每个成员都有。

 

和链表一样,每个成员中都有指向子成员的指针,而不同的是链表只有一个,而二叉树有2个指向子成员的指针(姑且我们称它为左右指针),分别指向左右子树。

 

那如何判断一个节点是否存在有效的左右子成员或者说左右子树呢?通常来说会有一个无效成员(空成员),如果某个成员左指针指向的成员为空成员,既表示此成员无左子树,有无右子树同理。而成员都会有一个标志位属性,用来判断此成员是否为无效成员(空成员)。

 

在逆向中,如果发现要追的寄存器,在循环中每次产生变化都来自于不同的两种情况:

 

比如: mov eax,[eax]

   这里通常是比较某两值,然后跳转的指令

   mov eax,[eax+8]

 

如果eax是你在循环中要追踪的寄存器,而每次循环它都可能来自[eax+0]或者[eax+8],那就说明此时你所回溯的对象是一个标准的二叉树。当然不要局限与+0或者+8,虽然这种情况比较常见,但是也可以是+10 或者 +20 ,重要的是新的变化来源于2种不同的情况。

 

当然实际情况可能会更加复杂,稍后的实战中:我们的老朋友 自己搭建用来学习的私服版幻想神域2 它的背包数据结构就是一个二叉树套数组的较复杂情况,在一步步的回溯中我们再详细说明更多细节。

 

 

快乐实战:

 

其实各行各业都存在所谓的逆向工程,百度百科对逆向工程的定义:

飞郁网络培训有限公司

简单来说就是通过观察和分析,再根据其透露给我们的信息结合我们自己的经验去推测 其本身的原理与构造。比如:中医里的问闻望切就是一种逆向,获得信息之后通过自己的经验来判断病因病情。

 

对游戏的逆向也是如此,在逆向一款游戏之前一定要先把这款游戏玩透,并且在逆向想要逆向的某结构之前,先要对透露给我们的表像认真观察和分析——比如接下来我们要逆向 幻想神域2的背包结构获得所有的背包物品信息,先观察游戏中背包的结构 哪怕多获得一丝信息或许也能在实际逆向中节约大量时间。

 

游戏中的背包如图:

飞郁网络培训有限公司

....平平无奇的背包....大量的物品存放在其中......说实话我再认真也观察不出啥了(上面吹的牛B据此不足十行,你在玩我吗?楼主!?)

 

没错..我就是...咳咳...虽然有用的信息不多,但是结合我们的经验也能推算个大概。根据程序员的直觉,面对对象(普遍使用的软件开发方法,不懂还是先学会再来看文章吧)的编程思想,这些物品在实际编程中一定都是被抽象的类,而且它们有相同的高级抽象特征——都是可拾取物品,所以它们应该都继承于同一系父类,既然它们都继承于同一父类 那么对象中许多属性都是相同的——在逆向中的映照比如:物品对象的数量属性偏移都是相同的。

 

可以看到背包中有很多物品,在内存中则是大量的数据 而且 可以说它们是同类型的,那么必然会存储在一个数据结构里面,才能方便的管理,我姑且称它为物品数组(当然是不是数组具体逆向之后才清楚,为了方便说明清楚 暂且这么说)。

 

 

再探背包:

飞郁网络培训有限公司

飞郁网络培训有限公司

在下面的格子中,还可以装备扩充背包,把鼠标放在相应的扩充背包上,还能现实相应扩充格子,如图:

飞郁网络培训有限公司

猜测背包估计也是某种类,在游戏中装备了2个扩容背包 加上 默认人物拥有的初始背包 此时应该有3个背包对象,而上文所说的物品数组,应该是其某个属性。

 

并且游戏中可以看到有5个扩容背包的格子,估计3个背包对象也保存在某个数据结构中,姑且称它为背包二叉树。

 

那么我们来整理一下思路:首先每个物品都是有数量的(至少是一),数量是物品对象的一个属性,物品对象是物品数组的一个成员,物品数组是背包对象的一个属性,而背包对象是背包二叉树的一个成员。

 

那么如果我要访问一个物品的数量,要经过以下步骤:访问背包二叉树—》访问背包对象—》访问物品数组—》访问物品对象—》得到物品数量属性。

 

那么只要我们在物品对象的数量属性上,下一个访问断点,就可以回溯出 物品数组、背包二叉树这2个结构,这样我们就可以自己写代码遍历它们从而获取背包中所有物品的数据了。

 

同时我们推测出,在访问的过程中有2个数据结构,那么想从数据结构中取出成员,通常都是通过循环遍历,所以做好心里准备我们逆向将经过2个循环,要注意所追寄存器的数值变化规律。

 

让我们开始吧!

    

首先,我们先用CE扫描出数量的内存地址,通常这种基本操作 由于文章篇幅问题都是之间忽略的,但是这里有点特殊情况 怕新人同志遇到这种情况搞不明白(毕竟咱这就是面向新人朋友的),稍微提几嘴。

飞郁网络培训有限公司

ce 使用4字节扫描图中圈出的红药数量,扫描几下会发现剩下2个地址排除不掉了。那么我们改变一下第二个地址里面的值,再打开关闭背包。

飞郁网络培训有限公司

游戏中药品的数量没有变化,那么真正的数值应该不是这个地址。

 

我们再改变一下第一个地址的数值,发现改变它之后 再关闭打开背包,地址中的值就被修改回去了。

 

那么有几种可能:

1. 第一个地址中就是真正的红药数量,打开关闭背包物品数量被服务器修改了。

2. 第一个地址中也不是真正的数量,而是打开关闭背包的过程程序从真正的数量地址中读取值修改了第一个地址中的值。

 

如果要实现关闭再打开背包服务验证本地数据,那么在打开背包界面的时候就应该发一个数据包,告诉服务器:嘿!你告诉我这人背包里面所有的数据。 这不合理,在编程中我们常把访问网络视为一个耗时操作,这样做势必会导致物品数据没办法立即得到并显示,而且也没必要。

 

那这么说真正的物品数量,并没有被我们扫描到真正的地址。为什么?我发现这游戏的物品大多的最大值并不怎么大(超出最大值的话 一个格子就会放不下),估计4字节的空间太大了,换2字节重新扫描一下。

 

果然,2字节扫描 扫描到了一个新的地址,而且改变它,再关闭打开背包 游戏中显示的数量会变成我们所改变的数值。

飞郁网络培训有限公司

当然,如果使用它的话其实还是原数量,因为我们只是修改了本地的数量。这是一个网络游戏,药品数量这种当然以服务器保存的值为准,在我们客户端发送嗑药的封包后,服务处理后还会把新的值发送给我们,使得我们的本地修改被修正。

 

好了,在其数量地址上下断(记得是word断点)再吃药使游戏就会断下(如果是关闭打开背包使游戏断下 断到的代码会比较复杂,原因很简单 打开背包是遍历每个对象并显示出来,而吃药所产生的遍历是为了取出某个特定的对象,有过规范的编程经验的人应该明白那种遍历代码会更少,更少也意味着我们更好分析 没必要和自己过不去):

飞郁网络培训有限公司

断在了如图处,此时eax+0x28是数量的地址,在往上面的地方翻一翻找eax的来源。

飞郁网络培训有限公司

运气不错,明显的特征 eax显然是物品对象的首地址,它来自于一个数组结构中。这时如果是刚开始比较粗心的小白,或者是一些网上粗制滥造的教程(“不要管什么结构,顺着寄存器追下去就是了”........),肯定急匆匆找到数组尾指针 获得数组界限再追出数组基地址,就写代码遍历了...然后..发现扩展背包里面的物品数据怎么都得不到。

飞郁网络培训有限公司

eis+0x8是数组首地址ecx的来源,上面的原理扯淡中说过尾指针应该在它附近(或者是数组的大小在它附近),在内存窗口里面一眼就可以看出尾指针在eis+0x8+0x4的位置。

飞郁网络培训有限公司

当然我们通过之前的一波逆向思维,知道了应该会面对2个嵌套的数据结构,现在面慌心不慌,先在 mov eaxdword ptr ds[ecx+edi*4]上下断点。吃药断下后,按F9使程序重新跑起来(因为是在循环里面,所以马上又会断下),观察数组首地址ecx、下标edi的数值变化。

飞郁网络培训有限公司

我们发现edi也就是下标的变化规律是 0~1D 0~13 0~4,这些都是16进制转换成十进制就是 0~29 0~19 0~4,而我们默认背包30格、第一个扩容背包20格、第二个扩容背包5格,正好对应3个背包的所有格子。

 

ecx数组首地址则是每当下标重新变成0时,产生变化 分别为0B4AC5802261B0D0 227D6060 应该是对应于3个背包,和我们之前的想法一致。

 

现在我们继续往上面追,看看到什么地方所追寄存器的值不产生变化。

飞郁网络培训有限公司

如图,eax不管几次断下 其中内容都不会再产生变化了。而mov eaxdword ptr ds[ecx+edi*4] ecx数组首地址 是来源于eax的,既然eax不变化了 但是ecx缺有3种不同的变化,那说明它们2条指令之间必有循环(具体就是下面往上面的跳转代码,代码执行是由上而下一条条执行下去,跳转可以重新跳到上面产生循环)。

 

上篇笔记我们找下面的跳转,一个一个的查看是否有那样产生循环的跳转。不过除了这样,还可以使用OD中的快捷键Ctrl+A分析代码,OD会帮我们标注出跳转。

飞郁网络培训有限公司

如图中红色箭头标出的黑线,长的是遍历背包二叉树的循环,短的是遍历物品数组的循环。

 

既然有循环就说明:

飞郁网络培训有限公司

数组头来源:

飞郁网络培训有限公司

如图 mov esidowrd ptr ds:[edi+0x10] 也是产生物品数组首地址的其中一环,它上面就是 所追寄存器开始不变化的那条指令,而这条指令的edi依旧有3种变化(所以产生了下面的物品数组首地址ecx的变化,因为物品数组首地址来源于它 <或者说来源于以它为基地址的表达式> ),而上面的来源已经不变化了 那这条指令的edi要想有变化,只能是来自于下面语句的改变!(因为有循环哦,cpu又会跳转上来执行)

飞郁网络培训有限公司

借助OD的高亮插件,很快就找到了循环中 下面改变edi的地方,在这里edi来源于第3个局部变量(OD帮我标注出来了),再接着在循环里面找了 没有发现[local.3](也就是[ebp-0xC])的来源,那只能说明其来源是循环中的call(函数)。

飞郁网络培训有限公司

如图中函数,它有一个参数ecx 取得是第四个局部变量的地址,局部变量的空间是堆栈分配的第四个局部变量的地址 +0x4 就是第三个局部变量的地址,所以很明显是这个Call是第三个局部变量的来源。

 

也就是说,每次循环经过这里 参生一个新的 背包对象,从背包二叉树中取出。我们进去看一看,它是怎么取对象的...

飞郁网络培训有限公司

...好多跳转..好复杂.. 人家数组和链表取出新对象都是一条语句完事.......,不过这也是二叉树的特征之一,因为二叉树的结构较为复杂,所以循环遍历它 想要取出新对象也自然比较复杂。

 

我们有几种选择:

1. 硬着头皮分析。

2. 在我们的代码遍历背包二叉树的时候 直接调用它得了。

 

硬着头皮分析...实在不推荐,干脆直接调用它,但是调用它的话毕竟走游戏代码了,不想走雷区 有什么更好的办法么?

 

有!背包二叉树里面保存着人物的背包对象,一般来说这种数据不会只有一个地方遍历它,也许有更简单的遍历代码!我们可以找一找。遍历必然是读取背包二叉树其中的对象,我们来到之前循环中,在循环产生新对象的位置下个断点。

飞郁网络培训有限公司

吃药断下后,我们就可以得到edi也就是新对象的地址,我们在其地址上下一个硬件访问断点,然后撤销上图的F2断点 按F9使程序重新跑起来,看看会不会断停在我们感兴趣的地方。

 

飞郁网络培训有限公司

这个循环遍历是不是很简单,就几句汇编代码 简直一眼望穿。(没有断在这种好地方的话,先不要取消硬件访问断点 再按几次F9试试)

 

看图 eax是我们的背包对象,它可能来自[eax+0x8] 或者 [eax] 两种可能,标准的二叉树。并且这里依靠je产生向上的跳转 构造循环,而条件是对象地址+0x15的地方不等于0就继续循环,看来 对象地址+0x15的地方是0的话 表示其对象为空或无效。

 

可以这个循环就是为了取某个对象,所以才如此简单:cmp word ptr ds:[eax+0xC],si 这是它用来取对象的比较,一般来说这种操作都是用唯一标识来比较,普遍都是ID 所以 对象地址+0xC 的地址保存着对象ID

 

我们现在终于将这个嵌套的结构给分析透了,只需要接着往上找出eax的基地址 我们就可以自己写代码遍历这个结构了,不过这里也有一些特别 稍微说一说。

 

飞郁网络培训有限公司

追到上面发现来源上层调用传下来的参数ecx,要继续往上面回溯的话 要下断点 使程序断下,然后Ctrl+F9返回上一层接着追ecx,通常如此。

 

但是此次代码不是我们做动作才断下的,是游戏自己不间断的扫描执行,看这个call的样子,我估计有很多地方调用它。

 

在其头部下一个F2代码执行断点,发现立即断下,几次按F9让代码重新跑起来 重新断下的时候发现 栈顶的函数返回地址是不同的,这说明确实有很多地方在调用这个函数,如果我们贸然的下断点 返回回去的地方可能根本不是我们想要的地方,很大可能和背包无关 自然也就追不到其基地址了。

 

只能重新在背包对象上下访问断点,这个时候返回上层才是我们想要的地方,再之后就是无味的找基地址了... 不过 后面可以发现这东西是挂在人物角色对象下面的(背包是人物的东西,这个游戏严格的遵循面对对象的思想呢)。

 

背包二叉树表达式总结:

[[[[人物对象+0xC]+0x3C0]+0x4]+0x4]  根节点

+0x 0 左子树

+0x 8 右子树

+0x 15 标志位 = 0 就是有效对象

+0x C ID  word

 

物品数组表达式总结:

[[[[[[[人物对象+0xC]+0x3C0]+0x4]+0x4]+0x10]+0x8]+edi*4]

+ 0x 0 word ID

+ 0x26 word 最大数量

+ 0x28 word 数量

 

通过此 就可以写递归算法遍历这个二叉树嵌套结构了。

 

 

参考代码:

 

struct T背包物品

{

DWORD ndID;

WORD nd数量;

WORD ndMAX数量;

DWORD ndIndex;

DWORD nd所在背包;

};

 

struct T背包列表

{

T背包物品 t物品[0x100];

DWORD nd数量;

T背包列表* GetData();

void F递归遍历(DWORD nd节点);

};

 

DWORD g_ndIndex = 0; // 背包i

T背包列表 * T背包列表::GetData()

{

/*[[[[人物对象 + 0xC] + 0x3C0] + 0x4] + 0x4]  根节点

+ 0x 0 左子树

+ 0x 8 右子树

+ 0x 15 标志位 = 0 就还有对象 byte

+ 0x C ID  word

 

[[[[[[[人物对象 + 0xC] + 0x3C0] + 0x4] + 0x4] + 0x10] + 0x8] + edi * 4]

+ 0x 0 word ID

+ 0x26 word 最大数量

+ 0x28 word 数量*/

t人物属性.GetData();

memset(this, 0, sizeof(T背包列表));

__try

{

g_ndIndex = 0;

DWORD ndObj = *(DWORD*)(t人物属性 .nd对象+0xC);

ndObj = *(DWORD*)(ndObj + 0x3C0);

ndObj = *(DWORD*)(ndObj + 0x4);

ndObj = *(DWORD*)(ndObj + 0x4);

F递归遍历(ndObj);//传入根节点进行遍历

this->nd数量 = g_ndIndex;

}

__except (1)

{

F输出调试信息("幻想神域 void T背包列表::F递归遍历() \r\n");

return nullptr;

}

return this;

}

 

void T背包列表::F递归遍历(DWORD nd节点)

{

__try

{

DWORD nd左子树 = *(DWORD*)(nd节点 + 0x0);

DWORD nd右子树 = *(DWORD*)(nd节点 + 0x8);

byte b标志位 = *(byte*)(nd节点 + 0x15);

 

if (b标志位 == 0)

{

DWORD ndObj = *(DWORD*)(nd节点 + 0x10);

//背包

DWORD nd背包序号 = *(WORD*)(nd节点 + 0xC);

 

DWORD nd数组头 = *(DWORD*)(ndObj + 0x8);

DWORD nd数组尾 = *(DWORD*)(ndObj + 0x8 + 0x4);

int len = (nd数组尾 - nd数组头)/4;

for (int i=0;i<len;i++)

{

ndObj = *(DWORD*)(nd数组头 +i * 0x4);

 

if (ndObj&&*(DWORD*)ndObj)

{

this->t物品[g_ndIndex].ndID = *(WORD*)ndObj;

this->t物品[g_ndIndex].ndIndex = i;

this->t物品[g_ndIndex].ndMAX数量 = *(WORD*)(ndObj + 0x2A);

this->t物品[g_ndIndex].nd数量 = *(WORD*)(ndObj + 0x28);

this->t物品[g_ndIndex].nd所在背包 = nd背包序号;

g_ndIndex++;

}

}

F递归遍历(nd左子树);

F递归遍历(nd右子树);

}

}

__except (1)

{

F输出调试信息("幻想神域 void T背包列表::F递归遍历() \r\n");

}

}


电话咨询
QQ客服