Reversing Native Libraries

Tip

AWS हैकिंग सीखें और अभ्यास करें:HackTricks Training AWS Red Team Expert (ARTE)
GCP हैकिंग सीखें और अभ्यास करें: HackTricks Training GCP Red Team Expert (GRTE) Azure हैकिंग सीखें और अभ्यास करें: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks का समर्थन करें

अधिक जानकारी के लिए देखें: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html

Android ऐप्स प्रदर्शन-सम्वेदनशील कार्यों के लिए सामान्यतः C या C++ में लिखी native libraries का उपयोग कर सकते हैं। Malware creators भी इन लाइब्रेरीज़ का दुरुपयोग करते हैं क्योंकि ELF shared objects अभी भी DEX/OAT byte-code की तुलना में decompile करने में अधिक कठिन होते हैं।
यह पेज उन व्यावहारिक workflows और हालिया tooling सुधारों (2023-2025) पर केंद्रित है जो Android .so फाइलों का reversing आसान बनाते हैं।


Quick triage-workflow for a freshly pulled libfoo.so

  1. लाइब्रेरी निकालें
# 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. आर्किटेक्चर और सुरक्षा पहचाने
file libfoo.so        # arm64 or arm32 / x86
readelf -h libfoo.so  # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so  # (peda/pwntools)
  1. एक्सपोर्ट किए गए सिंबल्स और JNI बाइंडिंग्स सूचीबद्ध करें
readelf -s libfoo.so | grep ' Java_'     # dynamic-linked JNI
strings libfoo.so   | grep -i "RegisterNatives" -n   # static-registered JNI
  1. एक decompiler में लोड करें (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) और auto-analysis चलाएँ।
    नए Ghidra वर्ज़नों ने AArch64 decompiler जोड़ा है जो PAC/BTI stubs और MTE tags को पहचानता है, जिससे Android 14 NDK के साथ बिल्ट लाइब्रेरीज़ का विश्लेषण काफी बेहतर हुआ है।
  2. static vs dynamic reversing पर निर्णय लें: stripped, obfuscated code अक्सर instrumentation (Frida, ptrace/gdbserver, LLDB) की मांग करता है।

Dynamic Instrumentation (Frida ≥ 16)

Frida की 16-सीरीज में कई Android-विशिष्ट सुधार आए हैं जो तब मदद करते हैं जब लक्ष्य आधुनिक Clang/LLD optimisations का उपयोग करता है:

  • thumb-relocator अब LLD की aggressive alignment (--icf=all) द्वारा जनरेट किए गए छोटे ARM/Thumb functions को hook कर सकता है।
  • Enumerating and rebinding ELF import slots Android पर काम करता है, जिससे inline hooks अस्वीकार होने पर per-module dlopen()/dlsym() patching संभव होता है।
  • Java hooking को नए ART quick-entrypoint के लिए ठीक किया गया है, जो तब उपयोग होता है जब apps Android 14 पर --enable-optimizations के साथ compile किए जाते हैं।

उदाहरण: रनटाइम पर RegisterNatives के जरिए रजिस्टर किए गए सभी फ़ंक्शन्स को enumerate करके उनके addresses dump करना:

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 PAC/BTI-enabled डिवाइसों (Pixel 8/Android 14+) पर बिना अतिरिक्त कॉन्फ़िगरेशन के काम करेगा, जब तक आप frida-server 16.2 या उसके बाद का उपयोग करते हैं – पुराने वर्शन inline hooks के लिए padding को locate करने में विफल थे।

मेमोरी से रनटाइम-डिक्रिप्टेड नेटिव लाइब्रेरीज़ को डंप करना (Frida soSaver)

जब कोई protected APK नेटिव कोड को encrypted रखता है या उसे केवल runtime पर map करता है (packers, downloaded payloads, generated libs), तो Frida को attach करें और mapped ELF को सीधे process memory से dump करें।

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

  • dlopen और android_dlopen_ext को hook करता है ताकि load-time library mapping का पता चल सके और पहले से लोड किए गए modules का प्रारंभिक sweep करता है।
  • समय-समय पर process memory mappings में ELF headers के लिए स्कैन करता है ताकि non-standard mappers के माध्यम से लोड हुए और loader APIs को कभी नहीं छूने वाले modules पकड़े जा सकें।
  • प्रत्येक module को memory से blocks में पढ़ता है और bytes को Frida messages के माध्यम से host को stream करता है; यदि कोई region पढ़ा नहीं जा सकता, तो उपलब्ध होने पर वह on-disk path से पढ़ने पर fallback करता है।
  • पुनर्निर्मित .so फ़ाइलें सहेजता है और प्रति-module extraction stats प्रिंट करता है, जो static RE के लिए artifacts प्रदान करता है।

चलाएँ (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

यह तरीका “only decrypted in RAM” सुरक्षा को बायपास करता है, लाइव मैप्ड इमेज को रिकवर करके, जिससे ऑफलाइन विश्लेषण IDA/Ghidra में संभव हो जाता है भले ही फाइलसिस्टम कॉपी ऑबफस्केटेड या अनुपस्थित हो।

पूर्व-लोडेड .so (SoTap) के माध्यम से प्रोसेस-लोकल JNI टेलीमेट्री

जब फुल-फीचर्ड instrumentation अत्यधिक हो या ब्लॉक हो, तब भी आप टार्गेट प्रोसेस के अंदर एक छोटा logger प्री-लोड करके native-स्तर की दृश्यता पा सकते हैं। SoTap एक lightweight Android native (.so) लाइब्रेरी है जो उसी ऐप प्रोसेस के भीतर अन्य JNI (.so) लाइब्रेरीज़ के runtime व्यवहार को लॉग करती है (रूट की आवश्यकता नहीं)।

Key properties:

  • पहले इनिशियलाइज़ होती है और उस प्रोसेस के अंदर JNI/native इंटरैक्शंस का अवलोकन करती है जो इसे लोड करता है।
  • लॉग्स को कई writable paths में संरक्षित करती है और स्टोरेज प्रतिबंधित होने पर graceful fallback के रूप में Logcat का उपयोग करती है।
  • सोर्स-कस्टमाइज़ेबल: sotap.c को एडिट करके लॉग क्या होगा, उसे बढ़ाएँ/समायोजित करें और प्रति ABI rebuild करें।

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:

  • ABI alignment अनिवार्य है। मिसमैच से UnsatisfiedLinkError उठेगा और logger लोड नहीं होगा।
  • Storage constraints आधुनिक Android पर सामान्य हैं; अगर file writes फेल हों, तो SoTap फिर भी Logcat के माध्यम से emit करेगा।
  • Behavior/verbosity को अनुकूलित करने के लिए डिज़ाइन किया गया है; sotap.c संपादित करने के बाद source से rebuild करें।

यह तरीका malware triage और JNI debugging के लिए उपयोगी है जहाँ process start से native call flows देखना महत्वपूर्ण होता है पर root/system-wide hooks उपलब्ध नहीं होते।


यह भी देखें: in‑memory native code execution via JNI

A common attack pattern है कि runtime पर एक raw shellcode blob डाउनलोड किया जाए और इसे JNI bridge के माध्यम से सीधे memory से execute किया जाए (कोई on‑disk ELF नहीं)। विवरण और ready‑to‑use JNI snippet यहाँ:

In Memory Jni Shellcode Execution


Recent vulnerabilities worth hunting for in APKs

YearCVEAffected libraryNotes
2023CVE-2023-4863libwebp ≤ 1.3.1Heap buffer overflow जो native code से reachable है जो WebP images को decode करता है। कई Android apps vulnerable versions को bundle करते हैं। जब आप किसी APK के अंदर libwebp.so देखते हैं, तो उसकी version जांचें और exploitation या patching का प्रयास करें.
2024MultipleOpenSSL 3.x seriesकई memory-safety और padding-oracle issues। कई Flutter & ReactNative bundles अपना libcrypto.so ship करते हैं।

जब आप किसी APK के अंदर third-party .so फाइलें देखें, तो हमेशा उनका hash upstream advisories के खिलाफ cross-check करें। SCA (Software Composition Analysis) mobile पर कम पाया जाता है, इसलिए outdated vulnerable builds आम हैं।


  • Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 supported ARMv8.3+ silicon पर system libraries में PAC/BTI सक्षम करता है। Decompilers अब PAC‑related pseudo-instructions दिखाते हैं; dynamic analysis के लिए Frida PAC को strip करने के बाद trampolines inject करता है, लेकिन आपके custom trampolines को जहाँ आवश्यक हो pacda/autibsp कॉल करना चाहिए।
  • MTE & Scudo hardened allocator: memory-tagging opt-in है पर कई Play-Integrity aware apps -fsanitize=memtag के साथ build होते हैं; tag faults कैप्चर करने के लिए setprop arm64.memtag.dump 1 और फिर adb shell am start ... का उपयोग करें।
  • LLVM Obfuscator (opaque predicates, control-flow flattening): commercial packers (e.g., Bangcle, SecNeo) तेजी से native code को भी protect कर रहे हैं, सिर्फ Java नहीं; .rodata में bogus control-flow और encrypted string blobs की उम्मीद रखें।

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

Highly protected apps अक्सर native constructors में root/emulator/debug checks रखते हैं जो .init_array के माध्यम से बहुत जल्दी चलते हैं, JNI_OnLoad से पहले और किसी भी Java कोड के काफी पहले। आप उन implicit initializers को explicit बना कर नियंत्रण वापस पा सकते हैं:

  • DYNAMIC तालिका से INIT_ARRAY/INIT_ARRAYSZ हटाना ताकि loader .init_array entries को auto-execute न करे।
  • RELATIVE relocations से constructor address resolve करना और उसे एक regular function symbol (उदा., INIT0) के रूप में export करना।
  • JNI_OnLoad का नाम JNI_OnLoad0 में बदलना ताकि ART इसे implicitly कॉल न करे।

Why this works on Android/arm64

  • AArch64 पर, .init_array entries अक्सर load time पर R_AARCH64_RELATIVE relocations द्वारा populate होते हैं जिनका addend target function address होता है जो .text के अंदर होता है।
  • स्टैटिकली .init_array के bytes खाली दिख सकते हैं; dynamic linker relocation processing के दौरान resolved address लिख देता है।

कंस्ट्रक्टर लक्ष्य की पहचान

  • AArch64 पर सटीक ELF parsing के लिए Android NDK toolchain का उपयोग करें:
# 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
  • उस relocation को खोजें जो .init_array virtual address range के अंदर land करता है; उस R_AARCH64_RELATIVE का addend ही constructor होता है (उदा., 0xA34, 0x954)।
  • sanity check के लिए उस address के आसपास disassemble करें:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40

Patch plan

  1. INIT_ARRAY और INIT_ARRAYSZ DYNAMIC tags हटाएँ। sections को delete न करें।
  2. constructor address पर एक GLOBAL DEFAULT FUNC symbol INIT0 जोड़ें ताकि इसे मैन्युअली कॉल किया जा सके।
  3. JNI_OnLoad का नाम JNI_OnLoad0 में बदलें ताकि ART इसे implicitly invoke न करे।

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'

Patching with LIEF (Python)

स्क्रिप्ट: INIT_ARRAY/INIT_ARRAYSZ हटाएँ, INIT0 को export करें, 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>

नोट्स और असफल तरीके (पोर्टेबिलिटी के लिए)
- `.init_array` बाइट्स को ज़ीरो करना या सेक्शन की लंबाई को 0 सेट करना मदद नहीं करता: the dynamic linker इसे relocations के माध्यम से फिर से भर देता है।
- `INIT_ARRAY`/`INIT_ARRAYSZ` को 0 सेट करने से inconsistent tags के कारण loader टूट सकता है। उन DYNAMIC एंट्रीज़ को साफ़ तरीके से हटाना ही विश्वसनीय उपाय है।
- `.init_array` सेक्शन को पूरी तरह डिलीट करने से आमतौर पर loader क्रैश हो जाता है।
- पैचिंग के बाद function/layout के पते शिफ्ट हो सकते हैं; अगर आपको पैच को फिर से रन करना है तो patched फ़ाइल पर `.rela.dyn` addends से constructor हमेशा दोबारा recompute करें।

Bootstrapping a minimal ART/JNI to invoke INIT0 and JNI_OnLoad0
- JNIInvocation का उपयोग करके standalone binary में एक छोटा ART VM context spin up करें। फिर किसी भी Java कोड से पहले मैन्युअली `INIT0()` और `JNI_OnLoad0(vm)` कॉल करें।
- target APK/classes को classpath में शामिल करें ताकि कोई भी `RegisterNatives` उसकी Java classes को खोज सके।

<details>
<summary>Minimal harness (CMake and C) to call 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

सामान्य समस्याएँ:

  • पैचिंग के बाद re-layout के कारण कंस्ट्रक्टर के पते बदल जाते हैं; अंतिम बाइनरी पर हमेशा .rela.dyn से पुनर्गणना करें।
  • सुनिश्चित करें कि -Djava.class.path उन सभी क्लासों को कवर करे जिन्हें RegisterNatives कॉल्स द्वारा उपयोग किया जा रहा है।
  • व्यवहार NDK/loader के वर्शन के साथ भिन्न हो सकता है; लगातार भरोसेमंद कदम INIT_ARRAY/INIT_ARRAYSZ DYNAMIC टैग्स को हटाना था।

संदर्भ

Tip

AWS हैकिंग सीखें और अभ्यास करें:HackTricks Training AWS Red Team Expert (ARTE)
GCP हैकिंग सीखें और अभ्यास करें: HackTricks Training GCP Red Team Expert (GRTE) Azure हैकिंग सीखें और अभ्यास करें: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks का समर्थन करें