弥合 Android 逆向工程中 Java 与原生代码之间的鸿沟。
自动化 RegisterNatives 恢复、剥离后的 ARM64 .so 类型重建,以及统一的 Jadx + Ghidra 或 IDA MCP 服务器,用于 AI 辅助的恶意软件分析。
快速开始 • 工作原理 • MCP 工具 • 隐蔽 Dobby 代理 • 路线图
现代 Android 恶意软件、银行木马、间谍软件和商业加壳工具将其核心逻辑(C2 加密、密钥派生、环境检查和漏洞利用例程)隐藏在**剥离符号的 C/C++ 原生库(lib*.so)**中。
在跨 JNI 边界进行逆向分析时,分析人员会遇到三道障碍:
classes.dex 中声明的 native 方法不使用标准的 Java_com_pkg_Class_method 符号。相反,它们通过 JNIEnv->RegisterNatives() 在混淆的 JNI_OnLoad 内部于运行时动态绑定。.so 会显示数千个未命名的 sub_XXXX 函数,其中 JNIEnv* 虚表调用看起来像原始指针运算((**(code **)(*param_1 + 0x538))(param_1, param_2))。BroadcastReceiver 一直到 libnative-lib.so 中 ARM64 加密例程的数据流。JNISleuth-MCP 消除了上下文切换。 它自动恢复静态和动态的 RegisterNatives 绑定,在 Ghidra 中重新为剥离符号的 ARM64 函数赋予类型,并提供一个统一的 模型上下文协议(MCP) 服务器,将 DEX 字节码与原生汇编连接起来。
| 能力 | 标准 Jadx / Ghidra MCP | JNISleuth-MCP |
|---|---|---|
解析标准 Java_* 导出 | ✅ | ✅ |
静态恢复剥离符号的 RegisterNatives 结构体数组(.rodata / .data.rel.ro + JNI_OnLoad CFG) | ❌ | ✅ |
通过 Dobby 在设备上隐蔽采集 RegisterNatives(绕过反 Frida 检测) | ❌ | ✅ |
在 Ghidra 中自动应用 JNIEnv* 虚表类型并重建 Call*Method / GetStringUTFChars | ❌ | ✅ |
单次调用跨边界数据流(Java 调用方 ➔ JNI 桥接 ➔ 反编译的 ARM64 C++) | ❌ | ✅ |
flowchart LR
subgraph Input["📦 Target APK / Malware"]
DEX["classes*.dex"]
ELF["Stripped lib/*.so (ARM64)"]
end
subgraph Harvester["🔍 Stage 1: JNI Bridge Harvester"]
StaticScan["Static ELF & DEX Scanner\n(LIEF + Capstone + Heuristic Triplets)"]
DobbyAgent["Stealth Native Agent\n(C++ / NDK / Dobby Inline Hook)"]
end
subgraph GhidraEngine["⚙️ Stage 2: Ghidra Headless Reconstructor"]
Renamer["Function Renamer & Signature Applier"]
VTable["JNIEnv* VTable & Struct Propagator"]
end
subgraph MCPServer["🤖 Stage 3: Unified FastMCP Server"]
CrossGraph["Cross-Boundary Callgraph\n(Jadx Java + Ghidra C/ARM64)"]
CallbackTrace["Upcall Tracer\n(Native -> Java Reflection)"]
end
DEX --> StaticScan
ELF --> StaticScan
ELF -. "Packed / OLLVM?" .-> DobbyAgent
StaticScan --> Renamer
DobbyAgent -- "register_natives.json" --> Renamer
Renamer --> VTable
VTable --> CrossGraph
VTable --> CallbackTrace
RegisterNatives 恢复对带有已剥离原生库的 APK 运行 jnisleuth scan:
$ jnisleuth scan --apk samples/banker_dropper.apk --arch arm64-v8a
[*] Parsing 3 DEX files... Found 14 declared `native` methods across 4 classes.
[*] Inspecting lib/arm64-v8a/libcore-crypto.so (Stripped ELF, 0 exported Java_* symbols)
[*] Running static JNINativeMethod triplet scanner on .rodata / .data.rel.ro...
[+] Resolved 11/14 methods via static struct table @ 0x0004d210
[!] 3 methods unresolved (constructed dynamically on stack in JNI_OnLoad).
[*] Launching headless Dobby collector on device (emulator-5554)...
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓
┃ Java Class & Method ┃ JNI Signature ┃ .so Offset (ARM64) ┃ Resolution Mode ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩
│ com.dropper.Core#initEngine │ (Landroid/content/.. │ libcore.so+0x001a40 │ Static (.rodata) │
│ com.dropper.Core#decryptPayload │ ([BLjava/lang/Stri.. │ libcore.so+0x0023c8 │ Static (.rodata) │
│ com.dropper.Core#signC2Beacon │ (Ljava/lang/String.. │ libcore.so+0x003f10 │ Dynamic (Dobby) │
│ com.dropper.net.SocketBridge#connectRaw │ (Ljava/lang/String.. │ libcore.so+0x004890 │ Dynamic (Dobby) │
└──────────────────────────────────────────┴──────────────────────┴─────────────────────┴──────────────────┘
[+] Synced 14 function signatures and JNIEnv* types to Ghidra project: banker_dropper.gpr
使用 JNISleuth-MCP 之前(FUN_000123c8):
long FUN_000123c8(long *param_1, undefined8 param_2, undefined8 param_3, undefined8 param_4) {
char *pcVar1;
long lVar2;
pcVar1 = (char *)(**(code **)(*param_1 + 0x548))(param_1, param_4, 0);
lVar2 = (**(code **)(*param_1 + 0x5c0))(param_1, param_3, 0);
sub_00014a00(lVar2, pcVar1);
(**(code **)(*param_1 + 0x550))(param_1, param_4, pcVar1);
return param_3;
}
在 JNISleuth-MCP 之后(Java_com_dropper_Core_decryptPayload):
jbyteArray Java_com_dropper_Core_decryptPayload(
JNIEnv *env, jobject thiz, jbyteArray encryptedData, jstring rc4KeyStr)
{
const char *key_utf = (*env)->GetStringUTFChars(env, rc4KeyStr, NULL);
jbyte *payload_bytes = (*env)->GetByteArrayElements(env, encryptedData, NULL);
rc4_crypt_buffer(payload_bytes, key_utf);
(*env)->ReleaseStringUTFChars(env, rc4KeyStr, key_utf);
return encryptedData;
}
JNISleuth-MCP 100% 与模型无关。你可以通过标准的模型上下文协议(stdio JSON-RPC 2.0)将其连接到任何云端或本地 LLM,或导出原生的 OpenAI / Gemini / Ollama 函数调用 schema:
jnisleuth llm-tools --format openai|gemini|mcp),适用于 LangChain、LlamaIndex、LiteLLM、OpenAI SDK 或 google-genai。| 工具名称 | 描述 |
|---|---|
resolve_apk_jni_map(apk_path) | 从 DEX 中提取所有 native 声明,并将其映射到 lib/*.so 中精确的 ELF 文件偏移(通过导出符号、静态 .rodata 三元组或动态跟踪导入)。 |
get_cross_boundary_slice(java_method) | 在单个统一响应中返回反编译的 Java 调用方(通过 Jadx)以及带类型的、反编译的 C/ARM64 实现(通过 Ghidra/IDA)。 |
trace_jni_upcalls(so_name, fn_offset) | 列出在原生 ARM64 函数及其被调用方内部调用的每个 Java 反射回调(FindClass、GetMethodID、CallObjectMethod、SetByteArrayRegion)。 |
sync_ghidra_jni_types(trace_json) | 将 JNIEnv*、JavaVM*、JNINativeMethod 结构体以及恢复的函数签名直接应用到活动或无头 Ghidra 项目中。 |
sync_ida_jni_types(trace_json) | 将 JNIEnv*、JavaVM*、JNINativeMethod C 头文件、恢复的函数原型以及 OLLVM fl_JN 代码交叉引用应用到 IDA Pro 9.0(idalib / idapyswitch / idat64)。 |
resolve_ollvm_indirect_branches(so_name, fn_offset) | 解析 JNI_OnLoad 中的 OLLVM CSEL + BR X8 状态机分发器和 .rodata 跳转表,并可选地用直接的 B.<cond> / B 指令修补 ELF 二进制文件。 |
extract_jni_onload_strings(so_name) | 转储 .init_array 和 JNI_OnLoad 内部由栈构造和 XOR 解密的字符串。 |
许多野外 Android 恶意软件家族和商业保护器一旦在 /proc/self/maps 中检测到 frida-agent.so、gum-js-loop 线程,或 libc.so 中被修改的函数序言,就会立即崩溃或退出。
JNISleuth-MCP 包含一个可选的零 Frida 原生收集器(agent-native/),使用 C++20 和 ARM64 汇编编写,基于 Dobby:
ArtMethod::RegisterNative / JNIEnvExt::RegisterNatives 分发路径内部,而非导出桩的第一条指令,从而规避基本的序言完整性扫描器。dladdr() 和链接器 soinfo 遍历来自动减去 ASLR 偏移并记录确定性的文件偏移(libfoo.so + 0x23c8),即使加壳器在 JNI_OnLoad 之后取消映射 ELF 节头也能正常工作。pip install uv)GHIDRA_INSTALL_DIR)或 IDA Pro 9.0+(IDA_INSTALL_DIR / idapyswitch)$PATH 中