1. 被动分析到底在分析什么
1.1 从一次真实的排查经历说起
去年帮朋友看一个卡牌游戏的战斗结算逻辑,他上来就想开调试器下断点,我拦住了他。原因很简单:那款游戏有完整性校验,调试器附加的瞬间就会触发异常退出,甚至连日志都不给你留。我们最后是怎么做的?全程没有附加调试器,没有修改一个字节的内存,只靠静态反汇编加运行时日志观察,就把伤害计算公式还原了个七七八八。
这就是被动分析的核心价值——在目标程序完全不知情的前提下,理解它的行为逻辑。它和主动分析(下断点、改内存、Hook API)最大的区别在于:被动分析不改变程序的任何状态,因此不会触发反调试、反篡改机制,也不会因为你的介入而让程序行为发生偏移。
很多人对游戏逆向的理解停留在“开个调试器跟一跟”的层面,但实际工作中,尤其是面对有保护的游戏,主动手段往往是最后才用的。被动分析才是第一步,也是最考验功底的一步。它要求你能从一堆汇编指令、调用关系、数据引用中,像拼图一样把程序的逻辑还原出来。
1.2 被动分析的三根支柱
被动分析的技术体系可以拆成三块:调用关系分析、交叉引用分析、数据流分析。这三者不是孤立的,而是层层递进的关系。
调用关系分析解决的是“谁调用了谁”的问题。一个函数被哪些地方调用,它又调用了哪些函数,这构成了程序的骨架。交叉引用分析解决的是“谁引用了这个数据/函数”的问题,它帮你找到某个关键数据(比如血量、攻击力)在哪些代码里被读写。数据流分析则更进一步,解决的是“数据怎么流动、怎么变换”的问题,它让你理解一个值从输入到输出经历了哪些计算。
打个比方:调用关系是城市的道路网,交叉引用是路牌和门牌号,数据流是车流的方向和货物在沿途的加工过程。你只有把这三样都搞清楚,才能真正理解一个程序在干什么。
1.3 为什么游戏逆向特别依赖被动分析
游戏和其他软件有个本质区别:游戏对实时性和状态一致性极其敏感。你在一个网络游戏里下个断点,游戏线程暂停几百毫秒,服务器那边可能就把你踢了。你改一个内存值,客户端和服务器校验不一致,直接封号。所以游戏逆向天然就更依赖被动手段。
另外,现代游戏普遍带有反调试、反注入、代码混淆、虚拟化保护。主动分析的门槛越来越高,而被动分析虽然慢,但胜在安全、稳定、可重复。你分析出来的结论可以写成文档,下次版本更新后对照着看哪里变了,这是主动分析做不到的。
2. 调用关系分析:把程序的骨架摸清楚
2.1 调用关系的基本概念
调用关系分析,说白了就是搞清楚函数之间的调用链。在逆向工程里,我们通常用调用图来表示这种关系。调用图是一个有向图,节点是函数,边是调用关系。A调用B,就有一条从A指向B的边。
但实际分析中,调用图往往非常庞大。一个中型游戏可能有几千上万个函数,调用关系错综复杂。这时候你需要的是有重点的调用关系分析,而不是把整张图都画出来。
我的习惯是:先找到入口点(比如主循环、消息处理函数),然后沿着关键路径往下追。什么叫关键路径?就是和你分析目标相关的那些调用链。比如你要分析伤害计算,那就从“受到伤害”这个事件的处理函数开始追。
2.2 静态调用关系的提取方法
静态提取调用关系,主要靠反汇编器。IDA Pro、Ghidra、Binary Ninja这些工具都能自动生成调用图。但工具生成的结果有两个问题:一是间接调用(比如通过函数指针、虚表)识别不准,二是会把一些数据当代码误识别。
我一般会先用工具生成一个初步的调用图,然后手动修正。修正的重点是间接调用。在C++写的游戏里,虚函数调用非常常见。一个obj->virtualFunc()在汇编层面是call [eax+offset]这种形式,工具往往识别不出具体调的是哪个函数。这时候你需要结合对象的类型信息、虚表布局来推断。
实操心得:IDA的“交叉引用”功能(快捷键X)是分析调用关系最常用的工具。但要注意,它显示的是“引用”,不一定是“调用”。数据引用和代码调用要区分开。
2.3 动态调用关系的观察方法
静态分析有局限,动态观察可以补充。但这里说的动态观察不是让你下断点,而是用日志和追踪的方式。
一种做法是使用API Monitor这类工具,观察程序调用了哪些系统API。虽然看不到内部函数调用,但能看出程序在什么时候做了什么系统级操作,比如读文件、发网络包、创建线程。这些信息能帮你定位关键代码区域。
另一种做法是利用程序自身的日志。很多游戏在开发模式下会输出详细的调试日志,即使发布版关闭了日志,你也可以通过分析日志字符串的引用来定位相关代码。比如你搜索“damage”这个字符串,找到引用它的地方,往往就能找到伤害计算相关的函数。
2.4 调用关系分析的实际案例
举个具体例子。假设你要分析一个游戏的“使用物品”功能。你可以这样做:
- 在反汇编器里搜索“use item”相关的字符串,找到引用这些字符串的函数。
- 查看这个函数的交叉引用,找到调用它的地方。
- 沿着调用链往上追,找到消息处理入口。
- 再往下追,看它调用了哪些子函数,比如“检查物品数量”、“扣除物品”、“应用效果”。
- 对每个子函数重复这个过程,直到你理清整条链路。
这个过程听起来简单,但实际操作中会遇到各种问题:函数被内联了、调用被混淆了、字符串被加密了。这时候就需要结合其他分析手段,比如数据流分析来辅助定位。
3. 交叉引用分析:找到关键数据的藏身之处
3.1 交叉引用的本质
交叉引用分析回答的是一个很具体的问题:这个地址(数据或函数)在哪些地方被引用了。在逆向工程中,交叉引用是你从“已知”到“未知”的桥梁。
你知道一个字符串“HP”,想知道哪里用到了它,就查它的交叉引用。你知道一个函数被调用了,想知道谁调用的,也查交叉引用。交叉引用分析的核心价值在于建立关联:把孤立的信息点连接成网。
3.2 数据交叉引用的典型场景
在游戏逆向中,数据交叉引用最常见的场景是定位关键数据。比如你想找到玩家的血量值存在哪里,可以这样做:
- 在游戏里观察血量的变化,比如从100变成80。
- 用内存扫描工具(如Cheat Engine)搜索变化的值,缩小范围。
- 找到候选地址后,在反汇编器里查看这个地址的交叉引用。
- 分析引用这个地址的代码,确认哪个是真正的血量读写逻辑。
但这里有个问题:内存扫描属于主动分析,会改变程序状态吗?严格来说,只读扫描不改变程序状态,但某些反作弊系统会检测扫描行为。所以更安全的做法是纯静态定位:通过分析游戏的数据结构布局,推断出血量字段的偏移,然后在反汇编器里搜索访问这个偏移的代码。
3.3 函数交叉引用的分析技巧
函数交叉引用分析主要用于定位关键函数。比如你想找到“计算伤害”的函数,可以这样做:
- 找到伤害数值在内存中的地址(通过静态分析或只读扫描)。
- 查看这个地址的交叉引用,找到写入这个地址的代码。
- 写入伤害值的代码往往就在伤害计算函数附近。
- 查看这个函数的交叉引用,确认它被谁调用。
另一种方法是通过特征字符串定位。很多游戏会在代码里留下调试字符串,比如“CalculateDamage”、“ApplyDamage”等。搜索这些字符串的交叉引用,往往能直接定位到关键函数。
3.4 交叉引用分析的注意事项
交叉引用分析有几个坑要注意:
- 间接引用:通过指针数组、虚表、跳转表进行的引用,工具往往识别不出来。你需要手动分析这些数据结构。
- 代码混淆:有些游戏会对引用进行混淆,比如把直接调用改成通过计算得出的地址调用。这时候需要结合数据流分析来还原。
- 多级引用:A引用B,B引用C,C才是你想要的。你需要沿着引用链多追几层。
实操心得:在IDA里,按X查看交叉引用时,注意区分“代码引用”和“数据引用”。代码引用通常是调用或跳转,数据引用通常是读取或写入。不同类型的引用,分析思路完全不同。
4. 数据流分析:理解数据的来龙去脉
4.1 数据流分析要解决什么问题
数据流分析的核心问题是:一个数据值从哪来,到哪去,中间经历了什么变换。在游戏逆向中,这通常意味着你要追踪一个关键值(比如伤害、金币、经验)的完整计算过程。
举个例子:你发现玩家攻击后,怪物血量减少了50。你想知道这50是怎么算出来的。数据流分析就是帮你从“血量减少”这个结果,反向追踪到“伤害计算”的源头。
4.2 静态数据流分析的基本方法
静态数据流分析主要靠反汇编代码的阅读。你需要理解每条指令对数据的影响,然后沿着指令序列追踪数据的流动。
以x86汇编为例,常见的指令有:
mov dest, src:把src的值复制到destadd dest, src:把src加到dest上sub dest, src:从dest减去srcimul dest, src:dest乘以srclea dest, [addr]:把地址加载到dest
追踪数据流时,你需要维护一个“当前值”的概念。比如:
mov eax, [ebp-4] ; eax = 局部变量1 add eax, 10 ; eax = 局部变量1 + 10 mov [ebp-8], eax ; 局部变量2 = eax通过这样的追踪,你能知道局部变量2的值是局部变量1加10。如果局部变量1是攻击力,那局部变量2就是攻击力加10。
4.3 动态数据流观察方法
静态分析有时会遇到困难,比如代码被混淆、数据流经过太多层。这时候可以用动态方法辅助,但要注意不改变程序状态。
一种安全的方法是使用日志钩子。有些游戏引擎(如Unity、Unreal)提供了日志输出接口,你可以通过分析日志字符串的引用来定位关键代码,然后观察日志输出中的数值变化。
另一种方法是内存快照对比。在游戏运行过程中,定期对关键内存区域做只读快照,然后对比不同时间点的数据变化。这种方法不修改任何内存,只是读取,相对安全。但要注意,频繁的内存读取可能被某些反作弊系统检测到,所以频率要控制。
4.4 数据流分析的实际案例
假设你要分析一个游戏的“经验值获取”逻辑。你可以这样做:
- 找到经验值在内存中的地址(通过静态分析或只读扫描)。
- 查看写入这个地址的代码,找到经验值更新的位置。
- 沿着代码往上追,看经验值是怎么计算出来的。
- 你可能会发现类似这样的逻辑:
; 计算经验值 mov eax, [monster_level] ; 怪物等级 imul eax, 10 ; 等级 * 10 mov ecx, [player_level] ; 玩家等级 sub ecx, [monster_level] ; 玩家等级 - 怪物等级 add eax, ecx ; 基础经验 + 等级差 mov [exp_gain], eax ; 存储经验获取量通过这样的分析,你就能还原出经验值的计算公式。
5. 被动分析的完整工作流
5.1 从目标出发制定分析计划
被动分析最忌讳的就是“漫无目的地看”。你打开IDA,面对几千个函数,如果没有明确目标,很快就会迷失。所以第一步永远是明确你要分析什么。
比如你的目标是“理解战斗伤害计算”,那你的分析计划可能是:
- 定位伤害数值在内存中的位置。
- 找到写入伤害值的代码。
- 分析伤害计算函数的输入和输出。
- 还原伤害计算公式。
- 验证公式的正确性。
每个步骤都有明确的产出,这样你才能一步步推进。
5.2 信息收集与整理
被动分析会产生大量信息:函数地址、调用关系、数据结构、计算公式。这些信息如果不整理,很快就会乱掉。我的习惯是边分析边记录,用表格整理关键信息。
| 信息类型 | 内容 | 地址/位置 | 备注 |
|---|---|---|---|
| 函数 | 伤害计算 | 0x00401234 | 输入:攻击力、防御力 |
| 数据 | 玩家血量 | 0x00506789 | 4字节整数 |
| 字符串 | "Damage" | 0x0040A000 | 用于定位 |
这样的表格能帮你快速回顾和验证。
5.3 验证与迭代
被动分析得出的结论需要验证。验证的方法不是去改代码,而是观察程序行为是否符合你的推断。
比如你推断伤害公式是“攻击力 - 防御力”,那你就观察:当攻击力增加时,伤害是否增加;当防御力增加时,伤害是否减少。如果符合,说明你的推断大概率正确。如果不符合,就需要重新分析。
这个过程可能需要多次迭代。每次迭代都会让你对程序的理解更深一层。
6. 常见问题与排查技巧
6.1 函数识别错误怎么办
反汇编器有时会把数据当代码,或者把代码当数据。这时候你需要手动判断。一个技巧是看指令的合理性:正常的函数序言通常是push ebp; mov ebp, esp,如果看到一堆无意义的指令,很可能是数据被误识别了。
6.2 间接调用追不下去怎么办
间接调用是静态分析的难点。我的经验是:先找虚表。C++对象的虚表通常在对象内存的开头,虚表里是一组函数指针。找到虚表后,结合对象的类型信息,就能推断出每个虚函数是什么。
6.3 数据流太复杂追不动怎么办
数据流分析遇到复杂逻辑时,可以分段追踪。先追踪一段,记录中间结果,然后从中间结果继续追。不要试图一次性追到底。
6.4 如何避免触发反调试
被动分析本身不触发反调试,但如果你使用了内存扫描工具,可能会被检测到。我的建议是:优先使用静态分析,只在必要时才用只读扫描,并且控制频率。
实操心得:有些游戏会检测调试器的窗口标题、进程名。如果你用IDA,记得把窗口标题改掉。虽然被动分析不开调试器,但养成这个习惯没坏处。
7. 工具链与效率提升
7.1 主力工具选择
被动分析的主力工具是反汇编器。IDA Pro是行业标准,功能最全,但价格贵。Ghidra是免费替代品,功能也很强。Binary Ninja介于两者之间,界面更现代。
我的建议是:如果预算允许,用IDA;如果预算有限,用Ghidra。两者都能满足大部分需求。
7.2 辅助工具推荐
除了反汇编器,还有一些辅助工具能提升效率:
- Strings:提取程序中的字符串,快速定位关键代码。
- PE Tools:查看PE文件结构,了解程序的节区布局。
- API Monitor:观察程序调用的系统API。
- Process Monitor:观察程序的文件、注册表、网络操作。
这些工具都是只读的,不会修改程序状态。
7.3 脚本自动化
IDA和Ghidra都支持脚本。你可以写脚本自动提取调用关系、批量重命名函数、生成分析报告。这能大幅提升效率。
比如,你可以写一个IDA脚本,自动找出所有引用某个字符串的函数,并打印它们的地址和名称。这样你就不用一个个手动查了。
8. 被动分析的边界与局限
被动分析不是万能的。它有几个明显的局限:
- 无法观察运行时状态:你不知道某个时刻寄存器的具体值,只能推断。
- 难以处理加密和混淆:如果代码被加密,静态分析基本无从下手。
- 间接调用难以完全还原:虚表、函数指针、跳转表都会让调用关系变得模糊。
所以,被动分析通常需要和主动分析结合使用。但在游戏逆向中,被动分析应该是第一步,也是贯穿始终的基础。只有把被动分析做扎实了,主动分析才能有的放矢。
我个人在实际操作中的体会是:被动分析就像拼图,你手里有很多碎片,但不知道全貌。你需要耐心地一块块拼,每拼上一块,就对整体多一分理解。这个过程很慢,但很稳。当你最终把关键逻辑还原出来的时候,那种成就感是主动分析给不了的。