Rétro-ingénierie des bibliothèques natives

Reading time: 13 minutes

tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

Pour plus d'informations, voir : https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html

Les applications Android peuvent utiliser des bibliothèques natives, typiquement écrites en C ou C++, pour des tâches critiques en performance. Les créateurs de malware abusent aussi de ces bibliothèques parce que les objets partagés ELF sont encore plus difficiles à décompiler que le byte-code DEX/OAT. Cette page se concentre sur les workflows pratiques et les améliorations récentes des outils (2023-2025) qui facilitent la rétro-ingénierie des fichiers .so Android.


Workflow de triage rapide pour un libfoo.so fraîchement extrait

  1. Extraire la bibliothèque
bash
# 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/
  1. Identifier l'architecture & les protections
bash
file libfoo.so        # arm64 or arm32 / x86
readelf -h libfoo.so  # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so  # (peda/pwntools)
  1. Lister les symboles exportés & les liaisons JNI
bash
readelf -s libfoo.so | grep ' Java_'     # dynamic-linked JNI
strings libfoo.so   | grep -i "RegisterNatives" -n   # static-registered JNI
  1. Charger dans un décompilateur (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper ou Cutter/Rizin) et lancer l'auto-analyse. Les versions récentes de Ghidra ont introduit un décompilateur AArch64 qui reconnaît les stubs PAC/BTI et les MTE tags, améliorant considérablement l'analyse des bibliothèques compilées avec l'Android 14 NDK.
  2. Décider entre reversing statique vs dynamique : le code stripped ou obfuscated nécessite souvent de l'instrumentation (Frida, ptrace/gdbserver, LLDB).

Instrumentation dynamique (Frida ≥ 16)

La série 16 de Frida a apporté plusieurs améliorations spécifiques à Android qui aident lorsque la cible utilise des optimisations modernes de Clang/LLD :

  • thumb-relocator can now hook tiny ARM/Thumb functions generated by LLD’s aggressive alignment (--icf=all).
  • L'énumération et le rebinding des ELF import slots fonctionnent sur Android, permettant le patching dlopen()/dlsym() par module lorsque les inline hooks sont rejetés.
  • Le Java hooking a été corrigé pour le nouveau ART quick-entrypoint utilisé lorsque les apps sont compilées avec --enable-optimizations sur Android 14.

Exemple : énumération de toutes les fonctions enregistrées via RegisterNatives et dumping de leurs adresses à l'exécution :

javascript
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 fonctionnera prêt à l'emploi sur PAC/BTI-enabled devices (Pixel 8/Android 14+) tant que vous utilisez frida-server 16.2 ou ultérieur – les versions antérieures ne parvenaient pas à localiser padding pour inline hooks.

Télémétrie JNI locale au processus via un .so préchargé (SoTap)

Lorsque l'instrumentation complète est excessive ou bloquée, vous pouvez toujours obtenir une visibilité au niveau natif en préchargeant un petit logger à l'intérieur du processus cible. SoTap est une bibliothèque native Android légère (.so) qui enregistre le comportement d'exécution d'autres bibliothèques JNI (.so) au sein du même processus d'application (pas de root requis).

Propriétés clés :

  • S'initialise tôt et observe les interactions JNI/native à l'intérieur du processus qui le charge.
  • Persiste les logs en utilisant plusieurs chemins inscriptibles avec un fallback gracieux vers Logcat lorsque le stockage est restreint.
  • Personnalisable au niveau source : éditez sotap.c pour étendre/ajuster ce qui est enregistré et recompilez par ABI.

Configuration (repack the APK):

  1. Déposez le build ABI approprié dans l'APK pour que le loader puisse résoudre libsotap.so :
  • lib/arm64-v8a/libsotap.so (for arm64)
  • lib/armeabi-v7a/libsotap.so (for arm32)
  1. Assurez-vous que SoTap se charge avant les autres libs JNI. Injectez un appel tôt (par ex., static initializer d'une sous-classe Application ou onCreate) afin que le logger soit initialisé en premier. Exemple de snippet Smali:
smali
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
  1. Rebuild/sign/install, lancez l'app, puis collectez les logs.

Chemins de logs (vérifiés dans cet ordre) :

/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 et dépannage:

  • L'alignement ABI est obligatoire. Une incompatibilité lèvera UnsatisfiedLinkError et le logger ne se chargera pas.
  • Les contraintes de stockage sont courantes sur les Android récents ; si les écritures de fichiers échouent, SoTap émettra toujours via Logcat.
  • Le comportement/verbosité est censé être personnalisé ; recompilez depuis la source après modification de 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.


Voir aussi: 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


Vulnérabilités récentes à rechercher dans les APK

AnnéeCVEBibliothèque affectéeRemarques
2023CVE-2023-4863libwebp ≤ 1.3.1Débordement de tampon sur le tas exploitable depuis du code natif qui décode des images WebP. Plusieurs apps Android embarquent des versions vulnérables. Lorsque vous trouvez un libwebp.so dans un APK, vérifiez sa version et tentez l'exploitation ou le patch.
2024MultipleOpenSSL 3.x seriesPlusieurs problèmes de sécurité mémoire et padding-oracle. Beaucoup de bundles Flutter & ReactNative incluent leur propre libcrypto.so.

Lorsque vous repérez des fichiers .so tiers à l'intérieur d'un APK, vérifiez systématiquement leur hash par rapport aux avis upstream. SCA (Software Composition Analysis) est rare sur mobile, donc les builds vulnérables et obsolètes sont répandus.


  • Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 active PAC/BTI dans les bibliothèques système sur les silicium ARMv8.3+ pris en charge. Les décompilateurs affichent maintenant des pseudo-instructions liées à PAC ; pour l'analyse dynamique Frida injecte des trampolines after stripping PAC, mais vos trampolines personnalisés devraient appeler pacda/autibsp lorsque nécessaire.
  • MTE & Scudo hardened allocator: Le memory-tagging est opt-in mais beaucoup d'apps conscientes de Play-Integrity sont compilées avec -fsanitize=memtag ; utilisez setprop arm64.memtag.dump 1 plus adb shell am start ... pour capturer les tag faults.
  • LLVM Obfuscator (opaque predicates, control-flow flattening): les packers commerciaux (e.g., Bangcle, SecNeo) protègent de plus en plus le code native, pas seulement Java ; attendez-vous à du bogus control-flow et des encrypted string blobs dans .rodata.

Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (ARM64 ELF)

Les apps fortement protégées placent souvent des vérifications root/emulator/debug dans des constructeurs natifs qui s'exécutent très tôt via .init_array, avant JNI_OnLoad et bien avant l'exécution de tout code Java. Vous pouvez rendre ces initialisateurs implicites explicites et reprendre le contrôle en :

  • Supprimant INIT_ARRAY/INIT_ARRAYSZ de la table DYNAMIC afin que le loader n'exécute pas automatiquement les entrées de .init_array.
  • Résolvant l'adresse du constructeur à partir des relocations RELATIVE et en l'exportant comme un symbole de fonction régulier (p.ex. INIT0).
  • Renommant JNI_OnLoad en JNI_OnLoad0 pour empêcher ART de l'appeler implicitement.

Why this works on Android/arm64

  • Sur AArch64, les entrées de .init_array sont souvent remplies au chargement par des relocations R_AARCH64_RELATIVE dont l'addend est l'adresse de la fonction cible à l'intérieur de .text.
  • Les octets de .init_array peuvent sembler vides statiquement ; le linker dynamique écrit l'adresse résolue lors du traitement des relocations.

Identify the constructor target

  • Utilisez la toolchain Android NDK pour un parsing ELF précis sur AArch64 :
bash
# 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
  • Trouvez la relocation qui tombe dans la plage d'adresses virtuelles de .init_array ; l'addend de ce R_AARCH64_RELATIVE est le constructeur (par ex. 0xA34, 0x954).
  • Désassemblez autour de cette adresse pour vérifier :
bash
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40

Patch plan

  1. Retirer les tags DYNAMIC INIT_ARRAY et INIT_ARRAYSZ. Ne supprimez pas les sections.
  2. Ajouter un symbole GLOBAL DEFAULT FUNC INIT0 à l'adresse du constructeur afin qu'il puisse être appelé manuellement.
  3. Renommer JNI_OnLoadJNI_OnLoad0 pour empêcher ART de l'invoquer implicitement.

Validation après le patch

bash
readelf -W -d libnativestaticinit.so.patched | egrep -i 'init_array|fini_array|flags'
readelf -W -s libnativestaticinit.so.patched | egrep 'INIT0|JNI_OnLoad0'

Patch avec LIEF (Python)

Script: supprimer INIT_ARRAY/INIT_ARRAYSZ, exporter INIT0, renommer JNI_OnLoad→JNI_OnLoad0
python
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')

Remarques et approches échouées (pour la portabilité)

  • Mettre à zéro les octets de .init_array ou fixer la longueur de la section à 0 n'aide pas : le linker dynamique la repopule via des relocations.
  • Définir INIT_ARRAY/INIT_ARRAYSZ à 0 peut casser le loader à cause de tags incohérents. La suppression propre de ces entrées DYNAMIC est le levier fiable.
  • Supprimer complètement la section .init_array a tendance à faire planter le loader.
  • Après le patch, les adresses de fonctions / du layout peuvent changer ; recalculer toujours le constructeur à partir des addends de .rela.dyn dans le fichier patché si vous devez relancer le patch.

Démarrage d'un ART/JNI minimal pour invoquer INIT0 et JNI_OnLoad0

  • Utilisez JNIInvocation pour lancer un petit contexte ART VM dans un binaire autonome. Ensuite appelez INIT0() et JNI_OnLoad0(vm) manuellement avant tout code Java.
  • Incluez l'APK/classes cible sur le classpath afin que tout RegisterNatives trouve ses classes Java.
Environnement minimal (CMake et C) pour appeler INIT0 → JNI_OnLoad0 → méthode Java
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)
c
// 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);
}
bash
# 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

Pièges courants :

  • Les adresses des constructeurs changent après patching en raison du re-layout ; recalculer toujours à partir de .rela.dyn sur le binaire final.
  • Assurez-vous que -Djava.class.path couvre chaque classe utilisée par les appels RegisterNatives.
  • Le comportement peut varier selon les versions du NDK/loader ; la méthode systématiquement fiable a été de supprimer les tags DYNAMIC INIT_ARRAY/INIT_ARRAYSZ.

Références

tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks