daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

reversing-native-libraries.md (17745B)


      1 ---
      2 title: "Reversing Native Libraries"
      3 section: "Mobile"
      4 sectionSlug: "mobile-pentesting"
      5 sourcePath: "src/mobile-pentesting/android-app-pentesting/reversing-native-libraries.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/mobile-pentesting/android-app-pentesting/reversing-native-libraries.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Reversing Native Libraries
     14 
     15 **For further information check:** [**https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html**](https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html)<sup>[[17]](#references)</sup>
     16 
     17 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.<sup>[[15]](#references)</sup>
     18 This page focuses on *practical* workflows and *recent* tooling improvements (2023-2025) that make reversing Android `.so` files easier.
     19 
     20 ---
     21 
     22 ### Quick triage-workflow for a freshly pulled `libfoo.so`
     23 
     24 1. **Extract the library**
     25    ```bash
     26    # From an installed application
     27    adb shell "run-as <pkg> cat lib/arm64-v8a/libfoo.so" > libfoo.so
     28    # Or from the APK (zip)
     29    unzip -j target.apk "lib/*/libfoo.so" -d extracted_libs/
     30    ```
     31 2. **Identify architecture & protections**
     32    ```bash
     33    file libfoo.so        # arm64 or arm32 / x86
     34    readelf -h libfoo.so  # OS ABI, PIE, NX, RELRO, etc.
     35    checksec --file libfoo.so  # (peda/pwntools)
     36    ```
     37    If the disassembly is unfamiliar, review the ARM/AArch64 instruction and calling-convention fundamentals before interpreting control flow.<sup>[[12]](#references)</sup>
     38 3. **List exported symbols & JNI bindings**
     39    ```bash
     40    readelf -s libfoo.so | grep ' Java_'     # dynamic-linked JNI
     41    strings libfoo.so   | grep -i "RegisterNatives" -n   # static-registered JNI
     42    ```
     43    Use the JNI specification and Android's JNI guidance to map exported or registered native methods back to their Java declarations accurately.<sup>[[13]](#references)[[14]](#references)</sup>
     44 4. **Load in a decompiler** (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) and run auto-analysis.  
     45    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.
     46 5. **Decide on static vs dynamic reversing:** stripped, obfuscated code often needs *instrumentation* (Frida, ptrace/gdbserver, LLDB).<sup>[[16]](#references)</sup>
     47 
     48 ---
     49 
     50 ### Dynamic Instrumentation (Frida ≥ 16)
     51 
     52 Frida’s 16-series brought several Android-specific improvements that help when the target uses modern Clang/LLD optimisations:<sup>[[1]](#references)</sup>
     53 
     54 * `thumb-relocator` can now *hook tiny ARM/Thumb functions* generated by LLD’s aggressive alignment (`--icf=all`).  
     55 * Enumerating and rebinding *ELF import slots* works on Android, enabling per-module `dlopen()`/`dlsym()` patching when inline hooks are rejected.  
     56 * Java hooking was fixed for the new **ART quick-entrypoint** used when apps are compiled with `--enable-optimizations` on Android 14.
     57 
     58 Example: enumerating all functions registered through `RegisterNatives` and dumping their addresses at runtime:
     59 ```javascript
     60 Java.perform(function () {
     61   var Runtime = Java.use('java.lang.Runtime');
     62   var register = Module.findExportByName(null, 'RegisterNatives');
     63   Interceptor.attach(register, {
     64     onEnter(args) {
     65       var envPtr  = args[0];
     66       var clazz   = Java.cast(args[1], Java.use('java.lang.Class'));
     67       var methods = args[2];
     68       var count   = args[3].toInt32();
     69       console.log('[+] RegisterNatives on ' + clazz.getName() + ' -> ' + count + ' methods');
     70       // iterate & dump (JNI nativeMethod struct: name, sig, fnPtr)
     71     }
     72   });
     73 });
     74 ```
     75 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.  
     76 
     77 ### Dumping runtime-decrypted native libraries from memory (Frida soSaver)
     78 
     79 When a protected APK keeps native code encrypted or only maps it at runtime (packers, downloaded payloads, generated libs), attach Frida and dump the mapped ELF directly from process memory.<sup>[[10]](#references)[[11]](#references)</sup>
     80 
     81 **soSaver workflow (Python host + TS/JS Frida agent):**
     82 - Hooks `dlopen` and `android_dlopen_ext` to detect load-time library mapping and performs an initial sweep of already loaded modules.
     83 - Periodically scans the process memory mappings for ELF headers to catch modules loaded through non-standard mappers that never hit the loader APIs.
     84 - Reads each module in blocks from memory and streams the bytes through Frida messages to the host; if a region cannot be read, it falls back to reading from the on-disk path when available.
     85 - Saves the reconstructed `.so` files and prints per-module extraction stats, providing artifacts for static RE.
     86 
     87 **Run (root + frida-server, Python ≥3.8, uv):**
     88 ```bash
     89 git clone https://github.com/TheQmaks/sosaver.git
     90 cd sosaver && uv sync
     91 source .venv/bin/activate    # .venv\Scripts\activate on Windows
     92 
     93 # target by package or PID; choose output/verbosity
     94 sosaver com.example.app
     95 sosaver 1234 -o /tmp/so-dumps --debug
     96 ```
     97 
     98 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.
     99 
    100 ### Process-local JNI telemetry via preloaded .so (SoTap)
    101 
    102 When full-featured instrumentation is overkill or blocked, you can still gain native-level visibility by preloading a small logger inside the target process. SoTap is a lightweight Android native (.so) library that logs the runtime behavior of other JNI (.so) libraries within the same app process (no root required).<sup>[[3]](#references)[[4]](#references)[[5]](#references)</sup>
    103 
    104 Key properties:
    105 - Initializes early and observes JNI/native interactions inside the process that loads it.
    106 - Persists logs using multiple writable paths with graceful fallback to Logcat when storage is restricted.
    107 - Source-customizable: edit sotap.c to extend/adjust what gets logged and rebuild per ABI.
    108 
    109 Setup (repack the APK):
    110 1) Drop the proper ABI build into the APK so the loader can resolve libsotap.so:
    111    - lib/arm64-v8a/libsotap.so (for arm64)
    112    - lib/armeabi-v7a/libsotap.so (for arm32)
    113 2) 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:
    114    ```smali
    115    const-string v0, "sotap"
    116    invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
    117    ```
    118 3) Rebuild/sign/install, run the app, then collect logs.
    119 
    120 Log paths (checked in order):
    121 ```text
    122 /data/user/0/%s/files/sotap.log
    123 /data/data/%s/files/sotap.log
    124 /sdcard/Android/data/%s/files/sotap.log
    125 /sdcard/Download/sotap-%s.log
    126 # If all fail: fallback to Logcat only
    127 ```
    128 
    129 Notes and troubleshooting:
    130 - ABI alignment is mandatory. A mismatch will raise UnsatisfiedLinkError and the logger won’t load.
    131 - Storage constraints are common on modern Android; if file writes fail, SoTap will still emit via Logcat.
    132 - Behavior/verbosity is intended to be customized; rebuild from source after editing sotap.c.
    133 
    134 This approach is useful for malware triage and JNI debugging where observing native call flows from process start is critical but root/system-wide hooks aren’t available.
    135 
    136 ---
    137 
    138 ### See also: in‑memory native code execution via JNI
    139 
    140 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).<sup>[[6]](#references)</sup> Details and ready‑to‑use JNI snippet here:
    141 
    142 [In Memory Jni Shellcode Execution](/hacktricks/mobile-pentesting/android-app-pentesting/in-memory-jni-shellcode-execution)
    143 
    144 ---
    145 
    146 ### Recent vulnerabilities worth hunting for in APKs
    147 
    148 | Year | CVE | Affected library | Notes |
    149 |------|-----|------------------|-------|
    150 |2023|CVE-2023-4863|`libwebp` ≤ 1.3.1|Heap 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.|<sup>[[2]](#references)</sup>|
    151 |2024|Multiple|OpenSSL 3.x series|Several memory-safety and padding-oracle issues. Many Flutter & ReactNative bundles ship their own `libcrypto.so`.|
    152 
    153 When you spot *third-party* `.so` files inside an APK, always cross-check their hash against upstream advisories. SCA (Software Composition Analysis) is uncommon on mobile, so outdated vulnerable builds are rampant.
    154 
    155 ---
    156 
    157 ### Anti-Reversing & Hardening trends (Android 13-15)
    158 
    159 * **Pointer Authentication (PAC) & Branch Target Identification (BTI):** Android 14 enables PAC/BTI in system libraries on supported ARMv8.3+ silicon. Decompilers now display PAC‐related pseudo-instructions; for dynamic analysis Frida injects trampolines *after* stripping PAC, but your custom trampolines should call `pacda`/`autibsp` where necessary.
    160 * **MTE & Scudo hardened allocator:** memory-tagging is opt-in but many Play-Integrity aware apps build with `-fsanitize=memtag`; use `setprop arm64.memtag.dump 1` plus `adb shell am start ...` to capture tag faults.
    161 * **LLVM Obfuscator (opaque predicates, control-flow flattening):** commercial packers (e.g., Bangcle, SecNeo) increasingly protect *native* code, not only Java; expect bogus control-flow and encrypted string blobs in `.rodata`.
    162 
    163 ---
    164 
    165 ### Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (ARM64 ELF)
    166 
    167 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:<sup>[[7]](#references)[[8]](#references)[[9]](#references)</sup>
    168 - Removing `INIT_ARRAY`/`INIT_ARRAYSZ` from the DYNAMIC table so the loader does not auto-execute `.init_array` entries.
    169 - Resolving the constructor address from RELATIVE relocations and exporting it as a regular function symbol (e.g., `INIT0`).
    170 - Renaming `JNI_OnLoad` to `JNI_OnLoad0` to prevent ART from calling it implicitly.
    171 
    172 Why this works on Android/arm64
    173 - 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`.
    174 - The bytes of `.init_array` may look empty statically; the dynamic linker writes the resolved address during relocation processing.
    175 
    176 Identify the constructor target
    177 - Use the Android NDK toolchain for accurate ELF parsing on AArch64:
    178   ```bash
    179   # Adjust paths to your NDK; use the aarch64-linux-android-* variants
    180   readelf -W -a ./libnativestaticinit.so | grep -n "INIT_ARRAY" -C 4
    181   readelf -W --relocs ./libnativestaticinit.so
    182   ```
    183 - 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`).
    184 - Disassemble around that address to sanity check:
    185   ```bash
    186   objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
    187   ```
    188 
    189 Patch plan
    190 1) Remove `INIT_ARRAY` and `INIT_ARRAYSZ` DYNAMIC tags. Do not delete sections.
    191 2) Add a GLOBAL DEFAULT FUNC symbol `INIT0` at the constructor address so it can be called manually.
    192 3) Rename `JNI_OnLoad` → `JNI_OnLoad0` to stop ART from invoking it implicitly.
    193 
    194 Validation after patch
    195 ```bash
    196 readelf -W -d libnativestaticinit.so.patched | egrep -i 'init_array|fini_array|flags'
    197 readelf -W -s libnativestaticinit.so.patched | egrep 'INIT0|JNI_OnLoad0'
    198 ```
    199 
    200 Patching with LIEF (Python)
    201 
    202 <details>
    203 <summary>Script: remove INIT_ARRAY/INIT_ARRAYSZ, export INIT0, rename JNI_OnLoad→JNI_OnLoad0</summary>
    204 
    205 ```python
    206 import lief
    207 
    208 b = lief.parse("libnativestaticinit.so")
    209 
    210 # Locate .init_array VA range
    211 init = b.get_section('.init_array')
    212 va, sz = init.virtual_address, init.size
    213 
    214 # Compute constructor address from RELATIVE relocation landing in .init_array
    215 ctor = None
    216 for r in b.dynamic_relocations:
    217     if va <= r.address < va + sz:
    218         ctor = r.addend
    219         break
    220 if ctor is None:
    221     raise RuntimeError("No R_*_RELATIVE relocation found inside .init_array")
    222 
    223 # Remove auto-run tags so loader skips .init_array
    224 for tag in (lief.ELF.DYNAMIC_TAGS.INIT_ARRAYSZ, lief.ELF.DYNAMIC_TAGS.INIT_ARRAY):
    225     try:
    226         b.remove(b[tag])
    227     except Exception:
    228         pass
    229 
    230 # Add exported FUNC symbol INIT0 at constructor address
    231 sym = lief.ELF.Symbol()
    232 sym.name = 'INIT0'
    233 sym.value = ctor
    234 sym.size = 0
    235 sym.binding = lief.ELF.SYMBOL_BINDINGS.GLOBAL
    236 sym.type = lief.ELF.SYMBOL_TYPES.FUNC
    237 sym.visibility = lief.ELF.SYMBOL_VISIBILITY.DEFAULT
    238 
    239 # Place symbol in .text index
    240 text = b.get_section('.text')
    241 for idx, sec in enumerate(b.sections):
    242     if sec == text:
    243         sym.shndx = idx
    244         break
    245 b.add_dynamic_symbol(sym)
    246 
    247 # Rename JNI_OnLoad -> JNI_OnLoad0 to block implicit ART init
    248 j = b.get_symbol('JNI_OnLoad')
    249 if j:
    250     j.name = 'JNI_OnLoad0'
    251 
    252 b.write('libnativestaticinit.so.patched')
    253 ```
    254 </details>
    255 
    256 Notes and failed approaches (for portability)
    257 - Zeroing `.init_array` bytes or setting the section length to 0 does not help: the dynamic linker repopulates it via relocations.
    258 - 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.
    259 - Deleting the `.init_array` section entirely tends to crash the loader.
    260 - 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.
    261 
    262 Bootstrapping a minimal ART/JNI to invoke INIT0 and JNI_OnLoad0
    263 - 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.
    264 - Include the target APK/classes on the classpath so any `RegisterNatives` finds its Java classes.
    265 
    266 <details>
    267 <summary>Minimal harness (CMake and C) to call INIT0 → JNI_OnLoad0 → Java method</summary>
    268 
    269 ```text
    270 # CMakeLists.txt
    271 project(caller)
    272 cmake_minimum_required(VERSION 3.8)
    273 include_directories(AFTER ${CMAKE_SOURCE_DIR}/include)
    274 link_directories(${CMAKE_SOURCE_DIR}/lib)
    275 find_library(log-lib log REQUIRED)
    276 add_executable(caller "caller.c")
    277 add_library(jenv SHARED "jnihelper.c")
    278 target_link_libraries(caller jenv nativestaticinit)
    279 ```
    280 
    281 ```c
    282 // caller.c
    283 #include <jni.h>
    284 #include "jenv.h"
    285 JavaCTX ctx;
    286 void INIT0();
    287 void JNI_OnLoad0(JavaVM* vm);
    288 int main(){
    289   char *jvmopt = "-Djava.class.path=/data/local/tmp/base.apk"; // include app classes
    290   if (initialize_java_environment(&ctx,&jvmopt,1)!=0) return -1;
    291   INIT0();                   // manual constructor
    292   JNI_OnLoad0(ctx.vm);       // manual JNI init
    293   jclass c = (*ctx.env)->FindClass(ctx.env, "eu/nviso/nativestaticinit/MainActivity");
    294   jmethodID m = (*ctx.env)->GetStaticMethodID(ctx.env,c,"stringFromJNI","()Ljava/lang/String;");
    295   jstring s = (jstring)(*ctx.env)->CallStaticObjectMethod(ctx.env,c,m);
    296   const char* p = (*ctx.env)->GetStringUTFChars(ctx.env,s,NULL);
    297   printf("Native string: %s\n", p);
    298   cleanup_java_env(&ctx);
    299 }
    300 ```
    301 
    302 ```bash
    303 # Build (adjust NDK/ABI)
    304 cmake -DANDROID_PLATFORM=31 \
    305   -DCMAKE_TOOLCHAIN_FILE=$HOME/Android/Sdk/ndk/26.1.10909125/build/cmake/android.toolchain.cmake \
    306   -DANDROID_ABI=arm64-v8a ..
    307 make
    308 ```
    309 </details>
    310 
    311 
    312 **Common Pitfalls:**
    313 - Constructor addresses change after patching due to re-layout; always recompute from `.rela.dyn` on the final binary.
    314 - Ensure `-Djava.class.path` covers every class used by `RegisterNatives` calls.
    315 - Behavior may vary with NDK/loader versions; the consistently reliable step was removing `INIT_ARRAY`/`INIT_ARRAYSZ` DYNAMIC tags.
    316 
    317 
    318 ## References
    319 
    320 - [1] [Frida 16.x changelog (Android hooking, tiny-function relocation)](https://frida.re/news/)
    321 - [2] [NVD - CVE-2023-4863 detail (libwebp heap buffer overflow)](https://nvd.nist.gov/vuln/detail/CVE-2023-4863)
    322 - [3] [SoTap: Lightweight in-app JNI (.so) behavior logger](https://github.com/RezaArbabBot/SoTap)
    323 - [4] [SoTap Releases](https://github.com/RezaArbabBot/SoTap/releases)
    324 - [5] [How to work with SoTap?](https://t.me/ForYouTillEnd/13)
    325 - [6] [CoRPhone — JNI memory-only execution pattern and packaging](https://github.com/0xdevil/corphone)
    326 - [7] [Patching Android ARM64 library initializers for easy Frida instrumentation and debugging](https://blog.nviso.eu/2025/10/14/patching-android-arm64-library-initializers-for-easy-frida-instrumentation-and-debugging/)
    327 - [8] [LIEF Project](https://github.com/lief-project/LIEF)
    328 - [9] [JNIInvocation](https://github.com/Ch0pin/JNIInvocation)
    329 - [10] [soSaver — Frida-based live memory dumper for Android `.so` libraries](https://github.com/TheQmaks/sosaver)
    330 - [11] [soSaver Frida agent (TypeScript/JS)](https://github.com/TheQmaks/soSaver-frida)
    331 - [12] [Azeria Labs – ARM Assembly Basics](https://azeria-labs.com/writing-arm-assembly-part-1/)
    332 - [13] [Oracle JNI Spec](https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html)
    333 - [14] [Android JNI Tips](https://developer.android.com/training/articles/perf-jni)
    334 - [15] [NDK Guides](https://developer.android.com/ndk/guides/)
    335 - [16] [Debug Android Native Libraries Using JEB Decompiler](https://medium.com/@shubhamsonani/how-to-debug-android-native-libraries-using-jeb-decompiler-eec681a22cf3)
    336 - [17] [maddiestone.github.io - Reversing Native Libs](https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html)