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

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

  1. Izvucite biblioteku
bash
# 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. Identifikujte arhitekturu i zaštite
bash
file libfoo.so        # arm64 or arm32 / x86
readelf -h libfoo.so  # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so  # (peda/pwntools)
  1. Navedite eksportovane simbole i JNI vezivanja
bash
readelf -s libfoo.so | grep ' Java_'     # dynamic-linked JNI
strings libfoo.so   | grep -i "RegisterNatives" -n   # static-registered JNI
  1. 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.
  2. 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-relocator sada 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-optimizations na Androidu 14.

Primer: enumerisanje svih funkcija registrovanih preko RegisterNatives i ispis njihovih adresa u runtime-u:

javascript
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):

  1. 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)
  1. 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:
smali
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
  1. 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

GodinaCVEPogođena bibliotekaNapomene
2023CVE-2023-4863libwebp ≤ 1.3.1Heap 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.
2024MultipleOpenSSL 3.x seriesViš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.


  • 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/autibsp gde je potrebno.
  • MTE & Scudo hardened allocator: memory-tagging je opcionalan, ali mnoge Play-Integrity aware aplikacije se kompajliraju sa -fsanitize=memtag; koristite setprop arm64.memtag.dump 1 plus adb 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_ARRAYSZ iz DYNAMIC tabele tako da loader ne auto-izvršava .init_array unose.
  • Razrešiti adresu konstruktora iz RELATIVE relocations i exportovati je kao regularan funkcioni simbol (npr. INIT0).
  • Preimenovati JNI_OnLoad u JNI_OnLoad0 da se spreči ART da ga implicitno pozove.

Zašto ovo radi na Android/arm64

  • Na AArch64, .init_array unosi su često popunjeni pri učitavanju pomoću R_AARCH64_RELATIVE relocations čiji je addend adresa ciljne funkcije unutar .text.
  • Bajtovi u .init_array mogu 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:
bash
# 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; addend te R_AARCH64_RELATIVE relocacije je konstruktor (npr. 0xA34, 0x954).
  • Disasemblirajte oko te adrese radi sanity check-a:
bash
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40

Plan za patch

  1. Uklonite INIT_ARRAY i INIT_ARRAYSZ DYNAMIC tagove. Ne brišite sekcije.
  2. Dodajte GLOBAL DEFAULT FUNC simbol INIT0 na adresu konstruktora tako da se može pozivati manuelno.
  3. Preimenujte JNI_OnLoadJNI_OnLoad0 kako biste sprečili ART da ga implicitno pozove.

Validacija nakon patch-a

bash
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
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')

Beleške i neuspešni pristupi (za prenosivost)

  • Zeroing .init_array bytes or setting the section length to 0 does not help: the dynamic linker repopulates it via relocations.
  • Setting INIT_ARRAY/INIT_ARRAYSZ to 0 can break the loader due to inconsistent tags. Clean removal of those DYNAMIC entries is the reliable lever.
  • Deleting the .init_array section entirely tends to crash the loader.
  • After patching, function/layout addresses might shift; always recompute the constructor from .rela.dyn addends 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() and JNI_OnLoad0(vm) manually before any Java code.
  • Include the target APK/classes on the classpath so any RegisterNatives finds its Java classes.
Minimalni harness (CMake i C) za pozivanje INIT0 → JNI_OnLoad0 → Java metode
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)
c
// 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);
}
bash
# 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.dyn na finalnom binarnom fajlu.
  • Osigurajte da -Djava.class.path obuhvata svaku klasu koju koriste pozivi RegisterNatives.
  • Ponašanje može da varira u zavisnosti od verzije NDK/loadera; korak koji se pokazao najpouzdanijim bio je uklanjanje INIT_ARRAY/INIT_ARRAYSZ DYNAMIC tagova.

Reference

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