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

  1. 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/
  1. 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)
  1. 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
  1. 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.
  2. 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-relocator sada 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-optimizations na 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 dlopen i android_dlopen_ext da 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 .so fajlove 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):

  1. 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)
  1. 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
  1. 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

YearCVEAffected libraryNotes
2023CVE-2023-4863libwebp ≤ 1.3.1Heap 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.
2024MultipleOpenSSL 3.x seriesSeveral 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.


  • 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/autibsp gde je potrebno.
  • MTE & Scudo hardened allocator: memory-tagging je opciono, ali mnoge aplikacije koje poštuju Play-Integrity builduju se sa -fsanitize=memtag; koristite setprop arm64.memtag.dump 1 plus adb 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_ARRAYSZ from the DYNAMIC table so the loader does not auto-execute .init_array entries.
  • Resolving the constructor address from RELATIVE relocations and exporting it as a regular function symbol (e.g., INIT0).
  • Renaming JNI_OnLoad to JNI_OnLoad0 to prevent ART from calling it implicitly.

Why this works on Android/arm64

  • On AArch64, .init_array entries are often populated at load time by R_AARCH64_RELATIVE relocations whose addend is the target function address inside .text.
  • The bytes of .init_array may 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_array virtual address range; the addend of that R_AARCH64_RELATIVE is 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

  1. Remove INIT_ARRAY and INIT_ARRAYSZ DYNAMIC tags. Do not delete sections.
  2. Add a GLOBAL DEFAULT FUNC symbol INIT0 at the constructor address so it can be called manually.
  3. Rename JNI_OnLoadJNI_OnLoad0 to 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 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 (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.dyn u finalnom binarnom fajlu.
  • Uverite se da -Djava.class.path obuhvata svaku klasu koju koriste pozivi RegisterNatives.
  • Ponašanje može varirati u zavisnosti od NDK/loader verzija; dosledno pouzdan korak bio je uklanjanje INIT_ARRAY/INIT_ARRAYSZ DYNAMIC tags.

Literatura

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