ffdec反编译SWF实战:ActionScript还原与资源回写 简介FFDec 18.5.0JPEXS Free Flash Decompiler是一款基于 Java 的开源 Flash 反编译软件面向需要剖析 SWF 文件结构、导出素材、查看或修改 ActionScript以及维护旧 Flash 项目的开发者、逆向工程学习者与教学人员。它可将二进制 SWF 还原为源代码与图形资源支持音频、图像、动画、字体等资源提取并提供代码查看编辑、版本与帧率等元数据分析对旧项目排错、教学演示和逆向研究较有价值。压缩包共 62 个文件约 13.51MB以 30 个 jar 核心库及第三方依赖为主配合 exe、bat、sh 提供多平台启动入口另有 txt 许可证、swc 运行库、xml 元信息、md 变更日志与系统图标等辅助文件结构清晰方便按功能定位。已有 1058 人学习适合希望快速搭建反编译环境、理解工具组成和跨平台使用方式的读者参考。1. 解压就能跑的反编译器ffdec 到底能拆开什么上周有个做课件的朋友拿来一个 6MB 的.swf说源工程随旧硬盘一起没了现在甲方只想改三处文案、换两张图、把一个按钮的跳转改掉。我解压了ffdec-18.5.0.zip双击启动脚本从打开文件到把 ActionScript 源码和全部图片导出到本地目录前后不到五分钟。ffdec 就是干这行的把 SWF 这个打包好的二进制容器一层层拆开让你看见里面的脚本、图形、精灵、音频、字体和二进制块改完还能塞回去。它适合三类人手上只有 SWF 没有源码、需要修复或二次改动的维护者需要批量提取素材做归档和比对的资源整理者以及想搞清楚某个老交互文件到底怎么跑起来的排查者。反编译出来的代码不是原始工程变量名、注释、代码组织方式都回不去这一点必须先说清楚后面所有操作都建立在这个前提上。2. 环境与首次打开JRE 选型、内存参数和文件树读法拿到压缩包先别急着双击。ffdec 是 Java 写的 Swing 应用包里的启动脚本只是去找系统里的 Java 运行时运行时不对界面能开但导出会莫名其妙地失败。2.1 JRE 选哪一版为什么不建议一味追新我的习惯是配一个 Java 8 或 Java 11 的 LTS 运行时专供这类老工具的图形界面使用。原因不玄学Swing 在较新的 JDK 上改过几轮 HiDPI 缩放和 GTK 主题适配某些 Linux 桌面环境下会出现字体发虚、下拉框点不开、文件选择框卡住的问题。这类现象不影响核心逻辑但会让你误以为是文件本身有问题白白浪费半天。Windows 上装完 JDK 后确认JAVA_HOME指向正确目录然后直接运行ffdec.batLinux 和 macOS 用ffdec.sh记得先chmod x。如果机器上有多个 Java 版本不想改全局环境变量可以临时指定# Linux / macOS临时指定运行时避免污染全局 JAVA_HOME export JAVA_HOME/usr/lib/jvm/java-11-openjdk-amd64 export PATH$JAVA_HOME/bin:$PATH ./ffdec.sh:: Windows只在当前命令行窗口生效 set JAVA_HOMEC:\Program Files\Java\jdk-11 set PATH%JAVA_HOME%\bin;%PATH% ffdec.bat这两段的意义在于把运行时和工具绑定在同一个会话里不动系统级配置。参数上唯一要记的是JAVA_HOME必须指向 JDK/JRE 的根目录而不是bin目录指错了启动脚本会直接报找不到主类。macOS 上如果提示无法验证开发者在系统设置里放行一次即可别去关全局安全策略。2.2 内存参数默认堆往往不够拆大文件默认堆大小对十几兆的 SWF 够用但遇到带大量嵌入音频或位图的文件导出到一半抛OutOfMemoryError是常事。启动脚本一般都留了改堆的口子改之前先备份原文件# Linux/macOS 的 ffdec.sh 里通常有一行 JAVA_OPTS把它改成显式指定 JAVA_OPTS-Xms256m -Xmx2048m -Dfile.encodingUTF-8:: Windows 的 ffdec.bat 同理找到 set JAVA_OPTS 那一行改成 set JAVA_OPTS-Xms256m -Xmx2048m -Dfile.encodingUTF-8-Xms是初始堆给 256MB 减少反复扩容-Xmx是上限2048MB 对绝大多数课件和小游戏足够再大就要看机器物理内存了。-Dfile.encodingUTF-8这一项容易被忽略但在中文 Windows 上不加导出的脚本文件名和字符串常量可能出现乱码尤其是 AS2 时代的老文件。改完重启界面上不会显示堆大小验证方式是打开一个大文件连续导出两三轮不报内存错误即为生效。2.3 从 SWF 到树状结构标签、类与资源的映射打开一个 SWF 后左侧是一棵树。根节点是文件本身下面按标签类型分组Scripts、Sprites、Shapes、Images、Sounds、Fonts、Texts、BinaryData、Frames等。理解这棵树的关键是明白 SWF 的本质它是一串带类型编号的标签Tag每个标签有自己的 characterId运行时按帧序把这些标签实例化到显示列表上。所以你在Shapes里看到的编号和Sprites里某个精灵引用的是同一个 characterId。改一个图形引用它的所有精灵都会跟着变反过来同一个图形被多个精灵复用你只想改其中一个就得先把它拆成独立标签再替换。这套 ID 引用关系是后面所有导出和回写操作的底层逻辑先在树上点几个节点看看右侧的属性面板里Character ID和引用关系比看任何文档都快。2.4 首屏四个面板的读法和一次完整的打开流程界面默认分四块左边树、右上反编译代码区、右下预览/十六进制区、底部日志区。日志区是黑匣子导出失败、字体缺失、标签解析异常都会在这里打出来很多人只看弹窗不看日志结果在同一个坑里反复翻车。一次完整的打开流程我一般这么走先看日志区有没有警告再看树的根节点属性里的 SWF 版本号和文件尺寸然后点开Scripts确认脚本标签数量和语言代次最后随便点一个Shapes看预览能否正常渲染。四步下来基本能判断这个文件是干净的原生 SWF还是被加过壳或混淆过的。如果Scripts下有大量名字类似_fla_开头的类或者类名全是Class_1、Class_2这种说明原始类名已丢失后面反编译出来的代码可读性会很差心理预期要先调整。3. 反编译 ActionScriptAS1/AS2/AS3 的还原度差异与 P-code 兜底同样叫反编译AS1/AS2 和 AS3 的难度完全不在一个量级。前者是 AVM1 虚拟机上的字节码结构简单还原度高后者跑在 AVM2 上有命名空间、闭包、泛型擦除后的痕迹反编译器要在还原成可读代码和保持语义正确之间做取舍。3.1 AVM1 与 AVM2 的分流先判断文件属于哪一代判断依据有两个SWF 文件头里的版本号以及是否存在DoABC标签。版本号小于 9 的基本是 AVM19 及以上可能混用真正的分水岭是有没有 ABC 字节码块。在树里展开Scripts如果子节点直接就是DoAction或DoInitAction这类标签那是 AVM1如果看到DoABC就是 AVM2。这个判断决定了你后面看代码的方式。AVM1 的代码往往以帧脚本形式散落在各个Frame上onClipEvent、on(release)这类写法会还原成事件块逻辑碎但直白。AVM2 的代码集中在类定义里一个类几百行很常见入口是文档类或者时间轴上的MainTimeline。我见过有人拿着 AVM2 的文件在找帧上的onClipEvent翻遍了也找不到就是因为没先做这一步分流。3.2 反编译选项怎么调自动去混淆、常量折叠与异常块还原设置里的反编译选项直接决定输出质量几个值得动的自动去混淆把Class_1这类名字按引用关系重排成有意义的短名、常量折叠把编译期算好的字符串和数字还原、内联函数展开、try/catch块还原、switch语句还原。默认配置偏保守还原出来的代码更接近字节码原貌想读得顺一点把去混淆和switch还原打开。这里有个取舍要说清楚去混淆只是让你读得舒服它不会告诉你原来叫什么名字。重排出来的名字和原始工程里的名字没有任何对应关系改完代码回写时也不会影响运行但如果你要和另一份反编译结果做 diff两次使用的选项必须完全一致否则 diff 出来的全是噪音。我一般会在项目目录里放一个文本文件记录这次用的选项组合下次比对前先对一遍。3.3 导出源码到本地工程目录界面里可以逐个类右键导出但文件一多就不现实。用命令行按类型批量导出更靠谱# 导出所有脚本按 AS 源码形式落盘脚本类型后缀决定输出格式 java -jar ffdec.jar \ -export ./out \ script:as \ ./target.swf-export后面依次是输出目录、导出类型、输入文件。script:as表示脚本按.as文本导出如果写成script:bin就是原始字节码。导出后的目录结构基本对应包路径带命名空间的类会落成多级文件夹。这一步之后我一般立刻做两件事git init提交一次原始导出作为基线以及统计文件数量和总行数对不上就说明有脚本没导出来。提示不同版本的子命令和类型后缀会有增减落脚本之前先跑一遍-help把当前版本的类型列表对一遍别拿旧笔记直接套。3.4 P-code 视图什么时候比 AS 源码更可信反编译器再强也是猜。遇到下面几种情况直接切到 P-code 视图看字节码指令更省时间反编译结果里有大段Unknown或者语法不完整的片段某个函数逻辑看起来对但运行行为不对怀疑有代码被刻意构造过导致控制流还原错误。P-code 视图里能看到每条指令、操作数栈的变化和跳转目标。读起来确实累但它是唯一不会骗你的视图。我的习惯是先用 AS 视图定位到出问题的函数再切到 P-code 对照着看两三屏确认分支条件和常量值最后回到 AS 视图手工修正。这个过程有点像对照汇编调 C 代码熟练之后定位速度会快很多。4. 资源导出与回写图片、音频、字体、二进制的处理链代码只是 SWF 的一半另一半是素材。这一章的处理链是导出—修改—回写三步每一步都有边界。4.1 批量导出类型后缀与命名规则导出类型用冒号分隔格式常用的组合如下导出类型含义输出格式备注script:asActionScript 脚本.as文本保留包路径目录结构image:png位图.png带透明通道的按原样导出shape:svg矢量形状.svg复杂形状可能丢失滤镜效果sprite:png精灵合成图.png按首帧合成多帧精灵只出一张sound:mp3音频.mp3或.wav事件声与声音流分开处理font:ttf嵌入字体.ttf字形可能已被裁剪binaryData二进制数据块原始格式常用来取配置文件text静态文本.txt只出文本内容不带排版一条命令可以同时导出多种类型用逗号连接。文件名默认是类型 characterId例如DefineSprite_12、DefineShape_5。这个命名很重要回写时要靠它反查 ID别手动改名。# 一次导出脚本、位图、音频、字体、二进制五类资源 java -jar ffdec.jar \ -export ./out \ script:as3,image:png,sound:mp3,font:ttf,binaryData \ ./target.swf这条命令里script:as3的as3是针对 AVM2 的格式提示AVM1 的文件用as即可。导出目录会按类型分文件夹重复导出前先清空输出目录否则旧文件残留会让你误判某张图有没有更新。4.2 图形与精灵shape、sprite、movieclip 的导出边界Shape是单个矢量图形Sprite是带时间轴的容器MovieClip在导出层面和Sprite基本等价。导出精灵时最常见的误区是以为会导出全部帧实际上多数情况下只合成首帧。要拿某一帧的画面得在预览面板里切到对应帧再单独导出。另一个边界是滤镜和混合模式。投影、模糊、发光这些效果在 SWF 里是运行时计算的滤镜标签导出成 SVG 或 PNG 时部分效果会丢失导出的图和实际播放效果不一致是正常的。如果你的目标是改一张图正确做法是导出原始位图或基础形状改完后按原尺寸替换回去而不是试图把合成后的效果图直接塞回去那样尺寸和锚点几乎必然对不上。4.3 音频与字体事件声、声音流与嵌入字体的替换音频分两类事件声DefineSound是完整的一段音频播放时整体触发声音流SoundStream是按帧切片的音频数据和时间轴严格对齐。替换事件声相对自由换成同样或相近时长的文件即可替换声音流必须保持切片边界长度差太多会导致音画不同步这种问题在播放器里表现为越播越偏排查起来很费时间。字体是另一个高频改动点。嵌入字体在 SWF 里以字形轮廓存储为保证体积原始文件往往只保留了用到的那些字。你把字体导出来会在本地生成.ttf但它是裁剪版用它去排版新文案缺字会显示成方框。我一般会直接换成完整的同名字体文件再回写代价是文件体积增加收益是新加的文案不会掉字。4.4 回写 SWF替换后重新打包的完整流程回写的核心思路是按 characterId 替换而不是重新生成整个文件。命令行的替换子命令大致是这个形态# 把 ID 为 5 的位图替换成本地新图输出到新文件不动原始文件 java -jar ffdec.jar \ -replace ./target.swf ./target_patched.swf \ image 5 ./new_icon.png参数顺序是输入 SWF、输出 SWF、资源类型、characterId、新文件。类型要和原始资源的类型一致拿 PNG 去替换一个矢量Shape是不行的得先确认树上那个节点的实际类型。输出文件必须另存直接覆盖原始文件在出错时没有后悔药。批量替换就写循环把ID 到新文件的映射做成清单脚本读一行执行一次。每替换一批就开一次预览确认画面没崩别一口气全替完再验证那样出问题无法定位是哪一条导致的。回写完成后用独立的播放器打开验证别只在 ffdec 自带预览里看它的容错比真实播放器宽。5. 避坑清单五类翻车现场与排查顺序这一节全部来自实际踩过的坑按发生频率排序。每条都按现象—原因—解决写遇到对应症状直接对号入座。5.1 文件打不开、一开就白屏或直接闪退现象双击 SWF 后界面卡住或者树只加载了一半右侧一片空白。原因三种可能。一是 Java 堆不够大文件在解析标签阶段就 OOM日志里会有OutOfMemoryError二是文件本身被加了壳文件头被改写或者结构被故意破坏解析器读到非法标签长度就中断三是 SWF 版本过高当前版本的解析器不认识新增的标签类型。解决先看日志区OOM 就按 2.2 节调大-Xmx重开日志里出现unknown tag或标签长度异常基本可以判定是壳或损坏这时用十六进制视图看文件头的前几个字节正常的 SWF 以FWS、CWS或ZWS开头不是这三个就说明被改过。确认是压缩格式的话工具本身能处理如果是自定义加密那就超出反编译器的能力范围了别硬啃。5.2 反编译出来的代码缺分支、变量名全丢现象一个明显的if/else只剩单边循环体缺失所有局部变量都是_loc_1、_loc_2。原因_loc_N是字节码里局部变量槽位的默认命名原始名字在编译时已经被丢弃这是不可逆的信息损失不是你操作错了。缺分支则通常是控制流还原失败常见于用switch加跳转表实现的复杂条件或者被混淆器打散过的代码。解决变量名只能靠人工根据上下文重命名没有捷径。缺分支就切到 P-code 视图按跳转指令手工补全逻辑再对照不要指望换个选项就能全自动修好。混淆代码的还原有个明确边界能把控制流理清、能让文件重新跑起来但还原不到原始工程的样子接受这一点能省下很多无效尝试。5.3 替换资源后文件变大、播放错位现象替换一张图之后 SWF 体积从 6MB 涨到 20MB或者画面元素整体偏移、按钮点不到。原因体积暴涨一般是替换进去的图片没压缩原始 SWF 里的位图可能用了 JPEG 或带调色板的压缩格式你塞进去一张无损 PNG体积自然翻几倍。画面偏移则是尺寸或锚点不一致SWF 里图形的坐标原点、缩放矩阵和导出图的实际像素尺寸是两套东西直接换图会破坏原有的变换矩阵。解决体积问题在替换前把新图压到和原图接近的尺寸和格式优先保持宽高比不变。偏移问题要记录原图的宽高和变换参数新图按相同比例缩放后再替换替换完立刻在预览里对齐检查。这类问题一次只改一个资源改完就验批量改完再查根本分不清是哪一张出的问题。5.4 混淆与加密壳的还原边界现象类名全是Class_1到Class_800字符串常量被拼成一堆String.fromCharCode调用代码里到处是永远为真的条件判断。原因这是典型的商业混淆器产物做法是把标识符重命名、把字符串拆散、插入无效分支。它的目的就是让静态分析变难反编译器只能忠实还原字节码语义不会替你去混淆。解决先用自动去混淆把类名按引用关系重排能读之后再针对字符串做手工还原——把fromCharCode的连续调用在编辑器里批量替换成字面量。这一步用正则配合脚本比手工快得多。要提醒的是去混淆后的代码只适合阅读和定位问题如果要回写改动越少越好改多了极易引入语义偏差。碰到明确加了完整性校验的文件改完回写大概率直接跑不起来这时候应该评估是否值得继续而不是死磕。5.5 大文件 OOM 与导出卡死现象导出跑到一半进度条不动或者弹出内存错误后界面无响应。原因导出是单线程逐标签处理的遇到超大位图或者几千个标签的文件堆内存峰值会远超文件本身大小。另外输出目录在网络盘或机械盘上时大量小文件写入也会让进度看起来像卡死。解决先加堆-Xmx给到物理内存的一半左右再把输出目录换到本地 SSD如果还是慢就分类型导出先出脚本再出图片最后出音频一次只处理一类内存压力会小很多。命令行模式下加-onerror ignore让单条失败不中断整体流程跑完再根据日志里的错误行逐个补。我一般还会在导出前看一眼磁盘剩余空间几百兆的素材导出来加上临时文件空间不够会在最后一步失败白跑一趟。6. 进阶差分对比与一条可复现的资源替换流水线资源替换做多了会发现真正费时间的不是替换本身而是改完之后到底改对了没有。我现在的做法是建一条固定流水线每次改动都走同样的四步靠 diff 而不是靠眼睛判断结果。第一步建立基线。拿到原始文件后先完整导出一次到base/目录提交到版本库。第二步在副本上做替换输出到patched/目录同样完整导出一次。第三步对两次导出做目录级 diff只看差异文件清单。第四步对差异文件逐个确认确认无误后再回写 SWF。# 两次导出结果做结构化对比只输出有差异的文件清单 diff -rq ./base ./patched | sort ./diff_report.txt # 统计各类型资源的改动数量快速判断改动范围是否合理 awk -F/ {print $2} ./diff_report.txt | sort | uniq -c第一段命令输出的是哪些文件不同-q只报差异不打印内容避免几千行输出刷屏。第二段按资源类型聚合如果预期只改了三张图结果image类出现二十条差异那说明某一步批量替换的范围写错了立刻回退重来。这个检查看起来多余但它拦下过我好几次因为通配符写错导致的批量误改。回写阶段的批处理脚本我固定成这个结构映射清单一行一条格式是类型,ID,新文件路径脚本逐行读取并调用替换命令每条执行后检查返回码非零就打印行号并停下。#!/usr/bin/env bash # 按映射清单逐条替换失败即停保留现场便于定位 set -euo pipefail IN_SWF./target.swf OUT_SWF./target_patched.swf MAP_FILE./replace_map.csv # 先复制一份作为替换基底避免第一条就覆盖原始文件 cp $IN_SWF $OUT_SWF while IFS, read -r type cid file; do [ -z $type ] continue echo replacing type${type} id${cid} - ${file} java -jar ffdec.jar -replace $OUT_SWF $OUT_SWF $type $cid $file done $MAP_FILEset -euo pipefail保证任何一条命令失败就终止不会出现改了一半还在继续的状态。IFS,把分隔符设为逗号配合read -r逐字段读入注意映射文件里不能有空格和空行。这里输入输出用同一个文件是刻意的因为-replace是整体重写读完之后才写回不会读到写坏的数据但代价是一旦中途失败输出文件处于半成品状态所以原始文件必须另存一份。这套流程的价值在于可复现。同一份映射清单在新版本文件上重跑一遍结果完全一致不需要回忆上次点了哪些菜单。验证环节也别只靠工具自带预览把回写后的文件放到独立的播放环境里跑一遍交互尤其是按钮和跳转这类逻辑在静态预览里看不出来。从那以后我每次动别人的 SWF都强制走一遍导出基线、改副本、diff 确认、回写验证这四步哪怕只改一个字符也不跳步——跳过的那次往往就是翻车的那次。希望帮到你。本文还有配套的精品资源点击获取