
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 경계를 넘나들며 리버싱할 때 분석가는 세 가지 장벽에 부딪힙니다:
classes.dex에 선언된 native 메서드는 표준 Java_com_pkg_Class_method 심볼을 사용하지 않습니다. 대신 난독화된 JNI_OnLoad 내부에서 JNIEnv->RegisterNatives()를 통해 런타임에 동적으로 바인딩됩니다..so를 열면 수천 개의 이름 없는 sub_XXXX 함수가 보이며, JNIEnv* vtable 호출은 원시 포인터 연산((**(code **)(*param_1 + 0x538))(param_1, param_2))처럼 보입니다.BroadcastReceiver에서 libnative-lib.so의 ARM64 암호화 루틴까지 데이터 흐름을 추적할 수 없습니다.JNISleuth-MCP는 컨텍스트 전환을 제거합니다. 정적 및 동적 RegisterNatives 바인딩을 자동으로 복구하고, Ghidra에서 스트리핑된 ARM64 함수의 타입을 재지정하며, DEX 바이트코드와 네이티브 어셈블리를 연결하는 단일 통합 Model Context Protocol (MCP) 서버를 제공합니다.
| 기능 | 표준 Jadx / Ghidra MCP | JNISleuth-MCP |
|---|---|---|
표준 Java_* 익스포트 해석 | ✅ | ✅ |
스트리핑된 RegisterNatives 구조체 배열 정적 복구 (.rodata / .data.rel.ro + JNI_OnLoad CFG) | ❌ | ✅ |
Dobby를 통한 기기 내 스텔스 RegisterNatives 수집 (안티 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)을 통해 모든 클라우드 또는 로컬 LLM에 연결하거나, 네이티브 OpenAI / Gemini / Ollama 함수 호출 스키마를 내보낼 수 있습니다:
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의 수정된 함수 프롤로그를 감지하면 즉시 크래시하거나 종료합니다.