Skip to main content
Glama
JNISleuth-MCP

JNISleuth-MCP

by nighthawkk

πŸ•΅οΈβ€β™‚οΈ JNISleuth-MCP

Bridge the Java-to-Native Chasm in Android Reverse Engineering.
Automated RegisterNatives Recovery, Stripped ARM64 .so Type Reconstruction, and a Unified Jadx + Ghidra or IDA MCP Server for AI-Assisted Malware Analysis.

Platform Ghidra MCP Native Engine License

Quickstart β€’ How It Works β€’ MCP Tools β€’ Stealth Dobby Agent β€’ Roadmap


⚑ Why JNISleuth-MCP?

Modern Android malware, banking trojans, spyware, and commercial packers hide their core logic (C2 encryption, key derivation, environment checks, and exploit routines) inside stripped C/C++ native libraries (lib*.so).

When reversing across the JNI boundary, analysts hit three walls:

  1. Broken Cross-References: native methods declared in classes.dex do not use standard Java_com_pkg_Class_method symbols. Instead, they are dynamically bound at runtime inside an obfuscated JNI_OnLoad via JNIEnv->RegisterNatives().

  2. Blind Decompilation: Opening a stripped .so in Ghidra or IDA shows thousands of unnamed sub_XXXX functions where JNIEnv* vtable calls look like raw pointer arithmetic ((**(code **)(*param_1 + 0x538))(param_1, param_2)).

  3. Disconnected AI Context: Existing Ghidra MCP and Jadx MCP servers live in silosβ€”an LLM cannot trace data flow from an Android BroadcastReceiver in Java straight into an ARM64 encryption routine in libnative-lib.so.

JNISleuth-MCP eliminates the context switch. It automatically recovers static and dynamic RegisterNatives bindings, re-types stripped ARM64 functions in Ghidra, and exposes a single unified Model Context Protocol (MCP) server connecting DEX bytecode to native assembly.

Capability

Standard Jadx / Ghidra MCPs

JNISleuth-MCP

Resolve standard Java_* exports

βœ…

βœ…

Recover stripped RegisterNatives struct arrays statically (.rodata / .data.rel.ro + JNI_OnLoad CFG)

❌

βœ…

Stealth on-device RegisterNatives harvesting via Dobby (bypasses anti-Frida checks)

❌

βœ…

Auto-apply JNIEnv* vtable types & reconstruct Call*Method / GetStringUTFChars in Ghidra

❌

βœ…

Single-call cross-boundary dataflow (Java Caller βž” JNI Bridge βž” Decompiled ARM64 C++)

❌

βœ…


Related MCP server: Frida Agent MCP

πŸ—οΈ Architecture & How It Works

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

🎬 See It In Action

1. CLI Static + Dynamic RegisterNatives Recovery

Run jnisleuth scan against an APK with stripped native libraries:

$ 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

2. Before vs. After in Ghidra

Before 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;
}

After 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;
}

πŸ€– Universal LLM & MCP Support (Works With Any Model)

JNISleuth-MCP is 100% model-agnostic. You can connect it to any cloud or local LLM via the standard Model Context Protocol (stdio JSON-RPC 2.0) or export native OpenAI / Gemini / Ollama function-calling schemas:

  • Cloud LLMs & Agents: Google Gemini CLI, OpenAI / ChatGPT Desktop / Codex CLI, Cursor, Windsurf, Cline, Roo Code, Claude Desktop / Claude Code

  • Local / Self-Hosted LLMs: Ollama, vLLM, LM Studio, OpenWebUI, Continue.dev (DeepSeek-R1, Qwen-2.5-Coder, Llama-3.3)

  • Custom Python Agent SDKs: Direct tool-schema export (jnisleuth llm-tools --format openai|gemini|mcp) for LangChain, LlamaIndex, LiteLLM, OpenAI SDK, or google-genai.

Tool Name

Description

resolve_apk_jni_map(apk_path)

Extracts all native declarations from DEX and maps them to exact ELF file offsets in lib/*.so (via export symbols, static .rodata triplets, or dynamic trace import).

get_cross_boundary_slice(java_method)

Returns the decompiled Java caller (via Jadx) and the typed, decompiled C/ARM64 implementation (via Ghidra/IDA) in a single unified response.

trace_jni_upcalls(so_name, fn_offset)

Lists every Java reflection callback (FindClass, GetMethodID, CallObjectMethod, SetByteArrayRegion) invoked inside a native ARM64 function and its callees.

sync_ghidra_jni_types(trace_json)

Applies JNIEnv*, JavaVM*, JNINativeMethod structs, and recovered function signatures directly into an active or headless Ghidra project.

sync_ida_jni_types(trace_json)

Applies JNIEnv*, JavaVM*, JNINativeMethod C headers, recovered function prototypes, and OLLVM fl_JN code cross-references into IDA Pro 9.0 (idalib / idapyswitch / idat64).

resolve_ollvm_indirect_branches(so_name, fn_offset)

Resolves OLLVM CSEL + BR X8 state-machine dispatchers and .rodata jump tables in JNI_OnLoad, and optionally patches the ELF binary with direct B.<cond> / B instructions.

extract_jni_onload_strings(so_name)

Dumps stack-constructed and XOR-decrypted strings inside .init_array and JNI_OnLoad.


πŸ₯· Stealth Dobby Agent (Anti-RASP & Packer Mode)

Many wild Android malware families and commercial protectors immediately crash or exit if they detect frida-agent.so in /proc/self/maps, gum-js-loop threads, or modified function prologues in libc.so.

JNISleuth-MCP includes an optional zero-Frida native collector (agent-native/) written in C++20 and ARM64 assembly using Dobby:

  • Mid-Function Hooking: Places hooks inside the ArtMethod::RegisterNative / JNIEnvExt::RegisterNatives dispatch path rather than the first instruction of exported stubs, evading basic prologue integrity scanners.

  • Base-Address Normalization: Uses dladdr() and linker soinfo traversal to automatically subtract ASLR slide and record deterministic file offsets (libfoo.so + 0x23c8), even if the packer unmaps ELF section headers after JNI_OnLoad.


πŸš€ 2-Minute Quickstart

Prerequisites

  • Python 3.10+ (pip install uv)

  • Ghidra 11.0+ (GHIDRA_INSTALL_DIR) or IDA Pro 9.0+ (IDA_INSTALL_DIR / idapyswitch)

  • Jadx CLI installed in $PATH

  • (Optional) Android NDK r26+ & ADB for dynamic Dobby collection

Installation

git clone https://github.com/<your-username>/JNISleuth-MCP.git
cd JNISleuth-MCP
uv sync

# 1. Scan the included obfuscated NDK crackme sample & sync with Ghidra / IDA Pro 9.0:
uv run jnisleuth scan --apk examples/obfuscated_jni_sample.apk --ghidra-headless --ida-headless

# 2. Resolve & patch OLLVM CSEL + BR X8 indirect branches in JNI_OnLoad:
uv run jnisleuth deobfuscate --so examples/libcore-crypto.so --offset 0x5200 --patch-out examples/libcore-crypto.deobf.so

Connect to Any LLM / IDE Agent (mcp.json / settings.json)

Works out of the box with Gemini CLI (~/.gemini/settings.json), Cursor (.cursor/mcp.json), Windsurf, Cline, Continue.dev (Ollama), ChatGPT Desktop, and Claude:

{
  "mcpServers": {
    "jnisleuth-mcp": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/JNISleuth-MCP", "jnisleuth-mcp"],
      "env": {
        "GHIDRA_INSTALL_DIR": "/opt/ghidra",
        "IDA_INSTALL_DIR": "/opt/idapro-9.0"
      }
    }
  }
}

Use with OpenAI / Gemini / Ollama Python SDKs Directly (No MCP Client Needed)

# Export function-calling tool schemas for OpenAI / Ollama / vLLM or Google Gemini:
uv run jnisleuth llm-tools --format openai > openai_tools.json
uv run jnisleuth llm-tools --format gemini > gemini_tools.json

πŸ—ΊοΈ Roadmap

  • Static DEX native method parser + ELF .rodata / .data.rel.ro JNINativeMethod triplet scanner

  • Headless Ghidra JNIEnv* vtable auto-typing script for ARM64 & x86_64

  • Unified FastMCP server bridging Jadx, Ghidra & IDA Pro 9.0

  • On-device Dobby mid-function RegisterNatives + DexFile::OpenCommon unpacker module

  • IDA Pro 9.0 (idapyswitch / idalib / headless idat64) backend support alongside Ghidra

  • Automated OLLVM indirect-branch (BR X8) resolver & ELF patcher for JNI_OnLoad state machines


⚠️ Disclaimer

JNISleuth-MCP is developed for defensive malware analysis, mobile application security assessments, and reverse-engineering research. All sample binaries in examples/ are synthetic crackmes created specifically for testing.

Available Tools

12 tools
deobfuscate_jni_stringsC

Alias for extract_jni_onload_strings.

Inputs: so_name (required), apk_path.

extract_jni_onload_stringsC

Dumps stack-constructed and XOR-decrypted strings inside .init_array and JNI_OnLoad.

Inputs: so_name (required), apk_path.

get_cross_boundary_callgraphD

Alias for get_cross_boundary_slice.

Inputs: java_method (required), apk_path.

get_cross_boundary_sliceC

Returns the decompiled Java caller (via Jadx) and the typed, decompiled C/ARM64 implementation (via Ghidra/IDA) in a single unified response.

Inputs: java_method (required), apk_path.

import_dynamic_traceD

Alias for sync_ghidra_jni_types.

Inputs: trace_json (required), project_name.

list_jni_methodsD

Alias for resolve_apk_jni_map.

Inputs: apk_path (required), arch.

resolve_apk_jni_mapC

Extracts all native declarations from DEX and maps them to exact ELF file offsets in lib/*.so.

Inputs: apk_path (required), arch, dynamic_trace_path.

resolve_ollvm_indirect_branchesC

Resolves OLLVM CSEL + BR X8 state-machine dispatchers and .rodata jump tables in JNI_OnLoad, optionally patching the ELF binary.

Inputs: so_name (required), apk_path, fn_offset, patch_output_path.

sync_ghidra_jni_typesB

Applies JNIEnv*, JavaVM*, JNINativeMethod structs, and recovered function signatures directly into an active or headless Ghidra project.

Inputs: trace_json (required), project_name.

sync_ida_jni_typesC

Applies JNIEnv*, JavaVM*, JNINativeMethod structs, and recovered function signatures into IDA Pro 9.0 (idalib / idapyswitch / idat64).

Inputs: trace_json (required), database_name.

trace_jni_callbacksD

Alias for trace_jni_upcalls.

Inputs: so_name (required), fn_offset (required), apk_path.

trace_jni_upcallsC

Lists every Java reflection callback (FindClass, GetMethodID, CallObjectMethod, SetByteArrayRegion) invoked inside a native ARM64 function.

Inputs: so_name (required), fn_offset (required), apk_path.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    C2.3/5.0

    Scored across 12 tools

    Disambiguation2/5

    Five of the twelve tools are explicit aliases of other tools, so nearly half the surface duplicates existing functionality. Although the descriptions label the aliases, an agent still has multiple names for the same operation, creating avoidable overlap and misselection risk.

    Naming Consistency4/5

    All tool names use snake_case and generally follow a verb_noun pattern, with parallel names such as sync_ghidra_jni_types and sync_ida_jni_types. The main weakness is that aliases use different verbs and resource names for the same underlying operation, which is a minor inconsistency in an otherwise predictable naming scheme.

    Tool Count3/5

    The nominal count of 12 is still within a reasonable range, but five entries are redundant aliases, leaving only about seven unique capabilities. The surface is artificially inflated rather than genuinely broad.

    Completeness3/5

    The core JNI reverse-engineering workflow is covered: mapping native methods, cross-boundary slicing, upcall tracing, type syncing for Ghidra/IDA, OLLVM branch resolution, and string extraction. However, some advertised operations such as callgraph retrieval and dynamic trace import are merely aliases, so an agent expecting those distinct capabilities will hit gaps.

    Maintenance

    ActivityMaintained
    ResponsivenessSyncing

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      A
      quality
      F
      maintenance
      An AI-driven Android dynamic analysis tool that enables AI models to perform automated reverse engineering by controlling Frida. It provides capabilities to inject scripts, manage application processes, and analyze real-time execution data through the Model Context Protocol.
      10
      70
      -
    • A
      license
      A
      quality
      C
      maintenance
      A multi-backend MCP server that exposes binary analysis capabilities from IDA Pro and Ghidra, allowing LLMs to directly drive reverse-engineering tools via natural language.
      11
      177
      Apache 2.0
    • A
      license
      Not graded
      quality
      D
      maintenance
      Local static-analysis assistant for Android malware research that manages investigation cases, exposes MCP tools via a local server, and persists evidence-backed findings without cloud dependency.
      MIT