Analisi inversa delle librerie native
Reading time: 12 minutes
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 task critici in termini di prestazioni. Anche gli autori di malware abusano di queste librerie perché gli 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 sui miglioramenti degli strumenti recenti (2023-2025) che rendono più semplice l'analisi delle .so Android.
Flusso di triage rapido per una libfoo.so appena estratta
- Estrai la libreria
# 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/
- Identifica architettura e protezioni
file libfoo.so # arm64 or arm32 / x86
readelf -h libfoo.so # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so # (peda/pwntools)
- Elenca simboli esportati e binding JNI
readelf -s libfoo.so | grep ' Java_' # dynamic-linked JNI
strings libfoo.so | grep -i "RegisterNatives" -n # static-registered JNI
- Carica in un decompilatore (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) e avvia l'analisi automatica. Le versioni più recenti di Ghidra hanno introdotto un decompilatore AArch64 che riconosce PAC/BTI stubs e tag MTE, migliorando notevolmente l'analisi delle librerie buildate con l'Android 14 NDK.
- Decidi tra reversing statico e dinamico: il codice stripped o offuscato spesso richiede strumentazione (Frida, ptrace/gdbserver, LLDB).
Strumentazione dinamica (Frida ≥ 16)
La serie 16 di Frida ha introdotto diversi miglioramenti specifici per Android che aiutano quando il target usa ottimizzazioni moderne di Clang/LLD:
thumb-relocatorpuò ora hook tiny ARM/Thumb functions generate dall'aggressivo allineamento di LLD (--icf=all).- L'enumerazione e il rebinding degli ELF import slots funziona su Android, permettendo patch per modulo con
dlopen()/dlsym()quando gli inline hooks vengono rifiutati. - Java hooking è stato risolto per il nuovo ART quick-entrypoint usato quando le app sono compilate con
--enable-optimizationssu Android 14.
Esempio: enumerare tutte le funzioni registrate tramite RegisterNatives e dumpare i loro indirizzi a 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 sui 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 inline hooks.
Telemetria JNI locale al processo tramite .so precaricato (SoTap)
Quando una 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 (.so) leggera che registra il comportamento a runtime di altre librerie JNI (.so) all'interno dello stesso processo dell'app (no root richiesto).
Key properties:
- Si inizializza precocemente e osserva le interazioni JNI/native all'interno del processo che la carica.
- Persiste i log usando più percorsi scrivibili con fallback elegante a Logcat quando lo storage è limitato.
- Personalizzabile a livello di sorgente: modifica sotap.c per estendere/aggiustare ciò che viene registrato e ricompila 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
- Ricompila/firma/installa, avvia l'app e poi 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
Notes and troubleshooting:
- L'allineamento ABI è obbligatorio. Un disallineamento causerà 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 a emettere output via Logcat.
- Comportamento/verbosità sono pensati per essere personalizzati; ricompila dalla sorgente dopo aver modificato sotap.c.
Questo approccio è utile per il triage di malware e il debugging JNI quando è cruciale osservare i flussi di chiamate native fin dall'avvio del processo ma non sono disponibili hook a livello di root/sistema.
See also: in‑memory native code execution via JNI
Un pattern d'attacco comune è scaricare al runtime un raw shellcode blob ed eseguirlo direttamente da memoria tramite un JNI bridge (nessun ELF su disco). Dettagli e snippet JNI pronti all'uso qui:
In Memory Jni Shellcode Execution
Recent vulnerabilities worth hunting for in APKs
| 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 distribuiscono versioni vulnerabili. Quando trovi un libwebp.so dentro un APK, verifica la versione e prova a sfruttarla o a applicare una patch. |
| 2024 | Multiple | OpenSSL 3.x series | Diversi problemi di memory-safety e padding-oracle. Molti bundle Flutter & ReactNative includono il proprio libcrypto.so. |
Quando individui file .so di terze parti dentro un APK, controlla sempre l'hash rispetto agli advisory upstream. SCA (Software Composition Analysis) è poco comune su mobile, quindi build vulnerabili e datate 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 su siliconi ARMv8.3+ supportati. I decompilatori 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/autibspdove necessario. - MTE & Scudo hardened allocator: memory-tagging è opzionale ma molte app che usano Play-Integrity sono buildate con
-fsanitize=memtag; usasetprop arm64.memtag.dump 1piùadb shell am start ...per catturare fault di tag. - LLVM Obfuscator (opaque predicates, control-flow flattening): i commercial packer (es. Bangcle, SecNeo) proteggono sempre più codice nativo, non solo Java; aspettati controllo-flusso fasullo e blob di stringhe criptate in
.rodata.
Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (ARM64 ELF)
App altamente protette spesso collocano controlli di root/emulatore/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 di 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_arrayvengono spesso popolate al load time da relocazioniR_AARCH64_RELATIVEil cui addend è l'indirizzo della funzione target dentro.text. - I byte di
.init_arraypossono apparire vuoti staticamente; il dynamic linker scrive l'indirizzo risolto durante il processing delle relocazioni.
Identify the constructor target
- Usa l'NDK toolchain Android per un parsing ELF accurato 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 relocazione che atterra all'interno del range di indirizzi virtuali di
.init_array; l'addenddi quellaR_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
Patch plan
- Rimuovere i tag DYNAMIC
INIT_ARRAYeINIT_ARRAYSZ. Non cancellare sezioni. - Aggiungere un simbolo GLOBAL DEFAULT FUNC
INIT0all'indirizzo del constructor così può essere chiamato manualmente. - Rinominare
JNI_OnLoad→JNI_OnLoad0per impedire ad ART di invocarlo implicitamente.
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'
Modifica con LIEF (Python)
Script: rimuovere INIT_ARRAY/INIT_ARRAYSZ, esportare INIT0, rinominare 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')
Note e approcci falliti (per portabilità)
- Azzerare i byte di
.init_arrayo impostare la lunghezza della sezione a 0 non aiuta: il dynamic linker la ripopola via relocations. - Impostare
INIT_ARRAY/INIT_ARRAYSZa 0 può rompere il loader a causa di tag inconsistenti. La rimozione pulita di quelle vociDYNAMICè la leva affidabile. - Eliminare completamente la sezione
.init_arraytende a far crashare il loader. - Dopo il patching, gli indirizzi di funzione/layout possono spostarsi; ricomputare sempre il costruttore dagli addendi in
.rela.dynsul file patchato se devi 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()eJNI_OnLoad0(vm)manualmente prima di qualsiasi codice Java. - Includi l'APK/classes di destinazione nel classpath in modo che qualsiasi
RegisterNativestrovi le sue classi Java.
Harness minimale (CMake e C) per chiamare INIT0 → JNI_OnLoad0 → metodo Java
# 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
Errori 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.pathincluda ogni classe usata dalle chiamateRegisterNatives. - Il comportamento può variare con le versioni di NDK/loader; il passaggio costantemente affidabile è stato rimuovere i tag DYNAMIC
INIT_ARRAY/INIT_ARRAYSZ.
Riferimenti
- Imparare l'assembly ARM: Azeria Labs – ARM Assembly Basics
- Documentazione JNI & NDK: Oracle JNI Spec · Android JNI Tips · NDK Guides
- Debug delle librerie native: Debug Android Native Libraries Using JEB Decompiler
- Change-log di Frida 16.x (Android hooking, tiny-function relocation) – frida.re/news
- Advisory NVD per l'overflow di
libwebpCVE-2023-4863 – nvd.nist.gov - SoTap: Logger leggero di comportamento JNI in-app (.so) – github.com/RezaArbabBot/SoTap
- Rilasci SoTap – github.com/RezaArbabBot/SoTap/releases
- Come usare 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
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.
HackTricks