使用JPEXS FFDec逆向分析SWF文件中的自定义加密算法与密钥生成
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的核心逻辑由ActionScript(AS)编写,主要有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的搜索框中(Ctrl+F),尝试搜索以下关键词:
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的密钥。
- 步骤1: 将种子转换为16进制字符串,并加上前缀
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) & 0xFF):- i=0:
(0x49 + 0 + 0x37) & 0xFF = 0x80 - i=1:
(0x4E + 1 + 0x37) & 0xFF = 0x86 - ... 以此类推,得到新数组。
- i=0:
- 用魔数
[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无法直接打开。
- 尝试其他工具: 用
swfdump(Flex 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.parameters(URL参数)。 |
| 算法逻辑复杂,难以人工还原 | 算法包含大量位运算和状态机。 | 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 = f"INIT_{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提供了绝佳的静态起点,但它给出的代码只是“文本”,如何将这些文本还原为“意图”和“逻辑”,则需要你对程序语言、常见加密模式和安全编码实践有足够的理解。每一次成功提取出密钥,就像是解开了一个尘封的谜题,那种成就感,正是安全分析工作最吸引人的地方之一。记住,无论技术如何变迁,这种抽丝剥茧、直面核心逻辑的分析能力,永远不会过时。
