
Android JNI RegisterNativesの自動復元、ストリップされたARM64 .soの型再構築、OLLVM BR X8難読化解除、およびJadx、Ghidra、IDA Pro 9.0向けの汎用LLM/MCPブリッジ。
AndroidリバースエンジニアリングにおけるJavaからネイティブへの隔たりを橋渡しする。
自動化されたRegisterNativesの復元、ストリップされたARM64 .soの型再構築、そしてAI支援マルウェア解析のためのJadx + GhidraまたはIDA統合MCPサーバー。
クイックスタート • 仕組み • MCPツール • ステルスDobbyエージェント • ロードマップ
現代のAndroidマルウェア、バンキングトロイの木馬、スパイウェア、商用パッカーは、そのコアロジック(C2暗号化、鍵導出、環境チェック、エクスプロイトルーチン)を**ストリップされたC/C++ネイティブライブラリ(lib*.so)**の中に隠しています。
JNI境界を越えてリバースエンジニアリングを行う際、アナリストは3つの壁に直面します:
classes.dexで宣言されたnativeメソッドは、標準的なJava_com_pkg_Class_methodシンボルを使用しません。代わりに、難読化されたJNI_OnLoad内でJNIEnv->RegisterNatives()を介して実行時に動的にバインドされます。.soをGhidraやIDAで開くと、数千もの無名のsub_XXXX関数が表示され、JNIEnv*のvtable呼び出しは生のポインタ演算((**(code **)(*param_1 + 0x538))(param_1, param_2))のように見えます。BroadcastReceiverからlibnative-lib.so内のARM64暗号化ルーチンへとデータフローをたどることができません。JNISleuth-MCPはコンテキストスイッチを排除します。 静的なRegisterNativesバインディングと動的なRegisterNativesバインディングを自動的に復元し、Ghidra内でストリップされたARM64関数の型を再設定し、DEXバイトコードをネイティブアセンブリに接続する単一の統合**Model Context Protocol(MCP)**サーバーを公開します。
| 機能 | 標準のJadx / Ghidra MCP | JNISleuth-MCP |
|---|---|---|
標準のJava_*エクスポートを解決 | ✅ | ✅ |
ストリップされたRegisterNatives構造体配列を静的に復元(.rodata / .data.rel.ro + JNI_OnLoad CFG) | ❌ | ✅ |
DobbyによるステルスなオンデバイスRegisterNatives収集(anti-Fridaチェックをバイパス) | ❌ | ✅ |
JNIEnv* vtable型を自動適用し、Ghidra内でCall*Method / GetStringUTFCharsを再構築 | ❌ | ✅ |
単一呼び出しでのクロスバウンダリデータフロー(Java Caller ➔ JNI Bridge ➔ Decompiled 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% モデル非依存です。標準の Model Context Protocol(stdio JSON-RPC 2.0)経由で、あるいはネイティブの OpenAI / Gemini / Ollama 関数呼び出しスキーマをエクスポートすることで、あらゆるクラウドまたはローカル LLM に接続できます:
google-genai 向けの直接的なツールスキーマエクスポート(jnisleuth llm-tools --format openai|gemini|mcp)。| ツール名 | 説明 |
|---|---|
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 ジャンプテーブルを解決し、オプションで ELF バイナリを直接 B.<cond> / B 命令でパッチします。 |
extract_jni_onload_strings(so_name) | .init_array および JNI_OnLoad 内でスタック上に構築され XOR で復号された文字列をダンプします。 |
多くの野生の Android マルウェアファミリや商用プロテクターは、/proc/self/maps 内の frida-agent.so、gum-js-loop スレッド、または libc.so 内の改変された関数プロローグを検出すると、直ちにクラッシュまたは終了します。