SuperAPI AI Directory

re-android-native

Data & analytics

View repository

Updated 10-15 21:34

About this skill

Android Native Library (JNI/.so) Reverse Engineering

Entry Decision Gate

When the target appears, first determine its form; once matched, switch to the corresponding skill (automatic scheduling and routing):

目标
├── APK
│    ├── lib/*.so 存在?──是──→ 本技能(re-android-native)
│    └── 无 so(纯 Java)──→ re-apk
├── 单独 .so
│    ├── JNI 符号(Java_* / JNI_OnLoad / RegisterNatives 调用点)──→ 本技能
│    ├── 加密库 API(SSL_*/EVP_*/crypto_* 等)──→ re-android-crypto(加密语义)
│    └── 普通 ELF(无 JNI 特征)──→ re-binary-core(re-format-elf)
├── 加固 so(壳壳/熵高/导入表极小)──→ re-anti-analysis / re-mobile-pack(脱壳后回本技能)
└── 加密体系审计目标(Keystore/Cipher)──→ re-android-crypto

When to Use / When Not to Use

  • Use: the lib/*.so native library in the APK — JNI interface restoration, registration method (static/dynamic), native logic analysis
  • Use: Java-layer logic is clear but the core algorithm/anti-debugging is in native (games, hardening, encryption logic)
  • Use: only System.loadLibrary + native declarations are visible at the Java layer, and the native implementation needs to be located and restored
  • Do not use: if you only need the APK Java layer statically (use [[re-apk]]); if you only need runtime hooking without needing to understand the .so structure (use [[re-frida]])
  • Do not use: if the so is a hardening/packer shell (first unpack via [[re-mobile-pack]] / [[re-anti-analysis]], see Pitfall 5)
  • Note: dynamic instrumentation must be executed within a controlled device / emulator snapshot per the highest principle of [[re-analyze/platform-tips]]

Tool Preparation

Static analysis (readelf / Ghidra importing .so) does not require a sandbox; dynamic (frida / xhook) must be executed in a controlled environment per the highest principle of [[re-analyze/platform-tips]]. Verify all tools before use.

Decompilation workbench (so loading) — [[re-ghidra]] / [[re-ida]]

  • [[re-ghidra]] (default):File > Import File directly import ELF .so (arm64/arm/x86_64 all supported), load jni.h types via Data Type Manager (see [[re-ghidra]] Tool Preparation)
  • [[re-ida]]: after loading the so, decompile for ARM64;Option > Load new type load jni.h
  • Verification: after importing an arm64 .so, it can decompile the JNI_OnLoad or Java_* exported functions

jni.h — understanding the JNIEnv function table structure (bundled with the Android NDK)

  • No standalone distribution package; provided with the Android NDK. Official way: install the NDK via Android Studio SDK Manager, or via command line:
    sdkmanager "ndk;27.2.12479018"     # 先确认 sdkmanager 在 PATH(Android SDK cmdline-tools)
    # 或 GitHub android-ndk 官方仓库下载对应版本 zip
    
  • Key files:$NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/jni.h(on macOS the path is darwin-x86_64)
  • Verification: can open jni.h and see the definitions of JNINativeInterface(function table) and JNIEnv(function table pointer)

readelf (binutils) — export table/architecture identification (in conjunction with [[re-format-elf]])

  • Linux: apt install binutils / dnf install binutils / pacman -S binutils
  • macOS: brew install binutils(greadelf)
  • Verification: readelf --version

frida ([[re-frida]]) — JNI runtime observation

  • Host pip install frida-tools; for device frida-server push installation see [[re-frida]] Tool Preparation
  • Verification: frida-ps -U can list device processes

adb — extract so from device / install APK

  • Linux: apt install adb(Debian/Ubuntu)/ dnf install android-tools / pacman -S android-tools
  • macOS: brew install android-platform-tools; Windows: official platform-tools zip
  • Verification: adb version

Operation Steps

Execute in order; for each step record the artifact (so path, readelf output, Ghidra project, frida script) with evidence path + sha256 (see [[re-triage]]), for reference in the report.

  1. so extraction and architecture identification:

    unzip app.apk -d app/                        # APK 是 zip
    find app -name "*.so"                        # lib/ 下所有原生库
    file app/lib/arm64-v8a/libtarget.so          # 确认架构(ELF 64-bit ARM aarch64)
    readelf -h app/lib/arm64-v8a/libtarget.so    # Machine: AArch64 / x86-64
    readelf -d app/lib/arm64-v8a/libtarget.so    # 依赖库与动态段
    
    • ABI directories:arm64-v8a(main on real devices),armeabi-v7a(32-bit),x86_64/x86(emulator) — for multi-ABI differences see Pitfall 4
    • Installed apps:adb pull /data/app/.../lib/arm64-v8a /tmp/lib/(requires root or debuggable)
    • Record: target ABI, export table size, dependencies (readelf -d NEEDED), whether stripped (readelf -s number of symbols)
  2. JNI interface recovery (JNIEnv function table):

    readelf --dyn-syms app/lib/arm64-v8a/libtarget.so | grep -i java    # 静态注册的 Java_* 导出
    readelf --dyn-syms app/lib/arm64-v8a/libtarget.so | grep -i JNI     # JNI_OnLoad / JNI_OnUnload
    
    • JNIEnv is a function table pointer:JNIEnv* env actually points to the JNINativeInterface function table (defined in jni.h), and function calls are (*env)->GetStringUTFChars(env, ...) -style table slot accesses—seeing "get a function pointer from a struct offset and then call it" in decompilation means it is a JNI API, see pitfall 1
    • After loading the jni.h types in Ghidra/IDA, recover by name:GetStringUTFChars(Java jstring → Modified UTF-8 byte sequence)、NewStringUTF(Modified UTF-8 → Java String)、CallVoidMethod/CallBooleanMethod(calls back into Java),GetJavaVM(gets the JavaVM in-process)
    • is Modified UTF-8, not standard UTF-8: the two differ in the encoding of the null character (0xC0 0x80) and supplementary characters (surrogate pairs)—parsing as standard UTF-8 yields incorrect strings, and strlen -class operations done by byte count will also mismatch (the character count must be obtained using JNI's own length interface)
    • Recovery goal: native function signature ((JNIEnv*, jclass/jobject, 业务参数...))—the first parameter is env, the second is jclass (static) or jobject (instance), and business parameters start from the third
  3. Registration method (static JNI_OnLoad / dynamic RegisterNatives):

    • Static registration: function name Java_包名_类名_方法名(underscore escaping), appears directly in the export table (visible in step 2)
    • Dynamic registration:JNI_OnLoad calls RegisterNatives(env, clazz, methods, count),methods is a JNINativeMethod{name, signature, fnPtr} array—all three fields are pointers, and the width varies with the ABI(8 bytes each on 64-bit;4 bytes each on 32-bit ABIs such as armeabi-v7a / x86, array stride 12 bytes)—the function addresses are not in the export table(see pitfall 2); in decompilation locate JNI_OnLoad and then recover item by item along the third argument array of RegisterNatives
    • Mechanism key points (knowledge layer):JNIEnv* points to the JNINativeInterface function table (see pitfall 1),RegisterNatives is located in the standard JNIEnv function table index 215(UnregisterNatives 216)—this is the fixed layout of the JNI ABI; actual byte offset = index × target pointer width (215×8=0x6B8 on 64-bit). Android Dalvik/ART, the NDK, and standard jni.h do not change this index
    • frida observes runtime registration (spawn the target App): for the script template and runtime verification strategy (JNIEnv* validity / hook table detection / ABI argument-passing branches) see [[probes]]—what is volatile is the ABI argument registers and libart internal structures, so rely on actual measurements in the target environment
    • Recovery output:Java 方法名 → 签名 → native 函数地址 mapping table
  4. Logic analysis ([[re-ghidra]] linkage):

    • Import the so into Ghidra → load the jni.h types (step 2) → F5 decompile each native function
    • When there is no symbolic aid, infer from API call sites: the parameters of the JNI API are the entry points of business data (e.g., the return value of GetStringUTFChars is the input string)
    • Dig deep into algorithm/verification/decryption logic using the [[re-binary-core]] methodology; if obfuscated (OLLVM mutation/string encryption) → switch to [[re-deobfuscate]] (pitfall 3)
    • Statically registered Java_* functions often have cross-calls (native internal function tables/callbacks), so trace the source of function pointers ([[re-binary-core]] R6 idea)
  5. Interacting with the Java layer (locating native call sites):

    • Java side ([[re-apk]] / jadx):System.loadLibrary("target") and native the class where the method declaration resides——Java_包名_类名_* the naming comes from here
    • In jadx, for each native method find the call sites (who triggers it on what business path), align with the comparison table from step 3, and form the Java 调用点 ↔ native 函数 mapping
    • Dynamic verification: in frida Java.perform directly call the native method (Java.use("com.x.Cls").method(...)) to observe parameters and return value; or use the xhook / PLT hook approach ([[re-frida]]'s Interceptor.attach(Process.getModuleByName(...).findExportByName(...))) to observe the native internal call chain to external libraries (libc / system libraries)
    • Closed loop: Java trigger point → parameter source → native processing logic → output back to the Java layer

Cross-domain combination

  • [[re-mobile]]: This skill is a specialized sub-skill of step 4 of its workflow (native libraries)——once the gateway identifies a target containing .so it is dispatched by fixed rule
  • [[re-frida]]: Dynamically observe RegisterNatives / hook native functions / Java↔native interaction verification (spawn to catch JNI_OnLoad)
  • [[re-format-elf]]: Export table / dynamic segment / symbol resolution (readelf three tables), a prerequisite for locating dynamically registered functions
  • [[re-ghidra]]: so decompilation workbench + jni.h type loading
  • [[re-apk]]: Java side static (jadx to find native declarations and call sites); after hardening identification, switch to the unpacking domain
  • [[re-deobfuscate]]: Restoration of OLLVM / string-encrypted native code
  • [[re-mobile-pack]] / [[re-anti-analysis]]: For hardened so packers, unpack first then analyze (pitfall 5)
  • [[re-android-crypto]]: Cryptographic system auditing (Keystore/Cipher/third-party crypto library semantics) is already handled independently——this skill focuses on JNI/native logic; the cryptographic semantics of crypto library APIs inside .so are transferred to [[re-android-crypto]]
  • [[re-analyze]]: Invoked by the triage "mobile App analysis" path (re-mobile → native libraries → this skill)
  • [[re-analyze/platform-tips]]: The highest principle for dynamic instrumentation in a controlled environment

Common pitfalls and traps

  • JNIEnv is a function table pointer (not a direct call): Symptom——In decompilation, JNI function call sites look like "take a function pointer from a struct offset and then call it", parameters don't match, or it is analyzed as an ordinary function GetStringUTFChars and mistakenly used directly as a string function; Cause——JNIEnv points to the JNINativeInterface function table, all JNI APIs are function pointers in table slots, the C form (*env)->fn(env, ...); Countermeasure——Load the jni.h types in Ghidra/IDA (import via Data Type Manager), declareJNIEnv as JNINativeInterface**, decompilation automatically restores it to the env->GetStringUTFChars(env, str) form; when there is no type library, manually build the struct by JNINativeInterface slot index
  • Dynamically registered function addresses are not in the export table: Symptom——readelf --dyn-syms / strings cannot find Java_* or business function names, IDA is full of addresses with no names; Cause——When dynamically registered, functions are static/local symbols, and only at runtime does RegisterNatives bind addresses to Java methods; Countermeasure——JNI_OnLoad is an optional library load hook, not necessarily exported: if present, decompile and inspect RegisterNatives the third-parameter array item by item to restore (name/signature/fnPtr each is a pointer: 64-bit 8 bytes, 32-bit 4 bytes); when it does not exist (or the export table does not contain it), obtain it from RegisterNatives other call sites, JNI function table xrefs, string location, or after frida spawn hook RegisterNatives (step 3 script) to directly get the runtime registration table
  • Obfuscated native (OLLVM): Symptom - decompilation is all control-flow flattening (switch dispatcher), strings are all encrypted, functions are huge and hard to read; Cause - game/hardening vendors use OLLVM (bcf, sub) or commercial obfuscation (VMP-like) to protect native code; Countermeasure - first confirm the obfuscation type (flattening vs instruction virtualization), for flattening restore according to [[re-deobfuscate]] (state variable + case blocks), for string encryption locate the decryption function then batch decrypt with a script; if still not possible, use frida dynamically to get the runtime plaintext (hook the decryption function and read memory)
  • Multi-ABI architecture differences: Symptom - offsets/instructions analyzed for arm64 are all wrong when applied to armeabi-v7a, or behavior on the x86_64 emulator differs from a real device; Cause - the APK has one so per ABI, and compilation optimization/instruction set/calling convention differ (arm64 uses x0-x7 to pass arguments, arm32 has thumb instructions, x86_64 uses rdi/rsi...), and some so files even return different implementations depending on ABI (e.g., arm64 real device vs x86_64 emulator branches); Countermeasure -file/readelf -h first confirm the target ABI, analyze based on the real-device ABI (arm64-v8a), and treat x86_64 results as reference only; JNI type widths are consistent across ABIs (jlong=64-bit, jint=32-bit) but at the C level long widths differ, so pay attention to the type annotations in the decompilation
  • Hardened so (packer): Symptom - static analysis of the so shows only a small stub / shell code,JNI_OnLoad decompilation is an unpacking process; Cause - the so is hardened (vendor packer / commercial packer), and the real logic is only decrypted into memory at runtime; Countermeasure - first identify the hardening ([[re-apk]] hardening identification + entropy), for static unpacking follow [[re-mobile-pack]], or at runtime use [[re-memdump]] to extract the decrypted so from memory and then analyze it; archive the sha256 of the unpacking product and return to step 1 to rerun

Experience library

For the full collection of practical experience, see [[experience]].