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

Για περισσότερες πληροφορίες δείτε: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html

Οι Android εφαρμογές μπορούν να χρησιμοποιούν native βιβλιοθήκες, συνήθως γραμμένες σε C ή C++, για εργασίες κρίσιμες ως προς την απόδοση. Οι δημιουργοί malware επίσης καταχρώνται αυτές τις βιβλιοθήκες, διότι τα ELF shared objects εξακολουθούν να είναι πιο δύσκολα στην αποσυμπίληση από ό,τι ο DEX/OAT byte-code. Αυτή η σελίδα επικεντρώνεται σε πρακτικά ροές εργασίας και πρόσφατες βελτιώσεις εργαλείων (2023-2025) που κάνουν το reversing Android .so αρχείων ευκολότερο.


Γρήγορο triage-workflow για ένα πρόσφατα εξαχθέν libfoo.so

  1. Εξαγωγή της βιβλιοθήκης
# 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. Αναγνώριση αρχιτεκτονικής & μηχανισμών προστασίας
file libfoo.so        # arm64 or arm32 / x86
readelf -h libfoo.so  # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so  # (peda/pwntools)
  1. Καταγράψτε εξαγόμενα σύμβολα & δεσμούς JNI
readelf -s libfoo.so | grep ' Java_'     # dynamic-linked JNI
strings libfoo.so   | grep -i "RegisterNatives" -n   # static-registered JNI
  1. Φορτώστε σε έναν decompiler (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) και τρέξτε auto-analysis. Οι νεότερες εκδόσεις του Ghidra εισήγαγαν έναν AArch64 decompiler που αναγνωρίζει PAC/BTI stubs και MTE tags, βελτιώνοντας σημαντικά την ανάλυση βιβλιοθηκών που χτίστηκαν με το Android 14 NDK.
  2. Αποφασίστε ανάμεσα σε static vs dynamic reversing: ο stripped, obfuscated κώδικας συχνά χρειάζεται instrumentation (Frida, ptrace/gdbserver, LLDB).

Dynamic Instrumentation (Frida ≥ 16)

Η σειρά 16 του Frida έφερε αρκετές βελτιώσεις ειδικές για Android που βοηθούν όταν ο στόχος χρησιμοποιεί τις σύγχρονες βελτιστοποιήσεις Clang/LLD:

  • thumb-relocator can now hook tiny ARM/Thumb functions generated by LLD’s aggressive alignment (--icf=all).
  • Enumerating and rebinding ELF import slots works on Android, enabling per-module dlopen()/dlsym() patching when inline hooks are rejected.
  • Java hooking was fixed for the new ART quick-entrypoint used when apps are compiled with --enable-optimizations on Android 14.

Παράδειγμα: απαρίθμηση όλων των συναρτήσεων που έχουν εγγραφεί μέσω του RegisterNatives και εμφάνιση των διευθύνσεών τους κατά το 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.

Εξαγωγή των κατά το runtime αποκρυπτογραφημένων native libraries από τη μνήμη (Frida soSaver)

When a protected APK keeps native code encrypted or only maps it at runtime (packers, downloaded payloads, generated libs), attach Frida and dump the mapped ELF directly from process memory.

soSaver workflow (Python host + TS/JS Frida agent):

  • Κάνει hook τις dlopen και android_dlopen_ext για να εντοπίσει το load-time library mapping και εκτελεί μια αρχική σάρωση των ήδη φορτωμένων modules.
  • Περιοδικά σαρώνει τα process memory mappings για ELF headers ώστε να εντοπίσει modules που φορτώνονται μέσω μη-τυπικών mappers και που ποτέ δεν περνούν από τα loader APIs.
  • Διαβάζει κάθε module σε μπλοκ από τη μνήμη και stream-άρει τα bytes μέσω Frida messages προς το host· αν μια περιοχή δεν μπορεί να διαβαστεί, επανέρχεται στην ανάγνωση από το on-disk path όταν είναι διαθέσιμο.
  • Αποθηκεύει τα ανασυντεθειμένα .so αρχεία και εκτυπώνει στατιστικά εξαγωγής ανά module, παρέχοντας artifacts για static RE.

Εκτέλεση (root + frida-server, Python ≥3.8, uv):

git clone https://github.com/TheQmaks/sosaver.git
cd sosaver && uv sync
source .venv/bin/activate    # .venv\Scripts\activate on Windows

# target by package or PID; choose output/verbosity
sosaver com.example.app
sosaver 1234 -o /tmp/so-dumps --debug

Αυτή η προσέγγιση παρακάμπτει τις προστασίες «only decrypted in RAM» ανακτώντας την ζωντανή mapped image, επιτρέποντας offline ανάλυση σε IDA/Ghidra ακόμη και αν το αντίγραφο στο filesystem είναι obfuscated ή απουσιάζει.

Process-local JNI telemetry via preloaded .so (SoTap)

Όταν η πλήρης instrumentation είναι υπερβολική ή αποκλείεται, μπορείτε να αποκτήσετε ορατότητα σε επίπεδο native φορτώνοντας προκαταβολικά ένα μικρό logger μέσα στη στοχευόμενη process. Το SoTap είναι μια ελαφριά Android native (.so) βιβλιοθήκη που καταγράφει τη runtime συμπεριφορά άλλων JNI (.so) βιβλιοθηκών μέσα στην ίδια process της εφαρμογής (no root required).

Key properties:

  • Εκκινεί νωρίς και παρατηρεί τις αλληλεπιδράσεις JNI/native μέσα στη διαδικασία που το φορτώνει.
  • Διατηρεί τα logs χρησιμοποιώντας πολλαπλές εγγράψιμες διαδρομές με ομαλή παλινδρόμηση σε Logcat όταν η αποθήκευση είναι περιορισμένη.
  • Source-customizable: τροποποιήστε το sotap.c για να επεκτείνετε/προσαρμόσετε τι καταγράφεται και ξανακτίστε ανά ABI.

Setup (repack the APK):

  1. Τοποθετήστε το σωστό build για κάθε ABI μέσα στο APK ώστε ο loader να μπορεί να επιλύσει libsotap.so:
  • lib/arm64-v8a/libsotap.so (for arm64)
  • lib/armeabi-v7a/libsotap.so (for arm32)
  1. Βεβαιωθείτε ότι το SoTap φορτώνει πριν από άλλες JNI libs. Εισάγετε μια κλήση νωρίς (π.χ. Application subclass static initializer ή onCreate) ώστε ο logger να αρχικοποιηθεί πρώτος. Smali snippet example:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
  1. Ξανακτίστε/υπογράψτε/εγκαταστήστε, τρέξτε την εφαρμογή και έπειτα συλλέξτε τα 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

Σημειώσεις και αντιμετώπιση προβλημάτων:

  • Η ευθυγράμμιση του ABI είναι υποχρεωτική. Μια ασυμφωνία θα προκαλέσει UnsatisfiedLinkError και ο logger δεν θα φορτωθεί.
  • Οι περιορισμοί αποθήκευσης είναι συχνοί σε σύγχρονα Android· αν οι εγγραφές αρχείων αποτύχουν, το SoTap θα εξακολουθήσει να εκπέμπει μέσω Logcat.
  • Η συμπεριφορά/λεπτομέρεια εξόδου προορίζεται να προσαρμοστεί· επανακατασκευάστε από τον πηγαίο κώδικα μετά την επεξεργασία του sotap.c.

Αυτή η προσέγγιση είναι χρήσιμη για malware triage και JNI debugging όπου η παρατήρηση των native call flows από την εκκίνηση της διεργασίας είναι κρίσιμη αλλά δεν υπάρχουν hooks σε επίπεδο root/συστήματος.


Δείτε επίσης: in‑memory native code execution via JNI

Ένα κοινό attack pattern είναι να κατεβάσει ένα raw shellcode blob κατά το runtime και να το εκτελέσει απευθείας από τη μνήμη μέσω μιας JNI bridge (no on‑disk ELF). Λεπτομέρειες και έτοιμο προς χρήση JNI snippet εδώ:

In Memory Jni Shellcode Execution


Πρόσφατες ευπάθειες που αξίζει να αναζητήσετε σε APKs

YearCVEAffected libraryNotes
2023CVE-2023-4863libwebp ≤ 1.3.1Heap buffer overflow που είναι προσβάσιμος από native code που αποκωδικοποιεί εικόνες WebP. Πολλές Android εφαρμογές ενσωματώνουν ευάλωτες εκδόσεις. Όταν βλέπετε ένα libwebp.so μέσα σε ένα APK, ελέγξτε την έκδοση και προσπαθήστε εκμετάλλευσης ή επιδιόρθωσης.
2024MultipleOpenSSL 3.x seriesΠολλά ζητήματα memory-safety και padding-oracle. Πολλά πακέτα Flutter & ReactNative περιλαμβάνουν το δικό τους libcrypto.so.

Όταν εντοπίζετε .so αρχεία τρίτων μέσα σε ένα APK, πάντα ελέγξτε τον hash τους σε σχέση με τις upstream advisories. Το SCA (Software Composition Analysis) είναι ασυνήθιστο στο mobile, οπότε παρωχημένα ευάλωτα builds είναι διαδεδομένα.


  • Pointer Authentication (PAC) & Branch Target Identification (BTI): Το Android 14 ενεργοποιεί PAC/BTI στις system libraries σε υποστηριζόμενο ARMv8.3+ silicon. Οι decompilers πλέον εμφανίζουν PAC‐related pseudo-instructions· για dynamic analysis το Frida injects trampolines after stripping PAC, αλλά τα custom trampolines σας πρέπει να καλούν pacda/autibsp όπου απαιτείται.
  • MTE & Scudo hardened allocator: Το memory-tagging είναι opt-in αλλά πολλές Play-Integrity aware apps build με -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 κώδικας. Μπορείτε να μετατρέψετε αυτούς τους implicit initializers σε ρητούς και να επανακτήσετε έλεγχο κάνοντας τα εξής:

  • Αφαίρεση των INIT_ARRAY/INIT_ARRAYSZ από τον DYNAMIC πίνακα ώστε ο loader να μην auto-execute τις εγγραφές της .init_array.
  • Επίλυση της διεύθυνσης του constructor από RELATIVE relocations και εξαγωγή της ως κανονικού function symbol (π.χ., INIT0).
  • Μετονομασία JNI_OnLoad σε JNI_OnLoad0 για να αποτραπεί το ART από το να το καλεί αυτόματα.

Γιατί αυτό δουλεύει σε Android/arm64

  • Σε AArch64, οι εγγραφές της .init_array συχνά γεμίζουν κατά το load time από R_AARCH64_RELATIVE relocations των οποίων το addend είναι η διεύθυνση-στόχος της συνάρτησης μέσα στο .text.
  • Τα bytes της .init_array μπορεί να φαίνονται κενά στατικά· ο dynamic linker γράφει τη λυμένη διεύθυνση κατά την επεξεργασία relocations.

Εντοπισμός του στόχου constructor

  • Χρησιμοποιήστε το 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

  1. Αφαιρέστε τα INIT_ARRAY και INIT_ARRAYSZ DYNAMIC tags. Μην διαγράψετε sections.
  2. Προσθέστε ένα GLOBAL DEFAULT FUNC symbol INIT0 στη διεύθυνση του constructor ώστε να μπορεί να κληθεί χειροκίνητα.
  3. Μετονομάστε JNI_OnLoadJNI_OnLoad0 για να σταματήσει το ART από το να το επικαλείται αυτόματα.

Επικύρωση μετά το patch

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

Τροποποίηση με LIEF (Python)

Σενάριο: αφαιρεί INIT_ARRAY/INIT_ARRAYSZ, εξάγει INIT0, μετονομάζει 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’)

</details>

Σημειώσεις και αποτυχημένες προσεγγίσεις (για φορητότητα)
- Το μηδενισμό των bytes του `.init_array` ή το να ορίσετε το μήκος του τμήματος σε 0 δεν βοηθά: ο δυναμικός φορτωτής το επαναγεμίζει μέσω relocations.
- Το να ορίσετε `INIT_ARRAY`/`INIT_ARRAYSZ` σε 0 μπορεί να διακόψει τον φορτωτή λόγω ασυνεπών ετικετών. Η καθαρή αφαίρεση αυτών των καταχωρήσεων DYNAMIC είναι η αξιόπιστη λύση.
- Η διαγραφή ολόκληρου του τμήματος `.init_array` τείνει να προκαλεί σφάλμα στον φορτωτή.
- Μετά το patching, οι διευθύνσεις συναρτήσεων/διάταξης μπορεί να μετατοπιστούν· πάντα επανυπολογίστε τον constructor από τα addends του `.rela.dyn` στο patched αρχείο αν χρειάζεται να τρέξετε ξανά το patch.

Εκκίνηση ενός ελάχιστου ART/JNI για να καλέσετε INIT0 και JNI_OnLoad0
- Χρησιμοποιήστε JNIInvocation για να ξεκινήσετε ένα μικρό ART VM context σε ένα αυτόνομο εκτελέσιμο. Στη συνέχεια καλέστε `INIT0()` και `JNI_OnLoad0(vm)` χειροκίνητα πριν από οποιοδήποτε Java code.
- Συμπεριλάβετε το στοχευόμενο APK/classes στο classpath ώστε οποιοδήποτε `RegisterNatives` να βρίσκει τις Java κλάσεις του.

<details>
<summary>Ελάχιστο harness (CMake και C) για να καλέσετε INIT0 → JNI_OnLoad0 → μέθοδο Java</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

Κοινές Παγίδες:

  • Οι διευθύνσεις των constructor αλλάζουν μετά το patching λόγω re-layout; πάντα επανυπολογίζετε από .rela.dyn στο τελικό binary.
  • Βεβαιωθείτε ότι το -Djava.class.path καλύπτει κάθε κλάση που χρησιμοποιείται από κλήσεις RegisterNatives.
  • Η συμπεριφορά μπορεί να διαφέρει ανάλογα με τις εκδόσεις NDK/loader· το σταθερά αξιόπιστο βήμα ήταν η αφαίρεση των INIT_ARRAY/INIT_ARRAYSZ DYNAMIC tags.

Αναφορές

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