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
- Vérifiez les plans d'abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.
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
- Extraire la bibliothèque
# 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/
- Identifier l'architecture & les protections
file libfoo.so # arm64 or arm32 / x86
readelf -h libfoo.so # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so # (peda/pwntools)
- Lister les symboles exportés & les liaisons JNI
readelf -s libfoo.so | grep ' Java_' # dynamic-linked JNI
strings libfoo.so | grep -i "RegisterNatives" -n # static-registered JNI
- 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.
- 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-relocatorcan 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-optimizationssur Android 14.
Exemple : énumération de toutes les fonctions enregistrées via RegisterNatives et dumping de leurs adresses à l'exécution :
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):
- 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)
- 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:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
- 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ée | CVE | Bibliothèque affectée | Remarques |
|---|---|---|---|
| 2023 | CVE-2023-4863 | libwebp ≤ 1.3.1 | Dé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. |
| 2024 | Multiple | OpenSSL 3.x series | Plusieurs 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.
Anti-Reversing & Hardening trends (Android 13-15)
- 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/autibsplorsque 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; utilisezsetprop arm64.memtag.dump 1plusadb 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_ARRAYSZde 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_OnLoadenJNI_OnLoad0pour empêcher ART de l'appeler implicitement.
Why this works on Android/arm64
- Sur AArch64, les entrées de
.init_arraysont souvent remplies au chargement par des relocationsR_AARCH64_RELATIVEdont l'addend est l'adresse de la fonction cible à l'intérieur de.text. - Les octets de
.init_arraypeuvent 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 :
# 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'addendde ceR_AARCH64_RELATIVEest le constructeur (par ex.0xA34,0x954). - Désassemblez autour de cette adresse pour vérifier :
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
Patch plan
- Retirer les tags DYNAMIC
INIT_ARRAYetINIT_ARRAYSZ. Ne supprimez pas les sections. - Ajouter un symbole GLOBAL DEFAULT FUNC
INIT0à l'adresse du constructeur afin qu'il puisse être appelé manuellement. - Renommer
JNI_OnLoad→JNI_OnLoad0pour empêcher ART de l'invoquer implicitement.
Validation après le patch
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
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_arrayou 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_arraya 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.dyndans 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()etJNI_OnLoad0(vm)manuellement avant tout code Java. - Incluez l'APK/classes cible sur le classpath afin que tout
RegisterNativestrouve ses classes Java.
Environnement minimal (CMake et C) pour appeler INIT0 → JNI_OnLoad0 → méthode 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
Pièges courants :
- Les adresses des constructeurs changent après patching en raison du re-layout ; recalculer toujours à partir de
.rela.dynsur le binaire final. - Assurez-vous que
-Djava.class.pathcouvre chaque classe utilisée par les appelsRegisterNatives. - 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
- Apprendre ARM Assembly: Azeria Labs – ARM Assembly Basics
- Documentation JNI & NDK: Oracle JNI Spec · Android JNI Tips · NDK Guides
- Débogage des bibliothèques natives: Debug Android Native Libraries Using JEB Decompiler
- Frida 16.x change-log (Android hooking, tiny-function relocation) – frida.re/news
- Avis NVD pour le overflow de
libwebpCVE-2023-4863 – nvd.nist.gov - SoTap : enregistreur léger du comportement JNI in-app (.so) – github.com/RezaArbabBot/SoTap
- SoTap Releases – github.com/RezaArbabBot/SoTap/releases
- Comment utiliser 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
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
- Vérifiez les plans d'abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.
HackTricks