Skip to content

Instantly share code, notes, and snippets.

@fuzzbuster
Last active April 20, 2026 12:25
Show Gist options
  • Select an option

  • Save fuzzbuster/c028f1d82aaa43644876559839912fbf to your computer and use it in GitHub Desktop.

Select an option

Save fuzzbuster/c028f1d82aaa43644876559839912fbf to your computer and use it in GitHub Desktop.
java-deobfuscator via jadx-mcp-ai or recaf scripting api

Recaf 反混淆方法论

基于 Recaf 4.x Headless 模式的 Java JAR 反混淆完整工作流


1. 环境准备

1.1 工具链

工具 用途
Recaf 4.x (平台特定 JAR) 字节码分析、反编译、映射、重命名(内置 Vineflower/CFR/Procyon)
JDK 21+ 运行 Recaf 4.x
Maven / Gradle 构建可编译工程(阶段6)

说明:Recaf 4.x 已内置反编译引擎(Vineflower、CFR、Procyon),无需额外下载。仅在需要特定版本反编译器时,才需单独获取其 CLI JAR。

1.2 Recaf Headless 模式

# 基本格式
java -jar recaf-<platform>.jar -h --input <target.jar> --script <script.java>

# -h          : Headless 模式,不启动 GUI
# --input     : 加载目标 JAR 文件
# --script    : 执行 Java 脚本(编译为 Runnable 匿名类)

1.3 脚本引擎注入变量

变量 类型 说明
workspace Workspace 当前工作区,包含加载的 JAR 所有类信息

获取其他服务需通过 CDI:

import jakarta.enterprise.inject.spi.CDI;
SomeService service = CDI.current().select(SomeService.class).get();

1.4 脚本引擎约束

Recaf 脚本引擎将脚本编译为匿名类执行,存在以下约束,需在编写导出脚本时遵循:

约束 应对
不支持匿名内部类语法 使用 Recaf 内置 API(如 BasicMappingsRemapper),或定义命名内部类
Lambda 只能捕获 effectively final 变量 ZipOutputStream 等资源声明 final 桥接引用
try-with-resources 不可用于 forEach lambda 内部 在 lambda 外层用 try-finally 手动管理资源生命周期

2. 标准操作流程 (SOP)

flowchart LR
  A[阶段1:结构分析] --> B[阶段2:类名恢复]
  B --> C[阶段3:包名恢复]
  C --> D[阶段4:方法/字段重命名]
  D --> E[阶段5:导出与验证]
  E --> F[阶段6:构建可编译工程]

  subgraph 阶段1输出
    T[第三方库过滤清单]
    M[元数据线索清单]
  end
  
  subgraph 阶段2输出
    S1[class mappings]
  end
  
  subgraph 阶段3输出
    S2[package mappings]
  end
  
  subgraph 阶段4输出
    S3[method/field mappings]
  end
  
  subgraph 阶段5输出
    O[deobfuscated.jar]
  end
  
  subgraph 最终验证
    P[Maven/Gradle可编译工程]
  end

  A -.-> T
  A -.-> M
  B -.-> S1
  C -.-> S2
  D -.-> S3
  E -.-> O
  F -.-> P
Loading

2.1 核心 Recaf 能力与脚本工具清单

以下是工作流中必须依赖的 Recaf 能力(均通过 Headless 脚本使用),以及推荐的"脚本化工具"拆分方式。

核心变量与注入机制

  • workspace:脚本引擎自动注入的 Workspace,用于访问 primary resource、枚举类、读取 SourceFile 等。
  • CDI.current():通过 CDI 获取 Recaf 服务(如反编译服务)。

核心 CDI 服务(脚本内调用)

  • DecompilerManager:反编译验证与语义分析(getJvmDecompiler("Vineflower") + decompile(...))。
  • MappingApplierService / MappingApplier:在"需要 Recaf 管理映射"的场景下使用(本项目导出采用 ASM Remap,可选)。

核心脚本工具(建议按职责拆分)

  • ListAppClasses(阶段1):列出类、按包名前缀过滤第三方库、定位入口点候选。
  • DecompileClass / VerifyDecompile(阶段1/5):反编译单个或关键类用于语义推断与验证。
  • SourceFileRename(阶段2):基于 JvmClassInfo.getSourceFileName() 生成类名映射(高优先级、低风险)。
  • FullDeobfuscate(阶段2/3/4/5 一体化):合并"SourceFile 类名 + 包名 +(可选)方法/字段"映射,并通过 ASM ClassRemapper 导出新 JAR。

导出核心能力(必须)

  • ASM ClassReader + ClassRemapper + ClassWriter:在 headless 下绕过 UI 导出器,直接对字节码做 Remap 并写入 Jar/Zip。
    • 关键注意:ZIP 条目名(newName + ".class")应由"原名→新名"的映射表驱动,而不是依赖 remapper.mapType(...) 的返回值。

3. 阶段 1:结构分析

3.1 目标

  • 识别第三方库 vs 应用核心代码
  • 确定应用入口点
  • 审计元数据,提取原始结构线索

3.2 第三方库识别策略

优先级从高到低

  1. MANIFEST.MF 分析:检查 Main-ClassClass-Path 等属性
  2. 包名模式匹配:已知库的包名前缀(如 org/apache/commons/
  3. 类数量统计:第三方库通常类数量多且包结构规整
  4. 字符串常量分析:版本号、URL、License 等特征字符串

3.3 入口点识别策略

应用类型 识别方法
独立应用 MANIFEST.MFMain-Class
插件/扩展 实现特定框架接口的类(如 IBurpExtenderPlugin 等)
Spring Boot @SpringBootApplication 注解
Servlet 实现 Servlet/Filter 接口

3.4 元数据审计:提取原始结构线索

混淆器通常只处理类名和包名,但 JAR 中存在多种不被混淆器处理的元数据,可直接暴露原始结构信息。这是结构分析阶段最重要的产出之一。

审计清单

元数据来源 可提取信息 审计方法
module-info.class exports 指令保留原始包名 unzip -p target.jar module-info.class | javap -v - | grep exports
SourceFile 属性 原始源文件名 → 类名 见阶段 2
InnerClasses 属性 内部类原始名称 javap -v -p ClassName.class | grep InnerClass
LocalVariableTable 局部变量/参数原始名称 javap -v -p ClassName.class | grep LocalVariableTable
注解常量 字符串常量、默认值 javap -v -p AnnotationClass.class
资源文件 配置、SPI 声明 jar tf target.jar | grep -v '\.class$'

核心原则:在进入任何语义推断之前,先穷尽所有元数据线索。元数据是客观事实,语义推断是主观判断,前者永远优先。

3.5 脚本模板:列出应用核心类

import software.coley.recaf.workspace.model.Workspace;
import software.coley.recaf.workspace.model.resource.WorkspaceResource;
import software.coley.recaf.workspace.model.bundle.Bundle;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

List<String> thirdPartyPrefixes = List.of(
    "org/yaml/snakeyaml/", "jregex/", "org/antlr/",
    "org/apache/commons/", "javax/", "kotlin/",
    "okhttp3/", "org/json/", "org/jsoup/"
);

WorkspaceResource primary = workspace.getPrimaryResource();
List<String> appClasses = new ArrayList<>();

primary.jvmClassBundleStream().flatMap(Bundle::stream).forEach(cls -> {
    String name = cls.getName();
    boolean isThirdParty = false;
    for (String prefix : thirdPartyPrefixes) {
        if (name.startsWith(prefix)) { isThirdParty = true; break; }
    }
    if (!isThirdParty) appClasses.add(name);
});

appClasses.sort(Comparator.naturalOrder());
System.out.println("Application classes: " + appClasses.size());
for (String name : appClasses) System.out.println("  " + name);

4. 阶段 2:类名恢复

4.1 类名恢复决策链

flowchart TD
  A[审计 SourceFile 属性] --> B{SourceFile 有效?}
  B -->|是| C[直接恢复类名]
  B -->|否| D[语义推断]
  D --> E[反编译分析类职责]
  E --> F[推断类名]
  C --> G[验证:javap 确认]
  F --> G
Loading

决策原则:元数据优先,语义推断回退。

4.2 SourceFile 属性恢复

Java 编译器在 .class 文件中保留 SourceFile 属性,记录原始源文件名(如 Config.java)。即使类名被混淆为 aSourceFile 仍为 Config.java,可直接恢复原始类名。

可靠性验证:使用 SourceFile 前必须验证其有效性。混淆器可能伪造(替换为统一字符串)或删除该属性。验证方法:

# 抽样检查 SourceFile 是否有区分度
for f in $(find . -name '*.class' -not -name 'module-info.class' | head -10); do
  sourcefile=$(javap -v "$f" 2>/dev/null | grep 'SourceFile:' | awk '{print $2}')
  echo "$f -> $sourcefile"
done

若所有类返回相同字符串(如 SourceFile),则该属性已被伪造,需回退到语义推断。

4.3 语义推断类名(回退方案)

当元数据不可用时,需逐类反编译分析其职责来推断类名:

分析线索 推断规则
继承关系 extends ExceptionXxxExceptionextends RuntimeExceptionXxxRuntimeException
实现的接口 @interface → 注解类;Function<A,B> → 转换/映射类
方法签名模式 parse(ByteBuffer) 方法 → XxxParser;有 build()XxxBuilder
字符串常量 包含领域关键词 → 类名包含该词
枚举特征 大量 static final Predicate 字段 + 枚举语法 → 类型定义枚举

4.4 脚本模板:SourceFile 类名恢复

import software.coley.recaf.info.JvmClassInfo;
import software.coley.recaf.workspace.model.Workspace;
import software.coley.recaf.workspace.model.resource.WorkspaceResource;
import software.coley.recaf.workspace.model.bundle.Bundle;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

WorkspaceResource primary = workspace.getPrimaryResource();
List<String> results = new ArrayList<>();

primary.jvmClassBundleStream().flatMap(Bundle::stream).forEach(cls -> {
    String name = cls.getName();
    String sourceFile = cls.getSourceFileName();
    
    if (sourceFile != null && sourceFile.endsWith(".java")) {
        String originalSimpleName = sourceFile.substring(0, sourceFile.length() - 5);
        String currentSimpleName = name.contains("/") 
            ? name.substring(name.lastIndexOf('/') + 1) : name;
        
        // 处理内部类:a$1 -> Original$1
        int dollarIdx = currentSimpleName.indexOf('$');
        String baseSimpleName = dollarIdx > 0 
            ? currentSimpleName.substring(0, dollarIdx) : currentSimpleName;
        String innerPart = dollarIdx > 0 
            ? currentSimpleName.substring(dollarIdx) : "";
        
        if (!baseSimpleName.equals(originalSimpleName)) {
            String packageName = name.contains("/") 
                ? name.substring(0, name.lastIndexOf('/')) : "";
            String newFullName = packageName.isEmpty() 
                ? originalSimpleName + innerPart 
                : packageName + "/" + originalSimpleName + innerPart;
            
            if (!name.equals(newFullName)) {
                results.add(name + " -> " + newFullName);
            }
        }
    }
});

System.out.println("SourceFile-based renames: " + results.size());
for (String r : results) System.out.println("  " + r);

4.5 SourceFile 属性的局限

  • 不改变包名pkg/a/Config 中的 a 包名不会改变
  • 不改变方法/字段名:仅恢复类名
  • 可能被伪造:混淆器可能替换为无意义字符串
  • 可能被删除:混淆器可能移除该属性

5. 阶段 3:包名恢复

5.1 包名恢复决策链

flowchart TD
  A[审计 module-info exports] --> B{exports 有效?}
  B -->|是| C[直接建立包名映射]
  B -->|否| D[语义推断]
  D --> E[分析包内类职责]
  E --> F[推断包名]
  C --> G[验证:类职责与包名语义一致]
  F --> G
Loading

决策原则:与类名恢复相同——元数据优先,语义推断回退。

5.2 module-info 优先策略

JPMS 模块系统中的 module-info.classexports 指令声明了模块对外暴露的包。混淆器通常不处理此文件,因此它保留了原始包名。

# 提取 module-info.class 中的原始包结构
unzip -p target.jar module-info.class | javap -v - 2>/dev/null | grep 'exports'

将 exports 中的原始包名与混淆后的包结构建立一一映射,再通过包内类职责验证映射的正确性。

5.3 语义推断包名(回退方案)

当 module-info 不可用时,通过分析包内类的职责推断包的语义名称:

  1. 收集信号:查看包内恢复后的类名、方法、字符串常量
  2. 寻找共性:这些类共同服务于什么功能/领域?
  3. 遵循惯例:参考 Java 社区常见的包名命名习惯
    • config: 配置、设置
    • util: 工具方法
    • api: 接口定义
    • model: 数据模型
    • service: 业务服务
    • controller: 控制层
  4. 最后决策:选择最能概括该包功能的名称

5.4 映射合并原则:单步原子化

BasicMappingsRemapper 不支持链式映射(A→B + B→C 不会产生 A→C),因此类名重命名和包名重命名必须合并为单一步骤

包路径匹配规则:包名映射必须基于类的实际包路径lastIndexOf('/') 前的部分)进行精确匹配,而非前缀匹配。前缀匹配会导致子包的类被错误映射到父包。

// 按包名长度降序排列,确保最长前缀优先匹配
List<Map.Entry<String, String>> sortedPkgRenames = new ArrayList<>(packageRenames.entrySet());
sortedPkgRenames.sort((a, b) -> Integer.compare(b.getKey().length(), a.getKey().length()));

for (Map.Entry<String, String> entry : classSimpleRenames.entrySet()) {
    String oldName = entry.getKey();
    String newSimpleName = entry.getValue();
    
    // 提取类的实际包路径
    int lastSlash = oldName.lastIndexOf('/');
    String classPackage = lastSlash > 0 ? oldName.substring(0, lastSlash) : "";
    
    // 精确匹配包名(不是前缀匹配)
    String newPackage = classPackage;
    for (Map.Entry<String, String> pkgEntry : sortedPkgRenames) {
        if (classPackage.equals(pkgEntry.getKey())) {
            newPackage = pkgEntry.getValue();
            break;
        }
    }
    
    String newFullName = newPackage.isEmpty() ? newSimpleName : newPackage + "/" + newSimpleName;
    if (!oldName.equals(newFullName)) {
        mappings.addClass(oldName, newFullName);
        classNameMap.put(oldName, newFullName);
    }
}

6. 阶段 4:方法/字段重命名

6.1 术语澄清

Java术语 对应字节码/Recaf能力 说明
函数 / 方法 Method mappings.addMethod(...)
变量(类成员) / 常量static final Field mappings.addField(...)
局部变量 / 参数 无直接映射API 除非有 LocalVariableTable 属性(混淆器一般会删除)

6.2 语义推断策略

优先级线索(从高到低)

  1. 字符串常量:方法内包含 "convert to xml" → 提示 XML 转换功能
  2. 返回类型推断:返回 Context/Config 等 → 可能是 getContext()/loadConfig()
  3. 参数类型推断:参数为 String, String 且返回 intcompare()/indexOf()
  4. 调用链分析:被单例类调用 → getInstance(),被工具类调用 → util
  5. 接口实现:实现接口的方法名优先从接口推断
  6. 设计模式识别:识别 Factory、Builder、Strategy 等常见模式

6.3 映射注册 API

Recaf 4.x 的 IntermediateMappings API 参数顺序为 (ownerName, descriptor, oldName, newName),其中 descriptor 位于 oldName 之前:

IntermediateMappings mappings = new IntermediateMappings();

// 类重命名
mappings.addClass("old/pkg/OldName", "new/pkg/NewName");

// 字段重命名 — (owner, descriptor, oldName, newName)
mappings.addField("owner/class", "Ltype/descriptor;", "oldName", "newName");

// 方法重命名 — (owner, descriptor, oldName, newName)
mappings.addMethod("owner/class", "(Lparam/Type;)Lreturn/Type;", "oldName", "newName");

重要:参数顺序错误不会产生编译错误或运行时异常,但映射完全不生效。这是映射不生效时的首要排查项。

6.4 描述符精确匹配原则

映射 API 中的 descriptor 必须与字节码中的描述符完全一致,否则映射不生效。

关键规则

场景 规则 示例
普通字段 使用字段声明类型的描述符 Ljava/lang/String;
枚举常量 描述符是枚举类型本身,不是字段声明类型 Lcom/example/TlvType; 而非 Ljava/util/function/Predicate;
重载方法 同名方法不同描述符可映射为不同名称 a()Vclose, a(I)Iread

验证方法:用 javap -p -v ClassName.class 查看字节码中的精确描述符。

6.5 映射完整性原则:接口-实现覆盖

BasicMappingsRemapper 不会自动将接口方法映射传播到实现类。对接口方法的重命名,必须为接口和每个实现类分别添加映射条目:

// 接口方法映射
mappings.addMethod("com/example/TlvElement", "()B", "is", "getType");

// 每个实现类必须单独映射
mappings.addMethod("com/example/TlvStructure", "()B", "is", "getType");
mappings.addMethod("com/example/TlvValueElement", "()B", "is", "getType");

完整性检查:在提交映射前,对每个接口方法,列出其所有实现类(通过 javap 或 Recaf 的类型层次查询),确保映射覆盖完整。

6.6 方法描述符格式参考

Java 类型 描述符
void V
int I
boolean Z
byte B
long J
String Ljava/lang/String;
byte[] [B
List<String> Ljava/util/List;

7. 阶段 5:导出与验证

7.1 导出方案

Recaf 的 PathExportingManager 在 headless 模式下会触发 UI 对话框,不可用。需使用 ASM 手动导出:

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.commons.ClassRemapper;
import software.coley.recaf.services.mapping.BasicMappingsRemapper;

BasicMappingsRemapper remapper = new BasicMappingsRemapper(mappings);
Set<String> writtenEntries = new HashSet<>();

ZipOutputStream zos = null;
try {
    zos = new ZipOutputStream(new FileOutputStream(outputPath));
    final ZipOutputStream finalZos = zos;
    
    primary.jvmClassBundleStream().forEach(bundle -> {
        bundle.stream().forEach(cls -> {
            try {
                String originalName = cls.getName();
                byte[] originalBytecode = cls.getBytecode();
                
                ClassReader cr = new ClassReader(originalBytecode);
                ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                ClassRemapper classRemapper = new ClassRemapper(cw, remapper);
                cr.accept(classRemapper, 0);
                
                // ZIP 条目名由 classNameMap 驱动,不依赖 remapper
                String newName = classNameMap.getOrDefault(originalName, originalName);
                String entryName = newName + ".class";
                
                // 防止重复条目
                if (writtenEntries.contains(entryName)) {
                    System.out.println("WARNING: Duplicate skipped: " + entryName);
                    return;
                }
                writtenEntries.add(entryName);
                
                finalZos.putNextEntry(new ZipEntry(entryName));
                finalZos.write(cw.toByteArray());
                finalZos.closeEntry();
                
                if (!originalName.equals(newName)) {
                    System.out.println("  " + originalName + " -> " + newName);
                }
            } catch (Exception e) {
                System.out.println("ERROR: " + cls.getName() + " - " + e.getMessage());
            }
        });
    });
} finally {
    if (zos != null) { try { zos.close(); } catch (Exception ignored) {} }
}

7.2 反编译验证

import software.coley.recaf.services.decompile.DecompilerManager;
import software.coley.recaf.services.decompile.JvmDecompiler;
import software.coley.recaf.services.decompile.DecompileResult;

DecompilerManager decompilerManager = CDI.current().select(DecompilerManager.class).get();
JvmDecompiler decompiler = decompilerManager.getJvmDecompiler("Vineflower");

DecompileResult result = decompilerManager.decompile(decompiler, workspace, target).get();
String decompiled = result.getText();

7.3 验证清单

  • 类名是否正确恢复
  • 包名是否正确恢复
  • 方法/字段名是否正确恢复
  • 跨类引用是否一致(import 语句正确)
  • 内部类/匿名类引用是否正确
  • 第三方库类未被误改
  • 接口方法映射是否覆盖所有实现类
  • 枚举常量描述符是否使用枚举类型本身

8. 阶段 6:构建可编译工程

8.1 反编译为 Java 源码

方案优先级:Recaf 内置反编译器(headless 脚本) > 反编译引擎 CLI

方案 A:Recaf 内置反编译器(推荐)

Recaf 4.x 内置 Vineflower、CFR、Procyon 三个反编译器,可通过 DecompilerManager 在 headless 脚本中直接调用,无需额外下载任何工具。

优势

  • 无需额外下载反编译引擎
  • 反编译与反混淆在同一 Recaf 会话中完成,可直接对 workspace 中的类操作
  • 可选择不同引擎对比反编译质量

可用引擎(通过 decompilerManager.getJvmDecompilers() 查询):

引擎 特点
Vineflower 推荐。基于 Fernflower 改进,Java 21+ 支持最好
CFR 对旧版 Java 兼容性好,Java 21+ switch 有已知缺陷
Procyon 泛型推断能力较强

批量反编译脚本模板

import software.coley.recaf.services.decompile.DecompilerManager;
import software.coley.recaf.services.decompile.JvmDecompiler;
import software.coley.recaf.services.decompile.DecompileResult;
import software.coley.recaf.workspace.model.Workspace;
import software.coley.recaf.workspace.model.resource.WorkspaceResource;
import software.coley.recaf.workspace.model.bundle.Bundle;
import software.coley.recaf.info.JvmClassInfo;
import jakarta.enterprise.inject.spi.CDI;
import java.io.*;
import java.nio.file.*;
import java.util.*;

WorkspaceResource primary = workspace.getPrimaryResource();
DecompilerManager decompilerManager = CDI.current().select(DecompilerManager.class).get();

// 选择反编译引擎(优先 Vineflower,回退 CFR)
JvmDecompiler decompiler = decompilerManager.getJvmDecompiler("Vineflower");
if (decompiler == null) decompiler = decompilerManager.getJvmDecompiler("CFR");

String outputDir = "src-recaf";
new File(outputDir).mkdirs();

int success = 0, fail = 0;
var classes = primary.jvmClassBundleStream()
    .flatMap(Bundle::stream)
    .filter(cls -> cls instanceof JvmClassInfo)
    .map(cls -> (JvmClassInfo) cls)
    .toList();

for (JvmClassInfo cls : classes) {
    String name = cls.getName();
    try {
        DecompileResult result = decompilerManager.decompile(decompiler, workspace, cls).get();
        String text = result.getText();
        if (text != null && !text.isEmpty()) {
            String filePath = outputDir + "/" + name.replace('/', File.separatorChar) + ".java";
            new File(filePath).getParentFile().mkdirs();
            Files.writeString(Path.of(filePath), text);
            success++;
        } else {
            System.out.println("EMPTY: " + name);
            fail++;
        }
    } catch (Exception e) {
        System.out.println("FAIL: " + name + " - " + e.getMessage());
        fail++;
    }
}
System.out.println("Done: " + success + " succeeded, " + fail + " failed");

注意:此脚本需配合 --input 加载反混淆后的 JAR(阶段5的输出),而非原始混淆 JAR。

方案 B:反编译引擎 CLI(备选)

当 Recaf 内置引擎无法满足需求时(如需要特定版本的反编译器),可直接使用引擎 CLI:

# Vineflower CLI(推荐)
java -jar vineflower.jar deobfuscated.jar src

# CFR CLI
java -jar cfr.jar deobfuscated.jar --outputdir src

# Procyon CLI
java -jar procyon-decompiler.jar -jar deobfuscated.jar -o src

8.2 反编译适配层

反编译器在处理 Java 21+ 字节码时存在系统性缺陷,反编译输出不能直接编译。需建立适配层,将反编译输出修复为合法 Java 源码:

缺陷类型 根因 修复模式
MatchException 引用 Java 21 switch 表达式的内部异常类泄露到反编译输出 删除 default: { throw new MatchException } 分支,让 default 落入 switch 后的异常处理
Switch 表替换 1.DH[ordinal] 编译器将 enum switch 优化为查找表,反编译器未能还原 将 switch-table 模式转换为 enum switch 语句
变量类型复用 编译器复用局部变量槽位,反编译器将不同类型赋值给同一变量 为每种类型引入独立的局部变量
枚举类反编译为 extends Enum 反编译器输出语法不合法 改写为 enum 关键字声明
泛型擦除 反编译器无法恢复擦除前的泛型参数 根据上下文推断并补全泛型通配符

不同引擎的适配差异

缺陷 Vineflower CFR Procyon
MatchException 较少出现 频繁出现 较少出现
Switch-table 可能出现 频繁出现 较少出现
变量类型复用 偶尔出现 频繁出现 较少出现
枚举 extends Enum 不出现 频繁出现 不出现
泛型擦除 部分恢复 部分恢复 恢复较好

8.3 编译修复循环

反编译后的源码通常需要手动修复才能通过编译。修复模式按优先级:

  1. 类型推断修复:变量复用 → 引入独立变量
  2. Switch 语句修复:MatchException / switch-table → 标准 enum switch
  3. 泛型修复:原始类型 → 正确的泛型参数
  4. 抽象方法实现:反编译遗漏 @Override 方法 → 添加实现
  5. 注解方法调用annotation.value()Annotation 基类型上 → 先强转为具体注解类型

8.4 构建 Maven 工程

mkdir -p src/main/java
cp -r <反编译输出>/com src/main/java/
# 创建 pom.xml(根据依赖分析结果)
mvn compile

9. Recaf 可用的 CDI 服务

服务类 用途
DecompilerManager 反编译(Vineflower/Cfr/Fernflower)
MappingApplierService 映射应用(inCurrentWorkspace() 获取 MappingApplier
WorkspaceManager 工作区管理
PathExportingManager 导出(headless 下不可用,需手动导出)

10. 常见问题与解决方案

10.1 BasicMappingsRemapper 不链式映射

问题:映射 A→B 和 B→C 不会自动产生 A→C。

解决:在构建映射时,将所有变换合并为单一步骤。

10.2 方法/字段映射不生效

排查清单(按优先级排序):

  1. API 参数顺序:确认使用 (owner, desc, oldName, newName) 顺序
  2. 描述符精确匹配:用 javap -p -v 验证描述符与字节码完全一致
  3. 枚举常量描述符:枚举常量的描述符是枚举类型本身
  4. 接口-实现覆盖:实现类需要单独添加方法映射

10.3 导出后 ZIP 条目名未更新

问题remapper.mapType() 可能返回 null 或原始名称。

解决:维护独立的 Map<String, String> classNameMap 用于 ZIP 条目名映射。

10.4 COMPUTE_MAXS 导致栈溢出

解决:改用 ClassWriter.COMPUTE_FRAMES,或使用 ClassWriter(0) 并手动处理帧。

10.5 重复 ZIP 条目

解决:在 putNextEntry 前用 Set<String> writtenEntries 检查重复。


11. 完整脚本模板

将所有阶段合并为一个可执行的 Recaf 脚本:

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.commons.ClassRemapper;
import software.coley.recaf.info.JvmClassInfo;
import software.coley.recaf.services.mapping.IntermediateMappings;
import software.coley.recaf.services.mapping.BasicMappingsRemapper;
import software.coley.recaf.workspace.model.Workspace;
import software.coley.recaf.workspace.model.resource.WorkspaceResource;
import software.coley.recaf.workspace.model.resource.WorkspaceResourceBuilder;
import software.coley.recaf.workspace.model.bundle.JvmClassBundle;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.zip.*;

// ===== 配置区 =====
List<String> thirdPartyPrefixes = List.of(/* 第三方库包名前缀 */);

Map<String, String> packageRenames = new LinkedHashMap<>();
// packageRenames.put("old/pkg", "new/pkg");

// 类名映射(SourceFile 或语义推断)
Map<String, String> classSimpleRenames = new LinkedHashMap<>();
// classSimpleRenames.put("old/pkg/OldClass", "NewClass");
// classSimpleRenames.put("old/pkg/OldClass$a", "NewClass$Inner");

String outputJarPath = "output-deobfuscated.jar";

// ===== 阶段 2+3:构建类名+包名映射 =====
WorkspaceResource primary = workspace.getPrimaryResource();
IntermediateMappings mappings = new IntermediateMappings();
Map<String, String> classNameMap = new LinkedHashMap<>();

// 按包名长度降序排列,确保最长前缀优先匹配
List<Map.Entry<String, String>> sortedPkgRenames = new ArrayList<>(packageRenames.entrySet());
sortedPkgRenames.sort((a, b) -> Integer.compare(b.getKey().length(), a.getKey().length()));

for (Map.Entry<String, String> entry : classSimpleRenames.entrySet()) {
    String oldName = entry.getKey();
    String newSimpleName = entry.getValue();
    
    // 提取类的实际包路径(精确匹配,不是前缀匹配)
    int lastSlash = oldName.lastIndexOf('/');
    String classPackage = lastSlash > 0 ? oldName.substring(0, lastSlash) : "";
    
    // 查找包名映射
    String newPackage = classPackage;
    for (Map.Entry<String, String> pkgEntry : sortedPkgRenames) {
        if (classPackage.equals(pkgEntry.getKey())) {
            newPackage = pkgEntry.getValue();
            break;
        }
    }
    
    String newFullName = newPackage.isEmpty() ? newSimpleName : newPackage + "/" + newSimpleName;
    if (!oldName.equals(newFullName)) {
        mappings.addClass(oldName, newFullName);
        classNameMap.put(oldName, newFullName);
    }
}

// ===== 阶段 4:方法/字段映射 =====
// API 参数顺序:(owner, descriptor, oldName, newName)

// 方法映射
// mappings.addMethod("owner/class", "(Lparam/Type;)Lreturn/Type;", "oldName", "newName");

// 字段映射
// mappings.addField("owner/class", "Ltype/descriptor;", "oldName", "newName");

// 枚举常量:描述符是枚举类型本身
// mappings.addField("com/example/TlvType", "Lcom/example/TlvType;", "oldName", "BYTE");

// 接口-实现覆盖:为每个实现类单独映射
// mappings.addMethod("com/example/TlvElement", "()B", "is", "getType");
// mappings.addMethod("com/example/TlvStructure", "()B", "is", "getType");

// ===== 阶段 5:导出 =====
BasicMappingsRemapper remapper = new BasicMappingsRemapper(mappings);
Set<String> writtenEntries = new HashSet<>();

ZipOutputStream zos = null;
try {
    zos = new ZipOutputStream(new FileOutputStream(outputJarPath));
    final ZipOutputStream finalZos = zos;
    
    primary.jvmClassBundleStream().forEach(bundle -> {
        bundle.stream().forEach(cls -> {
            try {
                String originalName = cls.getName();
                byte[] originalBytecode = cls.getBytecode();
                
                ClassReader cr = new ClassReader(originalBytecode);
                ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                ClassRemapper classRemapper = new ClassRemapper(cw, remapper);
                cr.accept(classRemapper, 0);
                
                String newName = classNameMap.getOrDefault(originalName, originalName);
                String entryName = newName + ".class";
                
                if (writtenEntries.contains(entryName)) {
                    System.out.println("WARNING: Duplicate skipped: " + entryName);
                    return;
                }
                writtenEntries.add(entryName);
                
                finalZos.putNextEntry(new ZipEntry(entryName));
                finalZos.write(cw.toByteArray());
                finalZos.closeEntry();
                
                if (!originalName.equals(newName)) {
                    System.out.println("  " + originalName + " -> " + newName);
                }
            } catch (Exception e) {
                System.out.println("ERROR: " + cls.getName() + " - " + e.getMessage());
            }
        });
    });
} finally {
    if (zos != null) { try { zos.close(); } catch (Exception ignored) {} }
}

System.out.println("Done: " + outputJarPath);
System.out.println("Class mappings: " + classNameMap.size());

12. 参考资源与工具

12.1 文档与社区


13. 核心原则总结

flowchart TD
  A[元数据优先原则] --> A1[SourceFile → 类名]
  A --> A2[module-info exports → 包名]
  A --> A3[InnerClasses → 内部类名]
  A --> A4[LocalVariableTable → 参数名]
  
  B[映射原子化原则] --> B1[类名+包名合并为单步映射]
  B --> B2[不依赖链式映射]
  
  C[描述符精确匹配原则] --> C1[javap 验证描述符]
  C --> C2[枚举常量用枚举类型作描述符]
  
  D[映射完整性原则] --> D1[接口方法覆盖所有实现类]
  D --> D2[提交前验证映射条目数]
  
  E[反编译适配原则] --> E1[反编译输出 ≠ 可编译源码]
  E --> E2[建立适配层修复系统性缺陷]

  classDef principle fill:#fbb,stroke:#333,stroke-width:2px;
  class A,B,C,D,E principle;
Loading
  1. 元数据优先:穷尽所有元数据线索后再进行语义推断。元数据是客观事实,语义推断是主观判断。
  2. 映射原子化:所有维度的重命名(类名、包名、方法名、字段名)合并为单一步骤提交,不依赖链式映射。
  3. 描述符精确匹配:映射 API 的描述符必须与字节码完全一致,用 javap -p -v 逐条验证。枚举常量的描述符是枚举类型本身。
  4. 映射完整性:接口方法映射必须覆盖所有实现类。提交前验证映射条目数是否与预期一致。
  5. 反编译适配:反编译器输出不等于可编译源码,需建立适配层修复系统性缺陷(变量复用、switch-table、MatchException 等)。
name java-deobfuscator
description Java deobfuscator using jadx-ai-mcp in jadx-gui. Rename obfuscated classes/methods/fields/packages/variables. Invoke when user wants to deobfuscate, analyze, or rename Java artifacts in jadx-gui.

JADX Java Deobfuscator

Java deobfuscation tool based on jadx-ai-mcp.

Core Goal

Help make obfuscated class names, package names, field names, and variable names readable in jadx-gui, prioritizing clues from comments. All operations are performed within jadx-gui interface.

Important Note

DO NOT use jadx command line to directly decompile for building projects. All renaming and deobfuscation must be done through jadx-gui with jadx-ai-mcp first. After deobfuscation is complete, user can manually export source code or copy individual files to build projects.

Features

  • Multi-format input: Supports JAR
  • Smart deobfuscation: Prioritize clues from comments (especially JADX INFO: compiled from), then analyze based on string constants and semantic inference
  • Third-party library detection: Identify and skip common third-party libraries (Apache Commons, Guava, Jackson, Gson, OkHttp, Spring, etc.), only deobfuscate application core logic
  • Complete workflow: Locate main entry → Identify third-party libraries → Deobfuscate self-contained identifiers → Analyze imports → Recursively deobfuscate referenced core classes → Rename class/package → Iterate & refine → Notify user when complete
  • Semantic renaming: Rename obfuscated classes, methods, fields, packages, variables
  • jadx-gui only: All operations performed within jadx-gui interface

Usage Workflow

IMPORTANT: JADX-GUI Pre-check (MUST execute, cannot skip)

This is the first step of this skill. Before executing any jadx-ai-mcp tool calls, you must complete in this order:

  1. Verify jadx-ai-mcp service is available

    • First try calling get_all_classes() to verify service responsiveness
    • Service availability criteria: Tool call succeeds and returns valid results
  2. If service is not available

    • Immediately stop all subsequent processes
    • Do not attempt any jadx-ai-mcp tool calls
    • Use AskUserQuestion tool to clearly prompt user: "Please first start jadx-gui and load the target file before continuing"
    • Wait for user's explicit confirmation before restarting from this check step
  3. Save jadx project file (recommended)

    • It is recommended that user saves the jadx project file (*.jdx) before starting deobfuscation
    • This allows saving the current state and all renaming changes
    • The project file preserves the renaming information across jadx-gui sessions

Standard Deobfuscation Workflow (All in jadx-gui)

Step 1: Load target source file and locate main entry class

  • User has loaded target file in jadx-gui
  • Get project overview via get_all_classes()
  • Identify main entry point using multiple methods:
    • Traditional main method: Search for public static void main(String[] args) using search_classes_by_keyword(search_term="main", search_in="method")
    • MANIFEST.MF: Check META-INF/MANIFEST.MF for Main-Class attribute
    • Spring Boot: Look for @SpringBootApplication annotation or SpringApplication.run() calls
    • Framework-specific: Identify framework initialization code (e.g., Servlet initializers)
    • Service applications: Find service startup classes (e.g., classes with @Service, @Component annotations)
  • Select the most likely main entry class as the starting point

Step 2: Deobfuscate the current class first (self-contained identifiers)

  • For the current class, first obtain its complete source code via get_class_source()
  • Prioritize renaming identifiers within the class itself:
    • Rename fields using rename_field() based on clues from comments, string constants, and usage context
    • Rename methods using rename_method() based on method signatures, logic, and comments
    • Rename variables using rename_variable() within method scopes
  • IMPORTANT: Prioritize clues from comments, especially JADX INFO: compiled from comments
  • After renaming the class's own identifiers, verify results with get_class_source()

Step 3: Analyze imported packages and identify third-party libraries

  • Examine import statements and referenced classes in the current class
  • Identify third-party libraries first:
    • Check META-INF/maven/ folder first: Look for pom.properties and pom.xml files - these contain exact Maven coordinates (groupId, artifactId, version)
    • Check META-INF folder in Resources: Look for manifest files, library metadata, and lib directory
    • Look for common library patterns: package structures, class names, method signatures
    • Check for recognizable APIs: Apache Commons, Guava, Jackson, Gson, OkHttp, Spring, etc.
    • Analyze string constants that might contain library names or versions
    • Fallback: Search for library-specific feature classes/methods:
      • For txtmark: search for Processor, Configuration, BlockType
      • For fastjson: search for JSON, JSONObject, JSONArray
      • For log4j2: search for LogManager, Logger, Level
      • Use this method when Maven metadata is not available
  • Third-party library verification criteria:
    • Cross-reference analysis: If a package has few external references from other packages, it's likely a third-party library
    • Classes that don't reference the application's own core logic
    • Standard library patterns and well-known API structures
    • Large number of utility methods with generic functionality
  • For confirmed third-party libraries:
    • DO NOT deobfuscate - mark them as external dependencies
    • Record the library name and probable version
    • Note them for later reference
  • For application core classes (non-third-party):
    • Check if the class name is obfuscated (single letters like a, b, c or random strings)
    • Use get_class_source() to view the referenced class source code
    • Use get_xrefs_to_class() to understand how this class is used

Step 4: Recursively deobfuscate referenced application core classes

  • SKIP third-party libraries - only process application core classes
  • For each obfuscated referenced application core class, repeat Step 2:
    • First rename its own fields, methods, and variables
    • Then analyze its imported/referenced classes
    • Continue this recursive process only for application core classes

Step 5: Rename the class itself and refactor package structure

  • Once sufficient identifiers within the class and its references are renamed:
    • Use rename_class() to give the class a meaningful name based on its purpose
    • Systematic package refactoring:
      1. Analyze current package structure: Use get_all_classes() to get all classes and group them by package
      2. Identify package purposes: Analyze the functionality of classes in each package
      3. Rename packages: Use rename_package() to give packages descriptive names
      4. Verify updates: Ensure all references are automatically updated
  • How package renaming takes effect:
    • The rename_package() tool call immediately updates the package name in jadx-gui
    • All references to classes in the renamed package are automatically updated
    • The package structure in the jadx-gui tree view will reflect the new package name
    • To preserve changes: Save the jadx project file (*.jdx) to keep all renaming changes
    • Use get_class_source() to verify that the package declaration at the top of the file has been updated
    • Use get_xrefs_to_class() to confirm that all cross-references now use the new package name

Step 6: Iterate and refine

  • Repeat the above steps for all classes:
    • Start from main entry, move outward
    • Each iteration improves context for subsequent renaming
    • Use cross-references (get_xrefs_to_class(), get_xrefs_to_method(), get_xrefs_to_field()) to verify consistency
  • Use get_methods_of_class() and get_fields_of_class() to view complete structures

Step 7: Notify user when deobfuscation is complete

  • DO NOT attempt to create Maven/Gradle projects automatically
  • DO NOT use jadx command line to decompile for building
  • When deobfuscation work is done in jadx-gui:
    • Provide a summary of what was deobfuscated
    • List all identified third-party libraries with their names and probable versions
    • How to make package changes effective:
      • In jadx-gui: The package renaming is immediately reflected in the UI and all references
      • When exporting: jadx-gui will create the correct directory structure based on the new package names
      • When copying individual files: Ensure you maintain the directory structure that matches the package hierarchy
    • Instruct user that they can now:
      • Use jadx-gui's export feature to export the modified source code (this will preserve the package structure)
      • Manually copy individual source files as needed (maintain directory structure)
      • Create their own Maven/Gradle project structure (use the new package names)
      • Add the identified third-party libraries as dependencies in their build configuration

Auxiliary Analysis Workflow

Java JAR Analysis

  • Search for traditional main method: Use search_classes_by_keyword(search_term="main", search_in="method") to find public static void main(String[] args) methods
  • Check MANIFEST.MF: Look in META-INF/MANIFEST.MF for Main-Class attribute specifying the entry point
  • Spring Boot applications: Search for @SpringBootApplication annotation or SpringApplication.run() calls
  • Framework-specific entry points: Look for framework initialization code (e.g., Servlet initializers, container entry points)
  • Service applications: Identify service startup classes (e.g., classes with @Service, @Component annotations)

Mandatory Constraints

  • JADX-GUI Requirement: Must first start jadx-gui and load target file
  • Service Pre-check: See "IMPORTANT: JADX-GUI Pre-check" section above, must strictly execute
  • Tool Usage Limitation: Only use jadx-ai-mcp tools listed above, do not call non-existent tools
  • Clue Priority: When semantically renaming, prioritize clues from comments (especially JADX INFO: compiled from comments), followed by string constants, inheritance relationships, etc.
  • Step-by-step: First get overview during analysis, then gradually delve into details
  • Third-party Library Policy:
    • ALWAYS identify third-party libraries first
    • NEVER deobfuscate or rename third-party library classes
    • Record library names and versions for user reference
  • Core Logic Focus: Only deobfuscate application's own core logic classes
  • NO Automatic Project Creation: Never attempt to create Maven/Gradle projects automatically
  • NO Command Line Decompilation: Never use jadx command line to directly decompile for building projects - all renaming must be done in jadx-gui first
  • Notify User When Complete: When deobfuscation is finished, notify user and let them handle export and project creation manually
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment