
简介FFDec 18.5.0 是一款面向 Flash 逆向分析、旧项目维护与教学研究的开源反编译软件适合需要理解 SWF 结构、提取素材或查看 ActionScript 的开发者与研究人员。压缩包共 62 个文件约 13.51MB主体为 30 个 jar 类库与核心引擎配合 Windows、Linux 下的 exe、bat、sh 启动脚本以及 19 个 txt 许可证说明、swc 播放器库和图标资源跨平台运行所需组件较为完整。已有 1058 人学习说明其在 Flash 相关维护场景中仍具参考价值。该工具可将 SWF 转为可读代码导出音频、图像、动画和字体支持 ActionScript 查看编辑与元数据分析并借助 Java 实现多平台使用对研究旧 Flash 项目结构、排查资源依赖和开展逆向教学都有实际帮助。1. ffdec-18.5.0 到手先别急着双击一个成品 SWF 到底能拆出什么一个 3MB 的.swf摆在面前源工程早就没了只剩这份成品要改成网页端能跑的版本——这类存量资产抢救的需求在互动课件、老游戏、广告素材里并不少见。ffdec-18.5.0.zip 就是为这种场面准备的解压即用的 SWF 反编译工具包把二进制标签流拆成 ActionScript 源码、矢量图形、位图、音频和字体还带一套命令行接口能把图形界面里的点击变成可批量复现的脚本。它是纯 Java 的软件/插件形态工具不写注册表、不装服务扔在 U 盘里也能跑。适合手上有历史 SWF 资产要抢救的前端、要做竞品拆解的互动开发、需要批量提取素材的美术环节不适合指望一键还原出能直接编译的完整工程的人反编译的结果永远是半成品。下面按我拆包的实际顺序走先看容器结构再动手导出最后说哪些地方容易翻车。2. 拆之前先看懂容器SWF 标签树与 ABC 字节码的还原链路2.1 SWF 文件头与标签流先搞清楚你在拆什么SWF 不是一坨加密数据它是个结构相当规整的容器。文件头固定 8 字节3 字节签名、1 字节版本号、4 字节小端序的文件总长度。签名只有三种FWS是未压缩CWS是从第 9 字节开始整体 zlib 压缩ZWS是 LZMA 压缩后两种在打开时会被工具自动解压你不用手动处理。紧跟着文件头的是一个按位打包的RECT结构描述舞台尺寸5 位存 nbits后面跟 4 个 nbits 宽的字段再往后 2 字节是 8.8 定点数的帧率低字节是小数部分2 字节帧数。这一段之后整个文件就是一条标签流。每个标签的头部只有 2 字节10 位 tagCode 加 6 位长度。长度等于0x3F时说明是长标签紧跟 4 字节的 SI32 扩展长度——DefineBitsJPEG2、DefineSound、DoABC这类大块数据全走这条路。看懂这条规则你就能写脚本自己扫标签而不是全靠工具。实际拆包时最常打交道的标签和它们在 ffdec 树形面板里的落点大致是这样标签承载内容ffdec 面板位置DefineBitsLossless2带 alpha 的位图ImagesDefineBitsJPEG3JPEG 像素 独立 alpha 通道ImagesDefineShape3/4矢量图形与渐变ShapesDefineSprite影片剪辑内嵌时间轴SpritesDefineSound音频数据SoundsDefineFont2/3嵌入字体轮廓FontsDoABCAS3 字节码块ScriptsDoActionAS2 动作字节码ScriptsAS2SymbolClass类与显示对象的绑定关系影响脚本可达性ExportAssets导出名绑定Others这张表的价值在于你在面板里点开一个节点就知道背后对应哪个标签、数据大概长什么样。很多导出结果是空的问题本质是你在找一个根本不存在的标签。2.2 ABC 字节码怎么变回可读代码还原链路的三档输出AS3 编译后的产物叫 ABC塞在DoABC标签里结构分常量池和方法体两大块。常量池里依次是整数、无符号整数、双精度、字符串、命名空间、命名空间集合和各类 Multiname方法体里存 max_stack、local_count、起始代码偏移、异常表和 trait 列表真正的指令序列在 code 里。反编译要做的就是解析常量池还原符号名按偏移切出基本块建控制流图识别出循环、条件跳转、switch、try/catch 这些模式再重建成表达式树最后生成源码。这条链路有一个绕不过去的损耗点局部变量名。ABC 里变量名存在可选的 debug_info 中发布版 SWF 经常把调试信息剥掉于是反编译出来就是var_1、param1、loc3。这不是工具不行是信息本身没了。所以 ffdec 给了三档输出ActionScript 源码、P-code 伪代码、原始 Hex。源码最像人写的但控制流可能被美化得偏离原意P-code 是栈式展开的中间形态读起来累但分支和目标地址是准的。我一般先用源码快速建立印象遇到可疑逻辑立刻切 P-code 对一遍。AS2 的样本完全是另一回事。它跑在 AVM1 上指令存在DoAction标签里没有类、没有命名空间、没有 ABC 常量池指令序列基本就是扁平的语句流。这类样本的还原度通常高得多几乎能一比一还原成函数调用和赋值。所以同一个工具拆 AS3 的复杂工程和拆 AS2 的老课件体验差距极大别拿一次失败就否定工具。2.3 选型理由为什么走反编译这条路拿到一个 SWF能走的路就那么几条各有各的代价方案拿到的东西代价直接运行 抓网络请求外链资源和接口内嵌资源和逻辑拿不到手工解析二进制完全可控能精确到字节时间成本高只适合定位单点问题反编译工具结构、源码、资源一次拿全混淆或加密的样本会失效ffdec 的优势是覆盖面和形态单个压缩包解压完就能跑不依赖安装图形界面和命令行两套入口共用一套解析内核你在界面里验证过参数复制到命令行立刻能批量跑导出类型覆盖脚本、图片、声音、字体、形状、帧渲染和二进制数据。这些组合起来才让它成为这个场景下的默认答案而不是某个单点功能特别强。2.4 解压与首次启动先把环境这关过了包解开之后经常打交道的文件是这几个ffdec.jar是图形界面入口ffdec-cli.jar是命令行入口ffdec.bat和ffdec.sh分别是两个平台的双击启动脚本lib/下是依赖不要单独移动。第一次跑之前先把启动流程固定下来# 1) 解压到不含空格和中文的路径避免路径解析出岔子 unzip ffdec-18.5.0.zip -d /opt/tools/ # 2) 确认 Java 可用。需要 Java 8 及以上我一般统一用 11 或 17 的 LTS java -version # 3) 图形界面启动。-Xmx 给足堆内存大 SWF 用默认堆很容易 OOM java -Xmx4g -jar /opt/tools/ffdec-18.5.0/ffdec.jar # 4) 命令行入口后面所有批量任务都走这个先看一眼帮助 java -Xmx4g -jar /opt/tools/ffdec-18.5.0/ffdec-cli.jar --help-Xmx4g是 JVM 的最大堆4g 对 100MB 以内的 SWF 基本够用如果你的机器是 32 位 JVM这个值上不去会直接报错退出。路径里带空格或中文时启动脚本里的引号处理不一定稳妥我踩过一次后来统一放/opt/tools/这类纯英文路径。--help的输出建议先扫一遍不同小版本之间个别参数名会有微调以本地实际输出为准别照抄网上的老命令。3. 图形界面实操脚本、图形、音频三类资源的导出路径3.1 树形面板各节点对应什么打开 SWF 之后左侧是一棵树展开后大致是 Scripts、Sprites、Shapes、Images、Sounds、Fonts、Frames、BinaryData、Others 这几组。Scripts 下面是按包名折叠的类AS2 样本则直接是脚本块列表Frames 展开就是时间轴双击某一帧右侧会渲染出那一帧的合成画面这是判断这个 SWF 到底长什么样最快的方式。右侧主区域在选中脚本时会给出视图切换ActionScript、P-code、Hex 三档可以互相印证。这里有个使用习惯值得养成先从 Frames 把整条时间轴翻一遍搞清楚动画节奏和关键帧再回到 Scripts 看逻辑。反过来做你会在几百个类里迷路分不清哪个是入口。树顶还有个搜索框可以按类名或 character id 过滤character id 就是标签流里的那个编号后面命令行按 id 导出时用的是同一个值。3.2 导出脚本从单类到整包导出入口在 File 菜单下的 Export或者在树节点上右键选导出。选 Script 类型后会让你指定输出目录和格式。格式选 ActionScript 得到反编译源码选 P-code 得到伪代码。勾选项里那个导出资产类的开关要留意它决定要不要顺手把脚本引用的嵌入资源一起吐出来。导出的目录结构是按包名映射的com.foo.bar.MyClass会落到com/foo/bar/MyClass.as。导完之后别急着读先统计一下规模判断导出是否完整# 统计导出目录的规模快速判断脚本导出是否完整 from pathlib import Path from collections import Counter root Path(./out/script) as_files list(root.rglob(*.as)) print(total .as files:, len(as_files)) pkg_counter Counter() toplevel [] for f in as_files: rel f.relative_to(root) if len(rel.parts) 1: toplevel.append(f.name) # 没有包名的顶层脚本多半是帧脚本或入口类 else: pkg_counter[str(rel.parent).replace(\\, /)] 1 print(packages:, len(pkg_counter)) for pkg, n in pkg_counter.most_common(10): print(f{pkg:40s} {n}) print(top-level scripts:, toplevel[:20])这段脚本做三件事统计.as总数、按包聚合计数、单独列出没有包名的顶层文件。参数上没什么可调的把root指向你的实际导出目录即可。判读逻辑是如果一个有几百个类的工程只导出个位数文件基本可以断定可达性过滤把大部分类挡在外面了如果顶层脚本特别多说明帧脚本占大头说明这个 SWF 的逻辑大量挂在时间轴上而不是写在类里后续读代码的重心要相应调整。3.3 图形与音频导出格式选择和丢失点Images 节点导出的是位图标签里的原始像素可选 PNG 或 JPEG。要透明通道就选 PNG但得先确认源标签是DefineBitsLossless2或DefineBitsJPEG3DefineBitsJPEG2本身就不带 alpha导出来必然是实底的这不是工具的问题。Shapes 和 Sprites 建议导 SVG矢量信息能保住缩放不糊Sprites 导出时会把时间轴拆成多个文件命名带帧序号看着乱但信息全。Sounds 节点导出时有两个方向解码成 WAV或者把原始压缩流直接取出来。事件音Event Sound一般能干净导出流式音Stream Sound跟时间轴绑定导出的边界可能和原始音轨对不齐做音频还原时要留一段余量人工对齐。所有资源导出后都按 character id 命名像1.png、DefineSprite_12.svg这种好处是能和标签流一一对应坏处是光看文件名完全不知道内容后面第 4 章会给一个批量重命名的思路。3.4 改一处再存回去边界在哪里在脚本视图里改完内容CtrlS 会弹出保存对话框可以覆盖也可以另存。这个动作背后的流程是ffdec 把修改过的脚本重新编译成 ABC再写回标签流。它用的是自己实现的编译器语法覆盖不是全集遇到内联写法、特殊的命名空间技巧、部分 E4X 语法时可能编译失败。血泪经验是只改字符串常量、只改数值、只改条件判断的边界这三类改动成功率最高动结构、加方法、改继承关系失败率陡增。保存出来之后怎么确认改动真的写进去了SWF 默认是 CWS 压缩的直接 grep 搜不到明文得先解压# 把 CWS 压缩的 SWF 还原成未压缩的 FWS便于做字符串和标签层面的核对 python3 - PY import zlib data open(patched.swf, rb).read() assert data[:3] bCWS, 不是 zlib 压缩的 SWF先确认签名 raw data[:8] zlib.decompress(data[8:]) # 前 8 字节文件头不参与压缩 open(patched_fws.swf, wb).write(raw) print(decompressed bytes:, len(raw)) PY # ABC 里的字符串常量以 UTF-8 存储未压缩状态下可以直接搜到 strings patched_fws.swf | grep -i 你改进去的新内容关键是那个data[:8]CWS 只压缩第 9 字节之后的内容文件头必须原样保留解压后拼回去才是一个合法的 FWS。zlib.decompress不接受尾部多余数据如果报错说明文件本身被截断或者签名判断错了。搜到字符串只能证明写进去了能不能跑还得回到播放器或模拟环境里验一遍这两步不能省。4. 命令行流水线把三十个 SWF 的导出一晚上跑完4.1 ffdec-cli 的参数体系图形界面适合摸清单个样本一旦样本上量就必须转命令行。核心参数不多摸清这几个就够用参数作用常用取值-export指定导出类型可逗号分隔多个script,image,sound,font-format指定输出格式script:as、script:pcode、image:png、image:svg-selectclass只导出指定类完整类名可多次指定-selectid按 character id 定向导出12,15,1024-onerror单个资源出错时的处理策略ignore跳过继续-timeout单文件解析超时秒120 到 300-config指定配置目录批量任务建议独立目录-swf2xml/-xml2swfXML 与 SWF 互转定位结构问题时用-format的取值在不同小版本之间略有差异跑之前先--help确认一遍本地的写法别直接抄。最小可用的一条命令长这样# 单文件一次导出脚本、图片、声音、字体四类资源 java -Xmx4g -jar ffdec-cli.jar \ -format script:as \ -onerror ignore \ -timeout 120 \ -export script,image,sound,font \ ./out/demo \ ./in/demo.swf参数顺序是-export后面第一段是类型清单第二段是输出根目录第三段是输入文件。输出根目录下会按类型分目录落盘脚本归到 script 之类的子目录里。不同小版本的分目录策略可能有细微差别第一次跑完先ls -R看一眼实际结构再把脚本里的路径固定下来别凭记忆写。4.2 批量脚本超时、日志、失败清单三件套批量任务的核心不是循环本身是失败处理。三十个 SWF 里只要有一个解析卡死整个夜间任务就废了#!/usr/bin/env bash set -u FFDECjava -Xmx6g -jar /opt/tools/ffdec-18.5.0/ffdec-cli.jar IN_DIR./swf_batch OUT_DIR./exported mkdir -p $OUT_DIR : failed.txt # 清空失败清单 # -print0 配合 read -d 处理文件名里的空格和特殊字符 find $IN_DIR -type f -iname *.swf -print0 | while IFS read -r -d f; do name$(basename $f .swf) out$OUT_DIR/$name mkdir -p $out # 外层 timeout 兜底ffdec 自身的 -timeout 只管部分解析阶段遇到死循环得靠系统信号 if ! timeout 300 $FFDEC -onerror ignore -timeout 180 \ -export script,image,sound,font $out $f $out/export.log 21; then echo $f failed.txt fi done echo 失败清单 cat failed.txt几个参数的作用值得说清楚timeout 300是外层硬超时到点直接发信号杀掉进程防止单个文件把整晚拖死-timeout 180是工具内部的软超时两者要留出差值软的要小于硬的。-onerror ignore让单个资源的解析异常不影响同文件里其他资源的导出代价是错误被吞掉所以每个文件必须单独重定向日志出事时能定位到具体是哪个文件哪一类资源。set -u是防止变量名写错导致整段静默失败批量脚本里这个保护很值。4.3 定向导出与结果重命名全量导出在样本多的时候很浪费很多时候你只关心几个类或者几张图。-selectclass指定完整类名-selectid指定 character id可以精确到单个资源。做两个版本的差异比对、或者只想捞某一套 UI 图时定向导出的速度能快一个数量级。导出后的资源名是按 id 生成的可读性很差。批量重命名之前先建一张 id 到内容的映射表# 按文件大小和类型给导出的图片建索引方便后续重命名和人工筛选 from pathlib import Path img_dir Path(./exported/demo/image) rows [] for f in sorted(img_dir.iterdir()): if f.is_file(): rows.append((f.name, f.stat().st_size, f.suffix.lower())) # 大文件优先看通常是主视觉几十字节的多半是占位图或纯色块 rows.sort(keylambda r: r[1], reverseTrue) for name, size, ext in rows[:15]: print(f{name:30s} {size:8d} {ext})排序依据选文件大小是有讲究的小到几十字节的位图基本是纯色占位块或者遮罩用的单通道图可以整批略过几万字节以上的才值得逐个看。这张表还能当校验用——如果某个 SWF 导出后图片总数是零但 Frames 里明明有画面那多半是画面全由 Shape 矢量绘制得去 shape 目录找。4.4 导出后校验怎么确认没漏批量跑完最怕的是看起来成功了。有一批文件是零字节的.as或者整个目录压根没建出来脚本照样返回成功。加一段校验把异常样本挑出来# 校验导出完整性文件数、零字节文件、脚本占比 from pathlib import Path root Path(./exported) report [] for d in sorted(root.iterdir()): if not d.is_dir(): continue all_files [f for f in d.rglob(*) if f.is_file()] empty [f for f in all_files if f.stat().st_size 0] scripts [f for f in all_files if f.suffix .as] report.append((d.name, len(all_files), len(empty), len(scripts))) print(f{样本:30s} {文件数:8s} {空文件:8s} {脚本数:8s}) for name, total, empty, scripts in report: flag 检查 if total 0 or empty total * 0.2 else print(f{name:30s} {total:8d} {empty:8d} {scripts:8d}{flag})判读标准自己定我的习惯是文件总数为零直接判失败空文件占比超过两成要去翻对应的export.log脚本数为零而样本明明有交互逻辑说明脚本被挡在了可达性过滤外面回到 3.2 那套方法排查。这段脚本只读不写跑多少次都不影响产物可以放进流水线末尾当固定环节。5. 五类翻车现场排查从闪退到回写失败5.1 双击闪退与版本不匹配现象双击ffdec.bat黑色窗口一闪就没了或者命令行报UnsupportedClassVersionError后面跟一串版本号。原因九成是 Java 环境问题。要么系统里根本没装运行时要么 PATH 上那个java版本低于这个包编译时用的目标版本。机器上装了多个 Java 时尤其容易中招命令行里java -version显示的和你以为的不是同一个。解决先java -version确认主版本低于 8 就装一个 LTS 版本。有多个版本时用绝对路径调用或者临时改JAVA_HOME再启动。闪退问题不要靠双击排查直接在终端里java -jar ffdec.jar跑一遍堆栈会老老实实打出来比猜快得多。5.2 脚本导出为空或类不齐现象面板里 Scripts 节点展开明明有一堆类导出目录里却只有几个.as甚至一个都没有。原因默认导出走的是可达性分析只导出从入口和SymbolClass绑定关系能追溯到的类。被反射调用、被字符串拼接出来的类名、或者压根没注册的死代码类都会被判定为不可达而跳过。另一种情况是 ABC 被拆成了多个DoABC标签某些版本对多块 ABC 的合并处理不完整。解决在导出选项里打开包含未使用/全部脚本这类开关命令行不加-selectclass全量导一遍还是不全的话改用-selectid直接按 character id 把脚本块整个导出来绕开可达性判断。用 P-code 格式再导一次做交叉验证源码导不出来但伪代码能出来说明是反编译阶段的问题不是解析阶段的问题。5.3 中文乱码的两种成因现象一导出的.as文件用编辑器打开中文全是问号或者方块。原因文件本身是 UTF-8 编码编辑器按 GBK 之类的本地编码打开。这种属于显示层问题文件内容是好的。解决把编辑器默认编码统一成 UTF-8别去改文件本身改了就真乱了。现象二从文本节点导出的文字内容本身就是乱码或者缺字。原因这个是真问题。SWF 里的静态文本按字形索引存储字符到字形的映射依赖嵌入的字体轮廓。字体被剥离、或者用了设备字体时字符信息就丢了任何工具都还原不出来。解决先去 Fonts 节点把嵌入字体完整导出来再看文本节点实在拿不到就把对应帧渲染成图片人工对照着敲一遍。别在这上面死磕信息没了就是没了。5.4 图形导出错位与透明丢失现象导出的 PNG 每一张都是对的但拼不回原来的画面布局或者本该透明的图导出来是白底。原因位图标签只存像素数据位置、缩放、旋转全在PlaceObject的变换矩阵里单导位图必然丢布局。透明丢失则是标签类型决定的DefineBitsJPEG2本来就没有 alpha 通道DefineBitsJPEG3的 alpha 是单独一条 zlib 压缩通道某些导出格式会把它丢掉。解决要保布局就导 Sprite 或 Shape 的 SVG变换信息在矢量结构里要版式参考就整帧渲染导出牺牲分辨率换准确度要透明就确认源标签是不是DefineBitsLossless2或DefineBitsJPEG3是的话导 PNG 并检查导出选项里的 alpha 开关。三件事分开做别指望一次导出全拿到。5.5 回写 SWF 后跑不起来现象改了一行字符串保存成功的提示也弹了播放器打开直接黑屏或者保存阶段就报编译错误。原因保存动作会把修改后的脚本重新编译成 ABC走的是工具自带的编译器语法覆盖面有限。命名空间技巧、内联元数据、复杂的闭包结构都可能过不去。还有一种隐蔽情况编译通过了但生成的 ABC 版本号和原文件的 SWF 版本对不上运行时直接拒绝加载。解决改动范围压到最小只碰字符串常量、数值和条件边界保存永远另存为新文件原文件不动编译过不去时退回字节层面做等长替换改完用 3.4 那套解压加strings的流程核对。改完必须实机跑一遍别拿保存成功当验收标准这两件事差得远。6. 进阶用法把反编译结果当半成品而不是答案反编译产物最容易被误用的地方是把它当成可以直接编译的工程。它更像一张标注过的地图路线大致对但路名可能是猜的有些岔路口还被抹平了。我处理过的样本里能直接编译通过的极少能读懂逻辑的占绝大多数而后者的价值其实更高。真正省时间的用法是拿它做差异定位。同一个项目的两个版本各导一次脚本直接 diff 目录改动点会自己跳出来# 两个版本各导一次脚本目录级 diff 比二进制 diff 可读得多 java -jar ffdec-cli.jar -export script ./cmp/v1 ./a.swf java -jar ffdec-cli.jar -export script ./cmp/v2 ./b.swf diff -ru ./cmp/v1 ./cmp/v2 | head -200参数上没什么花样两个输出目录必须分开否则第二次导出会覆盖第一次。head -200是防止大型工程一次刷几千行把终端冲爆先看头部定位改动集中的包再针对单个文件细看。这套流程比拿十六进制工具对着两个 SWF 硬比要快一个数量级也比逐帧看画面变化靠谱。本文还有配套的精品资源点击获取