游戏存档封装逆向:异或、校验与压缩三层拆解实战

发布时间:2026/10/13 13:02:50
游戏存档封装逆向:异或、校验与压缩三层拆解实战 1. 存档封装的三层结构拆解1.1 为什么存档文件总是一团乱码第一次拿到一个存档文件用十六进制编辑器打开满屏都是看不懂的字节很多人第一反应是“加密了”。其实大多数情况下存档封装并没有那么神秘它通常由三层结构叠在一起最外层是压缩中间层是校验最内层才是异或混淆。这三层的顺序不同拆解的策略也完全不同。我见过不少存档格式有的先压缩再异或有的先异或再压缩还有的在校验值上做手脚。判断顺序的方法很简单看文件头几个字节的熵值。如果开头就是高熵数据大概率是压缩层在最外面如果开头有明显的规律性重复那可能是异或层在外面。这个判断直接决定了你后面从哪里下手。1.2 沿字符串引用还原的核心思路“沿字符串引用还原”这个说法听起来有点绕说白了就是不要一上来就暴力破解而是从文件里能找到的明文字符串入手顺着引用关系一层层往回推。比如存档里通常会有玩家名、关卡名、时间戳这些字符串它们即使经过异或和压缩在某些位置仍然会留下痕迹。我的做法是先用strings命令或者十六进制编辑器的字符串搜索功能把文件里所有可读的 ASCII 和 Unicode 字符串列出来。然后观察这些字符串的位置和上下文找到它们被引用的地方——比如某个偏移量后面跟着一个长度值或者某个字符串前面有一个指针。这些引用关系就是还原封装结构的线索。注意不要跳过字符串搜索这一步直接去猜异或密钥。我踩过的坑就是一开始就假设密钥是单字节结果试了几百个值都不对后来发现密钥是从某个字符串的哈希派生出来的。1.3 三层结构的常见排列组合存档封装的层序不是固定的但根据我处理过的案例大致有以下几种常见组合层序外层中间层内层典型特征A压缩校验异或文件头高熵尾部有固定长度校验值B异或压缩校验文件头有规律解异或后能看到压缩魔数C校验异或压缩文件头是校验值后面跟着异或后的压缩数据D异或校验压缩解异或后能看到校验值再后面是压缩数据判断层序的实操方法是先尝试对文件头做单字节异或扫描看能否出现常见的压缩魔数如1F 8B、78 9C、50 4B等。如果出现了说明异或层在外面。如果没有就尝试直接解压缩看是否报错。如果解压缩报错但错误信息提示校验失败那校验层就在压缩层外面。2. 异或层的拆解与密钥推导2.1 单字节异或的快速识别单字节异或是最简单的混淆方式但也是最容易被误判的。很多人看到文件里有重复的字节模式就以为是单字节异或其实可能是多字节密钥或者压缩后的残留。识别单字节异或的关键是看字节频率分布异或后的数据其字节频率分布会整体平移但形状不变。具体操作用 Python 写个小脚本统计文件前 256 字节的字节频率然后和标准 ASCII 文本的频率分布做对比。如果形状相似但整体偏移了一个固定值那这个偏移量就是异或密钥。我实测下来这种方法对文本类存档特别有效准确率能到九成以上。from collections import Counter def guess_single_byte_xor(data): freq Counter(data[:256]) # 假设最常见的是空格 0x20 或 0x00 most_common freq.most_common(1)[0][0] for candidate in [0x20, 0x00, 0x0A]: key most_common ^ candidate decoded bytes(b ^ key for b in data[:64]) if all(32 c 127 or c in (9, 10, 13) for c in decoded): return key return None2.2 多字节异或密钥的推导技巧多字节异或密钥的推导要复杂一些但核心思路不变利用已知明文片段反推密钥。存档文件里通常有固定的文件头或者版本号字符串比如SAVE、V1.0之类的。把这些已知明文和密文对应位置异或就能得到密钥片段。如果已知明文不够长可以用重合指数法先猜密钥长度。具体做法是把密文按可能的密钥长度分组计算每组之间的重合指数。如果某个长度下重合指数明显偏高那这个长度很可能就是密钥长度。确定长度后再对每一列单独做单字节异或分析。实操心得我通常先用 1 到 32 的所有长度试一遍重合指数把得分最高的前三个长度记下来然后分别尝试推导密钥。很多时候密钥长度是 4 或 8 的倍数因为开发者喜欢用整数对齐。2.3 密钥派生自字符串的常见模式有些存档的异或密钥不是固定的而是从某个字符串派生出来的比如玩家名、设备 ID 或者时间戳。这种情况下你需要先找到那个字符串然后用相同的派生算法算出密钥。常见的派生方式有直接取字符串的字节密钥就是字符串本身循环使用。哈希后取模比如 MD5 后取前 16 字节再对 256 取模得到单字节密钥。简单累加把字符串所有字节相加取低 8 位作为密钥。我遇到过一个案例密钥是玩家名的每个字符的 ASCII 值异或后的结果。这种就需要你先从存档里找到玩家名然后手动算一遍。所以前面说的“沿字符串引用还原”在这里就派上用场了——字符串不仅是线索有时候直接就是密钥材料。3. 校验层的识别与绕过3.1 常见校验算法速查校验层的目的是防止存档被随意修改常见的算法有 CRC32、Adler32、MD5、SHA1 以及简单的累加和。识别校验算法的方法是看校验值的长度和位置。CRC32 和 Adler32 都是 4 字节MD5 是 16 字节SHA1 是 20 字节。位置通常在文件头或文件尾。算法长度特征识别方法累加和1/2/4 字节值随数据变化缓慢修改一个字节看校验值变化量CRC324 字节雪崩效应明显修改一个字节校验值完全变化Adler324 字节两个 16 位分量高 16 位和低 16 位变化规律不同MD516 字节固定长度高熵长度匹配即可初步判断SHA120 字节固定长度高熵长度匹配即可初步判断3.2 校验值的定位与验证定位校验值的位置有个笨办法但很有效把文件复制一份修改中间某个字节然后对比两份文件的差异。如果只有一处差异那处很可能就是校验值。如果有多处差异说明校验值可能分散在多个位置或者校验算法涉及多个字段。验证校验算法是否猜对的方法是用你猜测的算法重新计算校验值和文件里的值对比。如果一致恭喜你猜对了。如果不一致可能是算法变种比如 CRC32 的初始值或多项式不同。这时候可以尝试常见的变种参数或者用暴力搜索的方式确定初始值和异或输出值。import zlib def verify_crc32(data, expected): # 标准 CRC32 if zlib.crc32(data) 0xFFFFFFFF expected: return standard # 初始值为 0 的变种 crc 0 for b in data: crc ^ b for _ in range(8): crc (crc 1) ^ 0xEDB88320 if crc 1 else crc 1 if crc 0xFFFFFFFF expected: return init_zero return None3.3 绕过校验的实用策略如果你只是想把存档改掉不打算完全还原封装格式那绕过校验是最快的路径。常见策略有直接删除校验字段如果游戏读取存档时不做严格校验删掉校验值可能就能通过。固定校验值把校验值改成某个已知能通过的值比如全零或全 F。重算校验值如果你已经确定了算法改完数据后重新算一遍填回去。注意有些游戏会把校验值分散在多个位置或者用多个校验算法交叉验证。这种情况下只改一处是不够的。我建议先用二分法定位所有校验相关的位置把文件分成两半分别修改看哪一半会导致校验失败然后继续细分。4. 压缩层的解包与重打包4.1 压缩格式的快速识别压缩层的识别相对简单因为压缩格式通常有固定的魔数。常见的魔数有1F 8BGZIP78 01、78 9C、78 DAZLIB不同压缩级别50 4B 03 04ZIP42 5A 68BZIP2FD 37 7A 58 5A 00XZ28 B5 2F FDZstandard如果文件头没有这些魔数但数据熵值很高可能是原始 DEFLATE 流没有 ZLIB 头。这时候可以尝试用zlib.decompressobj(-15)来解负的 windowBits 表示无头 DEFLATE。4.2 解压后的数据布局分析解压之后你会得到一层新的数据。这时候不要急着往下拆先观察数据的布局。通常会有以下几种情况纯文本直接可读可能是 JSON、XML 或自定义的键值对格式。二进制结构有固定的字段偏移比如前 4 字节是版本号接着 4 字节是数据长度然后是数据体。嵌套压缩解压后还是压缩数据说明有多层压缩。我处理过一个存档解压后得到的是一个自定义的二进制格式里面有一个字段是“下一个数据块的偏移量”。这种就需要你顺着偏移量链一路跟下去把每个块都提取出来。4.3 重打包时的对齐与填充如果你修改了数据重打包时需要注意对齐和填充。很多游戏在读取压缩数据时会假设数据是按某个边界对齐的比如 4 字节或 16 字节。如果你压缩后的数据长度不是对齐边界的整数倍可能需要在末尾填充零字节。另外压缩级别也会影响输出。有些游戏对压缩后的数据长度有校验如果你用不同的压缩级别即使解压后内容一样压缩后的字节也可能不同导致校验失败。我的经验是尽量用和原文件相同的压缩级别。如果不确定可以尝试所有级别看哪个的输出长度和原文件最接近。import zlib def repack(data, original_compressed): # 尝试所有压缩级别找最接近原始长度的 best None best_diff float(inf) for level in range(0, 10): compressed zlib.compress(data, level) diff abs(len(compressed) - len(original_compressed)) if diff best_diff: best_diff diff best compressed return best5. 完整还原流程与实操案例5.1 从零开始拆解一个存档文件假设你拿到一个名为save.dat的文件大小 2KB。第一步用十六进制编辑器打开观察文件头。假设前 16 字节是A3 5F 2B 9C ...看起来是高熵数据。这时候先别急着下结论用strings命令搜一下可读字符串。如果搜到了player_name这样的字符串记下它的偏移量。然后看这个偏移量附近有没有长度字段或指针。通常字符串前面会有 2 或 4 字节的长度值。顺着这个长度值往前推可能会找到一个数据块的起始位置。接下来尝试对文件头做单字节异或扫描。如果扫描到某个值时出现了78 9C那说明异或密钥就是这个值而且异或层在压缩层外面。先用这个密钥解异或然后解压缩得到中间层数据。中间层数据可能是校验值加实际数据。校验值通常在开头或结尾。用前面说的修改对比法定位校验值确定算法后就可以绕过或重算了。5.2 实操案例一个模拟存档的完整拆解我构造了一个模拟存档来演示完整流程。原始数据是一个 JSON 字符串经过以下步骤封装用 ZLIB 压缩 JSON 数据。计算压缩数据的 CRC32附在末尾。对整个数据压缩数据 CRC32做单字节异或密钥为 0x5A。拆解过程如下import zlib import struct # 读取文件 with open(simulated_save.dat, rb) as f: raw f.read() # 第一步解异或 key 0x5A deobfuscated bytes(b ^ key for b in raw) # 第二步分离 CRC32 和压缩数据 compressed deobfuscated[:-4] expected_crc struct.unpack(I, deobfuscated[-4:])[0] # 验证 CRC32 actual_crc zlib.crc32(compressed) 0xFFFFFFFF print(fCRC32 匹配: {actual_crc expected_crc}) # 第三步解压缩 json_data zlib.decompress(compressed) print(json_data.decode(utf-8))这个案例的关键点是异或层在最外面校验层在中间压缩层在最里面。拆解顺序和封装顺序正好相反。5.3 还原后的数据验证与修改还原出 JSON 数据后你可以随意修改然后按相反顺序重新封装压缩修改后的 JSON。计算新压缩数据的 CRC32。拼接压缩数据和 CRC32。用相同密钥异或。重打包后把文件放回游戏目录启动游戏验证是否读取成功。如果失败检查压缩级别是否一致、CRC32 算法是否正确、异或密钥是否用对。实操心得我习惯在每一步都保存中间结果比如step1_deobfuscated.bin、step2_decompressed.json。这样如果某一步出错可以快速回退到上一步重新尝试不用从头再来。6. 常见问题与排查技巧实录6.1 解压报错但数据看起来没问题这种情况通常是压缩格式的变种。比如 ZLIB 的头部两个字节中第二个字节的低 5 位表示压缩级别但有些实现会把这个字段改成其他值。如果你用标准 ZLIB 解压报错可以尝试跳过前两个字节直接用原始 DEFLATE 解压。另一个可能是数据被截断或填充。检查压缩数据的长度是否和头部声明的长度一致。如果不一致可能需要根据长度字段截取正确的数据段。6.2 校验值对不上但算法确定没错先检查校验的范围。有些存档的校验值只覆盖数据的一部分比如跳过文件头或者只校验数据体。你可以尝试不同的范围组合从头到尾、从某个偏移到末尾、排除某些字段等。还要注意字节序。CRC32 和 Adler32 通常是小端序存储但有些实现用大端序。如果你算出来的值和文件里的值字节顺序相反那就是字节序问题。6.3 异或密钥推导失败的可能原因如果单字节扫描和多字节推导都失败可能是以下原因密钥是动态的比如每次保存时随机生成存在文件某个位置。异或不是简单的逐字节异或可能是逐位异或、加法模 256、或者更复杂的混合运算。数据经过了多次异或比如先异或 A 再异或 B等效于异或 A^B但如果你不知道这个关系就会推导失败。我的建议是先用已知明文攻击。找到文件里最长的可读字符串假设它是明文和密文对应位置异或得到密钥片段。如果密钥片段有重复模式说明密钥是循环的如果片段看起来随机可能是从某个哈希派生的。6.4 常见问题速查表问题现象可能原因排查方法解压报错压缩格式变种尝试无头 DEFLATE 或其他 windowBits校验失败校验范围不对尝试不同偏移和长度组合异或推导失败密钥动态或多次异或用已知明文攻击检查密钥来源重打包后游戏不读压缩级别或对齐问题尝试所有压缩级别检查填充字节字符串搜索无结果字符串被压缩或异或先解异或或解压缩再搜索6.5 独家避坑技巧技巧一用二分法定位校验字段。把文件分成两半分别修改看哪一半会导致校验失败。然后继续细分直到定位到具体字节。这个方法比盲目猜测快得多。技巧二保存所有中间状态。每拆一层就存一个文件命名清晰。这样即使后面走错了路也能快速回到某个已知正确的状态。技巧三用 Python 的zlib.decompressobj而不是zlib.decompress。前者可以处理流式数据而且能告诉你解压过程中消耗了多少字节对于定位压缩数据的边界很有帮助。技巧四注意文件末尾的填充字节。很多存档在末尾会有零填充这些填充字节可能不参与校验也可能参与。如果不确定先假设它们不参与如果校验失败再尝试包含它们。技巧五异或密钥可能是字符串的哈希。如果你找到了一个可疑的字符串尝试用 MD5、SHA1、CRC32 等常见哈希算法处理它取结果的前几个字节作为密钥。我遇到过好几次密钥就是玩家名的 CRC32 低 8 位。7. 工具选型与自动化脚本7.1 十六进制编辑器怎么选我常用的十六进制编辑器有 HxD、010 Editor 和 ImHex。HxD 胜在轻量快速适合快速查看和修改。010 Editor 有强大的模板功能可以定义结构体来解析二进制格式。ImHex 是开源免费的支持模式语言和脚本适合复杂分析。如果你只是偶尔拆个存档HxD 足够了。如果你要批量处理或者写自动化脚本建议用 Python 的struct和binascii模块灵活性和可复现性都更好。7.2 自动化拆解脚本框架我写了一个通用的拆解框架把每一层封装定义为一个函数然后按顺序调用。这样换一个存档格式只需要改几个参数就行。class SaveUnpacker: def __init__(self, data): self.data data self.layers [] def add_layer(self, name, func, **kwargs): self.layers.append((name, func, kwargs)) def unpack(self): result self.data for name, func, kwargs in self.layers: result func(result, **kwargs) print(f完成层: {name}, 输出长度: {len(result)}) return result def repack(self, modified_data): result modified_data for name, func, kwargs in reversed(self.layers): result func(result, reverseTrue, **kwargs) return result这个框架的好处是层与层之间解耦你可以单独测试每一层的函数确认无误后再串联起来。7.3 批量处理时的注意事项如果你要处理多个存档文件注意以下几点密钥可能因文件而异如果密钥是从玩家名派生的每个存档的密钥都不同。压缩级别可能不同不同时间保存的存档可能用了不同的压缩级别。校验算法可能升级游戏更新后可能换了校验算法需要重新分析。我的做法是先拿一个存档完整拆解一遍确定所有参数然后写脚本批量处理。处理过程中如果某个文件失败单独拿出来分析不要影响其他文件。8. 从存档封装看数据还原的通用方法论拆存档这件事表面上是逆向工程实际上是一套通用的数据还原方法论。核心就三步观察特征、假设结构、验证假设。观察特征包括熵值分析、字符串搜索、字节频率统计。假设结构就是猜层序和算法。验证假设就是实际拆一遍看能不能得到可读数据。这套方法论不仅适用于游戏存档也适用于任何封装过的数据格式比如某些配置文件、缓存文件、日志文件。我甚至用类似的方法拆过一些自定义的二进制协议数据包思路完全一样。关键是要有耐心不要一上来就暴力破解。先花十分钟做观察和假设能省下后面几个小时的盲目尝试。另外记录每一步的操作和结果这样即使走错了路也能快速回溯。我习惯用 Markdown 记笔记把每个偏移量、每个密钥、每个算法的猜测都写下来最后整理成一份完整的分析报告。最后分享一个小技巧如果你实在推不出异或密钥可以试试把文件里所有字节异或一遍得到一个单字节值。这个值有时候就是密钥因为开发者可能用“所有字节异或”作为简单的校验或密钥派生方式。我遇到过两次这种情况都是靠这个笨办法破的局。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询