नेटिव लाइब्रेरीज़ का रिवर्स इंजीनियरिंग

Reading time: 13 minutes

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 apps performance-critical tasks के लिए अक्सर native libraries का उपयोग करते हैं, जो सामान्यतः C या C++ में लिखी होती हैं। मैलवेयर बनाने वाले भी इन लाइब्रेरीज़ का दुरुपयोग करते हैं क्योंकि ELF shared objects अभी भी DEX/OAT byte-code की तुलना में decompile करना कठिन होते हैं।
यह पृष्ठ व्यावहारिक वर्कफ़्लो और उन हालिया tooling सुधारों (2023-2025) पर केंद्रित है जो Android .so फाइलों का रिवर्सिंग आसान बनाते हैं।


ताज़ा निकाली गई libfoo.so के लिए त्वरित ट्रायाज-वर्कफ़्लो

  1. लाइब्रेरी निकालें
bash
# 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. आर्किटेक्चर और सुरक्षा पहचानें
bash
file libfoo.so        # arm64 or arm32 / x86
readelf -h libfoo.so  # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so  # (peda/pwntools)
  1. एक्सपोर्टेड symbols और JNI bindings सूचीबद्ध करें
bash
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. स्टेटिक बनाम डायनामिक रिवर्सिंग का निर्णय लें: stripped, obfuscated code अक्सर instrumentation (Frida, ptrace/gdbserver, LLDB) की जरूरत होती है।

डायनेमिक instrumentation (Frida ≥ 16)

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

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

उदाहरण: RegisterNatives के माध्यम से रजिस्टर की गई सभी functions को enumerate करना और runtime पर उनके addresses dump करना:

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

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

जब full-featured instrumentation ज़रूरत से ज्यादा हो या blocked हो, तब भी आप target process के अंदर एक छोटा logger प्रीलोड करके native-लेवल visibility प्राप्त कर सकते हैं। SoTap एक हल्का Android native (.so) लाइब्रेरी है जो उसी app process के भीतर अन्य JNI (.so) लाइब्रेरीज़ के runtime व्यवहार को लॉग करता है (no root required)।

Key properties:

  • जल्दी initialize होता है और उस process के अंदर JNI/native interactions को observe करता है जो इसे लोड करता है।
  • कई writable paths का उपयोग करके logs को persist करता है और जब storage restricted हो तो graceful fallback के तौर पर Logcat का उपयोग करता है।
  • Source-customizable: sotap.c को edit करके यह निर्धारित/विस्तार किया जा सकता है कि क्या log होगा और फिर 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:
smali
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 अनिवार्य है। mismatch होने पर UnsatisfiedLinkError होगा और logger लोड नहीं होगा।
  • आधुनिक Android पर storage constraints सामान्य हैं; अगर file writes असफल होते हैं, तो SoTap फिर भी Logcat के माध्यम से emit करेगा।
  • Behavior/verbosity को कस्टमाइज़ करने के लिए बनाया गया है; sotap.c में बदलाव करने के बाद source से rebuild करें।

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


See also: in‑memory native code execution via JNI

A common attack pattern runtime पर raw shellcode blob डाउनलोड करके उसे सीधे memory से JNI bridge के माध्यम से 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 files देखें, तो हमेशा उनके hash को upstream advisories के साथ cross-check करें। SCA (Software Composition Analysis) मोबाइल पर uncommon है, इसलिए 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 capture करने के लिए setprop arm64.memtag.dump 1 के साथ adb shell am start ... चलाएँ।
  • LLVM Obfuscator (opaque predicates, control-flow flattening): commercial packers (जैसे Bangcle, SecNeo) native code की भी सुरक्षा करते हैं, सिर्फ 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 code के चलने से बहुत पहले। आप उन implicit initializers को explicit बनाकर नियंत्रण वापस पा सकते हैं:

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

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 statically खाली दिख सकते हैं; dynamic linker relocation processing के दौरान resolved address लिख देता है।

Identify the constructor target

  • AArch64 पर सटीक ELF parsing के लिए Android NDK toolchain का उपयोग करें:
bash
# 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 करें:
bash
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40

Patch plan

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

Validation after patch

bash
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 निर्यात करें, 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')

Notes and failed approaches (for portability)

  • Zeroing .init_array bytes or setting the section length to 0 does not help: the dynamic linker repopulates it via relocations.
  • 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.
  • Deleting the .init_array section entirely tends to crash the loader.
  • 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.

Bootstrapping a minimal ART/JNI to invoke INIT0 and JNI_OnLoad0

  • JNIInvocation का उपयोग करके standalone binary में एक छोटा ART VM context लॉन्च करें। फिर किसी भी Java कोड से पहले मैन्युअली INIT0() और JNI_OnLoad0(vm) को कॉल करें।
  • target APK/classes को classpath में शामिल करें ताकि कोई भी RegisterNatives उसके Java क्लासेस को ढूँढ सके।
न्यूनतम harness (CMake और C) ताकि INIT0 → JNI_OnLoad0 → Java method को कॉल किया जा सके
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)
c
// 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);
}
bash
# 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

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

  • Constructor addresses patching के बाद re-layout के कारण बदल सकते हैं; अंतिम binary पर हमेशा .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 का समर्थन करें