Reversing Native Libraries
Tip
Μάθετε & εξασκηθείτε στο AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Μάθετε & εξασκηθείτε στο Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Υποστηρίξτε το HackTricks
- Ελέγξτε τα σχέδια συνδρομής!
- Εγγραφείτε στην 💬 ομάδα Discord ή στην ομάδα telegram ή ακολουθήστε μας στο Twitter 🐦 @hacktricks_live.
- Μοιραστείτε κόλπα hacking υποβάλλοντας PRs στα HackTricks και HackTricks Cloud github repos.
Για περισσότερες πληροφορίες δείτε: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html
Οι εφαρμογές Android μπορούν να χρησιμοποιούν native libraries, συνήθως γραμμένες σε C ή C++, για εργασίες κρίσιμες ως προς την απόδοση. Δημιουργοί Malware επίσης καταχρώνται αυτές τις βιβλιοθήκες επειδή τα ELF shared objects εξακολουθούν να είναι πιο δύσκολα να decompile από το DEX/OAT byte-code.
Αυτή η σελίδα επικεντρώνεται σε πρακτικές ροές εργασίας και σε πρόσφατες βελτιώσεις εργαλείων (2023-2025) που καθιστούν το reversing των Android .so αρχείων ευκολότερο.
Quick triage-workflow for a freshly pulled libfoo.so
- Extract the library
# 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/
- Identify architecture & protections
file libfoo.so # arm64 or arm32 / x86
readelf -h libfoo.so # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so # (peda/pwntools)
- List exported symbols & JNI bindings
readelf -s libfoo.so | grep ' Java_' # dynamic-linked JNI
strings libfoo.so | grep -i "RegisterNatives" -n # static-registered JNI
- Load in a decompiler (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) and run auto-analysis. Newer Ghidra versions introduced an AArch64 decompiler that recognises PAC/BTI stubs and MTE tags, greatly improving analysis of libraries built with the Android 14 NDK.
- Decide on static vs dynamic reversing: stripped, obfuscated code often needs instrumentation (Frida, ptrace/gdbserver, LLDB).
Dynamic Instrumentation (Frida ≥ 16)
Η σειρά 16 του Frida έφερε αρκετές βελτιώσεις ειδικές για Android που βοηθούν όταν ο στόχος χρησιμοποιεί σύγχρονες βελτιστοποιήσεις Clang/LLD:
thumb-relocatorμπορεί πλέον να hook μικρές συναρτήσεις ARM/Thumb που παράγονται από την επιθετική στοίχιση του LLD (--icf=all).- Η απαρίθμηση και το rebinding των ELF import slots λειτουργεί σε Android, επιτρέποντας per-module
dlopen()/dlsym()patching όταν τα inline hooks απορρίπτονται. - Το Java hooking επιδιορθώθηκε για το νέο ART quick-entrypoint που χρησιμοποιείται όταν οι εφαρμογές μεταγλωττίζονται με
--enable-optimizationsστο Android 14.
Παράδειγμα: απαρίθμηση όλων των συναρτήσεων που καταχωρούνται μέσω του RegisterNatives και dumping των διευθύνσεών τους κατά το 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.
Τηλεμετρία JNI εντός διεργασίας μέσω προφορτωμένου .so (SoTap)
Όταν η πλήρης instrumentation είναι υπερβολική ή μπλοκαρισμένη, μπορείτε ακόμα να αποκτήσετε ορατότητα σε native επίπεδο προφορτώνοντας έναν μικρό logger μέσα στη στοχευμένη διεργασία. Το SoTap είναι μια ελαφριά Android native (.so) βιβλιοθήκη που καταγράφει τη συμπεριφορά στο runtime άλλων JNI (.so) βιβλιοθηκών μέσα στην ίδια διεργασία της εφαρμογής (no root required).
Κύρια χαρακτηριστικά:
- Εκκινεί νωρίς και παρατηρεί τις αλληλεπιδράσεις JNI/native μέσα στη διεργασία που το φορτώνει.
- Διατηρεί τα logs χρησιμοποιώντας πολλαπλές εγγράψιμες διαδρομές με ομαλή εναλλακτική επιστροφή στο Logcat όταν ο αποθηκευτικός χώρος είναι περιορισμένος.
- Προσαρμόσιμο στον πηγαίο κώδικα: επεξεργαστείτε το sotap.c για να επεκτείνετε/προσαρμόσετε τι καταγράφεται και επαναχτίστε ανά ABI.
Ρύθμιση (επανασυσκευασία του APK):
- Τοποθετήστε το κατάλληλο build για κάθε ABI στο APK ώστε ο loader να μπορεί να επιλύσει libsotap.so:
- lib/arm64-v8a/libsotap.so (for arm64)
- lib/armeabi-v7a/libsotap.so (for arm32)
- Βεβαιωθείτε ότι το SoTap φορτώνεται πριν από άλλες JNI βιβλιοθήκες. Εισάγετε μια κλήση νωρίς (π.χ. στο static initializer ενός υποκλάδου Application ή στο onCreate) ώστε ο logger να αρχικοποιείται πρώτος. Smali snippet example:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
- Επαναχτίστε/υπογράψτε/εγκαταστήστε, τρέξτε την εφαρμογή και μετά συλλέξτε τα logs.
Διαδρομές καταγραφής (ελέγχονται με αυτή τη σειρά):
/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
Σημειώσεις και αντιμετώπιση προβλημάτων:
- Η ευθυγράμμιση ABI είναι υποχρεωτική. Μη ταίριασμα θα προκαλέσει UnsatisfiedLinkError και ο logger δεν θα φορτωθεί.
- Ο περιορισμός αποθηκευτικού χώρου είναι σύνηθες σε σύγχρονο Android· αν αποτύχουν οι εγγραφές αρχείων, το SoTap θα συνεχίσει να εκπέμπει μέσω Logcat.
- Η συμπεριφορά/επίπεδο λεπτομέρειας προορίζεται να προσαρμοστεί· κάντε rebuild από τον πηγαίο κώδικα μετά την επεξεργασία του sotap.c.
Αυτή η προσέγγιση είναι χρήσιμη για malware triage και JNI debugging όπου η παρατήρηση των native ροών κλήσεων από την εκκίνηση της διεργασίας είναι κρίσιμη αλλά δεν υπάρχουν διαθέσιμα root/system-wide hooks.
Δείτε επίσης: εκτέλεση native κώδικα στη μνήμη μέσω JNI
Ένα κοινό μοτίβο επίθεσης είναι να κατεβάσετε ένα raw shellcode blob κατά το runtime και να το εκτελέσετε απευθείας από τη μνήμη μέσω μιας JNI γέφυρας (χωρίς ELF στο δίσκο). Λεπτομέρειες και έτοιμο προς χρήση snippet JNI εδώ:
In Memory Jni Shellcode Execution
Πρόσφατες ευπάθειες που αξίζει να ψάξετε σε APKs
| Year | CVE | Affected library | Notes |
|---|---|---|---|
| 2023 | CVE-2023-4863 | libwebp ≤ 1.3.1 | Heap buffer overflow προσβάσιμο από native κώδικα που αποκωδικοποιεί εικόνες WebP. Πολλές Android εφαρμογές πακετάρουν ευάλωτες εκδόσεις. Όταν βλέπετε ένα libwebp.so μέσα σε APK, ελέγξτε την έκδοση και δοκιμάστε εκμετάλλευση ή patching. |
| 2024 | Multiple | OpenSSL 3.x series | Πολλά θέματα ασφάλειας μνήμης και padding-oracle. Πολλά Flutter & ReactNative bundles παραδίδουν το δικό τους libcrypto.so. |
Όταν εντοπίσετε third-party .so αρχεία μέσα σε ένα APK, πάντα αντιπαραβάλετε το hash τους με upstream advisories. Η SCA (Software Composition Analysis) είναι ασυνήθης στο mobile, οπότε παλιές ευάλωτες builds είναι διαδεδομένες.
Anti-Reversing & Hardening trends (Android 13-15)
- Pointer Authentication (PAC) & Branch Target Identification (BTI): Το Android 14 ενεργοποιεί PAC/BTI στις system libraries σε υποστηριζόμενα ARMv8.3+ silicon. Οι decompilers τώρα εμφανίζουν PAC‑σχετιζόμενες pseudo-instructions· για δυναμική ανάλυση, το Frida εγχέει trampolines μετά το stripping του PAC, αλλά τα custom trampolines σας θα πρέπει να καλούν
pacda/autibspόπου χρειάζεται. - MTE & Scudo hardened allocator: το memory-tagging είναι opt-in αλλά πολλές Play-Integrity aware εφαρμογές χτίζονται με
-fsanitize=memtag; χρησιμοποιήστεsetprop arm64.memtag.dump 1μαζί μεadb shell am start ...για να καταγράψετε tag faults. - LLVM Obfuscator (opaque predicates, control-flow flattening): εμπορικά packers (π.χ., Bangcle, SecNeo) όλο και περισσότερο προστατεύουν native κώδικα, όχι μόνο Java· αναμένετε ψευδές control-flow και κρυπτογραφημένα string blobs στο
.rodata.
Εξουδετέρωση πρώιμων native initializers (.init_array) και JNI_OnLoad για πρώιμη instrumentation (ARM64 ELF)
Ισχυρά προστατευόμενες εφαρμογές συχνά τοποθετούν root/emulator/debug checks σε native constructors που τρέχουν πολύ νωρίς μέσω της .init_array, πριν το JNI_OnLoad και πολύ πριν εκτελεστεί οποιοςδήποτε Java κώδικας. Μπορείτε να κάνετε αυτούς τους έμμεσους initializers ρητούς και να ανακτήσετε τον έλεγχο με τα εξής:
- Αφαιρέστε τα DYNAMIC tags
INIT_ARRAY/INIT_ARRAYSZώστε ο loader να μην εκτελεί αυτόματα τις.init_arrayεγγραφές. - Επίλυση της διεύθυνσης του constructor από RELATIVE relocations και εξαγωγή της ως κανονικού function symbol (π.χ.,
INIT0). - Μετονομασία
JNI_OnLoadσεJNI_OnLoad0για να αποτρέψετε το ART από το να το καλεί αυτόματα.
Γιατί αυτό δουλεύει σε Android/arm64
- Σε AArch64, οι εγγραφές της
.init_arrayσυχνά γεμίζουν κατά το load time από relocationsR_AARCH64_RELATIVEτων οποίων το addend είναι η διεύθυνση της στοχο-συνάρτησης μέσα στο.text. - Τα bytes της
.init_arrayμπορεί να φαίνονται κενά στατικά· ο dynamic linker γράφει τη λυμένη διεύθυνση κατά την επεξεργασία των relocations.
Εντοπισμός του constructor target
- Χρησιμοποιήστε το Android NDK toolchain για ακριβή ELF parsing σε 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
- Βρείτε το relocation που προσγειώνεται μέσα στο virtual address range της
.init_array; τοaddendτουR_AARCH64_RELATIVEείναι ο constructor (π.χ.,0xA34,0x954). - Αποσυναρμολογήστε γύρω από αυτήν τη διεύθυνση για ανεπιφύλακτη επαλήθευση:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
Σχέδιο patch
- Αφαιρέστε τα DYNAMIC tags
INIT_ARRAYκαιINIT_ARRAYSZ. Μην διαγράψετε sections. - Προσθέστε ένα GLOBAL DEFAULT FUNC symbol
INIT0στη διεύθυνση του constructor ώστε να μπορεί να κληθεί χειροκίνητα. - Μετονομάστε
JNI_OnLoad→JNI_OnLoad0για να σταματήσει το ART από το να το καλεί αυτόματα.
Validation after 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 με LIEF (Python)
Σενάριο: αφαίρεση INIT_ARRAY/INIT_ARRAYSZ, εξαγωγή INIT0, μετονομασία 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>
Σημειώσεις και αποτυχημένες προσεγγίσεις (για φορητότητα)
- Ο μηδενισμός των bytes της `.init_array` ή η ρύθμιση του μήκους του section σε 0 δεν βοηθά: ο dynamic linker το επαναγεμίζει μέσω relocations.
- Η ρύθμιση των `INIT_ARRAY`/`INIT_ARRAYSZ` σε 0 μπορεί να σπάσει τον loader λόγω ασυνεπών tags. Ο καθαρός αφαιρεσμός αυτών των DYNAMIC entries είναι η αξιόπιστη λύση.
- Η διαγραφή ολόκληρου του section `.init_array` τείνει να προκαλέσει crash του loader.
- Μετά το patching, οι διευθύνσεις συναρτήσεων/διάταξης μπορεί να μετατοπιστούν· πάντα επανυπολογίζετε τον constructor από τα addends του `.rela.dyn` στο patched αρχείο αν χρειαστεί να επανατρέξετε το patch.
Bootstrapping a minimal ART/JNI to invoke INIT0 and JNI_OnLoad0
- Χρησιμοποιήστε το JNIInvocation για να εκκινήσετε ένα μικρό ART VM context σε ένα standalone binary. Στη συνέχεια καλέστε `INIT0()` και `JNI_OnLoad0(vm)` χειροκίνητα πριν από οποιονδήποτε Java κώδικα.
- Συμπεριλάβετε το target APK/classes στο classpath ώστε οποιοδήποτε `RegisterNatives` να βρει τις Java κλάσεις του.
<details>
<summary>Minimal harness (CMake and C) to call INIT0 → JNI_OnLoad0 → Java method</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
Συνηθισμένα λάθη:
- Οι διευθύνσεις των constructors αλλάζουν μετά το patching λόγω re-layout· πάντα επανυπολογίζετε από
.rela.dynστο τελικό binary. - Βεβαιωθείτε ότι το
-Djava.class.pathκαλύπτει κάθε κλάση που χρησιμοποιείται από κλήσειςRegisterNatives. - Η συμπεριφορά μπορεί να διαφέρει ανάλογα με τις εκδόσεις NDK/loader· το βήμα που αποδείχθηκε σταθερά αξιόπιστο ήταν η αφαίρεση των DYNAMIC tags
INIT_ARRAY/INIT_ARRAYSZ.
Αναφορές
- Εκμάθηση ARM Assembly: Azeria Labs – ARM Assembly Basics
- Τεκμηρίωση JNI & NDK: Oracle JNI Spec · Android JNI Tips · NDK Guides
- Αποσφαλμάτωση Native Libraries: Debug Android Native Libraries Using JEB Decompiler
- Frida 16.x change-log (Android hooking, tiny-function relocation) – frida.re/news
- Σύσταση NVD για overflow του
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
- 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
Μάθετε & εξασκηθείτε στο AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Μάθετε & εξασκηθείτε στο Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Υποστηρίξτε το HackTricks
- Ελέγξτε τα σχέδια συνδρομής!
- Εγγραφείτε στην 💬 ομάδα Discord ή στην ομάδα telegram ή ακολουθήστε μας στο Twitter 🐦 @hacktricks_live.
- Μοιραστείτε κόλπα hacking υποβάλλοντας PRs στα HackTricks και HackTricks Cloud github repos.
HackTricks

