☰
Flash小游戏存档逆向全记录:从SWF反编译到加密恢复
2026/10/17 6:51:06 网站建设 项目流程

周六晚上翻移动硬盘,翻出一个几年前从某4399平台缓存下来的Flash小游戏。点开试了试,游戏能跑,但存档加载直接报错。看那架势,游戏里的进度大概是找不回来了。本来想直接删文件了事,转念一想,反正手头有工具,干脆把这个SWF的存档机制逆向一遍。分析过程不算短,但走完之后收获比想象中大得多。如果你也在折腾老Flash游戏的数据迁移、想看懂某个SWF里的加密存档,或者想拿一个小目标练手学习逆向分析,这篇记录可以给你一条完整的参考路线。

先说这次逆向的目标:搞清楚游戏本地档案的存储位置、存储格式和加密方式,最终把旧档解开,让进度回到游戏里。整个过程绕开了任何联网授权,只针对本地文件做静态拆解和算法还原。所有素材都是自己硬盘里本来就有的东西,没有涉及付费内容绕过,也没有传播任何破解产物。下面按实际操作顺序,把从文件头到解密脚本的完整过程拆开讲。

1. 目标选定与工具准备

1.1 为什么选Flash小游戏做逆向入口

Flash游戏(SWF)在逆向学习里是个非常友好的入口,原因有三个。第一,SWF文件本身是公开格式,结构里标签边界清晰,没有复杂的壳,压缩也基本是zlib那点事。第二,ActionScript 3编译出来的字节码反编译还原度很高,用现成工具能得到接近源码的代码,读起来不像原生汇编那么劝退。第三,这类文件在本地缓存里散落很多,拿自己电脑上的旧文件做实验没有任何版权负担。对我这种边恢复存档边练手的人来说,再合适不过。

说完优点,也要泼一盆冷水:Flash逆向不是万能的,有些SWF做了很强的混淆,比如字符串加密、类名改写成A0B1之类,甚至把逻辑套到动画帧上,分析难度会直线上升。但日常小游戏绝大多数没有这么重的保护,尤其是很多从其他作品改版过来的,代码风格一眼能看出是赶工产物。所以选目标时先挑文件体积中等(几MB到十几MB)、来源简单的Flash小游戏,成功率最高。

1.2 工具清单和选型理由

这次全程用到的工具不多不少,刚好覆盖从静态到动态的完整链路。

工具用途为什么选它
JPEXS Free Flash Decompiler(FFDEC)SWF反编译、资源导出、P-code查看开源免费,跨平台,AS3还原度极高
010 Editor / HxD十六进制查看文件头、标签偏移看头部和原始字节分布很直观
7-Zip解压CWS的zlib内容处理压缩态SWF时顺手
Python3写脚本验证解密算法自带base64、zlib库,做算法复现最省事
Flash Player Projector调试版动态运行和trace输出独立播放器,不受浏览器限制

选型逻辑很简单:能用免费工具解决就不用商业工具。我之前试过用商业反编译器打开这个文件,界面倒是花哨,结果解析半分钟直接卡死。FFDEC反而两秒就响应,导出代码也更干净。再加上它支持把SWF里的PNG、声音素材批量拖出来,对分析美术资源也很管用。可以说这一套工具链已经覆盖了Flash逆向里最常用的环节,短期内不太需要再添别的。

2. 静态拆解:把SWF翻成可读代码

2.1 先确认文件格式

拿到SWF文件第一步不要急着反编译,先用十六进制工具点开。文件头三个字节如果看到FWS是未压缩,CWS是zlib压缩,ZWS是LZMA压缩。这次的文件头是CWS,说明整体内容被zlib压过。第三字节之后是Flash版本号,这里是13,对应Flash Player 11.x时代,算是比较老的产物。再往下四个字节是解压后的文件总长度,这些信息在后面的标签遍历里都要用到。

对于CWS文件,很多标签结构要等解压后才好读。我习惯先把文件丢进7-Zip看一眼压缩比,或者直接交给FFDEC,它会自动解压。真正关心的是其中一个标签:DoABC。几乎所有AS3代码都挂在这个标签下面,找到它就等于找到了整个游戏的逻辑核心。如果文件头是FWS,那直接用010 Editor在标签区搜"DoABC"四个字节也能定位到,只是CWS压缩状态下看不见明文,所以最佳路径还是先反编译。

2.2 第一次反编译怎么下手

把SWF拖进FFDEC,等待左侧脚本树加载。界面左侧主线是"脚本"目录,下面按类展开;右侧是当前类的属性、方法和反编译代码。第一眼可能觉得类很多,我的习惯是先在顶部的"搜索"里输入关键词,而不是一个个翻。常用关键词:SharedObject、save、load、data、encode、decode。这一轮搜下来基本能定位存档相关类的位置。

如果文件偏大,可以右击某标签选择只加载脚本树,避免渲染图片时卡顿。FFDEC还有一个"反编译P-code"和"显示AS3源码"切换按钮,平时默认看AS3源码就行,遇到反编译露馅的地方再切成P-code逐条看字节码。第一轮分析的目标不是看懂全部代码,而是把这些类和函数的地图画出来:谁来调用它、几个入口、数据流向哪里。有了这张地图,后面的还原工作才不至于迷路。

2.3 快速定位存档相关类

这次的搜索结果里出现一个叫GameSaveHelper的类(为避免涉及具体项目,类名做了简化处理),里面两个主要方法:saveGame和loadGame。方法内部调用了SharedObject.getLocal("game_save_01"),一眼就知道这是游戏档的载体。顺着代码往下看,saveGame做的事情很清晰:把对象的属性按顺序写入一个ByteArray,然后对这个ByteArray执行一系列操作,最后把结果转成字符串写进SharedObject。loadGame就是逆向操作。到这里,存档的存储位置基本确定,剩下的工作就是弄明白ByteArray被怎么处理。

这里有个小提醒:反编译出来的代码偶尔会多出编译器自动生成的临时代码,比如一些局部变量被拆成好几行判断,读的时候不要被干扰。重点关注的是数据流转链路,不是每一行都要看得懂。我通常会先看一眼类名看一遍属性列表,再看看方法的入参和返回类型,最后才进入方法体深读。这种方法比从头到尾硬读一整段代码高效得多。

3. 存档加密的还原过程

3.1 存档读写入口的代码结构

先给一个简化到只剩骨架的示意代码,帮助你理解后面要操作什么:

public function loadGame(): ByteArray { var so: SharedObject = SharedObject.getLocal("game_save_01"); if (so.data.raw == null) return null; // 第一步:还原成二进制 var bytes: ByteArray = Base64.decode(so.data.raw as String); // 第二步:逐字节异或 var key: ByteArray = this.makeKey(); for (var i: int = 0; i < bytes.length; i++) { bytes[i] ^= key[i % key.length]; } // 第三步:解压 bytes.uncompress(); return bytes; }

读这段代码的时候有几个点容易忽略。第一,Base64.decode返回的ByteArray默认是网络序还是主机序?不同引擎实现不同,这个直接影响下面异或循环的顺序,但当你用Python复现时统一按小端处理一般没问题。第二,SharedObject取名和版本号经常有关,游戏更新一次就会换个名字,然后老档就失效了——很多玩家"更新以后进度没了"就是这么来的。第三,uncompress对应的压缩格式默认是zlib,Python里直接用zlib.decompress就能对上。

3.2 从乱码数据里认出加密套路

看清那个sharedObject里存的raw数据时,第一反应是"Base64",因为字符串里只有大小写字母、数字、加号和斜杠,末尾还有一个等号。这种特征太明显。顺手写一个Base64解码脚本,解码出来的字节序列开头几个字节是0x78 0x9C——这几乎是zlib压缩格式的指纹,说明开发者先把对象序列化又压缩,然后再Base64编码。如果只到这一步,存档其实没加密,只是编码了。

继续往下才发现真正的加密藏在另一层。解码Base64后的数据前面几十个字节看着有规律,中间却乱得完全不像压缩流,还有个无法解释的短重复周期。用已知明文试探一下:正常情况下游戏会把玩家名称字符串写进取档,我把Base64解开的二进制和名词典中的明文做异或,几个字节对上了。这说明在Base64和压缩之间,还夹了一次异或操作。整个处理链应该是:序列化 -> 压缩 -> 异或 -> Base64。反编译代码里makeKey()位置也印证了这一点,密钥来自一个固定算法,和游戏版本号强绑定。

3.3 用Python把解密流程复现出来

加了异或之后,事情反而好办了。搞清楚顺序,逆向就变成了正向脚本。下面这个Python函数就是我当时用来解档的,你可以直接套用:

import base64 import zlib # 密钥由makeKey()生成,这里简化成一个ByteArray # 实际场景请从反编译代码中提取密钥生成逻辑 key = b"KEY_SUBSTITUTE" def decode_save(raw: str) -> bytes: # 第4步逆向:Base64解码 data = base64.b64decode(raw) # 第3步逆向:逐字节异或还原 buf = bytearray(len(data)) for i in range(len(data)): buf[i] = data[i] ^ key[i % len(key)] # 第2步逆向:解压 return zlib.decompress(bytes(buf)) # 从SharedObject的sol文件或者游戏导出窗口里复制出raw字符串 saved_raw = "在这里粘贴你从sol文件里复制出来的存档字符串" print(decode_save(saved_raw).decode("utf-8", errors="ignore"))

运行之后,终端里打出了玩家名称、金币数、关卡ID、道具列表。那一刻的感觉很奇妙,一个快被遗忘的老游戏,里面存的进度就这么被还原成了肉眼可读的文本。脚本跑通只是第一步,我还顺手把它改成加密方向,用负负得正的方式把新档写了回去,再塞进SharedObject的sol文件里,游戏顺利读取了新进度。要注意的是,写回前最好备份原sol文件,因为格式稍微错一位都可能把整个档整废。

4. 实操中的坑与排查方法

4.1 反编译结果残缺时怎么办

FFDEC并不是总能100%还原方法体,尤其是当代码里用了大量动态特性,比如反射或者自定义命名空间。这时候AS3视图里可能只看到一行代码或者一个空壳。我的做法是切到P-code视图,直接找指令段。AS3字节码虽然也抽象,但里面每一行都对应一个实际操作,比反编译结果可靠得多。那个类名乱成一团的情况也可以从P-code里的getlex指令找出真正引用的常量名,从而定位到实际类。

如果你发现某个类根本不在脚本树里,那可能是它被藏在了别的DoABC标签下,或者被放进了资源目录中的二进制数据标签。FFDEC有个功能是解析所有DoABC,需要手动点一下脚本树根节点附近的设置。某个SWF如果加载了外部SWF,主文件里常常只留了一个加载器桩,真正的逻辑得去找它运行时加载的那个子文件,这个在平台场景里很容易踩坑。

4.2 字符串搜索找不到目标数据

我在打开第二个游戏文件时就遇到了这种情况:搜"SharedObject"完全没有结果。后来发现开发者把所有关键字符串都做了拼装处理,比如把"Shared"和"Object"拆开,运行时再相加,或者在字符中间插入转义码。搜索时你可以换几个更长的关键片段在常量池里找,或者直接搜方法名。常量池里往往保留着所有类和字符串的明文记录,只要方法名没有被动态重新生成,就能搜到。

另一个常见情况是字符串本身经过编码处理,比如转成\uXXXX的Unicode序列。FFDEC的搜索结果会把这类显示成'\u'开始的一串字符,乍看不直观,但你用编码解码工具转回来,再拼出原词去搜,基本就能命中。总之,不要只依赖一种搜索表达式,多换几个角度试探比盲目翻代码效率高得多。

4.3 运行时调试和Flash版本兼容

静态分析够用之后,有时我还想确认某个函数的实际返回值。老Flash游戏在现在跑起来最大的障碍是浏览器不再支持Flash插件,解决办法是装Flash Player Projector独立版,它不依赖浏览器,双击就能运行SWF。配合调试版还能输出trace信息。启用方式是在运行目录下放一个mm.cfg,里面写一行TraceOutputFileName=log.txt,比如这样:

TraceOutputFileName=log.txt

然后在代码里或反编译补丁里调用trace("..."),运行结束后打开log.txt就能看到输出。

这个环节最头疼的是版本兼容。一些老SWF是用Flash 6-8时代的AS2写的,用新版Projector运行可能会白屏或报错,这时候要装对应年代的独立播放器。我电脑上就常备几个版本,遇到不兼容就换。另外,如果你把修改过的SWF重新导出到游戏里,要先把原文件复制出来备份,导出的SWF可能会因为工具链优化出现微小的行为变化,排查半天可能只是这个原因。

5. 合规边界与技能延伸

5.1 什么场景可以逆向,什么不能碰

这部分必须单独拎出来说。逆向本身是中性技术,但落到具体行为上有明显边界。可以做的是:对自己合法持有的本地文件做技术分析,比如这次恢复存档;对开源或者授权范围内的代码学习原理;在漏洞挖掘流程里对目标系统做合规评估。不能做的是:破解商业软件的付费授权,绕过账号验证,提取并分发他人版权素材用于商用,或者做游戏外挂。

我在这次分析里没有碰任何授权保护机制,存档加密只是数据编排方式,不承担版权保护职责,所以整个过程都在学习研究的合理范围内。这也提醒读者:拿到任何一个SWF,先想清楚自己有没有权限分析它,以及分析成果是否会造成侵权或破坏。边界遵守得好,这个技能能帮你处理很多实际问题;走得偏了,可能会惹上麻烦。技术本身没有善恶,怎么用才是关键。

5.2 这次分析长出来的可迁移技能

这一整套流程不只是解决一个存档问题,它更像一个完整的方法论:识别文件格式 -> 建立工具链 -> 静态拆解 -> 特征识别 -> 算法还原 -> 结果验证。这套流程在分析恶意SWF样本、查看老游戏的资源包、甚至分析某些移动端性能问题时都能复用。尤其是"从乱码中辨识Base64和zlib指纹"这一招,在很多场景里都用到过。

比如后来我在处理一个移动端游戏资源时也干了类似的事。游戏资源目录里有一批看似乱码的二进制文件,用同样方法定位到zlib头部,一把解压就把贴图和音频导了出来。虽然问题和SWF没有直接关系,但思路完全是相通的。所以说,逆向分析练习最值钱的不是某个具体结论,而是那种"先观察、再假设、最后用脚本验证"的经验积累。这个习惯一旦养成,遇到任何不透明的数据,你都敢拆开看看。

最后说点这次过程之外的体会。那次逆向的最终结论其实简单得很——一个Base64加异或再加zlib的套壳,密钥藏在版本号旁边。但真正让我感慨的是,一个快被遗忘的老游戏,开发者在存档校验上竟然也写了三层处理,说明当时的小游戏团队并不像大家想的那样都是敷衍了事。你要是手里也有类似的老Flash文件,先别急着删,也别急着四处找"存档修改器"。拿FFDEC拆一遍,花一个下午把它的数据链路摸清楚,你收获的会比游戏进度多得多。如果只是想单纯迁移存档,不用走整个逆向流程,直接把本机SharedObject对应sol文件复制过去更快。但如果你想搞明白它里面到底藏了什么,那还是老老实实按上面的步骤走一遍吧。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询