第九章:数据结构逆向——结构体、数组、字符串与链表

数据结构逆向,说白了就是跟内存布局打交道。你想想看,游戏里一个角色有多少属性?血量、蓝量、坐标、装备列表……这些数据不会散落在内存里,而是被组织成各种结构。我们逆向工程师要做的,就是把这些结构从二进制里“挖”出来。

我个人习惯把数据结构逆向分成四块:结构体、数组、字符串、链表。这四块搞定了,大部分游戏的数据逻辑你都能摸透。今天我就把这四块的核心思路和实战技巧,一次性讲清楚。

数据结构逆向核心体系 结构体 Struct 数组 Array 字符串 String 链表 Linked List 成员偏移量计算 内存对齐规则 嵌套结构体 基址 + 索引 * 大小 一维/多维数组 动态数组指针 ASCII / UTF-16 长度前缀 vs 空终止 字符串池 指针遍历 头节点/尾节点 循环检测 核心思路 先找特征值 → 确定结构边界 → 验证成员类型 → 还原完整布局

9.1 结构体逆向:从内存碎片到完整蓝图

结构体逆向,说白了就是猜布局。游戏里一个角色对象,在内存里就是一块连续的区域。比如一个玩家结构体,可能前4字节是血量(int),接着4字节是蓝量(int),再接着2字节是等级(short),然后可能有个4字节的指针指向名字字符串……

我刚开始做逆向时,最头疼的就是结构体对齐。明明看起来是连续的,中间却总有几个字节的“空洞”。后来我才明白,编译器为了性能,会在成员之间插入填充字节。

结构体逆向三步法:

  1. 找特征值:先定位一个已知的数值(比如血量100),然后看它周围的内存布局
  2. 猜成员类型:根据数值范围猜类型——0~65535可能是short,负数可能是int,很大的数可能是指针
  3. 验证偏移:修改某个偏移的值,看游戏里对应属性是否变化

举个例子。我在逆向某款FPS游戏时,发现玩家对象的血量地址是0x12345678。我查看周围内存:

0x12345678: 64 00 00 00  (血量: 100)
0x1234567C: 32 00 00 00  (蓝量: 50)
0x12345680: 0A 00        (等级: 10)
0x12345682: 00 00        (填充)
0x12345684: 78 56 34 12  (指针, 指向名字)

嗯,这里要注意:0x12345682处的两个00字节,就是对齐填充。因为下一个成员是指针(4字节),所以编译器把等级后面的2字节补齐了。

个人经验:我习惯在CE里用“结构体分析”功能,把候选地址按4字节对齐显示。这样一眼就能看出哪些是真正的成员,哪些是填充。

9.2 数组逆向:连续内存的暴力美学

数组逆向比结构体简单,因为它的规律性太强了。你想想看,如果一组数据每个元素大小相同,地址连续,那十有八九就是数组。

数组逆向的核心公式就一个:基址 + 索引 × 元素大小。但实际中,我们往往不知道元素大小和数组长度。怎么办?

我的做法是:先找到两个相邻的元素,算出它们的地址差,这就是元素大小。然后看数组末尾——如果后面全是0或者重复的无效数据,那基本就到头了。

数组逆向常见场景:

  • 背包物品数组:每个物品占固定大小(比如0x20字节),通过索引访问
  • 玩家列表:每个玩家对象指针组成一个数组,通过指针间接访问
  • 技能冷却数组:每个技能冷却时间是一个float,连续排列

我曾经逆向过一个MMORPG的背包系统。背包有40格,每格占0x20字节。我通过修改第5格的物品ID,发现游戏里第5个格子确实变了。然后我用“数组遍历”脚本,把40格的数据全部dump出来,直接还原了整个背包内容。

注意:有些游戏会用“稀疏数组”或“动态数组”。稀疏数组中间可能有空洞(无效元素),动态数组则是一个指针指向堆上分配的内存。遇到这两种情况,别死板地用连续地址去套。

9.3 字符串逆向:编码与长度的博弈

字符串逆向,说白了就是猜编码和长度。游戏里常见的字符串格式有两种:

编码类型 特点 常见场景
ASCII 每个字符1字节,以0x00结尾 英文游戏、玩家名字(纯英文)
UTF-16 每个字符2字节,以0x0000结尾 中文游戏、多语言支持
长度前缀 前2或4字节存长度,后面跟字符数据 网络协议、序列化数据

我个人习惯先看字符串末尾。如果看到连续的00字节,那基本就是空终止字符串。如果开头有个小整数(比如0x0A表示10个字符),那就是长度前缀格式。

嗯,这里有个坑:有些游戏会用“字符串池”来节省内存。所有字符串都放在一个大的缓冲区里,玩家名字只是一个索引或指针。这种情况下,你直接修改字符串内容可能不会生效,因为游戏可能从池里重新读取。

避坑指南:我曾经在逆向一个日系游戏时,发现玩家名字明明是ASCII格式,但修改后游戏里显示乱码。后来才发现,游戏内部用的是Shift-JIS编码,只是看起来像ASCII。所以,遇到非英文游戏,先确认编码格式再动手。

9.4 链表逆向:指针的舞蹈

链表逆向,是数据结构逆向里最考验耐心的。链表不像数组那样连续,它的节点分散在内存各处,靠指针串联起来。

典型的链表节点结构:

struct ListNode {
    void* data;       // 指向实际数据
    ListNode* next;   // 指向下一个节点
    // 有些链表还有 prev 指向前一个节点
};

逆向链表的关键,是找到“头节点”。头节点通常被一个全局指针或静态变量引用。找到头节点后,顺着next指针一路往下走,就能遍历整个链表。

我常用的方法:

  • 特征扫描:搜索一个已知的数据值,然后看它周围有没有指向其他地址的指针
  • 指针链追踪:在CE里用“指针扫描”功能,看哪些地址指向了当前节点
  • 循环检测:如果遍历时发现某个节点又指回了前面的节点,那就是循环链表

实战案例:我在逆向某款赛车游戏时,发现赛车列表是用链表实现的。每个赛车节点里,除了next指针,还有一个prev指针(双向链表)。我通过修改prev指针,让游戏里两辆赛车的顺序互换——结果两辆车在赛道上直接穿模了。嗯,这个bug后来成了我讲课时的经典反面教材。

警告:修改链表指针要格外小心。如果你把next指针改成一个无效地址,游戏在遍历链表时就会崩溃。我建议先备份原始指针值,或者用“只读”方式验证结构。

9.5 综合实战:从内存到逻辑

好了,前面讲了四种数据结构的逆向方法。实际游戏中,它们往往是组合出现的。比如一个玩家结构体里,可能包含一个数组(技能列表),数组元素又是指向字符串的指针(技能名称),而技能本身可能是一个链表(技能链)。

我的建议是:先拆后合。先把每个独立的数据结构逆向清楚,再组合起来看整体逻辑。就像拼图一样,先拼出每个小块的形状,再拼成大图。

举个例子,我逆向某款MOBA游戏时,遇到了这样一个结构:

struct Player {
    int hp;               // 血量
    int mp;               // 蓝量
    char name[16];        // 名字(固定长度字符串)
    int skillCount;       // 技能数量
    Skill* skillList;     // 指向技能数组的指针
};

struct Skill {
    int id;               // 技能ID
    float cooldown;       // 冷却时间
    char* namePtr;        // 指向技能名字符串的指针
};

你看,这里结构体、数组、字符串、指针全用上了。逆向时,我先确定了Player结构体的大小(0x20字节),然后找到skillList指针,顺着它找到技能数组,再通过每个技能的namePtr找到技能名称字符串。

整个过程就像剥洋葱,一层一层往里剥。每剥一层,你对游戏逻辑的理解就深一层。

最后说一句:数据结构逆向没有捷径,就是多练。我刚开始时,一个简单的结构体可能要花一整天。现在熟练了,大部分结构体10分钟就能搞定。你只要把今天讲的四种类型吃透,以后遇到再复杂的数据结构,心里都有底。


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