1. 先把“全都要”拆开:游戏逆向的三块硬骨头
“游戏逆向实战:我全都要”——这个标题我当时写下来的时候,纯粹是给自己打气。因为游戏逆向这个东西,真正做进去你会发现它根本不是一门技术,而是好几门技术拧在一起:你得懂汇编、懂内存管理、懂 PE 结构、懂反调试、懂加密算法,还得有耐心跟一个数值死磕到凌晨。更崩溃的是,网上正经教程要么讲得太浅(教你怎么用 CE 改金币),要么直接跳到 IDA 逆某个函数,中间那段“怎么从改数值到定位关键代码”的跨度,没有人好好讲。
所以这篇文章,我就按“我全都要”的贪心思路来拆。不是说我全都精通,而是我把游戏逆向通常要碰的东西都摸了一遍,整理成一套能复现的路径,你拿来就能跟着走。
先说结论——游戏逆向实战里,你真正要啃的是三块:
| 方向 | 核心工具 | 解决什么问题 |
|---|---|---|
| 动态调试与内存分析 | Cheat Engine、x64dbg | 定位数值、追踪指令、找指针链 |
| 静态分析与代码还原 | IDA Pro、Ghidra | 理解函数逻辑、识别加密算法 |
| 文件与网络协议逆向 | Hex Editor、Wireshark、Python | 解析存档、处理加密和校验 |
很多人学游戏逆向是一上来就开 IDA 看汇编,然后被绕晕。我自己的经验是:千万别从静态分析入手。游戏逆向天然的起点是“一个数值怎么在内存里变”,从这儿切入,你能亲眼看到内存、指令、模块、指针这些东西是怎么协同工作的,比干啃汇编生动得多。
另外一个所有新手都要先想清楚的问题——你为什么要学游戏逆向?这里我直接劝退一批人:如果你学这个是为了做联网游戏的外挂,或者绕过正版验证去白嫖别人的付费游戏,那接下来的内容你未必该看,一方面法律风险实实在在,另一方面那些方向的技术路径跟咱们聊的也完全不同。我下面会默认你做的是合法场景:修复自己买过的单机游戏存档、做老游戏的汉化或者修改器、打 CTF 逆向题、研究某款游戏的反外挂机制用于漏洞挖掘和防护研究。这些场景需要的能力,跟做坏事的路径在前期是重合的,但到了“写代码利用它”的环节,你该知道线划在哪里。
“我全都要”的真正含义,是把上面表格里三条支线都摸过一遍——哪怕每个只做到 60 分,你也能在日后碰到具体问题时知道该往哪个方向深挖。游戏逆向最怕的不是不会,而是不知道用什么工具、看什么代码、搜什么问题。
下面我按自己实际走过的路线,把这三块硬骨头逐个拆开讲。
2. 动态调试与内存分析:从改数值到找指针链
2.1 用 Cheat Engine 锁定第一个数值,但别止步于此
最入门的一课:打开一个单机游戏,搜血量或金币,改掉它。这个用 Cheat Engine(下称 CE)十分钟就能学会。但我发现太多人卡在这一步就以为自己会逆向,但其实连门都没进。
CE 的正经用法是把它当成一个“内存显微镜”。它的价值不只是搜索数值,而是能直接看到目标进程的整个地址空间,能够下内存断点,能查看这个地址被哪些指令访问和写入,甚至能直接看反汇编代码。
实操走一遍,以我改一个老单机的金币数为例:
- 先启动游戏,用 CE 附加到进程。
- 初次扫描当前金币值(比如 500 ),类型选 4 字节。
- 回游戏里花掉一点钱(比如变成 450 ),切回 CE 点“再次扫描”,输入 450。
- 如此反复几次,地址列表基本就能收缩到一个或少数几个地址。
- 双击剩下那个地址,把它加到下方的地址列表,双击数值改成 9999,回游戏看是否生效。
这套流程大多数人都熟。关键是下一步——很多人会问:为什么我改成功了,重启游戏之后地址变了、改不了了?
原因很简单:你锁定的那个地址是游戏在内存中动态分配的,每次启动运行时,地址可能都不同。要想“一次修改,永久生效”,你就得找到这个动态地址与某个固定基址之间的关系,也就是指针链。
2.2 指针链:为什么 CE 能找到地址但重启就失效
游戏程序中,动态数据通常不会直接用绝对地址访问,而是通过“指针→指针→……→实际数据”这种链式引用来解的。理解这个概念,你可以把游戏进程想象成一个储物仓库:
- 模块基址(比如 game.exe 的基址)像仓库墙壁上抹不掉的那块固定记号,每次开工它的位置基本不变(Windows 有 ASLR 随机化,但某些老游戏没开,或我们可以通过“模块基址+偏移”的方式绕开)。
- 全局指针存在固定位置(数据段),像一个固定柜子里放着一张纸条,纸条上写着下一层柜子的编号。
- 顺着纸条一层层找,最后那个柜子里装的才是你的金币数值。
所以你要做的事情,就是逆向出这一层层纸条(偏移)和起始柜子(基址)。
CE 自带一个“指针扫描”功能。操作方式大致是:
- 右键刚才找到的地址,选择“找出是什么改写了这个地址”,然后在游戏里让金币数变化。
- CE 会列出几条汇编指令,一般形如
mov [eax+1C], edx这类的写入操作。 - 点击“替换为新指针”,或者用“指针扫描”功能扫描当前进程,得到一个候选的指针链列表,筛选出
模块名+偏移开头的稳定链条。
这一步非常容易出现大量候选,而且很多是错的。我的经验是——别指望 CE 的指针扫描一把梭,真正可靠的做法是去静态分析工具里确认。动态扫描得到的指针链列表只能作为线索,最终要用 x64dbg 验证。
2.3 x64dbg 里验证指针链:附加、断点、看汇编
x64dbg 是 32 位和 64 位程序调试的利器,动态调试阶段的“最后一公里”几乎都得在它里面完成。步骤大致是:
- 打开 x64dbg,附加到游戏进程(注意有些游戏带反调试,附加之后可能立刻崩溃,后面专门讲)。
- 在 CE 里记录下金币地址,在 x64dbg 里对那个地址下一个硬件写入断点(右键 → 断点 → 硬件写入 → DWORD/QWORD)。
- 回游戏再触发一次金币变动,断点命中,调试器会停在那条写入指令附近。
- 观察当前寄存器的值,推算哪个寄存器或哪段内存是指针来源。
- 停到模块代码段(地址开头一般是
game.exe+xxxxx而不是7xxxxxxxxxxx),确认是在主模块内部。 - 沿着
mov reg, [reg+offset]一类指令向上追,找到最顶层的静态地址。
这个过程中你会频繁看到类似这样的指令:
mov eax, dword ptr [ebx+8] mov ecx, dword ptr [eax+10] mov edx, dword ptr [ecx+1C] mov [edx], 0x270F这就是一条典型的指针链解引用过程。从game.exe+2A3F10这样的静态地址出发,依次加偏移 8、10、1C,命中的才是数据。把基址和偏移记下来,你就得到了一个永久可用的“钥匙”。
我踩过的一个坑:硬件断点在 64 位下数量有限(通常只有 4 个),所以断点别乱下,我一般是先确认地址没问题,再下一个硬件写入断点,其他情况尽量用普通的内存断点或干脆直接在指令地址下断点。
2.4 写一个独立修改器:OD、CE 之外的元素
找到指针链之后,想让修改“自动化”或打包给别人用,就得写一个独立修改器。最常见的方案是 C++ 配合 Windows API:
OpenProcess获取进程句柄ReadProcessMemory/WriteProcessMemory读写内存- 按“模块基址 + 偏移链”逐级读取,得到最终地址,再写入数值
伪代码逻辑大致这样:
DWORD64 GetPointerByOffsets(HANDLE proc, DWORD64 base, std::vector<DWORD64> offsets) { DWORD64 addr = base; for (size_t i = 0; i < offsets.size() - 1; i++) { ReadProcessMemory(proc, (LPCVOID)(addr + offsets[i]), &addr, sizeof(addr), NULL); } return addr + offsets.back(); }当然实际项目里还要处理模块基址获取(EnumProcessModulesEx或CreateToolhelp32Snapshot+Module32First)。有这个基础之后,你就可以把任何“CE 里手动找到的结论”固化成工具。这是很多人卡住的第二道坎——CE 用得很溜,一旦离开 CE 就不会写代码。我的建议是:既然器要用到,就别怕这点代码量,一个最简单的修改器核心代码不到 50 行,比想象中好写。
3. 静态分析与代码还原:IDA 里读懂游戏逻辑
3.1 找关键函数的敲门砖:字符串、导入表和交叉引用
动态调试能回答“这个数值是怎么访问的”,但很多问题必须靠静态分析才能回答——比如:这个数值为什么会产生?游戏的随机数种子是什么?存档的加密算法是哪种?
我一般先用 IDA 打开游戏主模块,等待自动分析完成之后,第一件事不是看汇编,而是按Shift+F12打开字符串窗口,搜coin、gold、save、money、score这类关键词。字符串在逆向里是极其可靠的“地标”,任何程序要跟用户打交道,都得通过字符串输出,你要找的逻辑通常就挂在这些字符串附近。
举个例子。之前逆向一款老游戏的存档逻辑时,我直接在字符串窗口搜.sav,搜到了类似%s\\save\\game_%d.sav的格式化字符串,交叉引用过去,就找到了保存和读取存档的函数,再顺着这个函数往上翻调用者,就能理清整个存档流程。这比在十万行汇编里瞎找快得多。
导入表(Imports)也是重要线索:如果看到一个游戏导入了CryptEncrypt、CryptDecrypt之类的 Windows 加密 API,那几乎可以断定它的数据加密依赖了系统 CryptoAPI;如果导入的是XXTEA的自定义实现,那你就要在代码里找那个算法的特征常数。
3.2 从游戏行为反推逻辑结构
静态分析最容易犯的错是试图“从头到尾完整读完一个大函数”,这在大型游戏里根本不可能。正确姿势是从“行为”反推“逻辑”。
比如你想搞清楚一个存档文件的校验和是怎么算的。存档修改之后游戏提示“存档损坏”,这时候别急着去 IDA 里翻函数,先在 Hex 编辑器里观察一下原始存档的结构:
- 开头是不是有固定的文件头(比如
PK、PAS)? - 文件末尾是不是有一小段看起来像校验和的数据?
- 整个文件是不是除了头部之外看起来完全“随机”(如果是,可能整体加密或压缩了)?
有了这些观察,再去 IDA 里定位“读取存档的函数”,在读取完成之后、使用数据之前,往往有一段校验代码,你直接在读取函数的下游看汇编,很快会发现一个类似的模式:读取整块数据 → 对数据做某个计算(累加、异或、CRC 查找表) → 与文件中某处存储的值比较。
我实测里最常见的还是 CRC32 变体和字节累加校验,它们代码量小、特征明显。用 IDA 打开函数列表,按代码长度排序,短的普通函数很可能是校验函数,点进去看如果有一大串xor、shr、and 0x80000000之类的指令,八成就是某个 CRC 或者简单哈希。
3.3 识别加密算法的“指纹”
如果你遇到的是真正的加密数据,那就要识别算法。这同样靠特征:
- AES:查找 S 盒常数的重复模式,或者看是否有 10/12/14 轮结构。
- XXTEA:特征常数
0x9E3779B9(黄金分割率相关)出现,代码有 delta 位移和 32 位无符号循环。 - RC4:初始化时有一个 256 长度的置换表填充循环,特征是
xor和swap成对出现。
用 Ghidra 也行,反编译出来的伪代码比 IDA 的汇编对新手更友好。我自己的习惯是 IDA 看汇编,Ghidra 补伪代码,两个交叉对照,定位效率会高一些。
还是提醒一句:识别加密算法并理解它是一回事,用于绕过正版授权验证是另一回事。做存档修复或者数据研究,你修改的是本地文件,通常没什么问题;但如果要绕过某个商业化软件的授权、DRM,那就是另一码事。这个边界要分清楚。
4. 存档文件逆向:从内存走向磁盘
4.1 文件结构解剖:没有文档也要硬拆
“游戏逆向”如果是持续追踪游戏数据,最终都会遇到磁盘上的文件——存档、资源包、配置文件。内存里的数据分析完了,你的修改能不能固化下来,就看文件这一层了。
解析未知文件格式的核心思路是“猜测 + 验证”。我一般这么干:
- 先备份原存档,用 Hex 编辑器对比“新建存档”和“玩了一会儿之后存档”的差异。差异出现的位置就是要关注的核心结构。
- 找出文件头。很多游戏用 4 个字符的魔数标识格式(比如
PASV、DPK),魔数后面的几个字节通常是版本号、文件大小、数据段偏移之类。 - 看数字存储序。多字节整数有 Big-Endian 和 Little-Endian 之分,游戏在 Windows 上跑通常是小端序,但跨平台游戏可能反过来,可以从数值的大小和增长方式反推。
比如我处理过一个存档,前 16 字节是头部:4D 45 4D 30(MEM0),接着 2 字节版本号01 00,然后是 4 字节存档长度。看起来很简单,但实际文件里,后面的数据是被压缩过的,头部信息根本不够用——那就必须往前翻,找压缩流的边界。
4.2 定位压缩/解密函数:静态分析和动态调试结合
数据看起来像乱码,基本可以断定要么压缩要么加密了。这一步我的做法是动态调试为主:
- 在游戏里点“保存存档”按钮。
- x64dbg 对
WriteFile这个 Windows API 下断点。 - 断点命中后,看传入的缓冲区(
lpBuffer),如果缓冲区里的数据已经是明文格式化结构,说明数据在调用WriteFile之前已经被序列化格式化好了;如果缓冲区里就是乱码,说明加密/压缩发生在更早的调用链中。 - 顺着
WriteFile的调用栈往上翻,就能找到序列化和加密函数。
很多时候你会发现游戏只是用 zlib 压缩——zlib 压缩流有固定的头78 9C、78 DA等,Hex 编辑器里一眼就能认出。认出之后直接用 Python 的zlib模块把数据段解出来,修改内存里的数据再压缩回去,同理。
如果发现不是常见压缩格式,而是一个自定义的简单 XOR 加密,那就更简单了。XOR 加密的规律是:同样的明文位 XOR 同一个密钥位会得到同样的密文。你拿两份长度不同的存档对比,密文差异较大的区域基本就是明文数据,密钥可以通过“已知明文 + 已知密文 = 密钥”推导出来(XOR 的性质)。
这是个典型的“已知明文攻击”场景,前提是你能猜出存档某处对应的明文内容。大多数老游戏的存档加密强度都停留在“防君子不防小人”的层面,用这种方式就能解。真正用 AES 加密存档的游戏也有,但要处理的是密钥存储问题,密钥一般藏在程序本身里,这就又要回到静态分析去找密钥常量或密钥生成逻辑。
4.3 校验和修复:存档修改的最后一步
改完数据不一定能生效,很多游戏会校验存档的完整性——通常是 CRC32 或者一个简单的累加校验。改完数据校验值对不上,游戏就提示存档损坏。
处理思路:
- 从存档文件中找到校验值所在的字段(一般在头部或尾部,长度 2/4/8 字节)。
- 研究校验函数的算法(参考 3.2 的做法,在 IDA 里顺着读取函数找)。
- 算出修改后的新校验值,回填。
写脚本时注意,校验算法的作用范围有时不包括校验值本身所在的那几字节,有时又包含,不同游戏实现差异很大。最稳妥的方法还是拿到校验算法之后,自己写的校验函数和游戏的行为做交叉验证:先用脚本对未修改的存档算一次,跟文件里存的校验值对上了,再对修改后的存档计算并回填。
如果你判断算法是 CRC32,可以直接用 Python:
import zlib new_data = open('modified.sav', 'rb').read() crc = zlib.crc32(new_data) & 0xFFFFFFFF # 根据游戏的字节序和存储位置回填实际案例中,我遇到过一个游戏用的是自定义的“把全部数据当成有符号整数逐个累加,取低 32 位”的校验方式,跟标准 CRC32 完全不同。这告诉我们一个道理:永远不要假设,先去确认校验算法的真实代码。
5. 反调试与自校验:实战中最容易卡壳的环节
5.1 附加反调试进程导致立刻崩溃的排查
新手用 x64dbg 附加一个游戏,很多时候发现“附加之后程序立刻崩溃”或者“附加之后没法下断点”。这不是操作问题,是游戏里做了反调试检测。
最常见的反调试手法:
IsDebuggerPresent():检查进程环境块(PEB)的BeingDebugged标志位。NtQueryInformationProcess:通过系统调用查询调试端口。- 时间检测:在被下断点的指令处测量执行耗时,断点会显著拖慢执行速度。
- 自身 CRC 校验:程序运行中周期性把自己代码段读出来算哈希,跟原始值比对,被修改就自杀。
对我这种实战向的人来说,处理反调试的思路其实很朴素:
- 先通关字符串检查:在 IDA 里搜
IsDebuggerPresent、CheckRemoteDebuggerPresent的导入和调用点,碰到就 NOP 掉调用本身。 - 用 x64dbg 的“隐藏调试器”插件(比如 ScyllaHide),它通过钩住相关 API 并返回伪值,可以绕过大部分用户态反调试。
- 遇到更狠的内核态反调试,基本就别用动态调试了,静下心来用纯静态分析,或者干脆绕开游戏运行时的检测——比如操作存档文件本身,而不是实时改内存。
这里又回到“为什么存档逆向也值得学”的理由:当动态调试被反调试机制挡住时,存档文件逆向你是不需要进程在跑的,从磁盘数据下手往往能绕开所有运行时保护。
5.2 崩溃之前的快速定位:把修改的“现场”留住
还有一种情况是:你改了某个值,游戏没崩溃;但保存的时候崩溃了,或者存档读不出来。这种时候最容易一头雾水。
我的建议是给游戏做“最小化修改实验”:一次只改一个字节/一个字段,保存并测试。改多了之后出了问题,你根本不知道是哪个改动导致崩溃。记住每次修改前后文件或内存的完整快照——用 CE 可以直接保存内存快照(Memory View→File→Save snapshot),用 Hex 编辑器保存存档修改前后的对比版本,配合 Python 脚本diff一下,就能精确知道哪几个字节被改动。
我在处理某个游戏的存档时,修改等级数据从来不出问题,但一改背包物品就导致游戏存档读取失败,后来对比发现那个游戏的物品列表长度是固定编码,不是按数量动态增长的,我改的数据超出了物品槽位上界,游戏在创建对象时把越界索引当成了对象指针,当然解析失败。这个 bug 的排查过程全靠最小化修改实验定位,不然你面对一整片乱码根本无从下手。
6. 跨平台游戏的额外套路:设备模拟器与脚本化提取
6.1 移动端游戏逆向的差异点
现在很多游戏是移动端跨平台过来的,玩的是 Unity 或 Unreal 引擎的游戏。引擎游戏逆向跟原生 Win32 程序差异很大,核心套路也不同。
Unity 游戏的逻辑大量集中在Assembly-CSharp.dll这个托管程序集里,实现本质是 C#,用 dnSpy 之类的 .NET 反编译器直接打开就能看到近乎源码级别的代码。很多游戏所谓的“加密”只是对 DLL 做了一层简单的混淆,脱壳思路是先找到加壳函数、dump 内存再用修正工具修复导入表。
Unreal 引擎游戏则要看蓝图编译出来的字节码和UObject/UFunction的结构,常用工具是 FModel 之类的资源查看器,分析的是.pak文件,又回到“文件逆向”这条线。
跨平台的另一个大坑是设备差异。手游存到本地的存档文件位置在不同的安卓版本上访问权限差异非常大;模拟器、云手机的存档路径又跟真机不同。只靠游戏逆向技术不够,你还得懂一些系统工具链。
这方面我给新手一个非常实用的建议:定期用adb backup或者文件管理器勾选“备份到本地”,把存档抽出来备份。很多人以为存档文件在本地,游戏卸载就没了,实际上很多游戏存档在云端,本地文件只是一份缓存——你对缓存的任何修改,重启后都会被云端覆盖。要先搞清楚架构再动手,不然就会陷入“我明明改了,为什么一打开游戏又变回去了”这种迷惑。
6.2 用脚本批量提取资源:把逆向当数据处理
跨平台和引擎游戏的资源包往往动辄几千个文件,手动逆向不现实。我用 Python 写过一个解包 toolkit,核心逻辑是:用文件头识别资源格式(PNG、UnityFS、Wwise 的 WEM 等),按偏移和长度批量切出数据。
这类脚本对游戏逆向来说是个很好的杠杆:你只需要掌握一个格式的解析逻辑,几千个文件一晚上就能处理完。这也是“我全都要”的另一个侧面——除了汇编、反汇编之外,脚本语言几乎是游戏逆向的刚需。我见过太多人卡在“手动修改一个存档能行,但面对几千个资源文件就崩溃”,其实用 Python 写两个函数就能解决的事。
7. 我踩过的几个坑和让你少走弯路的建议
最后分享一些没法归类到任何章节但极其重要的经验。
坑 1:没有做系统备份就开始改。有一次我改一个大型游戏的存档,改完发现数据直接损坏,但原文件已经被覆盖,我又没有额外备份。所以现在任何修改,我都先复制一份原文件放在旁边的_backup目录里,或者用 Git 来跟踪存档文件的历史状态。别嫌麻烦,崩溃一次你就懂了。
坑 2:重复劳动,没有把过程“自动化”。刚开始我对每个数值都用 CE 手动搜、手动找地址、手动改,做一个功能要反复操作半小时。后来我学会把 CE 的“脚本引擎”(Lua)和 Python 结合起来,把“搜索→过滤→写入”的流程固化成脚本。用 CE 的 Lua 脚本可以直接调用底层的扫描函数,比手动点快一个量级。工具的价值在于把确定的流程变成一次点击,否则你只是在重复劳动。
坑 3:不写笔记。游戏逆向涉及大量的临时结论:某个偏移是多少、某个函数的作用、某段数据的含义——如果你不记录,两周之后回来看,跟没见过一模一样。我现在每个项目建一个 Markdown 笔记文件,记录每个发现的日期、上下文、证据链,跟以前读书时的实验记录本差不多。客户或者老师傅问起,我能直接翻出当时的笔记,思路清清楚楚。
坑 4:一上来就逆向最新的 3A 大作。这几年新游戏的反作弊、加密手段都是商业级水准,加上关键数据全程在服务端计算,本地逆向几乎看不到什么有效信息。练手一定要选老游戏、单机游戏、开源引擎做的免费游戏。我自己练出了手感,是因为把《植物大战僵尸》《宝石迷阵》这类老游戏翻了无数遍,它们结构简单、没有反调试、数据都在本地,足够练习所有的基本功。
写到这里,基本把我从“CE 改金币”到“Python 解包存档”的完整路径铺开了。回头再看那句“我全都要”——游戏逆向确实没有一个单一制高点,它更像一张网:内存、指令、文件、算法、脚本、工程化,每个方向都摸到一点,碰到什么问题都能接住,这大概就是实战中最有用的状态。这里面最值钱的能力未必是最深的那门绝技,而是你面对一个从来没有见过的东西时,知道怎么下手、怎么拆解、怎么验证你的假设——这套方法论可以在游戏逆向这行练一辈子,做任何技术也都用得上。