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)