Reverzovanje nativnih biblioteka
Reading time: 12 minutes
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 gde su performanse kritične. Autori malware-a takođe zloupotrebljavaju ove biblioteke jer su ELF shared objects i dalje teže dekompilovati od DEX/OAT byte-code-a.
Ova stranica se fokusira na praktične workflow-e i najnovija poboljšanja alata (2023–2025) koja olakšavaju reverzovanje Android .so fajlova.
Brzi workflow za trijažu za upravo preuzeti libfoo.so
- Izvucite biblioteku
# 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/
- Identifikujte arhitekturu i zaštite
file libfoo.so # arm64 or arm32 / x86
readelf -h libfoo.so # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so # (peda/pwntools)
- Navedite eksportovane simbole i JNI vezivanja
readelf -s libfoo.so | grep ' Java_' # dynamic-linked JNI
strings libfoo.so | grep -i "RegisterNatives" -n # static-registered JNI
- Učitajte u dekompajler (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) i pokrenite auto-analizu. Novije Ghidra verzije su uvele AArch64 dekompajler koji prepoznaje PAC/BTI stubove i MTE tagove, značajno poboljšavajući analizu biblioteka izgrađenih sa Android 14 NDK-om.
- Odlučite između statičkog i dinamičkog reverzovanja: stripped, obfuscated code često zahteva instrumentation (Frida, ptrace/gdbserver, LLDB).
Dinamička instrumentacija (Frida ≥ 16)
Frida serija 16 donela je nekoliko Android-specifičnih poboljšanja koja pomažu kada meta koristi moderne Clang/LLD optimizacije:
thumb-relocatorsada može da hook-uje male ARM/Thumb funkcije koje generiše agresivno poravnanje LLD-a (--icf=all).- Enumerisanje i rebinding ELF import slots radi 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 Androidu 14.
Primer: enumerisanje svih funkcija registrovanih preko RegisterNatives i ispis njihovih adresa u runtime-u:
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.
Proces-lokalna JNI telemetrija preko preuzetog .so (SoTap)
Kada je puna instrumentacija preterana ili blokirana, i dalje možete dobiti native-nivo uvida tako što ćete pre-loadovati mali logger unutar ciljnog procesa. SoTap je lagana Android native (.so) biblioteka koja beleži runtime ponašanje drugih JNI (.so) biblioteka unutar istog app procesa (nije potreban root).
Ključne osobine:
- Inicijalizuje se rano i posmatra JNI/native interakcije unutar procesa koji ga učitava.
- Čuva logove koristeći više zapisivih putanja sa elegantnim fallback-om na Logcat kada je skladište ograničeno.
- Moguće prilagođavanje izvornog koda: izmenite sotap.c da proširite/prilagodite šta se loguje i rebuildujte za svaku ABI.
Setup (repack the APK):
- Smeštanje odgovarajuće ABI build verzije u APK tako da loader može da razreši libsotap.so:
- lib/arm64-v8a/libsotap.so (for arm64)
- lib/armeabi-v7a/libsotap.so (for arm32)
- Obezbedite da se SoTap učita pre ostalih JNI biblioteka. Ubacite poziv rano (npr. Application subclass static initializer ili onCreate) tako da se logger prvo inicijalizuje. Smali snippet example:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
- Rebuildujte/potpišite/instalirajte, pokrenite aplikaciju, pa prikupite logove.
Putanje logova (proveravaju se redom):
/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
Napomene i rešavanje problema:
- Poravnanje ABI-ja je obavezno. Neusklađenost će izazvati UnsatisfiedLinkError i logger se neće učitati.
- Ograničenja skladištenja su česta na modernom Androidu; ako upis fajlova zakaže, SoTap će i dalje emitovati preko Logcat.
- Ponašanje/verbosnost je predviđeno za prilagođavanje; nakon izmena u sotap.c ponovo kompajlirajte iz izvornog koda.
Ovaj pristup je koristan za malware triage i JNI debugging gde je posmatranje native call flows od pokretanja procesa kritično, ali root/system-wide hooks nisu dostupni.
Pogledajte takođe: in‑memory native code execution via JNI
Uobičajen obrazac napada je preuzimanje raw shellcode bloba u runtime i njegovo izvršavanje direktno iz memorije preko JNI bridge-a (bez ELF na disku). Detalji i gotov JNI snippet ovde:
In Memory Jni Shellcode Execution
Nedavne ranjivosti koje vredi tražiti u APK-ovima
| Godina | CVE | Pogođena biblioteka | Napomene |
|---|---|---|---|
| 2023 | CVE-2023-4863 | libwebp ≤ 1.3.1 | Heap buffer overflow dostupan iz native koda koji dekodira WebP slike. Nekoliko Android aplikacija sadrži ranjive verzije. Kada vidite libwebp.so unutar APK-a, proverite njegovu verziju i pokušajte eksploataciju ili zakrpu. |
| 2024 | Multiple | OpenSSL 3.x series | Više problema vezanih za bezbednost memorije i padding-oracle. Mnogi Flutter & ReactNative bundli isporučuju sopstveni libcrypto.so. |
Kada u APK-u uočite third-party .so fajlove, uvek uporedite njihov hash sa upstream advisories. SCA (Software Composition Analysis) je retka na mobilu, pa su zastarele ranjive verzije raširene.
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 ubrizgava trampoline nakon skidanja PAC-a, ali vaši prilagođeni trampolini bi trebalo da pozivaju
pacda/autibspgde je potrebno. - MTE & Scudo hardened allocator: memory-tagging je opcionalan, ali mnoge Play-Integrity aware aplikacije se kompajliraju sa
-fsanitize=memtag; koristitesetprop arm64.memtag.dump 1plusadb shell am start ...da uhvatite tag faults. - LLVM Obfuscator (opaque predicates, control-flow flattening): komercijalni packeri (npr. Bangcle, SecNeo) sve više štite native kod, ne samo Java; očekujte bogus control-flow i encrypted string blob-ove u
.rodata.
Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (ARM64 ELF)
Jako zaštićene aplikacije često smeštaju provere za root/emulator/debug u native konstruktorima koji se izvršavaju veoma rano preko .init_array, pre JNI_OnLoad i mnogo pre nego što bilo koji Java kod bude izvršen. Možete učiniti te implicitne inicijalizatore eksplicitnim i povratiti kontrolu sledećim koracima:
- Ukloniti
INIT_ARRAY/INIT_ARRAYSZiz DYNAMIC tabele tako da loader ne auto-izvršava.init_arrayunose. - Razrešiti adresu konstruktora iz RELATIVE relocations i exportovati je kao regularan funkcioni simbol (npr.
INIT0). - Preimenovati
JNI_OnLoaduJNI_OnLoad0da se spreči ART da ga implicitno pozove.
Zašto ovo radi na Android/arm64
- Na AArch64,
.init_arrayunosi su često popunjeni pri učitavanju pomoćuR_AARCH64_RELATIVErelocations čiji je addend adresa ciljne funkcije unutar.text. - Bajtovi u
.init_arraymogu statički izgledati prazno; dynamic linker upisuje razrešenu adresu tokom obrade relocations.
Identifikujte ciljni konstruktor
- Koristite Android NDK toolchain za precizno parsiranje ELF-a na 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
- Pronađite relocaciju koja se nalazi unutar virtuelnog adresnog opsega
.init_array;addendteR_AARCH64_RELATIVErelocacije je konstruktor (npr.0xA34,0x954). - Disasemblirajte oko te adrese radi sanity check-a:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
Plan za patch
- Uklonite
INIT_ARRAYiINIT_ARRAYSZDYNAMIC tagove. Ne brišite sekcije. - Dodajte GLOBAL DEFAULT FUNC simbol
INIT0na adresu konstruktora tako da se može pozivati manuelno. - Preimenujte
JNI_OnLoad→JNI_OnLoad0kako biste sprečili ART da ga implicitno pozove.
Validacija nakon patch-a
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)
Skript: ukloni INIT_ARRAY/INIT_ARRAYSZ, eksportuj INIT0, preimenuj 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')
Beleške i neuspešni pristupi (za prenosivost)
- Zeroing
.init_arraybytes or setting the section length to 0 does not help: the dynamic linker repopulates it via relocations. - Setting
INIT_ARRAY/INIT_ARRAYSZto 0 can break the loader due to inconsistent tags. Clean removal of those DYNAMIC entries is the reliable lever. - Deleting the
.init_arraysection entirely tends to crash the loader. - After patching, function/layout addresses might shift; always recompute the constructor from
.rela.dynaddends on the patched file if you need to re-run the patch.
Bootstrapping a minimal ART/JNI to invoke INIT0 and JNI_OnLoad0
- Use JNIInvocation to spin up a tiny ART VM context in a standalone binary. Then call
INIT0()andJNI_OnLoad0(vm)manually before any Java code. - Include the target APK/classes on the classpath so any
RegisterNativesfinds its Java classes.
Minimalni harness (CMake i C) za pozivanje INIT0 → JNI_OnLoad0 → Java metode
# 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 posle patchovanja zbog re-layouta; uvek ih ponovo izračunajte iz
.rela.dynna finalnom binarnom fajlu. - Osigurajte da
-Djava.class.pathobuhvata svaku klasu koju koriste poziviRegisterNatives. - Ponašanje može da varira u zavisnosti od verzije NDK/loadera; korak koji se pokazao najpouzdanijim bio je uklanjanje
INIT_ARRAY/INIT_ARRAYSZDYNAMIC tagova.
Reference
- Učenje ARM Assembly: Azeria Labs – ARM Assembly Basics
- JNI & NDK dokumentacija: Oracle JNI Spec · Android JNI Tips · NDK Guides
- Debugovanje 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
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.
HackTricks