Skip to main content

Native library analysis

Use this route when lib/<abi>/*.so contains application logic, a framework payload, or a loader that DEX analysis cannot explain.

Inventory libraries and ABIs​

unzip -l sample.apk 'lib/*/*.so'

Common ABI directories:

ABIArchitectureNotes
arm64-v8aAArch64best default for most modern physical devices
armeabi-v7a32-bit ARMcommon in older/universal builds
x86_6464-bit x86often useful for emulators
x8632-bit x86legacy emulator target

Pick the ABI matching the planned device and keep all related artifacts from the same build. Addresses differ between ABIs.

Extract without flattening paths:

unzip sample.apk 'lib/arm64-v8a/*.so' -d output/native
file output/native/lib/arm64-v8a/*.so
sha256sum output/native/lib/arm64-v8a/*.so > output/native/SHA256SUMS

Perform inexpensive reconnaissance​

readelf -hW libnative.so
readelf -dW libnative.so
readelf -Ws libnative.so | less
nm -D --defined-only libnative.so | c++filt | less
strings -a -n 6 libnative.so | less

Record:

  • architecture, endianness, and ELF type;
  • imported libraries and functions;
  • exported functions and JNI symbols;
  • whether symbols are stripped;
  • unusual sections, packer clues, or embedded assets;
  • references to class descriptors, method signatures, paths, URLs, and errors.

Strings and symbols are navigation aids, not proof that a code path is reachable.

Map static JNI names​

A conventional exported JNI name encodes its Java class and method:

Java_com_example_crypto_NativeBridge_verify

Cross-reference it with a declaration such as:

package com.example.crypto;
final class NativeBridge {
static native boolean verify(byte[] input);
}

Overloaded JNI names can include encoded signatures. Underscores and special characters use JNI name escaping; consult the JNI specification when manually decoding them.

Map dynamic registration​

Many libraries export only JNI_OnLoad and call RegisterNatives. A native method table has entries conceptually equivalent to:

static JNINativeMethod methods[] = {
{"verify", "([B)Z", (void *)native_verify},
};

In Ghidra or IDA:

  1. locate JNI_OnLoad;
  2. identify calls through the JNIEnv function table or a recognized RegisterNatives import;
  3. trace the target class passed to FindClass;
  4. recover the JNINativeMethod array;
  5. label each function with its Java name and descriptor;
  6. verify against native declarations in jadx/smali.

Stripped symbols make labels more important. Keep address, original autogenerated name, new label, and evidence in notes.

Import into Ghidra​

A reproducible Ghidra pass:

  1. create a project dedicated to the target hash;
  2. import the selected .so as ELF;
  3. accept the detected language/compiler specification only after checking architecture;
  4. run auto-analysis;
  5. start from exports, imported API calls, strings, or recovered JNI mappings;
  6. rename functions and data types incrementally;
  7. preserve project/version metadata with the report.

The C-like decompiler is an interpretation. Confirm critical details in the disassembly, especially integer widths, signed comparisons, pointer arithmetic, structure layouts, and calling conventions.

Framework-specific native payloads​

  • Flutter: libapp.so is compiled Dart; snapshot-aware tooling is usually more productive than generic C recovery.
  • Unity IL2CPP: combine libil2cpp.so with its matching global-metadata.dat before navigating in Ghidra/IDA.
  • Rust/C++ libraries: demangling and runtime signatures can recover useful namespace/type clues even when Java only sees a small bridge.
  • Packed libraries: if sections or code are decrypted only at load time, static output may be intentionally incomplete. Runtime dumping requires explicit dynamic-analysis scope.

Report address-stable evidence​

ASLR changes runtime addresses. Report module-relative offsets or static image addresses with the exact hash and tool configuration:

Binary: lib/arm64-v8a/libnative.so
SHA-256: …
Tool: Ghidra … / AARCH64 little endian
Function: native_verify
Static address: 0012a430
Module offset: +0x12a430
Mapped from: NativeBridge.verify([B)Z via RegisterNatives

Continue with Dynamic analysis fallback only when static limits are established and runtime work is authorized.