
1. 项目概述当Flash已成往事安全分析却从未停止如果你在十年前问我怎么分析一个Flash小游戏里的加密逻辑我可能会直接告诉你用Sothink SWF Decompiler或者Trillix。但今天当Adobe Flash Player已经彻底退出历史舞台那些曾经遍布互联网的.swf文件却并没有完全消失。它们可能沉睡在某个老旧的课件光盘里躺在某个企业遗留的内部培训系统中或者更值得警惕的被一些别有用心的人重新打包利用其复杂的交互逻辑和曾经普遍存在的安全漏洞进行一些不那么光彩的操作。我最近就遇到了这样一个案例一个声称是“经典小游戏合集”的.swf文件在运行时行为异常怀疑其内部嵌入了后门或加密通信模块。要搞清楚它到底在做什么第一步就是把它“拆开”看看。这就是JPEXS Free Flash Decompiler (FFDec)登场的时候。它是一个开源、免费且功能强大的反编译工具堪称分析遗留Flash内容的“瑞士军刀”。但我们的目标不仅仅是看到它的图形和代码更是要深入其骨髓——提取和分析其中可能存在的自定义加密算法并尝试还原其密钥生成过程。这听起来像是安全研究或逆向工程的活儿没错。对于安全工程师、数字取证人员甚至是那些想要从老项目中抢救出核心逻辑的开发者来说掌握这套方法都至关重要。它不仅能帮你理解恶意代码的行为也能让你在需要对一些“黑盒”Flash组件进行安全审计时有路可循。2. 核心思路与工具选型为什么是FFDec面对一个.swf文件我们有很多选择。早期商业工具如Sothink功能强大但已停止更新且可能涉及版权在线反编译服务则存在隐私和安全风险。JPEXS FFDec之所以成为首选基于以下几个核心考量2.1 开源与免费带来的深度可塑性FFDec是开源的Java应用程序。这意味着首先它完全免费没有任何功能限制或水印。其次开源特性允许我们在必要时审查其代码甚至进行修改以适应特殊需求。例如如果遇到某种非标准的SWF压缩格式理论上我们可以修改其解析器来支持。这对于深度安全分析来说是一个巨大的优势。2.2 对ActionScript的卓越支持SWF的核心逻辑由ActionScriptAS编写主要有AS1/AS2基于ECMAScript 3和AS3基于AVM2虚拟机更接近现代JavaScript两个大版本。FFDec对这两者的反编译能力都非常出色。它不仅能将字节码Bytecode反编译成可读性很高的源代码还能进行一定的代码优化和重命名这对于分析经过混淆的代码至关重要。相比之下许多老旧工具对AS3的支持很差或者反编译出的代码充斥着难以理解的临时变量名。2.3 全方位的资源导出能力一个SWF文件不只有代码还有图像、声音、字体、二进制数据BinaryData等资源。恶意逻辑可能会将密钥或配置信息隐藏在某个不起眼的图片字节里或者将一段加密的Shellcode放在二进制数据标签中。FFDec可以完整地导出所有这些资源为我们进行静态分析提供了完整的素材库。2.4 脚本化与自动化潜力FFDec提供了命令行接口和基本的脚本支持通过其内置的插件系统或外部调用。当我们需要批量分析成百上千个SWF文件时自动化提取关键代码段或资源就成为可能。这在威胁情报分析和大规模遗产系统审计中非常有用。注意虽然FFDec很强大但它并非万能。对于使用了高强度商业代码混淆器如SecureSWF的SWF反编译出的代码可读性依然会大打折扣可能需要结合动态分析如使用调试器来理解逻辑。3. 实操流程一步步拆解SWF定位加密逻辑理论说再多不如动手做一遍。假设我们手头有一个名为suspicious_game.swf的文件。以下是完整的静态分析流程。3.1 环境准备与初步探查首先从JPEXS官网下载最新版的FFDec。它是一个可执行的JAR文件在装有Java运行环境的电脑上直接双击即可运行。 打开FFDec将suspicious_game.swf拖入主窗口。界面主要分为三个部分左侧是文件树状结构中间是主视图代码、十六进制等右侧是属性面板。左侧树状图会立即展示SWF的内部结构DoABC标签 这是存放ActionScript 3字节码的核心标签。通常一个SWF会有多个DoABC标签对应不同的类或脚本。SymbolClass 定义了导出资源的关联关系。FileAttributes 文件属性。各种DefineBits,DefineSound等 资源定义。我们的首要目标是找到代码。直接展开DoABC标签你会看到以包Package和类Class形式组织的反编译代码。FFDec通常会自动尝试将入口点命名为MainTimeline或类似名称。3.2 定位加密相关代码的线索在成千上万行代码中盲目寻找加密逻辑如同大海捞针。我们需要一些线索搜索特定字符串 在FFDec的搜索框中CtrlF尝试搜索以下关键词encrypt,decryptcipher,CryptoAES,DES,RC4,XOR常见的算法名即使自定义算法也可能引用这些类名key,iv(Initialization Vector, 初始化向量)getKey,generateKeyByteArray(ActionScript中处理二进制数据的主要类加密操作必然涉及它)关注网络通信相关类 加密常为了通信。查找URLLoader,URLStream,Socket,XMLSocket等类的使用。查看它们的dataFormat通常是BINARY以及load,send方法调用附近的数据处理逻辑。分析二进制数据操作 在AS3中ByteArray类的writeByte,readByte,position,length属性以及deflate/inflate压缩方法周围常常是自定义编码/解码逻辑的位置。查看导入Imports 在类的顶部查看import语句。如果看到flash.utils.ByteArray,flash.net.URLLoaderDataFormat或者一些第三方加密库的类路径如com.hurlant.crypto.*那么加密功能存在的可能性就大大增加。假设我们搜索encrypt在com.game.utils.CryptoUtils类中找到了一个encryptData(data:ByteArray, key:String):ByteArray方法。这就是我们的突破口。3.3 深入分析密钥生成函数找到加密函数后顺藤摸瓜找到密钥生成部分。通常密钥生成有两种方式硬编码Hard-coded 密钥直接以字符串或数组形式写在代码里。// 示例简单的硬编码密钥 private static const STATIC_KEY:String MySecretKey123!;这种情况最简单在反编译的代码中直接就能看到。但这也最不安全所以稍微有点安全意识的开发者都不会这么干。动态生成Dynamic Generation 密钥通过一个函数实时计算出来。这个函数就是我们的核心分析目标。 我们可能在CryptoUtils类中找到一个generateKey(seed:int):String或getServerKey():ByteArray方法。 动态生成可能基于固定算法固定盐值Salt 例如对字符串“defaultPassword”进行MD5哈希。与环境相关的信息 例如结合用户的某个ID、当前时间new Date().getTime()、SWF文件自身的某些属性如loaderInfo.url或外部加载的配置文件。来自服务器的响应 通过一次HTTP请求从服务器获取密钥或密钥种子。我们需要仔细阅读generateKey函数的每一行代码。FFDec反编译的代码可能包含一些冗余的临时变量需要耐心梳理其核心逻辑。例如它可能将几个字符串拼接然后进行循环位移和异或操作最后取一个子串。3.4 导出关键资源与代码为了进一步分析或在独立环境中测试算法我们需要将关键部分导出。导出ActionScript代码 在左侧树状图中右键点击包含目标类的DoABC标签或包选择“导出”。可以导出为.as文件或.json项目。我通常导出为.as方便用文本编辑器或IDE查看。导出二进制资源 如果发现密钥或初始向量被存储在某个图片或二进制数据标签中右键点击该资源选择“导出”保存为文件。之后可以用十六进制编辑器或编程语言进一步分析这些二进制文件。4. 案例拆解一个虚构的“混合”加密算法分析让我们虚构一个在suspicious_game.swf中发现的、相对复杂的场景。假设在CryptoUtils类中我们看到了如下反编译后的关键代码片段经过整理和简化package com.game.utils { import flash.utils.ByteArray; import flash.net.URLLoader; import flash.net.URLRequest; import flash.events.Event; public class CryptoUtils { private var _sessionSeed:int; public function CryptoUtils(seed:int) { this._sessionSeed seed; } // 密钥生成函数 public function generateDynamicKey():ByteArray { var baseKey:String INIT_ this._sessionSeed.toString(16); // 例如: INIT_5a3f var keyBytes:ByteArray new ByteArray(); keyBytes.writeUTFBytes(baseKey); // 第一轮变换简单的字节循环加盐 for (var i:int 0; i keyBytes.length; i) { keyBytes[i] (keyBytes[i] i 0x37) 0xFF; // 0xFF 确保结果在0-255 } // 第二轮变换与一个内置的魔数数组进行XOR var magic:Array [0x12, 0x34, 0x56, 0x78, 0x9A]; for (i 0; i keyBytes.length; i) { keyBytes[i] ^ magic[i % magic.length]; } // 取前16字节作为AES-128的密钥 keyBytes.length 16; // 截断 keyBytes.position 0; return keyBytes; } // 加密函数假设使用CBC模式 public function encryptCBC(plainData:ByteArray, iv:ByteArray):ByteArray { var key:ByteArray this.generateDynamicKey(); // ... 这里会调用AS3内置的crypto库或第三方库进行AES加密 // 例如var cipher:ICipher Crypto.getCipher(aes-cbc, key, new PKCS5Padding()); // cipher.encrypt(plainData); // 返回加密后的ByteArray return encryptedData; } } }4.1 算法逻辑还原密钥种子Seed来源_sessionSeed通过构造函数传入。我们需要在整个SWF中搜索new CryptoUtils(...)的地方看这个seed是什么。它可能是一个随机数也可能是从URL参数解析出来的用户ID。密钥生成流程步骤1 将种子转换为16进制字符串并加上前缀“INIT_”构成baseKey。步骤2 将字符串转换为字节数组keyBytes。步骤3变换1 对每个字节加上其索引i和一个固定值0x37然后取模256 0xFF。这是一个可逆的线性操作。步骤4变换2 将结果与一个固定的5字节魔数数组进行循环XOR。XOR操作也是可逆的。步骤5 最终将字节数组截断为前16个字节作为AES-128的密钥。4.2 逆向推导与密钥提取如果我们知道了_sessionSeed的值就可以完全复现这个密钥生成过程。例如假设我们通过动态调试或日志拦截发现某次通信时_sessionSeed 23087十进制。计算baseKey:“INIT_” (23087).toString(16).toUpperCase()“INIT_5A2F”。字符串“INIT_5A2F”的UTF-8字节数组为[0x49, 0x4E, 0x49, 0x54, 0x5F, 0x35, 0x41, 0x32, 0x46]。进行变换1(byte i 0x37) 0xFFi0:(0x49 0 0x37) 0xFF 0x80i1:(0x4E 1 0x37) 0xFF 0x86... 以此类推得到新数组。用魔数[0x12, 0x34, 0x56, 0x78, 0x9A]循环XOR。取前16位本例不足16位实际算法可能会填充这里仅为演示。我们可以用Python快速写一个脚本来模拟这个过程从而得到密钥。这就是从SWF中提取加密算法密钥生成逻辑的终极目的将静态的代码转化为可执行、可验证的密钥生成器。5. 高级技巧与疑难问题排查在实际操作中绝不会总是一帆风顺。下面分享几个我踩过的坑和对应的解决技巧。5.1 遇到代码混淆怎么办混淆后的代码变量名、函数名都变成了a,b,c1,func2这种无意义的字符。策略1 控制流分析。忽略变量名关注代码结构。寻找典型的循环for,while、条件判断if和数组操作。加密算法中常有对字节数组的遍历操作。策略2 常量提取。混淆通常不会改变字符串和数字常量。在FFDec中搜索所有字符串常量特别是那些看起来像URL、路径、固定密钥前缀的和大的数字数组可能是S盒、置换表或魔数。这些是重要的锚点。策略3 动态调试辅助。使用老版本的Flash Player调试器如Flash Player Debugger 32配合调试工具如FlashTracer或自己编写的代理在运行时下断点观察内存中的真实数据和函数调用栈。这能帮你将混淆的静态代码和动态行为对应起来。5.2 算法使用了第三方加密库如 as3crypto如果反编译代码中看到大量import com.hurlant.crypto.*说明它使用了成熟的as3crypto库。这是好事也是坏事。好处 算法是标准的无需逆向算法本身只需找到密钥和模式如AES-256-CBC。挑战 密钥和IV的传递方式可能被封装。你需要找到调用Crypto.getCipher()的地方追踪传入的key和iv参数是如何来的。它们可能来自我们之前分析的generateDynamicKey()也可能来自一个硬编码的ByteArray。5.3 SWF自身被加密或压缩有些SWF会使用非标准的加密或压缩方式保护自身导致FFDec无法直接打开。尝试其他工具 用swfdumpFlex SDK的一部分或Flare命令行工具尝试解析文件头看是否能识别。手动分析文件头 用十六进制编辑器打开SWF。标准的未压缩SWF以FWS开头压缩的以CWS开头。如果开头是乱码可能被异或加密。尝试寻找规律或者搜索残留的FWS/CWS标记它们可能出现在文件中部。内存转储 最后一招让一个干净的Flash Player加载这个SWF然后在内存中搜索解密后的SWF镜像。这需要较高的逆向工程技巧。5.4 常见问题速查表问题现象可能原因排查思路与解决方案FFDec打开SWF一片空白或报错1. 文件损坏。2. 非标准/加密的SWF。3. FFDec版本过旧。1. 用十六进制编辑器检查文件头前3字节应为46 57 53(FWS)或43 57 53(CWS)。2. 尝试更新到FFDec最新版。3. 搜索是否有针对该SWF的特定解包工具。反编译出的代码全是乱码或function #1()代码被高强度商业混淆器处理过。1. 聚焦于字符串和数字常量。2. 尝试使用FFDec的“简化表达式”功能如果可用。3.必须结合动态调试在运行时观察真实逻辑。能找到加密函数但找不到密钥生成调用密钥可能来自外部网络、本地共享对象LSO、其他SWF。1. 全局搜索SharedObject.getLocal。2. 搜索所有URLLoader的complete事件处理函数看其返回数据如何被处理。3. 检查loaderInfo.parametersURL参数。算法逻辑复杂难以人工还原算法包含大量位运算和状态机。1. 将关键函数代码导出为文本。2. 使用脚本语言Python/JS逐行翻译该函数进行“白盒复现”。3. 在复现过程中添加大量日志输出每一步的中间值与动态调试结果对比验证。导出的资源如图片看起来是加密的资源在嵌入SWF前已被加密。1. 分析负责加载/显示该资源的ActionScript代码附近必然有对应的解密函数。2. 将资源二进制数据作为输入尝试调用找到的解密函数需在ActionScript环境或复现的算法中。6. 从分析到应用构建本地密钥提取工具当我们成功逆向出密钥生成算法后最好的验证方式就是将其实现为一个独立的小工具。这不仅证明了分析的正确性也为后续的批量测试或解密数据提供了便利。以第4节的虚构算法为例我们可以用Python实现一个密钥提取器#!/usr/bin/env python3 根据逆向出的CryptoUtils.generateDynamicKey逻辑生成的密钥提取工具 def generate_dynamic_key(session_seed: int) - bytes: 模拟ActionScript中的generateDynamicKey函数 # 步骤1: 生成baseKey字符串 base_key fINIT_{session_seed:04X} # 转为4位大写十六进制模拟AS的toString(16) print(f[*] Base Key String: {base_key}) # 步骤2: 转换为字节数组 (UTF-8编码) key_bytes bytearray(base_key.encode(utf-8)) print(f[*] UTF-8 Bytes: {list(key_bytes)}) # 步骤3: 第一轮变换 (加索引和0x37) for i in range(len(key_bytes)): key_bytes[i] (key_bytes[i] i 0x37) 0xFF print(f[*] After Transformation 1: {list(key_bytes)}) # 步骤4: 第二轮变换 (循环XOR魔数) magic [0x12, 0x34, 0x56, 0x78, 0x9A] for i in range(len(key_bytes)): key_bytes[i] ^ magic[i % len(magic)] print(f[*] After Transformation 2 (XOR): {list(key_bytes)}) # 步骤5: 截取前16字节作为AES-128密钥 aes_key bytes(key_bytes[:16]) # 如果不足16字节这里可以根据原算法逻辑进行填充本例假设算法会填充或上下文保证长度。 print(f[*] Final AES-128 Key (hex): {aes_key.hex().upper()}) return aes_key if __name__ __main__: # 假设我们从日志或调试中获取到的session seed test_seed 23087 # 对应十六进制 5A2F print(f[] Generating key for session seed: {test_seed} (0x{test_seed:04X})) final_key generate_dynamic_key(test_seed) print(f[] Extracted Key: {final_key})这个脚本完美复现了SWF中的逻辑。运行它我们就能得到用于AES解密的密钥。接下来我们可以用这个密钥配合从网络流量中截获的加密数据和IV初始化向量使用标准AES库如Python的cryptography来尝试解密实际通信内容从而完成整个“分析-提取-验证”的闭环。最后一点心得分析这类遗留的、带有自定义保护的Flash内容七分靠耐心细致的静态代码分析三分靠脑洞大开的逻辑推理和验证。FFDec提供了绝佳的静态起点但它给出的代码只是“文本”如何将这些文本还原为“意图”和“逻辑”则需要你对程序语言、常见加密模式和安全编码实践有足够的理解。每一次成功提取出密钥就像是解开了一个尘封的谜题那种成就感正是安全分析工作最吸引人的地方之一。记住无论技术如何变迁这种抽丝剥茧、直面核心逻辑的分析能力永远不会过时。