Reversing Native Libraries
Tip
Impara e pratica il hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.
Per ulteriori informazioni consulta: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html
Le app Android possono usare librerie native, tipicamente scritte in C o C++, per compiti che richiedono alte prestazioni. I creatori di malware abusano anche di queste librerie perché ELF shared objects sono ancora più difficili da decompilare rispetto al byte-code DEX/OAT.
Questa pagina si concentra su flussi di lavoro pratici e recenti miglioramenti degli strumenti (2023-2025) che rendono più semplice reversing dei file .so di Android.
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 strumentazione (Frida, ptrace/gdbserver, LLDB).
Dynamic Instrumentation (Frida ≥ 16)
La serie 16 di Frida ha introdotto diversi miglioramenti specifici per Android che aiutano quando il bersaglio usa ottimizzazioni moderne di Clang/LLD:
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.
Esempio: 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 funzionerà immediatamente su dispositivi abilitati PAC/BTI (Pixel 8/Android 14+) purché si utilizzi frida-server 16.2 o versione successiva – le versioni precedenti non riuscivano a individuare il padding per gli inline hooks.
Dumping runtime-decrypted native libraries from memory (Frida soSaver)
Quando un APK protetto mantiene il codice nativo cifrato o lo mappa solo a runtime (packers, downloaded payloads, generated libs), attach Frida e dump l’ELF mappato direttamente dalla memoria del processo.
Flusso di lavoro di soSaver (Python host + TS/JS Frida agent):
- Collega hook a
dlopeneandroid_dlopen_extper rilevare la mappatura delle librerie al load-time ed esegue una scansione iniziale dei moduli già caricati. - Scansiona periodicamente le mappature della memoria del processo alla ricerca di header ELF per intercettare moduli caricati tramite mapper non standard che non passano mai per le API del loader.
- Legge ogni modulo a blocchi dalla memoria e streamma i byte attraverso messaggi Frida verso l’host; se una regione non può essere letta, ripiega sulla lettura dal percorso su disco quando disponibile.
- Salva i file
.soricostruiti e stampa statistiche di estrazione per modulo, fornendo artefatti per la static RE.
Esegui (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
Questo approccio bypassa le protezioni “only decrypted in RAM” recuperando l’immagine mappata in memoria attiva, permettendo l’analisi offline in IDA/Ghidra anche se la copia su filesystem è offuscata o assente.
Telemetria JNI locale al processo tramite .so precaricato (SoTap)
Quando la strumentazione completa è eccessiva o bloccata, puoi comunque ottenere visibilità a livello nativo precaricando un piccolo logger all’interno del processo target. SoTap è una libreria nativa Android leggera (.so) che registra il comportamento a runtime di altre librerie JNI (.so) all’interno dello stesso processo dell’app (non è richiesto root).
Caratteristiche principali:
- Si inizializza precocemente e osserva le interazioni JNI/native all’interno del processo che lo carica.
- Persiste i log usando più percorsi scrivibili con fallback a Logcat quando lo storage è limitato.
- Personalizzabile dal sorgente: modifica sotap.c per estendere/aggiustare ciò che viene registrato e ricompila per ABI.
Setup (ricostruisci l’APK):
- Inserisci la build per ABI corretta nell’APK in modo che il loader possa risolvere libsotap.so:
- lib/arm64-v8a/libsotap.so (per arm64)
- lib/armeabi-v7a/libsotap.so (per arm32)
- Assicurati che SoTap venga caricato prima delle altre librerie JNI. Inietta una chiamata presto (es. static initializer della sottoclasse Application o onCreate) in modo che il logger sia inizializzato per primo. Esempio di snippet Smali:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
- Ricostruisci/firma/installa, esegui l’app, quindi raccogli i log.
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
Note e risoluzione dei problemi:
- L’allineamento ABI è obbligatorio. Un mismatch solleverà UnsatisfiedLinkError e il logger non verrà caricato.
- I vincoli di storage sono comuni su Android moderno; se le scritture su file falliscono, SoTap continuerà comunque ad emettere tramite Logcat.
- Il comportamento/la verbosità sono pensati per essere personalizzati; ricompila dal sorgente dopo aver modificato sotap.c.
Questo approccio è utile per il triage di malware e il debug JNI dove osservare i flussi di chiamata native fin dall’avvio del processo è critico ma non sono disponibili hook root/system-wide.
Vedi anche: esecuzione di codice nativo in memoria via JNI
Un pattern d’attacco comune è scaricare un blob di shellcode raw a runtime ed eseguirlo direttamente dalla memoria tramite un bridge JNI (nessun ELF su disco). Dettagli e snippet JNI pronti all’uso qui:
In Memory Jni Shellcode Execution
Vulnerabilità recenti da cercare negli APK
| Anno | CVE | Libreria interessata | Note |
|---|---|---|---|
| 2023 | CVE-2023-4863 | libwebp ≤ 1.3.1 | Heap buffer overflow raggiungibile dal codice nativo che decodifica immagini WebP. Diverse app Android includono versioni vulnerabili. Quando trovi un libwebp.so dentro un APK, verifica la sua versione e prova a sfruttarla o a patcharla. |
| 2024 | Multiple | OpenSSL 3.x series | Diverse vulnerabilità di memory-safety e padding-oracle. Molti bundle Flutter & ReactNative includono il proprio libcrypto.so. |
Quando noti dei file .so third-party dentro un APK, controlla sempre il loro hash rispetto agli advisories upstream. SCA (Software Composition Analysis) è poco comune su mobile, quindi build vulnerabili e obsolete sono diffuse.
Anti-Reversing & Hardening trends (Android 13-15)
- Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 abilita PAC/BTI nelle librerie di sistema sui silici ARMv8.3+ supportati. I decompiler ora mostrano pseudo-istruzioni correlate a PAC; per l’analisi dinamica Frida inietta trampolini dopo aver rimosso PAC, ma i tuoi trampolini custom dovrebbero chiamare
pacda/autibspquando necessario. - MTE & Scudo hardened allocator: memory-tagging è opt-in ma molte app attente al Play-Integrity compilano con
-fsanitize=memtag; usasetprop arm64.memtag.dump 1piùadb shell am start ...per catturare i fault di tag. - LLVM Obfuscator (opaque predicates, control-flow flattening): packer commerciali (es., Bangcle, SecNeo) proteggono sempre più il codice native, non solo Java; aspettati control-flow fasullo e blob di stringhe crittate in
.rodata.
Neutralizzare gli initializer nativi precoci (.init_array) e JNI_OnLoad per strumenti di instrumentation precoce (ARM64 ELF)
Le app altamente protette spesso inseriscono controlli per root/emulator/debug nei constructor nativi che vengono eseguiti molto presto tramite .init_array, prima di JNI_OnLoad e molto prima che qualsiasi codice Java venga eseguito. Puoi rendere quegli initializer impliciti espliciti e riprendere il controllo:
- Rimuovere
INIT_ARRAY/INIT_ARRAYSZdalla tabella DYNAMIC in modo che il loader non esegua automaticamente le entry di.init_array. - Risolvere l’indirizzo del constructor dalle relocazioni RELATIVE ed esportarlo come simbolo funzione regolare (es.,
INIT0). - Rinominare
JNI_OnLoadinJNI_OnLoad0per impedire ad ART di chiamarlo implicitamente.
Perché questo funziona su Android/arm64
- Su AArch64, le entry di
.init_arraysono spesso compilate a load time tramite relocazioniR_AARCH64_RELATIVEil cui addend è l’indirizzo della funzione target dentro.text. - I byte di
.init_arraypossono sembrare vuoti staticamente; il dynamic linker scrive l’indirizzo risolto durante la fase di processing delle relocazioni.
Identificare il target del constructor
- Usa toolchain Android NDK per un’accurata parsing ELF su 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
- Trova la relocation che ricade nell’intervallo di indirizzi virtuali di
.init_array; l’addenddi quelR_AARCH64_RELATIVEè il constructor (es.,0xA34,0x954). - Disassembla intorno a quell’indirizzo per un controllo di sanità:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
Piano di patch
- Rimuovere i tag DYNAMIC
INIT_ARRAYeINIT_ARRAYSZ. Non cancellare sezioni. - Aggiungere un simbolo GLOBAL DEFAULT FUNC
INIT0all’indirizzo del constructor in modo che possa essere chiamato manualmente. - Rinominare
JNI_OnLoad→JNI_OnLoad0per impedire che ART lo invochi implicitamente.
Validazione dopo la 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 con LIEF (Python)
Script: remove INIT_ARRAY/INIT_ARRAYSZ, export INIT0, rename JNI_OnLoad→JNI_OnLoad0
```python import liefb = 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’)
</details>
Note e approcci falliti (per portabilità)
- Azzerare i byte di `.init_array` o impostare la lunghezza della sezione a 0 non aiuta: the dynamic linker la ripopola tramite relocations.
- Impostare `INIT_ARRAY`/`INIT_ARRAYSZ` a 0 può rompere il loader a causa di tag incoerenti. La rimozione pulita di quelle entry DYNAMIC è la leva affidabile.
- Cancellare completamente la sezione `.init_array` tende a far crashare il loader.
- Dopo il patching, gli indirizzi di funzione/layout possono spostarsi; ricalcola sempre il constructor dagli addendi in `.rela.dyn` sul file patchato se hai bisogno di rieseguire la patch.
Bootstrap di un ART/JNI minimale per invocare INIT0 e JNI_OnLoad0
- Usa JNIInvocation per avviare un piccolo contesto ART VM in un binario standalone. Poi chiama `INIT0()` e `JNI_OnLoad0(vm)` manualmente prima di qualsiasi codice Java.
- Includi l'APK/classes target nel classpath così che qualsiasi `RegisterNatives` trovi le sue classi Java.
<details>
<summary>Minimal harness (CMake and C) to call INIT0 → JNI_OnLoad0 → Java method</summary>
```cmake
# 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
Insidie comuni:
- Gli indirizzi dei Constructor cambiano dopo il patching a causa del re-layout; ricalcolare sempre da
.rela.dynsul binario finale. - Assicurarsi che
-Djava.class.pathcopra ogni classe usata dalle chiamateRegisterNatives. - Il comportamento può variare con le versioni di NDK/loader; il passo costantemente affidabile è stato rimuovere i tag DYNAMIC
INIT_ARRAY/INIT_ARRAYSZ.
Riferimenti
- 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
Impara e pratica il hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.


