Reversing Native Libraries

Tip

Jifunze na fanya mazoezi ya AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Jifunze na fanya mazoezi ya GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Jifunze na fanya mazoezi ya Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

For further information check: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html

Android apps can use native libraries, typically written in C or C++, for performance-critical tasks. Malware creators also abuse these libraries because ELF shared objects are still harder to decompile than DEX/OAT byte-code. Ukurasa huu unazingatia praktical workflows na recent tooling improvements (2023-2025) ambazo zinafanya reversing ya Android .so files kuwa rahisi.


Quick triage-workflow for a freshly pulled libfoo.so

  1. Extract the library
# 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. Identify architecture & protections
file libfoo.so        # arm64 or arm32 / x86
readelf -h libfoo.so  # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so  # (peda/pwntools)
  1. List exported symbols & JNI bindings
readelf -s libfoo.so | grep ' Java_'     # dynamic-linked JNI
strings libfoo.so   | grep -i "RegisterNatives" -n   # static-registered JNI
  1. Load in a decompiler (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) and run auto-analysis. Newer Ghidra versions introduced an AArch64 decompiler that recognises PAC/BTI stubs and MTE tags, greatly improving analysis of libraries built with the Android 14 NDK.
  2. Decide on static vs dynamic reversing: stripped, obfuscated code often needs instrumentation (Frida, ptrace/gdbserver, LLDB).

Dynamic Instrumentation (Frida ≥ 16)

Mfululizo wa 16 wa Frida ulete maboresho kadhaa maalum kwa Android ambayo husaidia wakati lengo linapotumia optimisations za kisasa za Clang/LLD:

  • thumb-relocator sasa inaweza hook tiny ARM/Thumb functions zinazotengenezwa na LLD’s aggressive alignment (--icf=all).
  • Enumerating and rebinding ELF import slots inafanya kazi kwenye Android, ikiruhusu per-module dlopen()/dlsym() patching wakati inline hooks zinakataa.
  • Java hooking iliorekebishwa kwa ajili ya ART quick-entrypoint mpya inayotumika wakati apps zinapokomilishwa na --enable-optimizations kwenye 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 itafanya kazi bila setup kwenye vifaa vyenye PAC/BTI (Pixel 8/Android 14+) mradi utumie frida-server 16.2 au baadaye – matoleo ya awali yalishindwa kupata padding kwa inline hooks.

Dumping maktaba za native zilizofumbuliwa wakati wa runtime kutoka kwenye memory (Frida soSaver)

Wakati APK iliyo na ulinzi inahifadhi native code iliyofichwa au kuipanga tu wakati wa runtime (packers, downloaded payloads, generated libs), ungana na Frida na dump ELF iliyopangwa moja kwa moja kutoka kwenye process memory.

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

  • Hooks dlopen na android_dlopen_ext ili kugundua load-time library mapping na hufanya skana ya awali ya modules zilizopakuliwa tayari.
  • Hufanya skana mara kwa mara ya process memory mappings kutafuta ELF headers ili kunasa modules zilizopakiwa kupitia non-standard mappers ambazo hazikuwahi kupitia loader APIs.
  • Inasoma kila module kwa bloksi kutoka memory na kutuma bytes kupitia Frida messages kwa host; ikiwa eneo halitoshwi kusomwa, inarudi kusoma kutoka kwenye on-disk path inapopatikana.
  • Inahifadhi faili za .so zilizojengwa upya na kuchapisha takwimu za uchimbaji kwa kila module, ikitoa artifacts kwa static RE.

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

Mbinu hii inavoidisha “only decrypted in RAM” protections kwa kurejesha live mapped image, ikiruhusu uchambuzi wa offline katika IDA/Ghidra hata kama nakala ya filesystem imefichwa au haipo.

Process-local JNI telemetry via preloaded .so (SoTap)

Wakati instrumentation yenye vipengele kamili inazidi mahitaji au imezuiwa, bado unaweza kupata uoni wa kiwango cha native kwa kupreload logger ndogo ndani ya mchakato lengwa. SoTap ni maktaba nyepesi ya Android native (.so) inayorekodi runtime behavior ya maktaba nyingine za JNI (.so) ndani ya mchakato sawa la app (hakuna root inahitajika).

Key properties:

  • Inaanzishwa mapema na inaangalia JNI/native interactions ndani ya mchakato unaoiinua.
  • Inahifadhi logs kwa kutumia multiple writable paths na fallback nzuri kwenda Logcat wakati storage imezuiwa.
  • Source-customizable: hariri sotap.c ili kuongeza/kubadilisha kile kinachorekodiwa na ujenge tena per ABI.

Setup (repack the APK):

  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

Notes and troubleshooting:

  • Ulinganifu wa ABI ni lazima. Kutokulingana kutasababisha UnsatisfiedLinkError na logger haitapakia.
  • Vikwazo vya uhifadhi ni vya kawaida kwenye Android ya kisasa; ikiwa uandishi wa faili utakosa, SoTap bado itatoa kupitia Logcat.
  • Tabia na kiwango cha taarifa vinakusudiwa kubadilishwa; jenga tena kutoka kwa chanzo baada ya kuhariri sotap.c.

Njia hii ni muhimu kwa malware triage na JNI debugging ambapo kuangalia mtiririko wa miito ya native tangu kuanza kwa mchakato ni muhimu lakini root/system-wide hooks hazipatikani.


Angalia pia: in‑memory native code execution via JNI

Mfano wa kawaida wa shambulio ni kupakua raw shellcode blob wakati wa runtime na kuiendesha moja kwa moja kutoka memory kupitia daraja la JNI (hakuna on‑disk ELF). Maelezo na snippet ya JNI tayari kwa matumizi hapa:

In Memory Jni Shellcode Execution


Udhaifu wa hivi karibuni zinazostahili kutafutwa ndani ya APKs

MwakaCVEMaktaba iliyoathirikaMaelezo
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.

Ukiona third-party .so files inside a APK, angalia hash yao dhidi ya upstream advisories. SCA (Software Composition Analysis) ni nadra kwenye mobile, hivyo builds zilizo kale na zilizo hatarishi ni nyingi.


  • Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 inaruhusu PAC/BTI katika system libraries kwenye silicon zinazounga mkono ARMv8.3+. Decompilers sasa zinaonyesha PAC‐related pseudo-instructions; kwa dynamic analysis Frida inaingiza trampolines baada ya kuondoa PAC, lakini trampolines zako za custom zinapaswa kuita pacda/autibsp inapohitajika.
  • MTE & Scudo hardened allocator: memory-tagging ni opt-in lakini apps nyingi zenye Play-Integrity zinajenga kwa -fsanitize=memtag; tumia setprop arm64.memtag.dump 1 pamoja na adb shell am start ... ili kunasa tag faults.
  • LLVM Obfuscator (opaque predicates, control-flow flattening): packers za kibiashara (mfano, Bangcle, SecNeo) zinazuia zaidi native code, sio Java pekee; tarajia bogus control-flow na encrypted string blobs katika .rodata.

Kukomesha early native initializers (.init_array) na JNI_OnLoad kwa instrumentation ya awali (ARM64 ELF)

Apps zilizo na ulinzi mkubwa mara nyingi huweka root/emulator/debug checks katika native constructors zinazotekelezwa mapema sana kupitia .init_array, kabla ya JNI_OnLoad na muda mrefu kabla ya code yoyote ya Java kuanza. Unaweza kufanya initializers hizo za kimya kuwa wazi na kurudisha udhibiti kwa:

  • Kuondoa INIT_ARRAY/INIT_ARRAYSZ kutoka kwenye DYNAMIC table ili loader isitekeleze .init_array entries moja kwa moja.
  • Kutatua anwani ya constructor kutoka RELATIVE relocations na kuipeleka nje kama alama ya function ya kawaida (mfano, INIT0).
  • Kubadilisha jina JNI_OnLoad kuwa JNI_OnLoad0 ili kumzuia ART kuiita kwa kimya.

Kwa nini hii inafanya kazi kwenye Android/arm64

  • Katika AArch64, vipengele vya .init_array mara nyingi hujazwa wakati wa load kwa relocations za R_AARCH64_RELATIVE ambazo addend yake ni anwani ya function lengwa ndani ya .text.
  • Bytes za .init_array zinaweza kuonekana tupu kwa static; dynamic linker huandika anwani iliyotatuliwa wakati wa usindikaji wa relocation.

Tambua lengo la constructor

  • Tumia Android NDK toolchain kwa parsing sahihi ya ELF kwenye 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
  • Tafuta relocation inayoweka ndani ya virtual address range ya .init_array; addend ya R_AARCH64_RELATIVE hiyo ndiyo constructor (mfano, 0xA34, 0x954).
  • Fanya disassembly karibu na anwani hiyo ili kukagua kwa busara:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40

Mpango wa patch

  1. Ondoa tags za DYNAMIC INIT_ARRAY na INIT_ARRAYSZ. Usifute sections.
  2. Ongeza alama ya GLOBAL DEFAULT FUNC INIT0 kwenye anwani ya constructor ili iweze kuitwa kwa mkono.
  3. Badilisha jina JNI_OnLoadJNI_OnLoad0 ili kumzuia ART kuiita kwa kimya.

Uthibitisho baada ya patch

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

Kurekebisha na LIEF (Python)

Skripti: ondoa INIT_ARRAY/INIT_ARRAYSZ, export INIT0, badilisha jina 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>

Maelezo na mbinu zilizoshindikana (kwa uhamaji)
- Kuweka byte za `.init_array` kuwa sifuri au kuweka urefu wa section kuwa 0 hakusaidi: the dynamic linker huzijaza tena kupitia relocations.
- Kuweka `INIT_ARRAY`/`INIT_ARRAYSZ` kuwa 0 kunaweza kuvunja loader kutokana na tags zisizolingana. Kuondoa kabisa yale entries za `DYNAMIC` ndilo njia ya kuaminika.
- Kuondoa kabisa sekta ya `.init_array` hupelekea loader kuanguka.
- Baada ya kupachika patch, anwani za function/layout zinaweza kubadilika; kila mara hesabu upya constructor kutoka kwa addends za `.rela.dyn` kwenye faili iliyopatchiwa ikiwa unahitaji kurudisha patch.

Kuanzisha ART/JNI ndogo ili kuita INIT0 na JNI_OnLoad0
- Tumia JNIInvocation kuanzisha muktadha mdogo wa ART VM katika binary huru. Kisha ita `INIT0()` na `JNI_OnLoad0(vm)` kwa mkono kabla ya Java code yoyote.
- Jumuisha APK/classes lengwa kwenye classpath ili `RegisterNatives` yoyote ipate Java classes zake.

<details>
<summary>Mfumo mdogo (CMake na C) wa kuita INIT0 → JNI_OnLoad0 → Java method</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

Makosa ya Kawaida:

  • Anwani za constructor hubadilika baada ya kuchomwa kwa sababu ya re-layout; hakikisha unahesabu tena kutoka .rela.dyn kwenye binary ya mwisho.
  • Hakikisha -Djava.class.path inajumuisha kila class inayotumiwa na wito za RegisterNatives.
  • Tabia inaweza kutofautiana kulingana na toleo la NDK/loader; hatua iliyokuwa imethibitishwa mara kwa mara ilikuwa kuondoa DYNAMIC tags INIT_ARRAY/INIT_ARRAYSZ.

Marejeo

Tip

Jifunze na fanya mazoezi ya AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Jifunze na fanya mazoezi ya GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Jifunze na fanya mazoezi ya Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks