ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

无需mapping文件,flaming-shame还原混淆Java代码实战

无需mapping文件,flaming-shame还原混淆Java代码实战 简介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 文件展开的retraceProGuard 自带的堆栈反混淆工具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(ILjava/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 配置文件里的methodinit机制上只查 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限定几个高价值包把输出规模控制住分析效率会高非常多。本文还有配套的精品资源点击获取
返回列表