ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

游戏逆向实战指南:从动态调试到存档修改的完整路径

游戏逆向实战指南:从动态调试到存档修改的完整路径 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 [eax1C], edx这类的写入操作。点击“替换为新指针”或者用“指针扫描”功能扫描当前进程得到一个候选的指针链列表筛选出模块名偏移开头的稳定链条。这一步非常容易出现大量候选而且很多是错的。我的经验是——别指望 CE 的指针扫描一把梭真正可靠的做法是去静态分析工具里确认。动态扫描得到的指针链列表只能作为线索最终要用 x64dbg 验证。2.3 x64dbg 里验证指针链附加、断点、看汇编x64dbg 是 32 位和 64 位程序调试的利器动态调试阶段的“最后一公里”几乎都得在它里面完成。步骤大致是打开 x64dbg附加到游戏进程注意有些游戏带反调试附加之后可能立刻崩溃后面专门讲。在 CE 里记录下金币地址在 x64dbg 里对那个地址下一个硬件写入断点右键 → 断点 → 硬件写入 → DWORD/QWORD。回游戏再触发一次金币变动断点命中调试器会停在那条写入指令附近。观察当前寄存器的值推算哪个寄存器或哪段内存是指针来源。停到模块代码段地址开头一般是game.exexxxxx而不是7xxxxxxxxxxx确认是在主模块内部。沿着mov reg, [regoffset]一类指令向上追找到最顶层的静态地址。这个过程中你会频繁看到类似这样的指令mov eax, dword ptr [ebx8] mov ecx, dword ptr [eax10] mov edx, dword ptr [ecx1C] mov [edx], 0x270F这就是一条典型的指针链解引用过程。从game.exe2A3F10这样的静态地址出发依次加偏移 8、10、1C命中的才是数据。把基址和偏移记下来你就得到了一个永久可用的“钥匙”。我踩过的一个坑硬件断点在 64 位下数量有限通常只有 4 个所以断点别乱下我一般是先确认地址没问题再下一个硬件写入断点其他情况尽量用普通的内存断点或干脆直接在指令地址下断点。2.4 写一个独立修改器OD、CE 之外的元素找到指针链之后想让修改“自动化”或打包给别人用就得写一个独立修改器。最常见的方案是 C 配合 Windows APIOpenProcess获取进程句柄ReadProcessMemory/WriteProcessMemory读写内存按“模块基址 偏移链”逐级读取得到最终地址再写入数值伪代码逻辑大致这样DWORD64 GetPointerByOffsets(HANDLE proc, DWORD64 base, std::vectorDWORD64 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或CreateToolhelp32SnapshotModule32First。有这个基础之后你就可以把任何“CE 里手动找到的结论”固化成工具。这是很多人卡住的第二道坎——CE 用得很溜一旦离开 CE 就不会写代码。我的建议是既然器要用到就别怕这点代码量一个最简单的修改器核心代码不到 50 行比想象中好写。3. 静态分析与代码还原IDA 里读懂游戏逻辑3.1 找关键函数的敲门砖字符串、导入表和交叉引用动态调试能回答“这个数值是怎么访问的”但很多问题必须靠静态分析才能回答——比如这个数值为什么会产生游戏的随机数种子是什么存档的加密算法是哪种我一般先用 IDA 打开游戏主模块等待自动分析完成之后第一件事不是看汇编而是按ShiftF12打开字符串窗口搜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 30MEM0接着 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可以直接用 Pythonimport 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 解包存档”的完整路径铺开了。回头再看那句“我全都要”——游戏逆向确实没有一个单一制高点它更像一张网内存、指令、文件、算法、脚本、工程化每个方向都摸到一点碰到什么问题都能接住这大概就是实战中最有用的状态。这里面最值钱的能力未必是最深的那门绝技而是你面对一个从来没有见过的东西时知道怎么下手、怎么拆解、怎么验证你的假设——这套方法论可以在游戏逆向这行练一辈子做任何技术也都用得上。
返回列表