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

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
# 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
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
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:

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:
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:
# 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:
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

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’)

</details>

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.

<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 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