Reverzovanje nativnih biblioteka
Tip
Učite i vežbajte AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Učite i vežbajte GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Učite i vežbajte Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Podržite HackTricks
- Proverite planove pretplate!
- Pridružite se 💬 Discord grupi ili telegram grupi ili pratite nas na Twitteru 🐦 @hacktricks_live.
- Podelite hakerske trikove slanjem PR-ova na HackTricks i HackTricks Cloud github repozitorijume.
Za više informacija pogledajte: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html
Android aplikacije mogu koristiti nativne biblioteke, obično napisane u C ili C++, za zadatke koji zahtevaju visok performans. Kreatori malvera takođe zloupotrebljavaju ove biblioteke jer su ELF shared objects i dalje teže dekompajlirati nego DEX/OAT bajtkod.
Ova stranica se fokusira na praktične tokove rada i najnovija poboljšanja alata (2023-2025) koja olakšavaju reverzovanje Android .so fajlova.
Brzi tok trijaže za novo izvučeni libfoo.so
- Ekstrakcija biblioteke
# 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/
- Identifikacija arhitekture i zaštita
file libfoo.so # arm64 or arm32 / x86
readelf -h libfoo.so # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so # (peda/pwntools)
- Lista izvezenih simbola i JNI vezivanja
readelf -s libfoo.so | grep ' Java_' # dynamic-linked JNI
strings libfoo.so | grep -i "RegisterNatives" -n # static-registered JNI
- Učitaj u dekompajler (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) i pokreni auto-analizu. Novije verzije Ghidra su uvele AArch64 dekompajler koji prepoznaje PAC/BTI stubove i MTE oznake, značajno poboljšavajući analizu biblioteka izgrađenih sa Android 14 NDK.
- Odlučite između statičkog i dinamičkog reverzovanja: stripped, obfuscated kod često zahteva instrumentation (Frida, ptrace/gdbserver, LLDB).
Dinamičko instrumentisanje (Frida ≥ 16)
Frida serija 16 je donela nekoliko Android-specifičnih poboljšanja koja pomažu kada cilj koristi moderne Clang/LLD optimizacije:
thumb-relocatorsada može da hook-uje male ARM/Thumb funkcije generisane agresivnim poravnanjem LLD-a (--icf=all).- Enumeracija i rebinding ELF import slots funkcioniše na Androidu, omogućavajući per-module
dlopen()/dlsym()patching kada inline hooks budu odbijeni. - Java hooking je ispravljen za novi ART quick-entrypoint koji se koristi kada su aplikacije kompajlirane sa
--enable-optimizationsna Android 14.
Example: enumerating all functions registered through RegisterNatives and dumping their addresses at 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 će raditi bez dodatne konfiguracije na PAC/BTI-enabled uređajima (Pixel 8/Android 14+) sve dok koristite frida-server 16.2 ili noviji — ranije verzije nisu uspevale da pronađu padding za inline hooks.
Dumping runtime-decrypted native libraries from memory (Frida soSaver)
Kada zaštićeni APK drži native code enkriptovan ili ga mapira samo u runtime (packers, downloaded payloads, generated libs), priključite Frida i dump-ujte mapped ELF direktno iz process memory.
soSaver workflow (Python host + TS/JS Frida agent):
- Postavlja hookove na
dlopeniandroid_dlopen_extda detektuje mapiranje biblioteka pri učitavanju i izvrši početni pregled već učitanih modula. - Periodično skenira mapiranja process memory za ELF headere kako bi uhvatio module učitane preko nestandardnih mappera koji nikada ne prolaze kroz loader APIs.
- Čita svaki modul u blokovima iz memorije i stream-uje bajtove kroz Frida messages ka hostu; ako region ne može biti pročitan, pada na čitanje sa on-disk path ako je dostupan.
- Čuva rekonstruisane
.sofajlove i ispisuje statistiku ekstrakcije po modulu, pružajući artefakte za static RE.
Run (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
Ovaj pristup zaobilazi zaštite “only decrypted in RAM” tako što oporavlja aktivno mapiranu binarnu sliku procesa, omogućavajući offline analizu u IDA/Ghidra čak i ako je kopija na fajl-sistemu obfuskirana ili odsutna.
Process-local JNI telemetry via preloaded .so (SoTap)
Kada je puna instrumentacija prekomerna ili je blokirana, i dalje možete dobiti vidljivost na nivou native biblioteka tako što ćete prethodno učitati mali logger unutar ciljnog procesa. SoTap je lagana Android native (.so) biblioteka koja beleži runtime ponašanje drugih JNI (.so) biblioteka unutar istog procesa aplikacije (nije potreban root).
Key properties:
- Inicijalizuje se rano i posmatra JNI/native interakcije unutar procesa koji ga učitava.
- Čuva logove koristeći više putanja za upis sa elegantnim prebacivanjem na Logcat kada je skladišni prostor ograničen.
- Mogućnost prilagođavanja izvornog koda: izmenite sotap.c da proširite/podesite šta se beleži i ponovo izgradite za svaki ABI.
Setup (repakovanje APK-a):
- Drop the proper ABI build into the APK so the loader can resolve libsotap.so:
- lib/arm64-v8a/libsotap.so (for arm64)
- lib/armeabi-v7a/libsotap.so (for 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
Beleške i rešavanje problema:
- ABI alignment je obavezan. Nekompatibilnost će izazvati UnsatisfiedLinkError i logger se neće učitati.
- Ograničenja skladišta su česta na modernom Androidu; ako upis u fajlovi ne uspe, SoTap će i dalje emitovati putem Logcat.
- Behavior/verbosity je namenjen za prilagođavanje; rebuildujte iz izvora nakon izmene sotap.c.
Ovaj pristup je koristan za malware triage i JNI debugging kada je kritično posmatrati tokove poziva native koda od starta procesa, ali root/system-wide hookovi nisu dostupni.
Vidi takođe: 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
Nedavne ranjivosti koje vredi tražiti u APK-ovima
| Year | CVE | Affected library | Notes |
|---|---|---|---|
| 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. |
Kada uočite third-party .so fajlove unutar APK-a, uvek uporedite njihov hash sa upstream advisories. SCA (Software Composition Analysis) je retka na mobilnim platformama, pa su zastareli ranjivi buildovi rašireni.
Anti-Reversing & Hardening trends (Android 13-15)
- Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 omogućava PAC/BTI u sistemskim bibliotekama na podržanom ARMv8.3+ silicijumu. Decompileri sada prikazuju PAC‐povezane pseudo-instrukcije; za dinamičku analizu Frida injektuje trampoline nakon uklanjanja PAC, ali vaši prilagođeni trampolini treba da pozivaju
pacda/autibspgde je potrebno. - MTE & Scudo hardened allocator: memory-tagging je opciono, ali mnoge aplikacije koje poštuju Play-Integrity builduju se sa
-fsanitize=memtag; koristitesetprop arm64.memtag.dump 1plusadb shell am start ...da biste zabeležili tag faults. - LLVM Obfuscator (opaque predicates, control-flow flattening): komercijalni packeri (npr. Bangcle, SecNeo) sve češće štite native kod, ne samo Java; očekujte lažni control-flow i enkriptovane string blobove u
.rodata.
Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (ARM64 ELF)
Highly protected apps often place root/emulator/debug checks in native constructors that run extremely early via .init_array, before JNI_OnLoad and long before any Java code executes. You can make those implicit initializers explicit and regain control by:
- Removing
INIT_ARRAY/INIT_ARRAYSZfrom the DYNAMIC table so the loader does not auto-execute.init_arrayentries. - Resolving the constructor address from RELATIVE relocations and exporting it as a regular function symbol (e.g.,
INIT0). - Renaming
JNI_OnLoadtoJNI_OnLoad0to prevent ART from calling it implicitly.
Why this works on Android/arm64
- On AArch64,
.init_arrayentries are often populated at load time byR_AARCH64_RELATIVErelocations whose addend is the target function address inside.text. - The bytes of
.init_arraymay look empty statically; the dynamic linker writes the resolved address during relocation processing.
Identify the constructor target
- Use the Android NDK toolchain for accurate ELF parsing on 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
- Find the relocation that lands inside the
.init_arrayvirtual address range; theaddendof thatR_AARCH64_RELATIVEis the constructor (e.g.,0xA34,0x954). - Disassemble around that address to sanity check:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
Patch plan
- Remove
INIT_ARRAYandINIT_ARRAYSZDYNAMIC tags. Do not delete sections. - Add a GLOBAL DEFAULT FUNC symbol
INIT0at the constructor address so it can be called manually. - Rename
JNI_OnLoad→JNI_OnLoad0to stop ART from invoking it implicitly.
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'
Patchovanje sa LIEF (Python)
Skripta: ukloni INIT_ARRAY/INIT_ARRAYSZ, izvezi INIT0, preimenuj 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>
Beleške i neuspešni pristupi (zbog prenosivosti)
- Postavljanje bajtova `.init_array` na nulu ili podešavanje dužine sekcije na 0 ne pomaže: dynamic linker ih ponovo popunjava putem relocations.
- Postavljanje `INIT_ARRAY`/`INIT_ARRAYSZ` na 0 može da pokvari loader zbog nekonzistentnih tagova. Čisto uklanjanje tih DYNAMIC unosa je pouzdan pristup.
- Brisanje cele `.init_array` sekcije obično dovodi do pada loader-a.
- Posle patchovanja, adrese funkcija/rasporeda mogu da se pomere; uvek ponovo izračunajte konstruktor iz `.rela.dyn` addenda u izmenjenom fajlu ako treba da ponovo pokrenete patch.
Bootstrapping a minimal ART/JNI to invoke INIT0 and JNI_OnLoad0
- Koristite JNIInvocation da podignete mali ART VM kontekst u samostalnom binarnom fajlu. Zatim pozovite `INIT0()` i `JNI_OnLoad0(vm)` ručno pre bilo kog Java koda.
- Uključite ciljnu APK/classes na classpath tako da svaki `RegisterNatives` pronađe svoje Java klase.
<details>
<summary>Minimalni harness (CMake i C) za pozivanje INIT0 → JNI_OnLoad0 → Java metode</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
Uobičajene zamke:
- Adrese konstruktora se menjaju nakon patchinga zbog re-layout-a; uvek ponovo izračunajte iz
.rela.dynu finalnom binarnom fajlu. - Uverite se da
-Djava.class.pathobuhvata svaku klasu koju koriste poziviRegisterNatives. - Ponašanje može varirati u zavisnosti od NDK/loader verzija; dosledno pouzdan korak bio je uklanjanje
INIT_ARRAY/INIT_ARRAYSZDYNAMIC tags.
Literatura
- Učenje ARM Assembly: Azeria Labs – ARM Assembly Basics
- JNI & NDK dokumentacija: Oracle JNI Spec · Android JNI Tips · NDK Guides
- Debagovanje native biblioteka: 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
- soSaver — Frida-based live memory dumper for Android
.solibraries – github.com/TheQmaks/sosaver - soSaver Frida agent (TypeScript/JS) – github.com/TheQmaks/soSaver-frida
Tip
Učite i vežbajte AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Učite i vežbajte GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Učite i vežbajte Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Podržite HackTricks
- Proverite planove pretplate!
- Pridružite se 💬 Discord grupi ili telegram grupi ili pratite nas na Twitteru 🐦 @hacktricks_live.
- Podelite hakerske trikove slanjem PR-ova na HackTricks i HackTricks Cloud github repozitorijume.


