简介flaming-shame 是一款面向 Java 逆向工程与安全分析方向的轻量级反混淆工具适合需要阅读、调试被混淆字节码的开发者与安全研究人员。它通过静态分析手段比较 Java 程序的结构图尝试自动还原被混淆的类名、方法名与变量名降低人工逆向成本受字节码优化与动态绑定影响其映射结果为最佳猜测而非精确还原使用时需结合人工判断。资源包共 23 个文件以 18 个 Java 源码文件为核心另含 README 说明文档、LICENSE 授权文件、.gitignore 配置及 asm 相关 jar 与源码压缩包整体约 550KB便于直接阅读实现或二次开发。目前已有 455 人学习下载读者可借此理解反混淆的算法思路、结构图比对流程与 ASM 字节码操作实践并基于源码搭建自己的分析实验环境。1. 反混淆这件事为什么值得单独造一个轮子线上事故排查时拿到一份堆栈类名是a.b.c方法名是llllll字符串全被拆成\x41\x42拼接控制流被拆成一堆while(true)加switch的跳转表——这种代码你盯着看三天也看不出业务逻辑。Java 反混淆工具要解决的就是这个场景把被 ProGuard、Allatori、Zelix KlassMaster 这类混淆器处理过的字节码还原成人类能读、能断点、能搜索的形态。flaming-shame就是冲着这个目标做的一个 Java 反混淆工具它不依赖源码直接吃.class和.jar输出可读性更高的字节码或反编译结果。它适合三类人做安全审计、需要还原第三方 SDK 真实行为的工程师做逆向分析、要定位恶意逻辑或漏洞点的同学以及维护老系统、源码丢失只剩混淆包的倒霉蛋。核心思路不是猜名字而是基于字节码层面的结构信息做还原——常量池、局部变量表、异常表、控制流图这些在混淆后往往还残留着可用的线索。下面按能干什么 → 怎么跑 → 参数怎么调 → 坑在哪 → 怎么验证的顺序讲透。2. flaming-shame 的还原链路从字节码到可读逻辑2.1 它到底改了字节码的哪些部分反混淆不是单一操作而是一条流水线。flaming-shame的处理链路大致分四层每层针对一类混淆手法第一层是名称还原。混淆器把getUserInfo改成a但方法体里对字符串常量、系统属性、反射调用的引用往往还留着语义痕迹。工具会扫描常量池里的字符串、类引用、方法描述符结合调用图做启发式推断给方法名和类名打候选标签。第二层是字符串解密。很多混淆器把明文字符串加密后存进常量池运行时用一段解密方法还原。flaming-shame的做法是定位解密方法特征是入参和返回值都是 String方法体只做异或/移位/查表然后模拟执行它把调用点直接替换成解密后的常量。第三层是控制流平坦化还原。这是最硬的一层。混淆器把顺序执行的代码拆成基本块塞进一个switch分发器用状态变量控制跳转。还原的关键是重建基本块之间的真实前后关系——通过分析状态变量的赋值和 switch 的 case 映射把分发器压平回顺序结构。第四层是无效代码清理。混淆器会插入大量nop、恒真/恒假分支、无用局部变量赋值。这层做的是死代码消除和常量传播让输出干净。2.2 最小可跑通流程拿一个 class 文件试手假设你手上有一个混淆过的demo.jar先解压出单个 class 做实验避免一上来就处理整个包。# 解压 jar取出一个 class 文件 mkdir -p work/classes cd work/classes unzip -o ../demo.jar com/example/* -d . # 用 javap 先看原始字节码确认混淆程度 javap -p -c com/example/a.class before.txt wc -l before.txt这一步的目的是建立基线。javap -p会连私有方法一起反汇编-c输出字节码指令。如果before.txt里全是a、b、c这种单字母名说明名称混淆很重如果看到大量lookupswitch或tableswitch说明控制流被平坦化了。接下来跑flaming-shame的核心处理。工具通常提供 CLI 入口参数设计上一般会有输入路径、输出路径、处理阶段开关# 对单个 class 做全流程反混淆 java -jar flaming-shame.jar \ --input com/example/a.class \ --output out/ \ --passes rename,string,controlflow,deadcode \ --verbose # 处理完再用 javap 看结果对比行数和结构 javap -p -c out/com/example/a.class after.txt diff (grep -c invoke before.txt) (grep -c invoke after.txt)--passes是关键参数。四个阶段可以单独开也可以组合。调试阶段建议一个一个开比如先只跑string确认字符串解密正确再加controlflow。全开容易在某一层出错时难以定位是哪一层的锅。2.3 处理整个 jar 包时的批量策略单 class 跑通后处理整个 jar 要注意类之间的依赖。名称还原如果只看单个类推断会不准——一个方法被谁调用、调用了谁这些跨类信息对还原至关重要。# 全量处理工具内部会先建调用图再逐类还原 java -jar flaming-shame.jar \ --input demo.jar \ --output demo-deobf.jar \ --passes rename,string,controlflow,deadcode \ --classpath libs/*:demo.jar \ --threads 4 \ --log-level info # 输出后重新打包验证 jar tf demo-deobf.jar | head -20--classpath要指向所有依赖 jar否则工具解析不到外部类引用调用图会断。--threads控制并行度类多的时候能明显加速但注意如果开了rename阶段并行处理时名称推断需要全局锁线程数开太大反而会因锁竞争变慢一般 4 到 8 比较稳。3. 四个必调参数字符串解密、控制流、名称推断、超时3.1 字符串解密怎么定位解密方法字符串解密是反混淆里性价比最高的一步因为解密后的字符串直接暴露业务语义。flaming-shame定位解密方法靠三个特征组合方法返回类型是String方法体里存在对入参的算术/位运算异或、加减、移位方法被大量调用且调用点传入的是常量工具内部会先做一次解密方法候选扫描把满足条件的方法列出来然后逐个模拟执行。这里有个参数控制模拟的深度java -jar flaming-shame.jar \ --input demo.jar \ --output out.jar \ --passes string \ --string-max-depth 8 \ --string-max-iter 10000--string-max-depth限制解密方法内部调用链的深度。有些混淆器把解密逻辑拆成多层方法调用深度设太小会解不出来设太大又可能陷入递归。8 层覆盖绝大多数情况。--string-max-iter限制模拟执行的指令数上限防止遇到死循环解密方法时卡死。10000 条指令对正常解密逻辑绰绰有余。如果解密失败先看日志里有没有candidate rejected字样。常见原因是解密方法用了反射或者依赖外部状态比如读系统时间做密钥这种纯静态模拟搞不定需要手动介入。3.2 控制流还原状态变量追踪的边界控制流平坦化的还原核心是追踪那个状态变量。混淆后的代码结构通常是int state 100; while (true) { switch (state) { case 100: /* block A */ state 200; break; case 200: /* block B */ state 300; break; case 300: return; } }还原的目标是把它变回A; B; return;。flaming-shame的做法是找到 switch 的分发变量分析每个 case 里对该变量的赋值建立块 → 后继块的映射然后按映射重建顺序。java -jar flaming-shame.jar \ --input demo.jar \ --output out.jar \ --passes controlflow \ --cf-max-blocks 500 \ --cf-strict false--cf-max-blocks限制单个方法内参与还原的基本块数量。超过这个数的方法控制流图会非常复杂还原容易出错工具会选择跳过并打警告。500 是个经验值一般业务方法不会超过。--cf-strict设为true时只有完全确定后继关系才还原宁可少还原也不还原错设为false时会做更多推测还原率高但可能引入错误跳转。生产环境建议先用true跑一遍看覆盖率再决定要不要放宽。3.3 名称推断启发式规则的权重怎么调名称还原没有标准答案全靠启发式。flaming-shame的推断依据包括字符串常量内容、调用的 JDK 方法、字段类型、继承关系。比如一个方法里反复出现password字符串且调用了MessageDigest.getInstance那它大概率是密码相关方法可以命名为encryptPassword之类。java -jar flaming-shame.jar \ --input demo.jar \ --output out.jar \ --passes rename \ --rename-strategy aggressive \ --rename-prefix deobf_--rename-strategy有conservative和aggressive两档。保守档只还原有强证据的名称比如直接引用了getUserName字符串的方法激进档会做更多联想还原率高但可能起错名。--rename-prefix给无法推断的方法加统一前缀方便你区分工具还原的和工具没把握的。3.4 超时与内存大 jar 包的现实约束处理几百 MB 的 jar 时内存和超时是绕不开的。JVM 堆要开够模拟执行和调用图分析都是内存大户。java -Xmx8g -jar flaming-shame.jar \ --input big.jar \ --output big-out.jar \ --passes rename,string,controlflow,deadcode \ --timeout-per-class 30000 \ --skip-on-error true--timeout-per-class单位是毫秒单个类处理超过 30 秒就跳过。有些混淆器会故意构造超大方法几万条指令来拖垮分析工具这个参数是保命的。--skip-on-error让工具遇到解析不了的类时跳过而不是整个任务失败处理大包时必开。4. 避坑指南反混淆翻车的五种典型场景4.1 还原后类加载失败VerifyError 排查现象反混淆后的 jar 一跑就抛java.lang.VerifyError提示某方法栈不平衡或局部变量类型不匹配。原因控制流还原时改动了字节码结构但没同步更新异常表或局部变量表。JVM 校验器对栈帧要求很严少一条astore或多一条pop都会挂。解决先只开string和rename两个阶段确认基础还原没问题再单独开controlflow用--cf-strict true跑。如果还挂用javap -v对比还原前后的StackMapTable看是哪一帧对不上。实在修不了就对该方法跳过控制流还原保留原始结构。4.2 字符串解出来是乱码编码与密钥问题现象解密后的字符串显示为????或一堆不可打印字符。原因两种可能。一是解密方法用的字符集不是 UTF-8工具默认按 UTF-8 解码二是解密依赖的密钥来自运行时环境比如某个静态字段在clinit里才初始化静态模拟时该字段还是默认值。解决检查解密方法的new String(byte[], Charset)调用确认字符集参数。如果是密钥问题看clinit里有没有对密钥字段的赋值手动把值填进工具的配置里。flaming-shame一般支持--string-key-override这类参数允许你指定静态字段的初始值。4.3 控制流还原后逻辑变了状态变量被复用现象还原后的代码能跑但行为跟原来不一样某个分支永远进不去。原因混淆器复用了同一个局部变量做状态变量和业务变量。工具追踪状态变量时把业务赋值也当成了状态跳转导致后继关系建错。解决用--cf-strict true让工具只在证据充分时还原。同时检查还原后的方法里有没有goto指向了错误位置。如果问题集中在某几个方法把它们加入--cf-skip-methods列表保留原始控制流。4.4 处理到一半 OOM调用图内存爆炸现象处理大 jar 时 JVM 抛OutOfMemoryError堆 dump 显示大量MethodNode对象。原因工具默认会把所有类的调用图全量加载进内存。类一多光方法节点就占几个 G。解决加-Xmx是一方面更有效的是开--lazy-callgraph如果工具支持按需加载调用图。另外把--threads降到 2减少并发时的内存峰值。实在不行就分批处理按包名切分 jar处理完再合并。4.5 反编译工具读不了输出字节码版本不兼容现象flaming-shame输出的 class 用 JD-GUI 或 CFR 打开报错提示不支持的 class 版本。原因工具在写回字节码时可能把 class 文件版本号改了或者用了某些反编译器不支持的指令组合。解决用javap -v看输出 class 的major version确认跟输入一致。如果反编译器读不了先用javap -c看字节码本身是否合法。合法的话就是反编译器的问题换一个工具比如用 Procyon 或 Fernflower再试。flaming-shame一般有--keep-version参数强制保持原始版本号。5. 验证还原质量三个可量化的检查手段反混淆做完怎么知道还原得好不好不能靠看着像得有可量化的指标。第一个手段是字符串命中率。统计还原后代码里可读字符串长度大于 3、包含字母的数量跟还原前对比。如果还原前常量池里全是加密字节数组还原后出现了大量user、login、token这类词说明字符串解密生效了。我一般会写个小脚本扫一遍import re def count_readable_strings(javap_output): # 匹配 javap 输出里的 String 常量 pattern r// String (.) matches re.findall(pattern, javap_output) readable [m for m in matches if len(m) 3 and re.search(r[a-zA-Z]{3,}, m)] return len(readable), len(matches) with open(after.txt) as f: readable, total count_readable_strings(f.read()) print(f可读字符串: {readable}/{total} {readable/total:.1%})这个比例能到 60% 以上说明字符串还原基本到位。低于 30% 就要回去查解密方法定位是不是漏了。第二个手段是控制流还原覆盖率。工具日志里一般会打flattened methods detected和successfully unflattened两个数。覆盖率 成功数 / 检测数。80% 以上算合格剩下的 20% 通常是超大方法或状态变量被复用的硬骨头手动处理。第三个手段是行为一致性验证。如果原始 jar 能跑哪怕逻辑被混淆把还原后的 jar 和原始 jar 在相同输入下跑一遍对比输出。这需要你有办法触发原始 jar 的逻辑——比如它是个 SDK写个测试用例调它的公开 API。输出一致说明还原没有改变语义。这一步最费事但最可靠关键业务场景建议必做。最后一个习惯每次反混淆前先把原始 jar 和所有中间产物before.txt、after.txt、工具日志归档到一个带时间戳的目录。反混淆是个反复试参数的过程没有后悔药只有归档能让你随时回退到上一个能用的版本。希望帮到你。本文还有配套的精品资源点击获取