| name | android-native-reversing |
| description | How to reverse engineer Android native libraries (.so files) for security analysis, malware triage, and vulnerability research. Use this skill whenever you need to analyze, decompile, or instrument Android native code, extract JNI bindings, dump runtime-decrypted libraries, or patch ELF initializers. Make sure to use this skill when you mention Android .so files, native libraries, JNI, Frida instrumentation, ELF analysis, or any Android security/reversing task involving native code. |
Android Native Library Reversing
This skill provides practical workflows for reversing Android native libraries (.so files) written in C/C++. These are commonly used for performance-critical tasks and are frequently abused by malware authors because ELF shared objects are harder to decompile than DEX/OAT bytecode.
Quick Triage Workflow
When you receive a fresh .so file, follow this systematic approach:
1. Extract the Library
adb shell "run-as <package_name> cat lib/arm64-v8a/libfoo.so" > libfoo.so
unzip -j target.apk "lib/*/libfoo.so" -d extracted_libs/
2. Identify Architecture & Protections
file libfoo.so
readelf -h libfoo.so
checksec --file libfoo.so
3. List Exported Symbols & JNI Bindings
readelf -s libfoo.so | grep ' Java_'
strings libfoo.so | grep -i "RegisterNatives" -n
4. Load in a Decompiler
Use one of these tools and run auto-analysis:
- Ghidra ≥ 11.0 (recommended - has AArch64 decompiler with PAC/BTI support)
- IDA Pro
- Binary Ninja
- Hopper
- Cutter/Rizin
Note: Newer Ghidra versions recognize PAC/BTI stubs and MTE tags, greatly improving analysis of libraries built with Android 14 NDK.
5. Decide Static vs Dynamic Reversing
- Static analysis works for most cases
- Dynamic instrumentation (Frida, ptrace/gdbserver, LLDB) is needed for stripped, obfuscated, or runtime-decrypted code
Dynamic Instrumentation with Frida (≥ 16)
Frida 16+ brings Android-specific improvements for modern Clang/LLD optimizations:
thumb-relocator hooks tiny ARM/Thumb functions from LLD's aggressive alignment
- ELF import slot enumeration enables per-module
dlopen()/dlsym() patching
- Java hooking fixed for ART quick-entrypoint (Android 14
--enable-optimizations)
Enumerate RegisterNatives at Runtime
Java.perform(function () {
var register = Module.findExportByName(null, 'RegisterNatives');
if (!register) {
console.log('[-] RegisterNatives not found');
return;
}
Interceptor.attach(register, {
onEnter(args) {
var envPtr = args[0];
var clazz = Java.cast(args[1], Java.use('java.lang.Class'));
var methods = args[2];
var count = args[3].toInt32();
console.log('[+] RegisterNatives on ' + clazz.getName() + ' -> ' + count + ' methods');
for (var i = 0; i < count; i++) {
var method = methods.add(i);
var name = Memory.readUtf8String(method.());
sig = .(method.());
fnPtr = .(method.());
.( + name + + sig + + fnPtr);
}
}
});
});
Run:
frida -U -f com.example.app -l frida-registernatives.js --no-pause
PAC/BTI Support: Frida works on PAC/BTI-enabled devices (Pixel 8/Android 14+) with frida-server 16.2+. Earlier versions failed to locate padding for inline hooks.
Dumping Runtime-Decrypted Libraries (soSaver)
When protected APKs keep native code encrypted or only map it at runtime (packers, downloaded payloads, generated libs), dump the mapped ELF directly from process memory.
soSaver Workflow
The tool:
- Hooks
dlopen and android_dlopen_ext to detect load-time library mapping
- Periodically scans process memory mappings for ELF headers
- Reads each module in blocks and streams bytes through Frida messages
- Saves reconstructed
.so files for offline analysis
Setup & Run
git clone https://github.com/TheQmaks/sosaver.git
cd sosaver && uv sync
source .venv/bin/activate
sosaver com.example.app
sosaver 1234 -o /tmp/so-dumps --debug
Requirements: Root + frida-server, Python ≥3.8, uv
This bypasses "only decrypted in RAM" protections by recovering the live mapped image.
Process-Local JNI Telemetry (SoTap)
When full instrumentation is overkill or blocked, preload a small logger inside the target process. SoTap is a lightweight Android native library that logs JNI/native interactions (no root required).
Setup
-
Drop the proper ABI build into the APK:
lib/arm64-v8a/libsotap.so # for arm64
lib/armeabi-v7a/libsotap.so # for arm32
-
Ensure SoTap loads before other JNI libs. Inject early in Application subclass:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
-
Rebuild, sign, install, run the app, then collect logs.
Log Paths (checked in order)
/data/user/0/<package>/files/sotap.log
/data/data/<package>/files/sotap.log
/sdcard/Android/data/<package>/files/sotap.log
/sdcard/Download/sotap-<package>.log
# Fallback: Logcat only
Troubleshooting
- ABI alignment is mandatory - mismatch raises
UnsatisfiedLinkError
- Storage constraints are common; SoTap falls back to Logcat
- Customize verbosity by editing
sotap.c and rebuilding
Use case: Malware triage and JNI debugging where observing native call flows from process start is critical but root/system-wide hooks aren't available.
Neutralizing Early Native Initializers
Highly protected apps place root/emulator/debug checks in native constructors that run via .init_array before JNI_OnLoad and any Java code. You can make these implicit initializers explicit and regain control.
The Problem
.init_array entries run automatically at load time
- On AArch64, entries are populated by
R_AARCH64_RELATIVE relocations
- The bytes may look empty statically; the dynamic linker writes resolved addresses during relocation
The Solution
- Remove
INIT_ARRAY/INIT_ARRAYSZ from DYNAMIC table (loader skips auto-execution)
- Resolve constructor address from RELATIVE relocations
- Export it as a regular function symbol (e.g.,
INIT0)
- Rename
JNI_OnLoad to JNI_OnLoad0 to prevent ART from calling it implicitly
Use the Patching Script
python scripts/remove_init_array.py libfoo.so libfoo.so.patched
This script:
- Locates
.init_array VA range
- Finds the
R_AARCH64_RELATIVE relocation landing in .init_array
- Removes
INIT_ARRAY/INIT_ARRAYSZ DYNAMIC tags
- Adds exported
INIT0 symbol at constructor address
- Renames
JNI_OnLoad → JNI_OnLoad0
Validation After Patch
readelf -W -d libfoo.so.patched | egrep -i 'init_array|fini_array|flags'
readelf -W -s libfoo.so.patched | egrep 'INIT0|JNI_OnLoad0'
Bootstrapping Manual Initialization
Use a minimal ART/JNI harness to call INIT0() and JNI_OnLoad0(vm) manually before any Java code. See the caller.c example in the references for a complete working harness.
Common Vulnerabilities to Check
When you spot third-party .so files inside an APK, cross-check their hash against upstream advisories:
| Year | CVE | Library | Notes |
|---|
| 2023 | CVE-2023-4863 | libwebp ≤ 1.3.1 | Heap buffer overflow in WebP decoder. Many apps bundle vulnerable versions. |
| 2024 | Multiple | OpenSSL 3.x | Memory-safety and padding-oracle issues. Common in Flutter/ReactNative bundles. |
Action: When you see libwebp.so or libcrypto.so in an APK, check the version and attempt exploitation or recommend patching.
Anti-Reversing Trends (Android 13-15)
Be aware of these modern hardening techniques:
Pointer Authentication (PAC) & Branch Target Identification (BTI)
- Android 14 enables PAC/BTI in system libraries on ARMv8.3+ silicon
- Decompilers display PAC-related pseudo-instructions
- For dynamic analysis, Frida injects trampolines after stripping PAC
- Custom trampolines should call
pacda/autibsp where necessary
MTE & Scudo Hardened Allocator
- Memory-tagging is opt-in but common in Play-Integrity aware apps
- Built with
-fsanitize=memtag
- Capture tag faults:
setprop arm64.memtag.dump 1 then adb shell am start ...
LLVM Obfuscator
- Commercial packers (Bangcle, SecNeo) protect native code, not just Java
- Expect opaque predicates, control-flow flattening, encrypted string blobs in
.rodata
References
Quick Reference Commands
unzip -j app.apk "lib/*/lib*.so" -d libs/
file libfoo.so && readelf -h libfoo.so && strings libfoo.so | grep -i "RegisterNatives"
frida -U -f com.example.app -l frida-registernatives.js --no-pause
sosaver com.example.app -o /tmp/dumps/
python scripts/remove_init_array.py libfoo.so libfoo.so.patched
readelf -W -d libfoo.so.patched | egrep -i 'init_array'
readelf -W -s libfoo.so.patched | egrep 'INIT0|JNI_OnLoad0'