Reverse Engineering nativer Bibliotheken
Reading time: 12 minutes
tip
Lernen & üben Sie AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking:
HackTricks Training GCP Red Team Expert (GRTE)
Lernen & üben Sie Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.
Für weitere Informationen siehe: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html
Android-Apps können native Bibliotheken verwenden, typischerweise in C oder C++ geschrieben, für leistungskritische Aufgaben. Malware-Autoren missbrauchen diese Bibliotheken ebenfalls, da ELF shared objects noch immer schwerer zu dekompilieren sind als DEX/OAT-Bytecode.
Diese Seite konzentriert sich auf praktische Workflows und kürzliche Verbesserungen von Tools (2023–2025), die das Reverse Engineering von Android .so-Dateien erleichtern.
Schneller Triage-Workflow für eine frisch extrahierte libfoo.so
- Bibliothek extrahieren
# 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/
- Architektur & Schutzmechanismen identifizieren
file libfoo.so # arm64 or arm32 / x86
readelf -h libfoo.so # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so # (peda/pwntools)
- Exportierte Symbole & JNI-Bindings auflisten
readelf -s libfoo.so | grep ' Java_' # dynamic-linked JNI
strings libfoo.so | grep -i "RegisterNatives" -n # static-registered JNI
- In einen Decompiler laden (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) und die Auto-Analyse ausführen. Neuere Ghidra-Versionen haben einen AArch64-Decompiler eingeführt, der PAC/BTI-Stubs und MTE-Tags erkennt und damit die Analyse von Bibliotheken, die mit dem Android 14 NDK gebaut wurden, deutlich verbessert.
- Entscheidung zwischen statischem und dynamischem Reverse Engineering: gestrippten, obfuskierten Code benötigt häufig Instrumentierung (Frida, ptrace/gdbserver, LLDB).
Dynamische Instrumentierung (Frida ≥ 16)
Die 16er-Serie von Frida brachte mehrere Android-spezifische Verbesserungen, die helfen, wenn das Ziel moderne Clang/LLD-Optimierungen verwendet:
thumb-relocatorkann jetzt winzige ARM/Thumb-Funktionen hooken, die durch LLDs aggressive Alignment (--icf=all) erzeugt werden.- Das Auflisten und Neuverbinden von ELF import slots funktioniert auf Android, was pro Modul
dlopen()/dlsym()-Patching ermöglicht, wenn Inline-Hooks abgelehnt werden. - Java-Hooking wurde für den neuen ART quick-entrypoint behoben, der verwendet wird, wenn Apps mit
--enable-optimizationsauf Android 14 kompiliert werden.
Beispiel: Auflisten aller Funktionen, die über RegisterNatives registriert wurden, und Dumpen ihrer Adressen zur Laufzeit:
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 funktioniert auf PAC/BTI-aktivierten Geräten (Pixel 8/Android 14+) sofort, solange du frida-server 16.2 oder neuer verwendest – frühere Versionen konnten das Padding für inline hooks nicht finden.
Prozess-lokale JNI-Telemetrie über vorab geladene .so (SoTap)
Wenn voll ausgestattete Instrumentierung übertrieben ist oder blockiert wird, kannst du trotzdem native Sichtbarkeit gewinnen, indem du einen kleinen Logger im Zielprozess vorlädst. SoTap ist eine leichte Android native (.so) Bibliothek, die das Laufzeitverhalten anderer JNI (.so) Bibliotheken innerhalb desselben App-Prozesses protokolliert (kein root erforderlich).
Wesentliche Eigenschaften:
- Initialisiert früh und beobachtet JNI/native-Interaktionen innerhalb des Prozesses, der es lädt.
- Persistiert Logs mit mehreren beschreibbaren Pfaden und fällt bei eingeschränktem Speicher sanft auf Logcat zurück.
- Quellcode-anpassbar: Bearbeite sotap.c, um zu erweitern/anpassen, was protokolliert wird, und baue pro ABI neu.
Setup (repack the APK):
- Drop the proper ABI build into the APK so the loader can resolve libsotap.so:
- lib/arm64-v8a/libsotap.so (für arm64)
- lib/armeabi-v7a/libsotap.so (für 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
- Rebuild/sign/install, run the app, then collect 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
Hinweise und Fehlerbehebung:
- ABI-Ausrichtung ist zwingend erforderlich. Ein Mismatch führt zu UnsatisfiedLinkError und der Logger wird nicht geladen.
- Speicherbeschränkungen sind bei modernen Android-Geräten üblich; falls Dateischreibvorgänge fehlschlagen, gibt SoTap weiterhin über Logcat aus.
- Verhalten und Ausführlichkeit sind zur Anpassung vorgesehen; nach dem Bearbeiten von sotap.c aus dem Quellcode neu bauen.
Dieser Ansatz ist nützlich für malware triage und JNI debugging, wenn das Beobachten nativer Aufrufabläufe vom Prozessstart an kritisch ist, aber keine root-/systemweiten Hooks verfügbar sind.
See also: 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
Recent vulnerabilities worth hunting for in APKs
| Jahr | CVE | Betroffene Bibliothek | Anmerkungen |
|---|---|---|---|
| 2023 | CVE-2023-4863 | libwebp ≤ 1.3.1 | Heap buffer overflow reachable from native code that decodes WebP images. Several Android apps bundle vulnerable versions. When you see a libwebp.so inside an APK, check its version and attempt exploitation or patching. |
| 2024 | Multiple | OpenSSL 3.x series | Several memory-safety and padding-oracle issues. Many Flutter & ReactNative bundles ship their own libcrypto.so. |
Wenn Sie third-party .so-Dateien in einem APK entdecken, gleichen Sie deren Hash immer mit upstream advisories ab. SCA (Software Composition Analysis) ist auf Mobile selten, daher sind veraltete, verwundbare Builds weit verbreitet.
Anti-Reversing & Hardening-Trends (Android 13-15)
- Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 aktiviert PAC/BTI in Systembibliotheken auf unterstützter ARMv8.3+ Hardware. Decompiler zeigen nun PAC‑bezogene Pseudo‑Instruktionen; für dynamische Analyse injiziert Frida Trampolines nach dem Entfernen von PAC, aber eigene Trampolines sollten bei Bedarf
pacda/autibspaufrufen. - MTE & Scudo hardened allocator: memory-tagging ist optional, aber viele Play-Integrity-bewusste Apps werden mit
-fsanitize=memtaggebaut; verwenden Siesetprop arm64.memtag.dump 1plusadb shell am start ..., um Tag-Fehler zu erfassen. - LLVM Obfuscator (opaque predicates, control-flow flattening): kommerzielle packer (z. B. Bangcle, SecNeo) schützen zunehmend native code, nicht nur Java; erwarten Sie gefälschte Kontrollflüsse und verschlüsselte String‑Blobs in
.rodata.
Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (ARM64 ELF)
Stark geschützte Apps platzieren oft root-/emulator-/debug-Prüfungen in nativen Konstruktoren, die sehr früh über .init_array ausgeführt werden, vor JNI_OnLoad und lange bevor Java-Code läuft. Sie können diese impliziten Initialisierer explizit machen und Kontrolle zurückgewinnen durch:
- Entfernen von
INIT_ARRAY/INIT_ARRAYSZaus der DYNAMIC-Tabelle, damit der Loader.init_array-Einträge nicht automatisch ausführt. - Auflösen der Konstruktoradresse aus RELATIVE-Relokationen und Exportieren als reguläres Funktionssymbol (z. B.
INIT0). - Umbenennen von
JNI_OnLoadinJNI_OnLoad0, um zu verhindern, dass ART es implizit aufruft.
Warum das auf Android/arm64 funktioniert
- Auf AArch64 werden
.init_array-Einträge häufig zur Ladezeit durchR_AARCH64_RELATIVE-Relokationen befüllt, deren addend die Ziel-Funktionsadresse im.textist. - Die Bytes von
.init_arraykönnen statisch leer aussehen; der dynamic linker schreibt die aufgelöste Adresse während der Relokationsverarbeitung hinein.
Konstruktorziel identifizieren
- Verwenden Sie die Android NDK-Toolchain für genaue ELF-Parsing auf 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
- Finden Sie die Relokation, die in den virtuellen Adressbereich von
.init_arrayfällt; dasaddendderR_AARCH64_RELATIVEist der Konstruktor (z. B.0xA34,0x954). - Disassemblieren Sie um diese Adresse herum zur Plausibilitätsprüfung:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
Patch-Plan
- Entfernen Sie die DYNAMIC-Tags
INIT_ARRAYundINIT_ARRAYSZ. Löschen Sie dabei nicht die Sections. - Fügen Sie ein GLOBAL DEFAULT FUNC-Symbol
INIT0an der Konstruktoradresse hinzu, damit es manuell aufgerufen werden kann. - Benennen Sie
JNI_OnLoad→JNI_OnLoad0um, damit ART es nicht mehr implizit aufruft.
Validierung nach dem Patch
readelf -W -d libnativestaticinit.so.patched | egrep -i 'init_array|fini_array|flags'
readelf -W -s libnativestaticinit.so.patched | egrep 'INIT0|JNI_OnLoad0'
Patchen mit LIEF (Python)
Skript: entferne INIT_ARRAY/INIT_ARRAYSZ, exportiere INIT0, benenne JNI_OnLoad→JNI_OnLoad0 um
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')
Anmerkungen und fehlgeschlagene Ansätze (zur Portabilität)
- Das Nullsetzen der
.init_array-Bytes oder das Setzen der Sektionslänge auf 0 hilft nicht: der dynamic linker füllt sie über relocations wieder auf. - Das Setzen von
INIT_ARRAY/INIT_ARRAYSZauf 0 kann den loader durch inkonsistente Tags beschädigen. Sauberes Entfernen dieser DYNAMIC-Einträge ist der zuverlässige Hebel. - Das vollständige Löschen der
.init_array-Sektion führt häufig zum Absturz des loaders. - Nach dem Patchen können sich Funktions-/Layout-Adressen verschieben; berechne bei Bedarf den Konstruktor immer neu aus den
.rela.dyn-addends in der gepatchten Datei, wenn du den Patch erneut ausführen musst.
Bootstrapping eines minimalen ART/JNI, um INIT0 und JNI_OnLoad0 aufzurufen
- Verwende JNIInvocation, um in einem standalone Binary einen kleinen ART-VM-Kontext zu starten. Rufe dann
INIT0()undJNI_OnLoad0(vm)manuell auf, bevor irgendein Java-Code ausgeführt wird. - Füge das Ziel-APK/die Klassen in den classpath ein, damit
RegisterNativesdie Java-Klassen findet.
Minimales Testgerüst (CMake und C), um INIT0 → JNI_OnLoad0 → Java-Methode aufzurufen
# 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
Häufige Fallstricke:
- Konstruktoradressen ändern sich nach dem Patchen aufgrund einer Neu-Anordnung; immer aus
.rela.dynder finalen Binärdatei neu berechnen. - Stelle sicher, dass
-Djava.class.pathjede Klasse abdeckt, die vonRegisterNatives-Aufrufen verwendet wird. - Das Verhalten kann je nach NDK/Loader-Version variieren; der durchgehend zuverlässige Schritt war das Entfernen der
INIT_ARRAY/INIT_ARRAYSZDYNAMIC-Tags.
Referenzen
- Learning ARM Assembly: Azeria Labs – ARM Assembly Basics
- JNI & NDK Documentation: Oracle JNI Spec · Android JNI Tips · NDK Guides
- Debugging Native Libraries: Debug Android Native Libraries Using JEB Decompiler
- Frida 16.x change-log (Android hooking, tiny-function relocation) – frida.re/news
- NVD advisory for
libwebpoverflow CVE-2023-4863 – nvd.nist.gov - SoTap: Lightweight in-app JNI (.so) behavior logger – 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
tip
Lernen & üben Sie AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking:
HackTricks Training GCP Red Team Expert (GRTE)
Lernen & üben Sie Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.
HackTricks