Reversing Native Libraries
Tip
Learn & practice AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
Support HackTricks
- Check the subscription plans!
- Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.
For further information check: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html
Android apps can use native libraries, typically written in C or C++, for performance-critical tasks. Malware creators also abuse these libraries because ELF shared objects are still harder to decompile than DEX/OAT byte-code.
This page focuses on practical workflows and recent tooling improvements (2023-2025) that make reversing Android .so files easier.
Quick triage-workflow for a freshly pulled libfoo.so
- Extract the library
# From an installed application adb shell "run-as <pkg> cat lib/arm64-v8a/libfoo.so" > libfoo.so # Or from the APK (zip) unzip -j target.apk "lib/*/libfoo.so" -d extracted_libs/ - Identify architecture & protections
file libfoo.so # arm64 or arm32 / x86 readelf -h libfoo.so # OS ABI, PIE, NX, RELRO, etc. checksec --file libfoo.so # (peda/pwntools) - List exported symbols & JNI bindings
readelf -s libfoo.so | grep ' Java_' # dynamic-linked JNI strings libfoo.so | grep -i "RegisterNatives" -n # static-registered JNI - Load in a decompiler (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) and run auto-analysis.
Newer Ghidra versions introduced an AArch64 decompiler that recognises PAC/BTI stubs and MTE tags, greatly improving analysis of libraries built with the Android 14 NDK. - Decide on static vs dynamic reversing: stripped, obfuscated code often needs instrumentation (Frida, ptrace/gdbserver, LLDB).
Dynamic Instrumentation (Frida ≥ 16)
Frida’s 16-series brought several Android-specific improvements that help when the target uses modern Clang/LLD optimisations:
thumb-relocatorcan now hook tiny ARM/Thumb functions generated by LLD’s aggressive alignment (--icf=all).- Enumerating and rebinding ELF import slots works on Android, enabling per-module
dlopen()/dlsym()patching when inline hooks are rejected. - Java hooking was fixed for the new ART quick-entrypoint used when apps are compiled with
--enable-optimizationson Android 14.
Example: enumerating all functions registered through RegisterNatives and dumping their addresses at runtime:
Java.perform(function () {
var Runtime = Java.use('java.lang.Runtime');
var register = Module.findExportByName(null, 'RegisterNatives');
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');
// iterate & dump (JNI nativeMethod struct: name, sig, fnPtr)
}
});
});
Frida will work out of the box on PAC/BTI-enabled devices (Pixel 8/Android 14+) as long as you use frida-server 16.2 or later – earlier versions failed to locate padding for inline hooks.
Dumping runtime-decrypted native libraries from memory (Frida soSaver)
When a protected APK keeps native code encrypted or only maps it at runtime (packers, downloaded payloads, generated libs), attach Frida and dump the mapped ELF directly from process memory.
soSaver workflow (Python host + TS/JS Frida agent):
- Hooks
dlopenandandroid_dlopen_extto detect load-time library mapping and performs an initial sweep of already loaded modules. - Periodically scans the process memory mappings for ELF headers to catch modules loaded through non-standard mappers that never hit the loader APIs.
- Reads each module in blocks from memory and streams the bytes through Frida messages to the host; if a region cannot be read, it falls back to reading from the on-disk path when available.
- Saves the reconstructed
.sofiles and prints per-module extraction stats, providing artifacts for static RE.
Run (root + frida-server, Python ≥3.8, uv):
git clone https://github.com/TheQmaks/sosaver.git
cd sosaver && uv sync
source .venv/bin/activate # .venv\Scripts\activate on Windows
# target by package or PID; choose output/verbosity
sosaver com.example.app
sosaver 1234 -o /tmp/so-dumps --debug
This approach bypasses “only decrypted in RAM” protections by recovering the live mapped image, allowing offline analysis in IDA/Ghidra even if the filesystem copy is obfuscated or absent.
Process-local JNI telemetry via preloaded .so (SoTap)
When full-featured instrumentation is overkill or blocked, you can still gain native-level visibility by preloading a small logger inside the target process. SoTap is a lightweight Android native (.so) library that logs the runtime behavior of other JNI (.so) libraries within the same app process (no root required).
Key properties:
- Initializes early and observes JNI/native interactions inside the process that loads it.
- Persists logs using multiple writable paths with graceful fallback to Logcat when storage is restricted.
- Source-customizable: edit sotap.c to extend/adjust what gets logged and rebuild per ABI.
Setup (repack the APK):
- Drop the proper ABI build into the APK so the loader can resolve libsotap.so:
- lib/arm64-v8a/libsotap.so (for arm64)
- lib/armeabi-v7a/libsotap.so (for arm32)
- Ensure SoTap loads before other JNI libs. Inject a call early (e.g., Application subclass static initializer or onCreate) so the logger is initialized first. Smali snippet example:
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/%s/files/sotap.log
/data/data/%s/files/sotap.log
/sdcard/Android/data/%s/files/sotap.log
/sdcard/Download/sotap-%s.log
# If all fail: fallback to Logcat only
Notes and troubleshooting:
- ABI alignment is mandatory. A mismatch will raise UnsatisfiedLinkError and the logger won’t load.
- Storage constraints are common on modern Android; if file writes fail, SoTap will still emit via Logcat.
- Behavior/verbosity is intended to be customized; rebuild from source after editing sotap.c.
This approach is useful for malware triage and JNI debugging where observing native call flows from process start is critical but root/system-wide hooks aren’t available.
See also: in‑memory native code execution via JNI
A common attack pattern is to download a raw shellcode blob at runtime and execute it directly from memory through a JNI bridge (no on‑disk ELF). Details and ready‑to‑use JNI snippet here:
In Memory Jni Shellcode Execution
Recent vulnerabilities worth hunting for in APKs
| Year | CVE | Affected library | Notes |
|---|---|---|---|
| 2023 | CVE-2023-4863 | libwebp ≤ 1.3.1 | Heap buffer overflow reachable from native code that decodes WebP images. Several Android apps bundle vulnerable versions. When you see a libwebp.so inside an APK, check its version and attempt exploitation or patching. |
| 2024 | Multiple | OpenSSL 3.x series | Several memory-safety and padding-oracle issues. Many Flutter & ReactNative bundles ship their own libcrypto.so. |
When you spot third-party .so files inside an APK, always cross-check their hash against upstream advisories. SCA (Software Composition Analysis) is uncommon on mobile, so outdated vulnerable builds are rampant.
Anti-Reversing & Hardening trends (Android 13-15)
- Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 enables PAC/BTI in system libraries on supported ARMv8.3+ silicon. Decompilers now display PAC‐related pseudo-instructions; for dynamic analysis Frida injects trampolines after stripping PAC, but your custom trampolines should call
pacda/autibspwhere necessary. - MTE & Scudo hardened allocator: memory-tagging is opt-in but many Play-Integrity aware apps build with
-fsanitize=memtag; usesetprop arm64.memtag.dump 1plusadb shell am start ...to capture tag faults. - LLVM Obfuscator (opaque predicates, control-flow flattening): commercial packers (e.g., Bangcle, SecNeo) increasingly protect native code, not only Java; expect bogus control-flow and encrypted string blobs in
.rodata.
Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (ARM64 ELF)
Highly protected apps often place root/emulator/debug checks in native constructors that run extremely early via .init_array, before JNI_OnLoad and long before any Java code executes. You can make those implicit initializers explicit and regain control by:
- Removing
INIT_ARRAY/INIT_ARRAYSZfrom the DYNAMIC table so the loader does not auto-execute.init_arrayentries. - Resolving the constructor address from RELATIVE relocations and exporting it as a regular function symbol (e.g.,
INIT0). - Renaming
JNI_OnLoadtoJNI_OnLoad0to prevent ART from calling it implicitly.
Why this works on Android/arm64
- On AArch64,
.init_arrayentries are often populated at load time byR_AARCH64_RELATIVErelocations whose addend is the target function address inside.text. - The bytes of
.init_arraymay look empty statically; the dynamic linker writes the resolved address during relocation processing.
Identify the constructor target
- Use the Android NDK toolchain for accurate ELF parsing on AArch64:
# Adjust paths to your NDK; use the aarch64-linux-android-* variants readelf -W -a ./libnativestaticinit.so | grep -n "INIT_ARRAY" -C 4 readelf -W --relocs ./libnativestaticinit.so - Find the relocation that lands inside the
.init_arrayvirtual address range; theaddendof thatR_AARCH64_RELATIVEis the constructor (e.g.,0xA34,0x954). - Disassemble around that address to sanity check:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
Patch plan
- Remove
INIT_ARRAYandINIT_ARRAYSZDYNAMIC tags. Do not delete sections. - Add a GLOBAL DEFAULT FUNC symbol
INIT0at the constructor address so it can be called manually. - Rename
JNI_OnLoad→JNI_OnLoad0to stop ART from invoking it implicitly.
Validation after patch
readelf -W -d libnativestaticinit.so.patched | egrep -i 'init_array|fini_array|flags'
readelf -W -s libnativestaticinit.so.patched | egrep 'INIT0|JNI_OnLoad0'
Patching with LIEF (Python)
Script: remove INIT_ARRAY/INIT_ARRAYSZ, export INIT0, rename JNI_OnLoad→JNI_OnLoad0
import lief
b = lief.parse("libnativestaticinit.so")
# Locate .init_array VA range
init = b.get_section('.init_array')
va, sz = init.virtual_address, init.size
# Compute constructor address from RELATIVE relocation landing in .init_array
ctor = None
for r in b.dynamic_relocations:
if va <= r.address < va + sz:
ctor = r.addend
break
if ctor is None:
raise RuntimeError("No R_*_RELATIVE relocation found inside .init_array")
# Remove auto-run tags so loader skips .init_array
for tag in (lief.ELF.DYNAMIC_TAGS.INIT_ARRAYSZ, lief.ELF.DYNAMIC_TAGS.INIT_ARRAY):
try:
b.remove(b[tag])
except Exception:
pass
# Add exported FUNC symbol INIT0 at constructor address
sym = lief.ELF.Symbol()
sym.name = 'INIT0'
sym.value = ctor
sym.size = 0
sym.binding = lief.ELF.SYMBOL_BINDINGS.GLOBAL
sym.type = lief.ELF.SYMBOL_TYPES.FUNC
sym.visibility = lief.ELF.SYMBOL_VISIBILITY.DEFAULT
# Place symbol in .text index
text = b.get_section('.text')
for idx, sec in enumerate(b.sections):
if sec == text:
sym.shndx = idx
break
b.add_dynamic_symbol(sym)
# Rename JNI_OnLoad -> JNI_OnLoad0 to block implicit ART init
j = b.get_symbol('JNI_OnLoad')
if j:
j.name = 'JNI_OnLoad0'
b.write('libnativestaticinit.so.patched')
Notes and failed approaches (for portability)
- Zeroing
.init_arraybytes or setting the section length to 0 does not help: the dynamic linker repopulates it via relocations. - Setting
INIT_ARRAY/INIT_ARRAYSZto 0 can break the loader due to inconsistent tags. Clean removal of those DYNAMIC entries is the reliable lever. - Deleting the
.init_arraysection entirely tends to crash the loader. - After patching, function/layout addresses might shift; always recompute the constructor from
.rela.dynaddends on the patched file if you need to re-run the patch.
Bootstrapping a minimal ART/JNI to invoke INIT0 and JNI_OnLoad0
- Use JNIInvocation to spin up a tiny ART VM context in a standalone binary. Then call
INIT0()andJNI_OnLoad0(vm)manually before any Java code. - Include the target APK/classes on the classpath so any
RegisterNativesfinds its Java classes.
Minimal harness (CMake and C) to call INIT0 → JNI_OnLoad0 → Java method
# CMakeLists.txt
project(caller)
cmake_minimum_required(VERSION 3.8)
include_directories(AFTER ${CMAKE_SOURCE_DIR}/include)
link_directories(${CMAKE_SOURCE_DIR}/lib)
find_library(log-lib log REQUIRED)
add_executable(caller "caller.c")
add_library(jenv SHARED "jnihelper.c")
target_link_libraries(caller jenv nativestaticinit)
// caller.c
#include <jni.h>
#include "jenv.h"
JavaCTX ctx;
void INIT0();
void JNI_OnLoad0(JavaVM* vm);
int main(){
char *jvmopt = "-Djava.class.path=/data/local/tmp/base.apk"; // include app classes
if (initialize_java_environment(&ctx,&jvmopt,1)!=0) return -1;
INIT0(); // manual constructor
JNI_OnLoad0(ctx.vm); // manual JNI init
jclass c = (*ctx.env)->FindClass(ctx.env, "eu/nviso/nativestaticinit/MainActivity");
jmethodID m = (*ctx.env)->GetStaticMethodID(ctx.env,c,"stringFromJNI","()Ljava/lang/String;");
jstring s = (jstring)(*ctx.env)->CallStaticObjectMethod(ctx.env,c,m);
const char* p = (*ctx.env)->GetStringUTFChars(ctx.env,s,NULL);
printf("Native string: %s\n", p);
cleanup_java_env(&ctx);
}
# Build (adjust NDK/ABI)
cmake -DANDROID_PLATFORM=31 \
-DCMAKE_TOOLCHAIN_FILE=$HOME/Android/Sdk/ndk/26.1.10909125/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a ..
make
Common Pitfalls:
- Constructor addresses change after patching due to re-layout; always recompute from
.rela.dynon the final binary. - Ensure
-Djava.class.pathcovers every class used byRegisterNativescalls. - Behavior may vary with NDK/loader versions; the consistently reliable step was removing
INIT_ARRAY/INIT_ARRAYSZDYNAMIC tags.
References
- Learning ARM Assembly: Azeria Labs – ARM Assembly Basics
- JNI & NDK Documentation: Oracle JNI Spec · Android JNI Tips · NDK Guides
- Debugging Native Libraries: Debug Android Native Libraries Using JEB Decompiler
- Frida 16.x change-log (Android hooking, tiny-function relocation) – frida.re/news
- NVD advisory for
libwebpoverflow CVE-2023-4863 – nvd.nist.gov - SoTap: Lightweight in-app JNI (.so) behavior logger – github.com/RezaArbabBot/SoTap
- SoTap Releases – github.com/RezaArbabBot/SoTap/releases
- How to work with SoTap? – t.me/ForYouTillEnd/13
- CoRPhone — JNI memory-only execution pattern and packaging
- Patching Android ARM64 library initializers for easy Frida instrumentation and debugging
- LIEF Project
- JNIInvocation
- soSaver — Frida-based live memory dumper for Android
.solibraries – github.com/TheQmaks/sosaver - soSaver Frida agent (TypeScript/JS) – github.com/TheQmaks/soSaver-frida
Tip
Learn & practice AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
Support HackTricks
- Check the subscription plans!
- Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.


