gaoyu06/c2j-native-deobfuscator
GitHub: gaoyu06/c2j-native-deobfuscator
针对 native-obfuscator 及其衍生工具的 Java 原生混淆还原器,通过动态 JVMTI 追踪或静态 Ghidra 反编译将 JNI 调用重建为可读的 JVM 字节码。
Stars: 1 | Forks: 0
**英语** | [中文](README.zh-CN.md)
# c2j-native-deobfuscator
将 **经过 JNI-native 混淆的 JAR** 逆向工程还原为可读的 Java
字节码。目标是针对 [`native-obfuscator`](https://github.com/radioegor146/native-obfuscator)
及其衍生工具(例如 j2cc)—— 即任何将 JVM 字节码
转译为 C++,然后从打包的 `.dll` / `.so` 中通过 JNI 重新调用 Java 的工具。
两种互补的还原路径:
| 路径 | 输入 | 方法 |
|---|---|---|
| **动态** | 混淆的 jar + 可运行命令 | 附加 JVMTI agent,观察 JNI 调用流,将其提升回 JVM 字节码 |
| **静态** | 混淆的 jar + Ghidra | 在 native 块中定位 JNI 方法表,反编译每个函数,将伪 C 代码提升为 JVM 字节码 |
任何一种路径都会生成一个干净的 `out.jar`,其原生方法现在拥有
真实的字节码主体,并且 loader / native 块的条目已被剥离。
许可证:**GPLv3**。
## 工作原理
### 动态路径
- **JVMTI agent** (`native/`, C++)。通过 `-agentpath:` 加载。订阅
`NativeMethodBind`、`MethodEntry`、`MethodExit`、`Exception`、
`ExceptionCatch` JVMTI 事件。
- **JNI 函数表替换**。在 `VMInit` 和每次 `ThreadStart` 时,
agent 使用一个副本覆盖 `JNIEnv->functions` 指针,该副本中大约 80 个条目被重定向到日志包装器。每个包装器
委托给原始函数,并将调用记录为 `trace.jsonl` 中的一行 JSON。可变参数 `Call*Method` 系列
根据每个类的 jmethodID 描述符缓存解码它们的 `va_list`。
- **符号表传播** (`jvm/trace-to-bytecode/`)。提升器
遍历跟踪记录,根据产生对象的内容(`FindClass` → jclass、`GetMethodID` → jmethodID 等)对每个 jobject 引用进行分类,并
发出具有完全解析的 owner / name / desc 的相应 JVM 操作码。
- **SSA 风格的合成局部变量**。方法在
语句间重复使用的每个 jobject 都会获得一个合成局部变量槽。提升器在
生成 JNI 调用之后发出 `DUP + ASTORE `,并在每个
重用站点发出 `ALOAD `,因此还原的字节码无需重新派生值即可保持真实的引用
一致性。
- **操作数栈平衡器**。跟踪活动栈并插入
`POP` / `CHECKCAST` / `ACONST_NULL` 修正,以便发出的
序列能在 ASM `COMPUTE_FRAMES` 下通过验证。
### 静态路径
- **反汇编级别的表发现** (`py/binary_introspect/`,
`capstone`)。遍历 native 块的可执行节,定位
每个 `call qword ptr [reg + 0x6B8]`(`RegisterNatives` JNI vtable
槽),对前面的指令进行反向扫描,查找 PC 相对的 `lea`,其目标位于 `.text` 中(即在
栈上构建的 `JNINativeMethod[]` 中的函数指针)加上最近的
`mov , imm`(即表大小)。
- **Ghidra 反编译器** (`ghidra/scripts/DumpFromManifest.java`,
Ghidra Headless)。从 `manifest.json` 读取 `(class, method, fnAddr)` 三元组,并在每个地址上运行 Ghidra 的 p-code 反编译器,
生成一个单一的 `ghidra-dump.json`,其中每个方法包含一个伪 C 主体。
- **tree-sitter-c 解析** (`py/ast_matcher/`, `tree-sitter-c`)。解析
伪 C 代码,然后使用特性标志驱动器遍历 AST,该
驱动器能识别 `env->FnName(args)` 调用(从
Ghidra 的 `(**(code **)(*reg + 0xN))(...)` 形式重写而来)、JNI 辅助模式和异常检查保护。
- **抛出原因推断**。原生混淆器系列的混淆器在每个将要进行 Java 调用之前发出
`"Cannot invoke X.Y.Z(args)"` 字符串,
用于运行时异常消息。提升器将它们提取为
调用提示,并在符号跟踪无法通过混淆器辅助函数解析 jmethodID 时将其用作后备(owner、name、args-desc)。
- **Profile 自动检测**。活动的混淆器收集策略
(按类 vs 共享调度)、抛出原因正则表达式以及 if-guard 跳过规则均来自通过根据内置检测器扫描
二进制文件来选择的 :class:`Profile`。
### 共享部分
- **JSON 管道**。每个阶段的输入 + 输出都是 `schemas/` 下的带版本控制的 JSON
产物。
- **ASM** (`org.objectweb.asm`) 驱动所有类文件生成。
`ClassWriter.COMPUTE_FRAMES` 是验证关卡;触发栈失衡的
方法会获得一个哨兵桩主体,jar 仍然可以正常发布。
## 何时使用哪种路径
这两种路径针对相同的输入,但在覆盖率和准确性之间进行了权衡:
| | 动态 | 静态 |
|---|---|---|
| **最适用** | 二进制文件被打包 / VM 保护 / 具有反调试功能 —— JVMTI agent 位于 Java 侧,因此原生保护层无关紧要。 | 二进制文件未受保护(例如,直接的 native-obfuscator + zig c++ 输出)。Ghidra 可以直接反编译每个 `fnAddr`。 |
| **要求** | 能运行混淆类且可执行的命令行 (`java -jar ...`)。 | 已安装 Ghidra 11.x。 |
| **覆盖率** | 仅限于你实际运行的分支。从未被调用的方法将被完全遗漏。 | 通过 `RegisterNatives` 注册的每个方法,无论是否曾调用过。 |
| **准确性** | 高 —— 每个发出的操作码都对应于 JVM 观察到的真实 JNI 调用。 | 尽力而为 —— 提升器对反编译的 C 代码进行模式匹配,并在无法维持栈平衡时回退到桩代码。 |
| **速度** | 受限于目标程序执行其代码路径所需的时间 + agent 的开销。 | 受限于 Ghidra 的自动分析(对于约 1 MB 的块需要几分钟)。 |
## 局限性
- **静态路径的正确性是尽力而为的。** 提升器对
Ghidra 的伪 C 进行模式匹配;具有 Ghidra 未干净地结构化的控制流的方法会产生栈失衡的字节码,`class-rebuilder` 会
静默地将其降级为桩代码。该类的其余部分仍然正常发布。
- **动态路径只能看到目标实际执行的分支。**
如果只运行了 `if` 分支的 if/else 语句,将被还原为仿佛 `else` 不存在。循环体显示一次迭代跟踪;
具体值会被烘焙为 LDC,除非被 agent 标记为动态。
- **纯原生控制流 / 算术对两种路径均不可见。**
当混淆器将不需要 JVM
往返的计算(字符数组操作、整数数学运算)完全翻译为 C++ 时,
没有 JNI 调用发生,因此动态跟踪和 JNI 调用模式
匹配都什么也看不到。这些区域被还原的方法主体最终是空的或被桩代码代替。
- **AOT 转换的逻辑无法恢复。** 高级混淆器
检测到不需要 JVM 合作的 Java 代码(例如
对称加密、字节数组转换、通过
POSIX/Win32 的文件 I/O),并将其作为直接的原生代码而不是
JNI 回调 C++ 发出。该输出根本没有 JNI 签名 —— 这两种
路径都会为这些方法生成桩代码。唯一的恢复方法是手工阅读
反汇编代码。
- **`` 中解密表里的字符串字面量仍保持**
未解析状态。每个混淆类将其字符串常量包装在
在类加载时解码的 XOR/旋转表中。提升器逐字保留了
索引访问(`Foo.a(0, 17)`)而不是替换值;运行一次清理后 jar 的 `` 并快照该
表已在路线图中(请参阅 `docs/ROADMAP.md`)。
## 截图
图片位于 [`screenshots/showcase/`](screenshots/showcase/)。完整
目录请参阅 [`screenshots/README.md`](screenshots/README.md)。
**反编译器视图 (IntelliJ / CFR)**
| 之前 | 之后 |
|---|---|
|  |  |
| 原生桩代码 + loader 类仍然存在 | 主体已重构,loader 已剥离 |
**`javap -c -p` — 单个方法**
| 之前 | 之后 |
|---|---|
|  |  |
| 仅包含 `native` 标志,无 Code 属性 | 真实的 Code 属性 + 操作码流 |
**端到端管道与 Ghidra 伪 C**
| 还原管道 | Ghidra 伪 C |
|---|---|
|  |  |
| 动态路径每阶段的输出 | 静态路径上提升器的实际输入 |
## 快速开始
### 一次性构建
```
# JVM 模块
cd jvm && ./gradlew installDist
# Python 工作区
cd py && uv sync --all-packages
# Native agent (仅 dynamic path 需要)
cd native && JDK_HOME="$JAVA_HOME" bash build.sh
```
### 动态还原(当 jar 可以在你的环境中运行时优先使用)
```
python -m j2c_dumper_cli.main recover \
path/to/obfuscated.jar \
-o path/to/clean.jar \
--run-cmd "java -jar path/to/obfuscated.jar"
```
此过程链包含:
1. `parse-jar` → `classes.json`
2. `inspect-binary` (自动从 jar 中提取原生块)
3. `merge-manifest` → `manifest.json`
4. `dynamic-trace` 使用 JVMTI agent 运行目标 → `trace.jsonl`
5. `trace-to-bc` 提升为 `recovered/*.json`
6. `rebuild` 生成剥离了 loader 的输出 jar
### 静态还原(当你无法运行 jar 时 —— 需要 Ghidra)
```
# 1. 如上所述解析 jar + 内省二进制文件 (无需 --run-cmd)
python -m j2c_dumper_cli.main parse-jar in.jar -o classes.json
python -m j2c_dumper_cli.main inspect-binary natives.bin -o binary.json
python -m j2c_dumper_cli.main merge-manifest classes.json binary.json -o manifest.json
# 2. 针对 native blob 运行 Ghidra headless
/support/analyzeHeadless.bat proj \
-import natives.bin \
-scriptPath /ghidra/scripts \
-postScript DumpFromManifest.java manifest.json ghidra-dump.json
# 3. 将 pseudo-C 提升为 bytecode + 重建
python -m ast_matcher.cli ghidra-dump.json --manifest manifest.json -o recovered/
python -m j2c_dumper_cli.main rebuild --input in.jar --recovered recovered/ \
--manifest manifest.json -o out.jar
```
### 按阶段执行
每个阶段都在 `j2c-dumper` 下有自己的子命令;请参阅
`python -m j2c_dumper_cli.main --help` 获取完整列表。
## 通用性
该项目附带了两个可自动检测的混淆器 **Profile**:
- `native_obfuscator` — radioegor146/native-obfuscator + 兼容的衍生工具
- `j2cc` — me.x150.j2cc (单一共享的 `initClass` 调度)
- `generic` — 当没有 profile 匹配时的回退方案;仅使用纯 JNI 规范知识
自定义变体可以在不触及主流程的情况下插入新的 profile。
请参阅 [`docs/adding-obfuscator-profile.md`](docs/adding-obfuscator-profile.md)。
静态路径的提升器将每个推理/匹配步骤公开为
特性标志(抛出原因提示解析、ExceptionCheck 保护跳过、
符号表跟踪、查找表解析等)。当某个标志在特定二进制文件上
行为异常时,请禁用该标志:
```
python -m ast_matcher.cli ghidra-dump.json -o recovered/ \
--disable use_throw_reason_invoke_hints \
--disable skip_native_exception_guards
python -m ast_matcher.cli --list-flags
```
## 仓库布局
```
├── jvm/ Kotlin/ASM modules (Gradle multi-project)
│ ├── jar-parser/ input.jar → classes.json
│ ├── trace-to-bytecode/ manifest + trace.jsonl → recovered/*.json
│ ├── class-rebuilder/ input.jar + recovered/ → output.jar
│ └── common/ shared schema types
├── native/ C++ JVMTI agent (zig c++ build)
├── ghidra/scripts/ Ghidra headless scripts (Java)
├── py/ Python modules (uv workspace)
│ ├── jar_parser/ —
│ ├── binary_introspect/ .dll / .so / natives.bin → binary.json
│ │ ├── arch/ per-arch / ABI implementations
│ │ ├── jni_tables.py RegisterNatives table discovery
│ │ ├── profile.py obfuscator-variant profiles
│ │ └── stub_recovery.py synthesize stub bodies for unrecovered methods
│ ├── manifest_merge/ classes.json + binary.json → manifest.json
│ ├── ast_matcher/ pseudo-C → JVM bytecode
│ │ └── lifter/ driver + per-feature submodules
│ ├── j2c_dumper_cli/ top-level CLI orchestrator
│ └── snippet_importer/ (optional) native-obfuscator cppsnippets ingestor
├── docs/ ARCHITECTURE.md, ROADMAP.md, profile guide, …
├── schemas/ JSON Schema for every artifact
└── tests/ e2e fixtures and pipeline tests
```
## 文档
- [ARCHITECTURE.md](docs/ARCHITECTURE.md) — 模块边界,管道,
产物模式,扩展点
- [ROADMAP.md](docs/ROADMAP.md) — 已知局限和计划工作
- [adding-obfuscator-profile.md](docs/adding-obfuscator-profile.md) — 如何
注册新的混淆器变体
- [static-reverse-approach.md](docs/static-reverse-approach.md) — 基于 Ghidra 路径的
设计说明
## 许可证
在 **GPL v3** 下发布。请参阅 [LICENSE](LICENSE)。
标签:Android安全, C++, DAST, DNS 反向解析, DNS 解析, GHAS, Ghidra, jar反编译, Java安全, Java逆向, JNI分析, JS文件枚举, JVMTI, native-obfuscator, 二进制分析, 云安全监控, 云安全运维, 云资产清单, 代码混淆与还原, 动态二进制插桩, 反混淆, 字节码恢复, 安全工具开发, 恶意软件分析, 数据擦除, 桌面安全, 逆向工具, 逆向工程, 静态分析