Reversing Native Libraries

Tip

Leer en oefen AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Leer en oefen GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Leer en oefen Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Ondersteun HackTricks

Vir verdere inligting, kyk: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html

Android apps kan native biblioteke gebruik, tipies in C of C++ geskryf, vir prestasie-kritieke take. Malware-skepper misbruik ook hierdie biblioteke omdat ELF shared objects steeds moeiliker is om te dekompileer as DEX/OAT byte-code. Hierdie blad fokus op praktiese werkvloei en onlangse hulpmiddelverbeterings (2023-2025) wat dit makliker maak om Android .so-lêers te reverse-engineer.


Vinnige triage-werkvloei vir ’n pas onttrekte libfoo.so

  1. Trek die biblioteek uit
# 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. Identifiseer argitektuur & beskerming
file libfoo.so        # arm64 or arm32 / x86
readelf -h libfoo.so  # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so  # (peda/pwntools)
  1. Lys geëksporteerde simbole & JNI-koppelinge
readelf -s libfoo.so | grep ' Java_'     # dynamic-linked JNI
strings libfoo.so   | grep -i "RegisterNatives" -n   # static-registered JNI
  1. Laai in ’n dekompiler (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) en voer outo-analise uit. Nuweer Ghidra-weergawes het ’n AArch64 dekompiler bekendgestel wat PAC/BTI stubs en MTE tags herken, wat die analise van biblioteke wat met die Android 14 NDK gebou is aansienlik verbeter.
  2. Besluit tussen statiese vs dinamiese reversing: gestript, obfuskede kode benodig dikwels instrumentasie (Frida, ptrace/gdbserver, LLDB).

Dynamic Instrumentation (Frida ≥ 16)

Frida se 16-reeks het verskeie Android-spesifieke verbeterings gebring wat help wanneer die teiken moderne Clang/LLD optimalisasies gebruik:

  • thumb-relocator kan nou hook tiny ARM/Thumb functions wat deur LLD’s aggressiewe alignment (--icf=all) gegenereer word.
  • Die enumerasie en herbinding van ELF import slots werk op Android, wat per-module dlopen()/dlsym() patching moontlik maak wanneer inline hooks geweier word.
  • Java hooking is reggestel vir die nuwe ART quick-entrypoint wat gebruik word wanneer apps saamgestel is met --enable-optimizations op Android 14.

Voorbeeld: enumerasie van alle funksies wat via RegisterNatives geregistreer is en die dumping van hul adresse tydens 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 sal direk werk op PAC/BTI-ondersteunde toestelle (Pixel 8/Android 14+) solank jy frida-server 16.2 of later gebruik – vroeër weergawes het versuim om padding vir inline hooks te vind.

Dumping runtime-decrypted native libraries from memory (Frida soSaver)

Wanneer ’n beskermde APK native kode versleuteld hou of dit slegs tydens uitvoering map (packers, downloaded payloads, generated libs), koppel Frida en dump die gemapte ELF direk uit die prosesgeheue.

soSaver workflow (Python host + TS/JS Frida agent):

  • Hooks dlopen and android_dlopen_ext om load-time library mapping te detecteer en voer ’n aanvanklike sweep van reeds gelaaide modules uit.
  • Skandeer periodiek die prosesgeheue mappings vir ELF headers om modules vas te vang wat deur nie-standaard mappers gelaai is en nooit die loader APIs getref het nie.
  • Lees elke module in blokke uit die geheue en stream die bytes deur Frida messages na die host; as ’n streek nie gelees kan word nie, val dit terug op lees vanaf die on-disk path wanneer beskikbaar.
  • Stoor die gerekonstrueerde .so lêers en druk per-module extraction stats, wat artefakte vir static RE verskaf.

Voer uit (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

This approach bypasses “only decrypted in RAM” protections by recovering the live mapped image, allowing offline analysis in IDA/Ghidra even if the filesystem copy is obfuscated or absent.

Proses-lokale JNI-telemetrie via voorafgelaaide .so (SoTap)

Wanneer volwaardige instrumentasie oorbodig of geblokkeer is, kan jy steeds native-vlak sigbaarheid verkry deur ’n klein logger vooraf in die teikenproses te laai. SoTap is ’n liggewig Android native (.so) biblioteek wat die runtime-gedrag van ander JNI (.so) biblioteke binne dieselfde app-proses log (geen root benodig nie).

Belangrike eienskappe:

  • Initialiseer vroeg en monitor JNI/native-interaksies binne die proses wat dit laai.
  • Bewaar logs deur verskeie skryfbare paaie te gebruik met elegante terugval na Logcat wanneer stoorplek beperk is.
  • Bron-aanpasbaar: wysig sotap.c om te brei/aan te pas wat gelog word en herbou per ABI.

Opstelling (repak die APK):

  1. Plaas die toepaslike ABI-build in die APK sodat die loader libsotap.so kan oplos:
  • lib/arm64-v8a/libsotap.so (for arm64)
  • lib/armeabi-v7a/libsotap.so (for arm32)
  1. Verseker SoTap laai voor ander JNI-libs. Injiseer ’n oproep vroeg (bv. Application-subklas static initializer of onCreate) sodat die logger eers geïnitialiseer word. Smali-stuk voorbeeld:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
  1. Herbou/onderteken/installeer, voer die app uit, en versamel dan logs.

Logpaaie (gekontroleer in volgorde):

/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

Aantekeninge en probleemoplossing:

  • ABI alignment is mandatory. ’n Mismatch sal UnsatisfiedLinkError veroorsaak en die logger sal nie laai nie.
  • Bergingbeperkings is algemeen op moderne Android; as lêerskryfwerk misluk, sal SoTap steeds via Logcat uitstuur.
  • Behavior/verbosity is intended to be customized; herbou vanaf bron nadat jy sotap.c gewysig het.

Hierdie benadering is nuttig vir malware triage en JNI debugging waar dit kritiek is om native oproepvloei vanaf prosesbegin waar te neem, maar root/sisteem‑wye hooks nie beskikbaar is nie.


Sien ook: in‑memory native code execution via JNI

’n Algemene aanvalspatroon is om ’n rou shellcode-blob tydens runtime af te laai en dit direk vanuit geheue deur ’n JNI-bridge uit te voer (geen on‑disk ELF nie). Besonderhede en gereed‑tot‑gebruik JNI-snippet hier:

In Memory Jni Shellcode Execution


Onlangse kwesbaarhede wat die moeite werd is om in APKs te soek

YearCVEAffected libraryNotes
2023CVE-2023-4863libwebp ≤ 1.3.1Heap buffer overflow bereikbaar vanaf native code wat WebP‑beelde dekodeer. Verskeie Android‑apps bundel kwesbare weergawes. Wanneer jy ’n libwebp.so binne ’n APK sien, kontroleer die weergawe en probeer exploitation of patching.
2024MultipleOpenSSL 3.x seriesVerskeie memory-safety en padding-oracle kwessies. Baie Flutter & ReactNative-bundels lewer hul eie libcrypto.so.

Wanneer jy third-party .so lêers binne ’n APK raaksien, kruisverwys altyd hul hash teen upstream advisories. SCA (Software Composition Analysis) is ongewoon op mobiele platforms, so verouderde kwesbare builds is wydverspreid.


Anti-Reversing & Hardening neigings (Android 13-15)

  • Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 skakel PAC/BTI in system libraries in op ondersteunde ARMv8.3+ silikon. Decompilers vertoon nou PAC‑verwante pseudo-instruksies; vir dinamiese analise inject Frida trampolines after stripping PAC, maar jou eie trampolines moet waar nodig pacda/autibsp aanroep.
  • MTE & Scudo hardened allocator: memory-tagging is opt‑in maar baie Play-Integrity-bewuste apps bou met -fsanitize=memtag; gebruik setprop arm64.memtag.dump 1 plus adb shell am start ... om tag faults vas te vang.
  • LLVM Obfuscator (opaque predicates, control-flow flattening): kommersiële packers (bv. Bangcle, SecNeo) beskerm toenemend native code, nie net Java nie; verwag vals control-flow en enkripteerde string‑blobs in .rodata.

Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (ARM64 ELF)

Baie sterk beskermde apps plaas dikwels root/emulator/debug kontrole in native constructors wat uiters vroeg via .init_array loop, voor JNI_OnLoad en lank voor enige Java‑kode uitgevoer word. Jy kan daardie implisiete initializers eksplisiet maak en beheer herwin deur:

  • Verwyder INIT_ARRAY/INIT_ARRAYSZ uit die DYNAMIC tabel sodat die loader nie automatisch .init_array inskrywings uitvoer nie.
  • Los die constructor adres op vanaf RELATIVE relocations en voer dit uit as ’n gewone funksie‑simbool (bv. INIT0).
  • Hernoem JNI_OnLoad na JNI_OnLoad0 om te verhoed dat ART dit implisiet aanroep.

Hoekom dit op Android/arm64 werk

  • Op AArch64 word .init_array inskrywings dikwels tydens laai gevul deur R_AARCH64_RELATIVE relocations waarvan die addend die teiken funksie-adres binne .text is.
  • Die bytes van .init_array kan staties leeg lyk; die dynamic linker skryf die opgeloste adres tydens relocasie verwerking.

Identifiseer die constructor teiken

  • Gebruik die Android NDK toolchain vir akkurate ELF parsing op 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
  • Vind die relocasie wat binne die .init_array virtuele adresreeks land; die addend van daardie R_AARCH64_RELATIVE is die constructor (bv. 0xA34, 0x954).
  • Disassemble rondom daardie adres om te kontroleer:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40

Patch-plan

  1. Verwyder INIT_ARRAY en INIT_ARRAYSZ DYNAMIC tags. Moet nie sections uitvee nie.
  2. Voeg ’n GLOBAL DEFAULT FUNC simbool INIT0 by op die constructor adres sodat dit manueel aangeroep kan word.
  3. Hernoem JNI_OnLoadJNI_OnLoad0 om ART te keer om dit implisiet aan te roep.

Validatie na die patch

readelf -W -d libnativestaticinit.so.patched | egrep -i 'init_array|fini_array|flags'
readelf -W -s libnativestaticinit.so.patched | egrep 'INIT0|JNI_OnLoad0'

Aanpassings met LIEF (Python)

Skrip: verwyder INIT_ARRAY/INIT_ARRAYSZ, eksporteer INIT0, hernoem 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>

Aantekeninge en mislukte benaderings (vir draagbaarheid)
- Die nulmaak van `.init_array`-bytes of om die section-lengte op 0 te stel help nie: die dynamic linker vul dit weer aan via relocations.
- Om `INIT_ARRAY`/`INIT_ARRAYSZ` op 0 te stel kan die loader breek weens onsamehangende tags. Skoon verwydering van daardie DYNAMIC entries is die betroubare metode.
- Die volledige verwydering van die `.init_array` section neig om die loader te laat crash.
- Na patching kan funksie-/layout-adresse skuif; herbereken altyd die constructor van die `.rela.dyn` addends op die gepatchte lêer as jy die patch weer moet uitvoer.

Opstel van 'n minimale ART/JNI om INIT0 en JNI_OnLoad0 aan te roep
- Gebruik JNIInvocation om 'n klein ART VM-konteks in 'n afsonderlike binêre op te laai. Roep dan `INIT0()` en `JNI_OnLoad0(vm)` handmatig aan voor enige Java-kode.
- Sluit die teiken APK/klasse op die classpath in sodat enige `RegisterNatives` sy Java-klasse kan vind.

<details>
<summary>Minimale harness (CMake and C) om INIT0 → JNI_OnLoad0 → Java metode aan te roep</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

Algemene valkuils:

  • Konstruktorsadresse verander na patching weens heruitleg; herbereken altyd vanaf .rela.dyn op die finale binêre.
  • Maak seker dat -Djava.class.path elke klas dek wat deur RegisterNatives-oproepe gebruik word.
  • Gedrag kan wissel met NDK/loader-weergawes; die konsekwent betroubare stap was die verwydering van INIT_ARRAY/INIT_ARRAYSZ DYNAMIC tags.

Verwysings

Tip

Leer en oefen AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Leer en oefen GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Leer en oefen Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Ondersteun HackTricks