10、数据结构逆向:数组与结构体识别、链表与树结构分析、虚函数表与C++对象模型

各位同学,今天我们聊一个硬核话题——数据结构逆向。说白了,就是你在内存里看到一堆二进制,怎么判断它是个数组、结构体,还是链表、树?甚至怎么认出C++的虚函数表?

我刚开始做逆向那会儿,最头疼的就是这个。记得有一次分析一个网络协议栈,内存里全是0x10、0x20这样的偏移量,我盯着看了三天才意识到那是个链表节点。嗯,从那以后我就总结了一套方法,今天全部分享给你们。

10.1 数组与结构体的识别

数组和结构体,是逆向中最基础的数据结构。它们的区别其实很简单:数组里所有元素类型相同,结构体里元素类型可以不同。

10.1.1 数组的识别特征

  • 连续内存:数组元素在内存中是紧挨着的,没有空隙(除非有对齐要求)。
  • 相同步长:每个元素占用的字节数相同。比如int数组每个4字节,char数组每个1字节。
  • 索引访问:汇编代码中常见 base + index * size 的模式。

实战经验:我在逆向一个游戏外挂时,发现内存里有一段连续的0x00到0xFF,步长正好是4字节。我立刻意识到这是个256长度的int数组,用来做颜色查找表。后来验证果然如此。

10.1.2 结构体的识别特征

  • 成员偏移量固定:结构体每个字段的偏移量是编译时确定的。
  • 对齐填充:结构体末尾或中间可能有padding字节,用来对齐到4或8字节边界。
  • 成员类型多样:同一个结构体里可能有int、char、指针等不同类型。

举个例子,假设你在内存里看到这样的模式:

偏移0: 4字节整数 (0x0000000A)
偏移4: 4字节整数 (0x00000014)
偏移8: 8字节指针 (0x7FFFFFFF1234)
偏移16: 1字节字符 (0x41, 即'A')
偏移17: 7字节填充 (0x00)
偏移24: 下一个结构体开始

这明显是个结构体,总大小32字节(0x20)。我习惯用IDA的"Create Struct"功能,把这些字段定义出来,逆向效率能提升好几倍。

小技巧:如果你看到某个偏移量反复出现在不同地址的相同位置,那基本可以确定这是个结构体字段。我曾经用这个方法,在半小时内还原了一个包含30多个字段的复杂结构体。

10.2 链表与树结构分析

链表和树,是动态数据结构的代表。它们不像数组那样连续存储,而是通过指针串联起来。

10.2.1 链表的逆向方法

链表的核心特征:每个节点包含数据域和指针域。单链表有一个next指针,双链表有prev和next两个指针。

怎么在内存里找到链表?我个人的经验是:

  1. 找循环引用:如果一个地址指向另一个地址,而那个地址又指回来,那很可能是链表。
  2. 找NULL终止:链表的最后一个节点,next指针通常是NULL(0x0)。
  3. 分析遍历代码:看汇编里有没有 while (ptr != NULL) { ptr = ptr->next; } 这种模式。

注意:我曾经踩过一个坑——分析一个嵌入式固件时,发现链表节点里有两个指针,我以为是双向链表。结果后来发现,第二个指针指向的是父节点,这其实是一棵树!所以千万别看到两个指针就以为是双链表,要结合上下文判断。

10.2.2 树结构的逆向

树结构比链表复杂一些。常见的二叉树节点包含:左子指针、右子指针、数据域。红黑树还会多一个颜色标记或父指针。

识别树结构的几个关键点:

  • 递归遍历:代码里如果有递归调用,并且每次传入左子或右子指针,那基本是树。
  • 平衡操作:如果看到左旋、右旋、颜色翻转等操作,那肯定是平衡树(如红黑树)。
  • 节点大小固定:树节点的大小通常是固定的,比如24字节或32字节。

我记得有一次逆向一个数据库引擎,内存里全是16字节的节点:4字节key、4字节value、8字节两个指针。我一看这结构,立刻判断出是二叉搜索树。后来验证,确实是用作索引的B+树简化版。

10.3 虚函数表与C++对象模型

这部分是C++逆向的重头戏。虚函数表(vtable)是C++实现多态的核心机制。

10.3.1 虚函数表的结构

每个包含虚函数的类,都有一个虚函数表。这个表本质上是一个函数指针数组。对象的内存布局中,第一个成员通常是一个指向vtable的指针(vptr)。

vtable的典型布局:

偏移0: 指向type_info对象的指针(用于RTTI)
偏移8: 第一个虚函数的地址
偏移16: 第二个虚函数的地址
偏移24: 第三个虚函数的地址
...

关键点:vtable里的函数地址,顺序和类定义中虚函数的声明顺序一致。基类的虚函数在前,派生类的在后。如果派生类重写了某个虚函数,vtable里对应位置会替换成派生类的函数地址。

10.3.2 逆向识别vtable的方法

我个人习惯用以下步骤:

  1. 找vptr:在对象内存的第一个8字节(64位)或4字节(32位),看到一个指向代码段的指针。
  2. 验证vtable:顺着这个指针,看到一连串的函数地址,而且这些地址都在同一个模块的代码段内。
  3. 分析函数签名:vtable里的函数,通常第一个参数是this指针(ECX/RCX寄存器)。
  4. 识别继承关系:如果有多个vtable,或者vtable里有多个层级,那说明存在多重继承。

举个例子,假设你在内存里看到:

对象地址 0x1000: [0x4000]  // vptr指向0x4000
0x4000: [0x5000]  // type_info指针
0x4008: [0x6000]  // 第一个虚函数
0x4010: [0x7000]  // 第二个虚函数
0x4018: [0x8000]  // 第三个虚函数

那这个类至少有3个虚函数。如果另一个对象的vptr指向0x4100,而0x4100的内容和0x4000类似但第三个函数地址不同,那说明这是个派生类,重写了第三个虚函数。

避坑指南:我曾经遇到过一个混淆过的C++程序,vtable里插入了很多垃圾函数指针。我一开始以为这个类有几十个虚函数,后来才发现只有前3个是真的,后面的全是用来迷惑逆向分析的。所以,分析vtable时一定要结合代码调用点来验证。

10.3.3 C++对象模型总结

C++对象的内存布局,我总结成一张表:

场景 内存布局 说明
无虚函数的类 成员变量依次排列 和C结构体一样,没有vptr
有虚函数的类 vptr + 成员变量 vptr在对象开头(通常)
单继承 基类部分 + 派生类成员 共享一个vptr
多重继承 多个基类子对象 + 派生类成员 每个基类有自己的vptr
虚继承 包含虚基类指针 解决菱形继承问题

嗯,这里要注意:多重继承下,派生类对象会有多个vptr。每个vptr指向不同的vtable,分别对应不同的基类。这是逆向时最容易搞混的地方。

知识体系总览

下面这张图,是我自己整理的数据结构逆向知识体系。你可以把它当成一个检查清单,遇到实际问题时对照着看:

数据结构逆向知识体系 数据结构逆向 静态结构 数组:连续+同类型 结构体:偏移+对齐 动态结构 链表:指针串联 树:递归+分支 C++对象模型 vptr + vtable 继承与多态 核心方法:内存布局 + 指针追踪 + 代码模式匹配

这张图把数据结构逆向分成了三条主线:静态结构(数组和结构体)、动态结构(链表和树)、C++对象模型(虚函数表)。我个人做逆向时,会先判断目标属于哪一类,然后套用对应的分析方法。

好了,今天的内容就到这里。数据结构逆向是个熟能生巧的活,多分析几个二进制文件,你自然就能一眼看出内存里藏的是什么结构。


公众号:蓝海资料掘金营,微信deep3321