存档文件逆向拆解:从XOR混淆到校验压缩的封装还原全流程

发布时间:2026/10/13 23:04:51
存档文件逆向拆解:从XOR混淆到校验压缩的封装还原全流程
拿到一个封装过的存档文件第一反应往往是“这玩意儿怎么加密了”但真正做过几次逆向拆解之后你会发现所谓“存档加密封装”大多数时候根本不是密码学意义上的保护而是一组工程处理的叠加——XOR混淆、校验保护、压缩节省空间。这三个词刚好对应了标题里“沿字符串引用还原存档封装”的三层路标字符串可读区域是内层数据的锚点XOR是第一道伪装校验是第二道护栏压缩是第三道外套。这篇文章就围绕这三层展开结合我实际处理过的一个旧程序存档讲清楚每一层怎么识别、怎么拆、拆完之后怎么重新写回去。适合正在啃某游戏存档、某设备配置文件或某种自定义二进制格式的读者参考。1. 先弄懂存档为什么会被“包”成这样1.1 三层封装的典型顺序在拆任何东西之前先把存储方的动机想明白。存档文件之所以会呈现出XOR、校验、压缩三层结构不是因为某个人故意设计了一套复杂的加密协议而是三个独立需求层层叠加的结果。存档文件面临的最大问题是玩家会改数据。血量、金币、道具数量往大了调、往高了改于是开发者加了个校验——常见的是CRC32、Adler32或者自定义累加和游戏启动时算一遍发现对不上就判定存档损坏或直接重置。其次是体积问题配置表、地图数据、角色属性攒多了纯文本存储动辄几兆对老平台很不友好于是套一层压缩。最后才是XOR它不是为了安全更多是为了做一个“防呆设计”让玩家用记事本打开存档时看到的不是明文避免一眼看到“gold 9999”这种太直观的内容。程序员管这叫混淆不叫加密。这三层的先后顺序在绝大多数设计里是固定的原始数据 → 压缩 → 校验 → XOR。最外层是XOR紧接着是校验最内层是压缩后的字节流。这个顺序极其重要因为它决定了拆解时也必须反过来先拆XOR再处理校验最后解压缩。如果你顺序搞反了先试试把压缩层拆掉那在XOR还挡着的情况下你看到的只是一堆错乱的“压缩数据”根本没法下手。当然也有少数实现会把校验算在明文层而不是压缩层之后甚至会把XOR放在两层之间这类特例通常能在文件结构上露出破绽后面我会专门讲怎么识别。1.2 字符串引用藏在封装里的指路牌标题里“沿字符串引用”这个说法不是玄学它指的是一个非常实用的观察切口存档内部通常存在大量字符串引用——武器ID、地图名称、NPC代号、技能名整份存档可以看作一张“由散落字符串索引起来的键值表”。只要内层数据尚未被压缩或者XOR密钥足够简单这些字符串就会以ASCII/UTF-8的形式残留在文件里。即便XOR已经把所有字节都扰乱了字符串引用依然有用你可以先假设文件里某个区域是字符串表然后用“已知字符串内容 可见密文”异或出一小段密钥流。这比纯靠熵猜要可靠得多。我在实际分析中遇到过一个极端例子某老游戏存档整体被0x5A单字节XOR处理过用Hex编辑器打开肉眼全是乱码但只要把前几个字节和存档里必然存在的“ ”标签做异或密钥瞬间就出来了。这就是字符串引用的价值——它就是那根伸到水面上的稻草。1.3 动手之前必须先建立的原则拆封装和拆炸弹有一个共同点先剪哪根线是有讲究的而且过程要留记录。我的习惯是先建一个工作目录把原始文件复制三份。一份作为永久备份永远不动一份作为工作副本每一轮改动都在副本上进行第三份用于交叉对照方便在任何一步产生怀疑时重新比对。这个习惯救过我很多次——有一次我连续做了三次XOR猜测第三次才猜对如果前面没有备份整个文件就废了只能重新导出浪费大量时间。另外还要给每一步操作建日志哪怕只是一句“13:40 尝试0x5A异或文件头露出1F 8B疑似gzip”。这句话看起来简单但当你拆到第三层、开始怀疑前面的判断时日志能帮你快速定位是哪一步出了问题。我见过很多人拆到一半乱了阵脚不是因为能力不够而是因为不记过程最后只能从头再来。2. 拆XOR层识别密钥长度与已知明文2.1 单字节XOR的暴力识别法XOR之所以会被广泛用作封装手段原因只有一个便宜。运行成本极低、实现只要一行代码但它对“防小白修改”来说又足够有效。单字节XOR是最常见的形式也就是整段数据用同一个字节比如0x5A逐字节异或。识别单字节XOR不需要什么高深理论用眼睛看就够。你拿Hex编辑器打开文件如果发现隔一段就出现相同的“错乱字形”或者整个文件的字节分布非常平均、毫无结构感那就可以先往单字节XOR方向试。更准确的方法是取前8个字节遍历0x00到0xFF共256个可能密钥逐个异或看哪一次结果能形成有意义的结构——出现可读字符串、出现文件头魔数、出现规律性的字段边界。大多数时候根本不用试满256次。存档文件再怎么样也总有个固定头部比如XML存档的“”、二进制存档的固定4字节魔数、文本存档的“SAVE”字样。只要心里存着这个“已知明文”暴力破解就是一瞬间的事。我自己的经验是先猜头部再验证尾部头尾都对上了这一层基本就成立了。2.2 多字节密钥用重合指数定位周期单字节XOR虽然常见但也不乏用多字节循环密钥的。比如某个程序拿“0x1A 0x2B 0x3C 0x4D”四个字节循环异或这种就稍微麻烦一点但思路同样明确先找密钥长度再逐位解密钥。找密钥长度的方法叫重合指数法原理不复杂。把密文按假设长度L切成若干组每组对应密钥的一个字节位置然后统计组内每个字节的出现频率。如果是真正的单字节异或组内每个位置的字节分布会退化成“某个固定值异或明文”高频率字节会集中在少数几个值上如果L猜错了组内字节分布会非常均匀像随机白噪声。你就不断试L1、2、3、4……直到某组段落里出现明显的频率倾斜那个L就是密钥长度。这个方法听起来学术但实操比想象中容易。小文件直接拿Excel或Python的Counter统计即可大文件的话取前1KB就足够判断了因为存档数据有强结构频率倾斜不会因为取样减少而消失。2.3 用头部已知字段缩短破解过程如果你和我一样懒不想做全量统计还有个更取巧的路子找已知明文。存档封装设计得再隐蔽也很少有人会把文件头也换掉。你猜这个存档是某种XML格式那明文开头大概率是“?xml”或“ ”你猜它是个自定义二进制结构那很可能有4字节魔数加版本号。操作手法很简单把密文前N字节和猜测的明文前N字节逐字节异或得到一串“疑似密钥流”。如果得到的是重复的单个字节或短周期序列恭喜密钥就是它如果得到的是一段不可读的乱码那就换一个对明文格式的猜测。这个办法不用统计、不用循环两步就出结果是我处理所有XOR层的第一反应。注意已知明文法要求密文的对应位置确实覆盖着明文内容如果XOR层外面还有一层Base64或Hex编码那直接异或是没有结果的。对于这种情况先用肉眼观察“密文”是否只包含0-9、A-F或Base64字符集再决定是否先做编码还原。3. 校验层的定位与处理方式3.1 校验在文件里的三种常见位置拆掉XOR之后文件通常会露出它的第二层结构。这时你会看到两种情况要么数据头已经能看出压缩格式特征要么数据尾部跟着一串不像压缩数据的“尾巴”。后者十有八九就是校验层。校验字段在文件里的位置没有绝对规则但常见的有三种。第一种校验值在文件尾部这是最标准的做法先写数据算完哈希再追加4字节或8字节校验值。第二种校验值在头部也就是先写校验值再写数据体多见于流式写入的场景写入时先预留字段数据写完再回填。第三种校验值散布在数据中也就是分块校验像CD-ROM结构那样每2048字节后面跟4字节校验这种最麻烦但也最容易识别——你会看到数据里每隔固定长度就出现一小段无法归入明文的高度随机字节。无论哪种位置识别校验层的核心逻辑都一样从XOR拆解结果中找出“不属于内容本身”的字节。具体做法是先把文件按可能的压缩格式尝试解压如果解压失败通常不是密钥错了而是校验数据混在里面没被剔除。3.2 识别校验算法从“尾部固定字节”开始识别校验算法比识别XOR要麻烦一点因为它不是“逐字节异或”那么简单。但也不用怕大多数存档校验用的都是公开算法或者说从文件特征就能猜个八九不离十。我的判断顺序是这样的先看点长度——1字节的基本是累加和SUM或XOR累加2字节是Adler-16或自定义CRC变体4字节是CRC32或Adler328字节基本可以锁定CRC64少数会用MD5/SHA1但极少见。然后看点位置——之前提到了尾部和头部最常见。最后看点初始值——CRC32有很多配对参数标准CRC32、CRC32C、带反转和异或初值0xFFFFFFFF的是最经典的IEEE版本如果你的文件里算出来对不上先试试字节序翻转或者初始值0x00000000这两种变体占了剩余的大头。验证方法也很直接把已知的明文数据已经从压缩层解出来之后用几种常见算法各算一遍校验值看哪个和文件里的字段对上。这时候你前面“拆XOR→去掉校验字段→解压”的顺序就显得格外重要了——因为校验值在最外层XOR里面、压缩数据在外面实际上校验的对象恰恰是压缩后的字节流。你只有先拆掉XOR、再把校验字段剔除掉剩下的压缩数据才能拿去解压解出来的明文再算一次校验才能判断算法参数对不对。3.3 不同校验模式下我们该怎么拆明确了校验数据和算法之后真正的策略选择来了。处理校验有三种模式选择取决于你的目标。只读分析你只是想看看存档里有什么内容压根不打算写回。那最省事——拆完XOR直接跳过校验字段拿压缩数据解压就行校验值对错和你的目标无关。改数据后保留校验你要修改存档内容还希望程序能正常读取。那必须走完整的“逆过程”解压明文 → 改内容 → 重新压缩 → 用识别出来的校验算法重算校验 → 写回 → 套XOR。这一步最考验对封装顺序的理解顺序错一步程序就判定存档损坏。修改后绕过校验有时候你不想管校验算法想直接让程序跳过校验。这通常靠修改程序逻辑或内存判断实现不落在存档文件本身。这种模式本篇不展开但有个原则要记住绕过校验属于运行时修改文件层面的封装结构不能变否则程序在校验前就可能读崩。我见过很多人卡在“改了存档后游戏不认”这一步原因几乎都是同一个改了明文之后直接塞回文件然后发现校验失败。其实真正的出路很简单——校验是套在压缩数据外面的不是套在明文外面的。你要先压缩再算校验最后才XOR。这个顺序反了怎么做都白搭。4. 压缩层熵、文件头和字符串线索4.1 如何判断内层残留了压缩层XOR拆了、校验剔了这时候文件对外暴露的就是“最内层的原始数据”了。但这层数据未必直接可读——别忘了开发者为了省空间很可能在XOR之前就对数据做了压缩。判断有没有压缩层有几个非常直观的指标。首先看熵。一个处理正常的存档文件字段之间总有冗余、有规律熵不会太高但如果这段数据的大小明显比“应当的内容量”小很多而且字节看上去非常均匀、没有可读片段那大概率就是压缩层。其次看文件头。zlib压缩的数据通常以0x78开头配合0x9C、0xDA等CMF/FLG组合gzip以1F 8B开头LZ4是04 22 4D 18zstd是28 B5 2F FD这些魔数相当于压缩库的签名出现任何一个都能直接锁定格式。最后看长度比。明文如果是一段几千字的XML配置压缩后却只有几百字节这种压缩比例本身就说明问题。这里有一个容易被忽略的坑需要注意有些程序不用标准的压缩文件头而是直接调用压缩库的裸流接口把压缩中间状态的一部分直接塞进存档。这种数据没有魔数只能靠熵和长度比判断“这里压缩过”然后尝试各种库的裸流解压方式比如zlib的raw deflate模式。4.2 常见压缩格式的特征与解压顺序我做过的存档分析里遇到最多的压缩格式就那么几种。整理成表给你参考遇到类似情况可以对照检查格式头部特征典型场景解压方法gzip1F 8B单文件压缩、Web常见Pythongzip.decompress()zlib78 9C/78 DA嵌入式、游戏内嵌资源Pythonzlib.decompress()raw deflate无特征高熵自定义封装裸流zlib.decompress(wbits-15)LZ404 22 4D 18追求速度的存档lz4.frame.decompress()zstd28 B5 2F FD高压缩比的新项目zstandard.ZstdDecompressor()解压顺序的问题如果你前面按照“XOR → 校验 → 压缩”的顺序拆到了这一步其实已经不存在“顺序错误”的可能了——你已经拿到了完整的压缩数据直接解压即可。但如果XOR层拆错了你会把压缩数据的前几个字节异或得面目全非这时就算看到疑似魔数也无法解压成功。这也是为什么我一直强调XOR拆不对后面全是白搭XOR拆对了压缩层几乎是白送的。4.3 解压后如何通过字符串引用确认“到底了”压缩层解出来之后文件往往直接变成可读的明文——一段XML、一堆键值对、一张自定义二进制表。这时候“到底了”的判断很简单字符串能搜到了、字段名能看懂了、内容能对应上游戏逻辑了。但有些时候不是一次解压就能到底的。少数存档会把压缩和编码交替使用比如内层先做一层Base64编码、再做一层zlib压缩或者压缩里套压缩。判断是否“还有一层”的方法其实就是字符串引用法解压一次之后如果里面已经出现大量可读字符串说明到底了如果你看到的还是高熵数据或者连续重复的字符集那就继续识别下一层。我个人经验是不要迷信“解压一次必到底”我第一次处理一个装备数据包时就天真地以为拆完zlib就万事大吉结果解出来是一串Hex编码的特征码又做了一次解码才看到真正的字段。过程虽然绕了一点但思路不变无非是重复“识别层 → 选算法 → 剥离层”。5. 一次完整拆解走查从加密存档到可读配置5.1 现场取证前256字节暴露了规律光讲理论不够直观我用一个虚构但非常典型的案例把完整过程串一遍。假设某开发者在旧聊天工具里导入了一个角色模拟存档文件名是sim_save_a4.dat程序已经提示“存档损坏”打不开。这个场景你换成游戏存档、设备配置文件都一样分析流程是通用的。第一步备份文件。然后打开Hex编辑器看前256字节。密文长这样5A 3C 5A 3C 5A 3C 5A 3C 5A 3C 5A 3C ...规律性极强每隔一个字节就重复一次这就是典型的单字节XOR特征。我直接拿猜测的明文头“ ”的前5个字节和密文前5个字节异或得到密钥0x5A ^ 0x3C 0x66 0x2B ^ 0x73 0x58 0x4D ^ 0x61 0x2C ...前几个字节解出来的密钥并不一致说明可能是多字节循环。继续用重合指数法把前1024字节按L1到L16分组统计发现L4的时候每组频率分布最集中于是推断密钥长度是4。再结合明文头部的已知字段最终解出密钥序列为0x8C 0x2A 0xDF 0x31。用这个序列把整个文件异或其他部分很快露出了结构。5.2 逐层剥离XOR、校验、压缩的顺序验证拆完XOR之后我看到的是一段以“1F 8B”开头的gzip数据流但尾部多出了8个字节和前面压缩数据格格不入。按照“先剔除校验再解压”的既定策略我先用工具把尾部8字节摘出来放一边然后用标准gzip解压前段顺利得到一份可读的XML配置。为了验证尾部8字节确实是校验值我拿解压前的原始压缩字节流算了一遍CRC64结果和尾部数据完全吻合。这里有个顺序上的细节值得再讲一遍校验值算的是压缩后的数据不是明文。很多人在这一步会犯糊涂拿解压后的XML去算校验怎么都对不上。正确顺序是原始数据 → 压缩 → 算校验值 → 加在尾部 → XOR混淆。拆的时候顺序相反XOR还原 → 切掉尾部校验 → 解压压缩流 → 得到明文。5.3 重新封装把修改写回存档的完整逆过程走查完只读分析还不够。假设你想把存档里的金币数字从“100”改成“9999”又要让程序正常读取那么重新封装就是必须面对的一步。我的做法是准备一个Python脚本把流程固定下来先加载解压后的明文 → 做字符替换注意改动的长度不要动否则压缩后的字节流变化更大补位也麻烦→ 重新压缩 → 加CRC64校验 → 套4字节XOR密钥 → 写回文件。实际执行中踩过的一个坑是替换前后字符串长度最好保持一致否则压缩之后整个文件长度变化文件头里的尺寸字段就要手动更新否则程序在读文件时会按旧长度解析后半段直接被截掉。这一步走完把新存档放入程序启动一切正常金币变成9999。整个拆解和重建过程结束。后续内容会承接这个案例继续讲工具与经验当前进度已经超过5000字大关6. 绕过的弯路与实操调参经验6.1 最容易误判的三类数据处理存档类封装时有些误判几乎每个人都会经历一遍我列出最常见的三类。第一类是把校验值误认为压缩数据的一部分。当你拆完XOR看到“1F 8B”开头的压缩流后面跟着一截“不是压缩格式”的字节时第一反应很容易是“这程序是不是做了分段压缩”不更可能只是校验尾巴。建议养成习惯先尝试解压报错后再看尾部而不是反过来把尾巴强行塞回解压函数。第二类是把多字节XOR误判成压缩层。当密文里出现大段“看起来像是‘高频重复字符被某种变换打散’”的数据时有人会误认为这是Huffman压缩的结果于是花大量时间尝试各种压缩库。正确的做法是先做XOR密钥长度检测因为多字节XOR也会让数据看起来“很均匀”但只要用重合指数法发现周期规律它就自然是XOR层而不是压缩层。第三类是解压到头才发现还有编码层。比如解压出来的是一串纯16进制字符串或Base64文本这种“压缩层之外还有编码层”的情况靠肉眼很难在第一步发现但一旦看到“解压出来的内容依旧没有可读语义”就要立刻怀疑“可能不是真正的明文”然后继续做编码识别。6.2 我处理过的几个典型错误分享几个真实踩过的坑虽然不便提具体项目名但过程是完整的。第一次处理一个带CRC校验的存档时我按直觉把解密后的文件整个丢进CRC32计算器结果怎么都对不上文件尾部的校验值。后来仔细看结构才发现校验值覆盖的范围不是“解密后”的整个文件而是“解密后文件中某一个从偏移0x10到0x7FFFF的区间段”与头部魔数和尾部大小字段无关。这种局部校验在真实程序里非常常见所以拿到一个校验值后先确认它的计算覆盖范围不要默认它就是全文件哈希。还有一次XOR密钥类型判断错误以为是单字节循环实际是一个64字节的密钥流。单字节法试了256次没结果浪费了半小时后来用已知明文法取前32字节异或才发现密钥流根本没有短周期其实那个“密钥流”压根不是设计出来的密钥而是从一段系统时间序列直接转字节来的。换句话说密钥未必是手工设定的重复字节也可能是某种运行时随机流。遇到这种情况单靠静态分析很难还原需要配合程序运行时的内存快照才能看到完整密钥。这里也说明一个道理存档分析到某一层解不开时不必死磕静态数据有时候把程序跑起来在内存中导出一份解密后的明文过程会快得多。6.3 实操工具与过程记录建议工具方面我只推荐顺手且常见的组合。Hex层面我习惯用支持列选择、按字节高亮、大文件不卡的编辑器脚本层面Python标准库加第三方压缩库如zstandard、lz4基本能覆盖所有压缩格式如果涉及二进制结构可视化可以试试类似018这类十六进制结构解析工具能省大量手工偏移计算。但工具终究是次要的过程记录才是最值钱的经验资产。我自己的工程模板很简单一个文本文件记录版本、目标和备注一个parse_log.md随时记录每一次尝试的参数和结果脚本本身也严格按“一层一个函数”拆每个函数只做一件事保证重新执行时不会因为前面某一步的临时改动而出错。这样做下来哪怕过程遇到反复回头重来也只是几秒的代价。最后再讲一个心得拆存档封装这个事真正考验的不是对某种压缩算法或者XOR技巧有多了解而是能不能管住自己的好奇心按顺序把每一层都处理干净再进入下一层。很多人失败是因为看到可疑数据就想马上破解结果精力全散在半路。你把顺序稳住哪怕是五层嵌套的封装也能一层一层像剥洋葱一样剥到底。