简介:flaming-shame是一款供Java开发者使用的开源反混淆工具,核心价值在于帮助用户恢复经过混淆处理的代码逻辑。该工具通过静态分析和结构图建模的方式,尝试还原被改写的类名、方法名与变量名,因而适用于Java逆向工程、混淆机制研究以及恶意代码分析等场景。整个资源包共包含23个文件,其中18个为Java源码文件,反映了反混淆器的主体实现;此外还提供说明文档、开源许可、版本控制配置,以及lib目录下集成的ASM字节码库和对应源码,包体大小约550KB,便于开发者快速查阅和重新编译。借助这份资源,读者可以深入观察从解析字节码、构造程序结构图到输出近似映射的完整过程,也能学习到混淆与反混淆对抗中的常见思路和工程技巧;已有452人浏览学习,值得正在研究JVM字节码或从事安全分析的开发者参考。需要注意的是,受编译优化、多态调用等因素影响,工具给出的还原结果属于启发式猜测,适合辅助阅读,而非精确恢复原始代码。 被打乱的时候,我习惯先把proguard输出的 mapping.txt 扔进 retrace 脚本。但这招有个硬前提:你手上得有原始 mapping 文件。说白了,很多实战场景根本没有这份文件——要么甲方交接的时候漏给了,要么原始构建机器早就报废了,要么你面对的干脆是国内某个固件里扒出来的 jar,压根没人给你准备映射。这种情况下,传统 retrace 思路直接就废了。
我最近在折腾的就是这么个情况,最后找到一个思路完全不同的工具:flaming-shame,一个纯 Java 实现的反混淆工具,不需要 mapping 文件,靠字节码本身的统计特征和上下文线索,把class a、method b这种面目全非的命名重新还原成可阅读的结构。这篇文章我把它的原理、实战用法和踩过的坑全写了,给同样被混淆 jar 折磨过的人一个参考。
1. 反混淆这件事,为什么不简单
1.1 混淆的本质:不是改个名这么简单
Java 混淆工具其实很少只做“重命名”这一件事。以最常见的 ProGuard 为例,它做的是组合拳:
- 标识符重命名:把
UserService改成a,把getUserInfo()改成b(); - 类结构压缩:把不必要的 public 方法改为 private,把接口合并,甚至删掉调试信息;
- 控制流平坦化:把循环和条件分支改写成
switch + 状态变量的形式,让静态阅读彻底失去逻辑; - 字符串加密:把敏感字符串(SQL、URL、异常信息)先编码后藏进字节码,运行时再解密。
这四种手段里,字符串加密和控制流平坦化是反混淆的“硬骨头”。因为它们丢掉的不是名字,而是程序的结构和语义本身。像String x = decrypt("ab12cd")这种代码,就算你把所有变量名都改回userName,没有算法还原字符串内容,业务逻辑还是没法看。
所以我得先说清楚一个现实:反混淆工具不是在“恢复源码”,而是在“重建可读性”。混淆把源码变成了一堆机器可执行但人类看不懂的字节码,反混淆做的是逆向工程中“让人类重新看懂”的那一步。这两者的差距,就是工具能力的天花板。
1.2 没有 mapping 文件,传统方案直接失效
大部分入门资料教你的反混淆方式,都是围绕 mapping 文件展开的:
- retrace(ProGuard 自带的堆栈反混淆工具);
- R8 生态里的
r8-retrace; - 各种在线粘贴 stacktrace 的平台。
这些工具的本质,是一张原文与混淆名之间的对照表。它们不分析字节码,只做“文本替换”:把堆栈里的a.a.b()替换成com.example.UserService.getUserInfo()。
这套方案的优点是非常快、非常准,因为它有唯一正确答案。但缺点也致命——一旦 mapping 文件丢了,这套体系就全崩了。而现实里 mapping 文件恰恰是最容易丢的东西:CI 服务器重装、第三方 SDK 厂商不提供、或者你接手的项目根本没有持续集成记录……我在一次外采项目里就遇到过,供应商给了个混淆后的 SDK jar,死活要不到 mapping,最后只好对着a.a.b发呆。
在这种情况下,能依靠的只有字节码本身。这也是flaming-shame这类“映射无关反混淆工具”存在的意义。
1.3 flaming-shame 的思路:不给结果,给线索
需要先说明的是,flaming-shame 不会像 retrace 那样给你 100% 准确的源码,它做不到也不需要做到。它的目标是:把从字节码能推断出的语义,以尽量友好的形式重新呈现出来。
具体来说,它做了三件事:
- 解析 class 文件,还原每个类、每个方法的完整调用关系;
- 利用命名统计(
a、b、c这种单字母命名、无意义的a.a.a包名结构)识别“被混淆的标识符”; - 基于上下文启发式信息,给这些标识符打上有意义的标签:比如方法里大量调用了某个字符串解密函数,这个方法就极可能跟配置读取有关;某个类继承了一个已知框架的基类,它就很可能是个业务控制器。
最终输出不像 retrace 那样是“一句话的堆栈替换”,而是一份带注释的类结构清单。你拿它当索引,回到字节码里二次人工分析,比直接面对象形文字要轻松太多。
2. 工具原理拆解:它是怎么“猜”出语义的
2.1 识别“混淆过的名字”的统计特征
反混淆的第一步,是要知道哪些名字是被混淆的。这个判断,flaming-shame 用的是统计学思路。
正常人类的命名习惯是:类名用大驼峰(UserController)、方法名用小驼峰(getOrderList)、常量全大写(MAX_RETRY)。就算偷懒,也会写MyClass、doStuff这种有意义的词根。而混淆器生成的名称往往长这样:
| 混淆结果 | 原名称概率 | 说明 |
|---|---|---|
a.a.a(a.a) | 极低 | 单字母包名+类名,人类不会这么写 |
b.a(int, java.lang.String) | 极低 | 方法名只用一个字母,且大量重载 |
aa.bb.cc.dd() | 极低 | 短字母随机组合,无词根语义 |
_a._b._c | 低 | 部分混淆器用下划线前缀做混淆名 |
flaming-shame 会为每个标识符计算一个“混淆评分”。评分的依据,我翻源码逻辑时注意到核心是这几个参数:
- 名称长度(1–3 个字符的字母组合,得分极高);
- 是否包含语义词根(
get、set、find、User、Service等词根命中则降分); - 包名层级深度与命名规范性;
- 与 JDK / 主流框架类库的名称相似度。
评分超过阈值,就进入待重命名队列。这一步很关键,因为如果判断错了,把正常类名硬改成臆测名字,反而会让代码更难读。所以工具在这个环节默认比较保守,宁可少改,不可错改。
2.2 类依赖图和调用链的语义推断
准确识别出“名字是混淆过的”,紧接着的问题是:那它原本可能是什么意思?
flaming-shame 的核心思路是“从邻居推断自己”。它先完整解析目标 jar 中所有 class 文件的依赖关系,构建一张类调用图,然后针对混淆类做几类推理:
第一类是继承推断。被混淆的类a继承了java.util.concurrent.FutureTask,那a极可能是一个异步任务类,flaming-shame 会建议命名AsyncTaskImpl,同时把它的主要方法标记为run、cancel、get的候选语义。
第二类是注解与接口推断。如果混淆类实现了javax.servlet.Filter,它大概率是一个过滤器;如果类上挂着@RestController注解,那它的方法名大概率是接口路径的 Handler 方法。这些线索虽然不能精确到“这个方法原名叫 getOrderById”,但足够让你从框架语义倒推回业务逻辑。
第三类是字符串常量池线索。这是最有意思的。一个方法里如果频繁操作字符串常量,比如"SELECT * FROM user WHERE id=?",那这个方法八成是数据库访问代码;如果引用了"secret_key"这种名称,那就跟加密配置有关。flaming-shame 会把字符串常量与所在方法做关联,生成“该方法疑似涉及 SQL 操作”“疑似涉及配置读取”这类批注。
2.3 字符串解密与常量池的还原策略
字符串加密是最让逆向者头痛的操作。ProGuard 的-obfuscate-strings和 R8 的实现,都会把字符串变成一个decrypt(byte数组)的调用。反混淆工具厂家常用的处理方式是:尝试在 class 文件里找到解密器入口,挂钩子动态执行,拿到真实字符串。
flaming-shame 的策略比较克制,它不会在 JVM 里动态执行解密逻辑(这有安全风险——你永远不知道那段字节码里埋了什么东西)。它的做法是:
- 静态提取常量池中的字符串字面量;
- 识别明显是 Base64、Hex、自定义 XOR 加密后的密文特征;
- 对可逆加密(Base64、Hex、简单 XOR、位移运算)直接尝试还原;
- 对复杂的密码学算法(AES/DES),尝试搜索 class 文件中硬编码的密钥字节,能找到就解,找不到就在输出中标记
[encrypted string]。
这个设计我一开始觉得太弱,后来真遇到一个恶意样本才明白它是对的。动态解密确实能解更多,但也会把机器搞挂——你永远不该在一个分析工具里执行不可信代码。所以 flaming-shame 的“静态分析为主 + 有限动态辅助”设计,反而更像一个成熟安全工具的做法。
2.4 输出格式与人工介入接口
工具的输出不只是打印一堆“疑似注释”,它还会生成一个rename-suggestions.json,把所有建议改名的类、方法、字段都列出来,附带置信度。这个文件你可以手动编辑:觉得某个类应该叫OrderService,直接改掉名字,再跑一次重命名流程,输出一个干净的普通 jar。
这种设计非常务实——自动推断不可能 100% 对,但你也不需要它 100% 对,你只需要它能给出一个“90% 可用、10% 人工校验”的工作流。
3. 实操:从混淆 jar 到可读结构
3.1 环境准备与工具安装
flaming-shame 本身是一个打包好的可执行 jar,依赖 JDK 11 及以上版本。我自己用的是 JDK 17,实测没问题。安装方式就是拉源码自己编译,这是最稳妥的方式,能确保你和当前版本功能一致。
# 拉取源码 git clone https://github.com/example/flaming-shame.git cd flaming-shame # 用 Maven 打包 mvn clean package -DskipTests # 打包产物在 target 目录下 ls target/flaming-shame-*.jar如果你本地没装 Maven,也可以用项目里自带的 Maven wrapper:
./mvnw clean package -DskipTests编译需要联网拉依赖,第一次会比较久。如果网络环境差,建议直接拉 release 页面的 pre-built jar。
3.2 基础用法:单 jar 文件反混淆
我用一个从某路由器固件里提取出来的services.jar做测试(已经脱敏处理过,只用于技术验证)。真实场景里这种 tar 包里的 jar,厂商基本不会提供 mapping。
java -jar flaming-shame-1.0.jar \ --input services.jar \ --output services-restored.jar \ --min-confidence 0.6几个参数我解释一下:
--input:待处理的 jar 或单个 class 文件;--output:反混淆处理后的输出路径;--min-confidence:置信度阈值,0.6 表示“只有置信度 60% 以上的重命名建议才自动应用”;--generate-suggestions:额外生成 rename-suggestions.json 建议文件;--include-packages:限定只处理特定包名下的类,比如--include-packages com.example.internal。
在实际运行时,它会先把 jar 解包到临时目录,逐个 class 解析、建图、打分,最后把改过名的 class 重新打包成新 jar。第一次跑整个流程大概花了几十秒,对于一个几 MB 的 jar 来说性能是够用的。
运行完在终端里能看到类似这样的输出:
[INFO] Parsing classes: 1284 classes loaded [INFO] Building dependency graph: 24513 edges [INFO] Detected obfuscated classes: 902 (70.2%) [INFO] Applied renames: 1804 methods, 341 fields [INFO] Restored string constants: 156 / 243 [INFO] Output written to services-restored.jar注意它识别出 70% 的类是被混淆过的,但并不是每个类都启用了重命名——因为有些类的名字虽然混乱如a,但没有足够的上下文线索能推断出合理语义,强改反而有害。遇到这种类,工具宁可保留原名。
3.3 验证还原效果:javap 对比法
工具跑完不代表收工,你得验收。我最常用的验证手段是用javap对比处理前后的字节码可读性:
# 混淆前的类信息 javap -p -c services.jar -cp services.jar javap -p com.example.internal.a # 反混淆后的类信息 javap -p -c services-restored.jar -cp services-restored.jar java -cp services-restored.jar ...一对比就能看到显著差异。混淆版本里是a.a(ILjava/lang/String;)Ljava/lang/String;,反混淆后变成ConfigLoader.loadByName(I,Ljava/lang/String;)Ljava/lang/String;。花十分钟翻了几个类之后,配合字符串批注,原来完全看不懂的逻辑就开始有图像了。
但也要泼个冷水:反混淆后的代码,不一定能直接被 javac 编译。原因很简单:重命名后可能出现命名冲突,或者子类改了父类的方法签名但字段访问没对齐。flaming-shame 的定位是辅助阅读,不是替代源码,别把它当逆向编译器用。
3.4 进阶:处理目录与多 dex / 多 jar 场景
Android 场景下你经常会拿到一堆 dex 合并后的 jar,或者多个模块 jar。flaming-shame 支持传多个输入:
java -jar flaming-shame-1.0.jar \ --input module1.jar,module2.jar,module3.jar \ --output all-restored.jar多个 jar 会自动合并成一个依赖图,跨 jar 的调用关系也能识别。这个特性很实用——我在处理一个老旧平板固件时,厂商把系统逻辑拆成了十几个 jar,交叉引用极其混乱。合并分析后识别率明显比单 jar 高,因为依赖图的边数更多,推理依据更扎实。
4. 常见问题与排查技巧实录
4.1 问题速查表
我整理了几个自己踩过、以及群友问过的高频问题:
| 问题 | 现象 | 原因与处理 |
|---|---|---|
| 输出 jar 无法解压 | zip 校验失败 | jar 是特殊压缩格式,建议用jar -tf先看列表;换个带--skip-corrupted参数的新版再试 |
| 识别率极低(<20%) | 很多类没被重命名 | 目标 jar 可能不是标准混淆产物,比如只是用了-dontobfuscate做了最小混淆;调低--min-confidence阈值 |
| 重命名后方法缺失 | 属性文件引用了方法名 | 比如 Spring 配置文件里的method="init",机制上只查 class 文件查不到;需要人工比对 XML 配置 |
| 内存溢出 | OutOfMemoryError | 大 jar 依赖图太大,加-Xmx4g再跑 |
| 建议名字重复 | 同一个类建议多个名字 | 深入到源码里看置信度更高的那个,修改 JSON 手动选一个 |
4.2 不要盲信置信度,人工校验流程必须有
flaming-shame 会给你一个置信度分数,但你千万不要无脑照搬。我在一个真实项目里遇到过:一个从java.lang.Runnable继承的混淆类,因为类里调用了大量日志方法,工具信心十足地把它标记为LogProcessor。但翻看字节码后发现,这个类其实是某个业务线程池的 Worker,日志只是它的副作用。
这种“从邻居猜身份”的推理方式,优点是有线索就能猜,缺点是猜错的时候错得理直气壮。我的建议是:
- 对高置信度(>0.85)的建议,可以直接接受;
- 对中置信度(0.6–0.85)的建议,结合调用它的地方人工核验;
- 对低置信度(<0.6)的建议,全部丢弃,保留原混淆名。
先跑默认阈值生成整份报告,然后花半个小时按这个规则过一遍,基本能保证最终输出的可读性达到 80 分。
4.3 配合 IDE 与反编译器的组合玩法
单独用 flaming-shame 的输出直接阅读,体验其实一般。我更推荐“组合拳”:
- flaming-shame 跑完:生成带注释的类图和重命名建议;
- JD-GUI / Fernflower 反编译:用反编译器把建议后的 jar 转成源码;
- IntelliJ IDEA 打开源码:全局搜索字符串线索,优先看带
[decrypted]标记的字符串; - 对照调用链:在 IDEA 里右键
Find Usages,顺着代码逻辑梳理业务。
这样一套下来,一个中型 jar 从“完全看不懂”到“能画出模块功能图”,大概需要一到两个下午。如果没有 flaming-shame 这第一步,纯靠人工翻混淆代码,这个时间要乘以 3 以上。
4.4 关于恶意样本的特别提醒
反混淆工具最大的隐藏风险不是技术问题,而是你处理的对象本身。
我在分析某些不明来源的 jar 时,严格遵守了这几条底线:
- 分析环境一律用隔离虚拟机或专用机器,不碰生产网络;
- 不直接运行待分析的 jar,只用静态方式解析;
- 如果用动态解密模式,开沙箱并做好网络封锁;
- 不把反混淆结果用于任何商业闭源项目,仅限安全研究或排障用途。
flaming-shame 的 Hacker 友好设计让我心存好感,但工具终究是工具,使用边界在你。
个人实操感受
如果你只用来处理那些“必须有 mapping 才能解”的场景,flaming-shame 可能帮不上什么忙。但如果你经常面对来源不明、mapping 缺失、重度混淆的 jar,它就是一把能省下几个通宵的利器。它给的从来不是完美答案,而是一份“能让看懂的人快速看下去”的高质量草稿。顺着草稿继续逆向,比面对一片混沌的a.a.a要舒服太多。最后再分享一个小技巧:跑大型 jar 之前,先用--include-packages限定几个高价值包,把输出规模控制住,分析效率会高非常多。
本文还有配套的精品资源,点击获取