Análisis inverso de bibliotecas nativas
Reading time: 13 minutes
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
Las apps de Android pueden usar bibliotecas nativas, típicamente escritas en C o C++, para tareas críticas de rendimiento. Los creadores de malware también abusan de estas bibliotecas porque los objetos compartidos ELF siguen siendo más difíciles de descompilar que el byte-code DEX/OAT.
Esta página se centra en flujos de trabajo prácticos y en mejoras recientes de herramientas (2023-2025) que facilitan el reversing de archivos .so en Android.
Flujo de triage rápido para un libfoo.so recién extraído
- 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 descompilador (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) y ejecutar el auto-análisis. Las versiones más recientes de Ghidra incluyen un decompilador AArch64 que reconoce PAC/BTI stubs y MTE tags, mejorando mucho el análisis de bibliotecas compiladas con el Android 14 NDK.
- Decidir entre reversing estático vs dinámico: el código stripped u ofuscado a menudo necesita instrumentation (Frida, ptrace/gdbserver, LLDB).
Instrumentación dinámica (Frida ≥ 16)
La serie 16 de Frida trajo varias mejoras específicas para Android que ayudan cuando el objetivo usa optimizaciones modernas de Clang/LLD:
thumb-relocatorahora puede hook tiny ARM/Thumb functions generadas por la alineación agresiva de LLD (--icf=all).- La enumeración y rebinding de ELF import slots funciona en Android, permitiendo parchear por módulo con
dlopen()/dlsym()cuando los inline hooks son rechazados. - Se corrigió el Java hooking para el nuevo ART quick-entrypoint usado cuando las apps se compilan con
--enable-optimizationsen 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 funcionará de forma inmediata en dispositivos con PAC/BTI (Pixel 8/Android 14+) siempre que uses frida-server 16.2 o posterior — versiones anteriores no podían localizar padding para inline hooks.
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 librería nativa ligera de Android (.so) que registra el comportamiento en tiempo de ejecución de otras librerías JNI (.so) dentro del mismo proceso de la app (no requiere root).
Propiedades clave:
- Se inicializa temprano y observa las interacciones JNI/native dentro del proceso que la carga.
- Persiste logs usando múltiples rutas escribibles con fallback elegante a Logcat cuando el almacenamiento está restringido.
- Personalizable desde el código fuente: edita sotap.c para extender/ajustar lo que se registra y recompila por ABI.
Configuración (reempacar el APK):
- Coloca la build correspondiente al ABI dentro del APK para que el cargador pueda resolver libsotap.so:
- lib/arm64-v8a/libsotap.so (for arm64)
- lib/armeabi-v7a/libsotap.so (for arm32)
- Asegúrate de que SoTap se cargue antes que otras libs JNI. Inyecta una llamada temprana (p. ej., inicializador estático de una Application subclass o 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
- Reconstruye/firma/instala, ejecuta la app y luego recoge los 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
Notas y resolución de problemas:
- El alineamiento de ABI es obligatorio. Un desajuste lanzará UnsatisfiedLinkError y el logger no se cargará.
- Las restricciones de almacenamiento son comunes en Android moderno; si las escrituras a archivo fallan, SoTap seguirá emitiendo vía Logcat.
- El comportamiento/verbosidad está pensado para personalizarse; recompila desde la fuente después de editar sotap.c.
Este enfoque es útil para triage de malware y depuración JNI donde observar los flujos de llamadas nativas desde el inicio del proceso es crítico pero no hay hooks a nivel 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 snippet 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 | Heap buffer overflow reachable from native code that decodes WebP images. Varias apps Android integran 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 verifica su hash contra 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 y Hardenización (Android 13-15)
- Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 habilita PAC/BTI en las librerías del sistema en silicio ARMv8.3+ con soporte. Los decompiladores ahora muestran pseudo-instrucciones relacionadas con PAC; para análisis dinámico Frida inyecta trampolines después de quitar 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 conscientes de Play-Integrity compilan con
-fsanitize=memtag; usasetprop arm64.memtag.dump 1junto conadb shell am start ...para capturar tag faults. - 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 control-flow falso y blobs de strings encriptados en
.rodata.
Neutralizar inicializadores nativos tempranos (.init_array) y JNI_OnLoad para instrumentación temprana (ARM64 ELF)
Las apps altamente protegidas a menudo colocan comprobaciones de root/emulator/debug en constructores nativos que se ejecutan extremadamente pronto vía .init_array, antes de JNI_OnLoad y mucho antes de que se ejecute cualquier código Java. Puedes hacer esos inicializadores implícitos explícitos y recuperar el control mediante:
- Eliminar
INIT_ARRAY/INIT_ARRAYSZde la tabla DYNAMIC para que el loader no ejecute automáticamente las entradas de.init_array. - Resolver la dirección del constructor desde relocations RELATIVE y exportarla como un símbolo de función regular (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 relocationsR_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 relocations.
Identificar el objetivo del constructor
- Usa el toolchain del Android NDK para un parseo ELF preciso en 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
- Encuentra la relocation que cae dentro del rango de direcciones virtuales de
.init_array; eladdendde eseR_AARCH64_RELATIVEes el constructor (p. ej.,0xA34,0x954). - Desensambla alrededor de esa dirección para comprobar:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
Plan de parche
- Elimina las tags DYNAMIC
INIT_ARRAYyINIT_ARRAYSZ. No borres secciones. - Añade un símbolo GLOBAL DEFAULT FUNC
INIT0en la dirección del constructor para que pueda ser llamado manualmente. - Renombra
JNI_OnLoad→JNI_OnLoad0para impedir 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
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')
Notas y enfoques fallidos (por portabilidad)
- Poner a cero los bytes de
.init_arrayo establecer la longitud de la sección a 0 no ayuda: el dynamic linker los repuebla vía relocations. - Establecer
INIT_ARRAY/INIT_ARRAYSZa 0 puede romper el loader debido a tags inconsistentes. La eliminación limpia de esas DYNAMIC entries es la palanca fiable. - Eliminar por completo la sección
.init_arraysuele provocar un crash del loader. - Tras el parcheo, las direcciones de funciones/layout pueden desplazarse; siempre recalcula el constructor a partir de los addends en
.rela.dyndel archivo parcheado si necesitas volver a aplicar el patch.
Bootstrapping a minimal ART/JNI to invoke INIT0 and JNI_OnLoad0
- Usa JNIInvocation para levantar un pequeño contexto ART VM en un binario independiente. Luego llama a
INIT0()yJNI_OnLoad0(vm)manualmente antes de cualquier código Java. - Incluye el APK/clases objetivo en el classpath para que cualquier
RegisterNativesencuentre sus clases Java.
Entorno mínimo (CMake y C) para llamar a INIT0 → JNI_OnLoad0 → método 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
Errores comunes:
- Las direcciones de los constructores cambian después de parchear debido al re-layout; siempre recalcula desde
.rela.dynen el binario final. - Asegúrate de que
-Djava.class.pathincluya todas las clases usadas por las llamadasRegisterNatives. - El comportamiento puede variar con versiones de NDK/loader; el paso consistentemente fiable fue eliminar las etiquetas DYNAMIC
INIT_ARRAY/INIT_ARRAYSZ.
Referencias
- Aprendizaje de ensamblador ARM: Azeria Labs – ARM Assembly Basics
- Documentación JNI & NDK: Oracle JNI Spec · Android JNI Tips · NDK Guides
- Depuración de bibliotecas 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 para desbordamiento de
libwebpCVE-2023-4863 – nvd.nist.gov - SoTap: Lightweight in-app JNI (.so) behavior logger – github.com/RezaArbabBot/SoTap
- SoTap Releases – github.com/RezaArbabBot/SoTap/releases
- ¿Cómo trabajar con 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
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.
HackTricks