Reversión de bibliotecas nativas
Tip
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
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.
Flujo rápido de triaje para una libfoo.so recién extraída
- Extraer la biblioteca
# 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/
- Identificar arquitectura y protecciones
file libfoo.so # arm64 or arm32 / x86
readelf -h libfoo.so # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so # (peda/pwntools)
- Listar símbolos exportados y bindings JNI
readelf -s libfoo.so | grep ' Java_' # dynamic-linked JNI
strings libfoo.so | grep -i "RegisterNatives" -n # static-registered JNI
- Cargar en un decompiler (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) y ejecutar el análisis automático. Las versiones más recientes de Ghidra incluyen un decompiler AArch64 que reconoce PAC/BTI stubs y MTE tags, mejorando considerablemente el análisis de bibliotecas compiladas con el NDK de Android 14.
- Decidir entre reversing estático vs dinámico: el código stripped u ofuscado a menudo necesita 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.
Volcado de bibliotecas nativas desencriptadas en tiempo de ejecución desde memoria (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.
Flujo de trabajo de soSaver (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.
Ejecutar (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
Este enfoque elude las protecciones de “only decrypted in RAM” recuperando la imagen mapeada en vivo, lo que permite el análisis offline en IDA/Ghidra incluso si la copia en el sistema de archivos está ofuscada o ausente.
Telemetría JNI local al proceso mediante .so precargado (SoTap)
Cuando la instrumentación completa es exagerada o está bloqueada, aún puedes obtener visibilidad a nivel nativo precargando un pequeño logger dentro del proceso objetivo. SoTap es una biblioteca nativa ligera de Android (.so) que registra el comportamiento en tiempo de ejecución de otras bibliotecas JNI (.so) dentro del mismo proceso de la app (no root required).
Key properties:
- Se inicializa temprano y observa las interacciones JNI/nativas dentro del proceso que lo carga.
- Persiste logs usando varias rutas escribibles con retroceso a Logcat cuando el almacenamiento está restringido.
- Personalizable en el código fuente: edita sotap.c para ampliar/ajustar lo que se registra y recompílalo por ABI.
Setup (repack the APK):
- Coloca la build correspondiente al ABI dentro del APK para que el loader pueda resolver libsotap.so:
- lib/arm64-v8a/libsotap.so (para arm64)
- lib/armeabi-v7a/libsotap.so (para arm32)
- Asegúrate de que SoTap se cargue antes que otras libs JNI. Inyecta una llamada temprano (p. ej., en el inicializador estático de la subclase Application o en onCreate) para que el logger se inicialice primero. Ejemplo de snippet Smali:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
- Reconstruir/firmar/instalar, ejecutar la app y luego recopilar logs.
Rutas de logs (comprobadas en este orden):
/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
Notas y solución de problemas:
- La alineación del ABI es obligatoria. Un desajuste provocará UnsatisfiedLinkError y el logger no se cargará.
- Las limitaciones de almacenamiento son comunes en Android moderno; si las escrituras de archivo fallan, SoTap seguirá emitiendo a través de Logcat.
- El comportamiento/verbosidad está pensado para personalizarse; recompila desde la fuente tras editar sotap.c.
Este enfoque es útil para el triage de malware y la depuración JNI cuando observar los flujos de llamadas nativas desde el inicio del proceso es crítico pero no hay hooks a nivel de root/sistema disponibles.
Ver también: ejecución de código nativo en memoria vía JNI
Un patrón de ataque común es descargar un blob de shellcode en bruto en tiempo de ejecución y ejecutarlo directamente desde la memoria a través de un puente JNI (sin ELF en disco). Detalles y un fragmento JNI listo para usar aquí:
In Memory Jni Shellcode Execution
Vulnerabilidades recientes que vale la pena buscar en APKs
| Año | CVE | Librería afectada | Notas |
|---|---|---|---|
| 2023 | CVE-2023-4863 | libwebp ≤ 1.3.1 | Desbordamiento de heap accesible desde código nativo que decodifica imágenes WebP. Varias apps Android incluyen versiones vulnerables. Cuando veas un libwebp.so dentro de un APK, comprueba su versión e intenta explotarlo o parchearlo. |
| 2024 | Multiple | OpenSSL 3.x series | Varios problemas de seguridad de memoria y padding-oracle. Muchos bundles de Flutter & ReactNative incluyen su propio libcrypto.so. |
Cuando encuentres archivos .so de terceros dentro de un APK, siempre compara su hash con los avisos upstream. SCA (Software Composition Analysis) es poco común en móvil, por lo que las compilaciones vulnerables y desactualizadas son frecuentes.
Tendencias de Anti-Reversing & Hardening (Android 13-15)
- Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 habilita PAC/BTI en las librerías del sistema en siliconas ARMv8.3+ compatibles. Los decompiladores ahora muestran pseudo-instrucciones relacionadas con PAC; para análisis dinámico, Frida inyecta trampolines después de eliminar PAC, pero tus trampolines personalizados deberían llamar a
pacda/autibspcuando sea necesario. - MTE & Scudo hardened allocator: El memory-tagging es opt-in pero muchas apps concienciadas con Play-Integrity se compilan con
-fsanitize=memtag; usasetprop arm64.memtag.dump 1másadb shell am start ...para capturar fallos de tags. - LLVM Obfuscator (opaque predicates, control-flow flattening): Los packers comerciales (p. ej., Bangcle, SecNeo) protegen cada vez más el código nativo, no solo Java; espera flujos de control falsos y blobs de cadenas encriptadas en
.rodata.
Neutralizar inicializadores nativos tempranos (.init_array) y JNI_OnLoad para instrumentación temprana (ARM64 ELF)
Apps altamente protegidas a menudo colocan comprobaciones de root/emulador/debug en constructores nativos que se ejecutan muy temprano vía .init_array, antes de JNI_OnLoad y mucho antes de que cualquier código Java se ejecute. Puedes convertir esos inicializadores implícitos en explícitos y recuperar el control mediante:
- Eliminar
INIT_ARRAY/INIT_ARRAYSZde la tabla DYNAMIC para que el loader no auto-ejecute las entradas de.init_array. - Resolver la dirección del constructor a partir de relocaciones RELATIVE y exportarla como un símbolo de función normal (p. ej.,
INIT0). - Renombrar
JNI_OnLoadaJNI_OnLoad0para evitar que ART lo invoque implícitamente.
Por qué esto funciona en Android/arm64
- En AArch64, las entradas de
.init_arraya menudo se rellenan en tiempo de carga por relocacionesR_AARCH64_RELATIVEcuyo addend es la dirección de la función objetivo dentro de.text. - Los bytes de
.init_arraypueden parecer vacíos estáticamente; el linker dinámico escribe la dirección resuelta durante el procesamiento de relocaciones.
Identificar el objetivo del constructor
- Usa la toolchain del Android NDK para un parseo preciso de ELF en AArch64:
# Ajusta las rutas a tu NDK; usa las variantes aarch64-linux-android-*
readelf -W -a ./libnativestaticinit.so | grep -n "INIT_ARRAY" -C 4
readelf -W --relocs ./libnativestaticinit.so
- Encuentra la relocación que cae dentro del rango de direcciones virtuales de
.init_array; eladdendde esaR_AARCH64_RELATIVEes el constructor (p. ej.,0xA34,0x954). - Desensamblar alrededor de esa dirección para comprobar:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
Plan de parche
- Eliminar los tags DYNAMIC
INIT_ARRAYyINIT_ARRAYSZ. No borres secciones. - Añadir un símbolo GLOBAL DEFAULT FUNC
INIT0en la dirección del constructor para que pueda llamarse manualmente. - Renombrar
JNI_OnLoad→JNI_OnLoad0para evitar que ART lo invoque implícitamente.
Validación después del parche
readelf -W -d libnativestaticinit.so.patched | egrep -i 'init_array|fini_array|flags'
readelf -W -s libnativestaticinit.so.patched | egrep 'INIT0|JNI_OnLoad0'
Parcheo con LIEF (Python)
Script: eliminar INIT_ARRAY/INIT_ARRAYSZ, exportar INIT0, renombrar 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>
Notas y enfoques fallidos (para portabilidad)
- Poner a cero los bytes de `.init_array` o ajustar la longitud de la sección a 0 no ayuda: el enlazador dinámico la repuebla mediante relocations.
- Establecer `INIT_ARRAY`/`INIT_ARRAYSZ` a 0 puede romper el cargador debido a etiquetas inconsistentes. La eliminación limpia de esas entradas DYNAMIC es la palanca fiable.
- Eliminar completamente la sección `.init_array` suele provocar que el cargador se bloquee.
- Después de parchear, las direcciones de funciones/layout pueden desplazarse; siempre recalcula el constructor a partir de los addends de `.rela.dyn` en el archivo parcheado si necesitas volver a aplicar el parche.
Arranque de un ART/JNI mínimo para invocar INIT0 y JNI_OnLoad0
- Usa JNIInvocation para iniciar un pequeño contexto ART VM en un binario independiente. Luego llama a `INIT0()` y `JNI_OnLoad0(vm)` manualmente antes de cualquier código Java.
- Incluye el APK/las clases objetivo en el classpath para que cualquier `RegisterNatives` encuentre sus clases Java.
<details>
<summary>Arnés mínimo (CMake and C) para llamar a INIT0 → JNI_OnLoad0 → método Java</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
Errores comunes:
- Las direcciones de los constructores cambian después del parcheo debido al re-layout; siempre recalcula desde
.rela.dynen el binario final. - Asegúrate de que
-Djava.class.pathcubra todas las clases utilizadas por las llamadas aRegisterNatives. - El comportamiento puede variar según las versiones de NDK/loader; el paso consistentemente fiable fue eliminar las etiquetas DYNAMIC
INIT_ARRAY/INIT_ARRAYSZ.
Referencias
- Aprender ARM Assembly: Azeria Labs – ARM Assembly Basics
- Documentación de JNI & NDK: Oracle JNI Spec · Android JNI Tips · NDK Guides
- Depuración de librerías nativas: Debug Android Native Libraries Using JEB Decompiler
- Registro de cambios de Frida 16.x (Android hooking, tiny-function relocation) – frida.re/news
- Aviso NVD sobre desbordamiento de
libwebpCVE-2023-4863 – nvd.nist.gov - SoTap: registrador ligero in-app del comportamiento JNI (.so) – 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 — volcado de memoria en vivo basado en Frida para librerías
.sode Android – github.com/TheQmaks/sosaver - Agente Frida de soSaver (TypeScript/JS) – github.com/TheQmaks/soSaver-frida
Tip
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.


