Yerel (Native) Kütüphanelerin Tersine Mühendisliği

Tip

AWS Hacking’i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking’i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin

Daha fazla bilgi için bakınız: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html

Android uygulamaları performans-kritik görevler için tipik olarak C veya C++ ile yazılmış native kütüphaneler kullanabilir. Malware yazarları da bu kütüphaneleri kötüye kullanır; çünkü ELF shared objects, DEX/OAT byte-code’a göre decompile etmek halen daha zordur. Bu sayfa, Android .so dosyalarının tersine mühendisliğini kolaylaştıran pratik iş akışları ve son (2023-2025) araç geliştirmelerine odaklanır.


Yeni çekilmiş libfoo.so için hızlı triage-iş akışı

  1. Kütüphaneyi çıkarın
# 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. Mimari ve korumaları tespit edin
file libfoo.so        # arm64 or arm32 / x86
readelf -h libfoo.so  # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so  # (peda/pwntools)
  1. Export edilen sembolleri ve JNI bağlarını listeleyin
readelf -s libfoo.so | grep ' Java_'     # dynamic-linked JNI
strings libfoo.so   | grep -i "RegisterNatives" -n   # static-registered JNI
  1. Bir decompiler’a yükleyin (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) ve otomatik analiz çalıştırın. Yeni Ghidra sürümleri, AArch64 decompiler’ı ile PAC/BTI stub’larını ve MTE tag’lerini tanıyarak Android 14 NDK ile derlenmiş kütüphanelerin analizini büyük ölçüde iyileştirdi.
  2. Statik vs dinamik tersine mühendislik arasında karar verin: stripped, obfuscated kod genellikle instrumentation (Frida, ptrace/gdbserver, LLDB) gerektirir.

Dinamik Enstrümantasyon (Frida ≥ 16)

Frida’nın 16 serisi, hedef modern Clang/LLD optimizasyonları kullandığında yardımcı olan birkaç Android-özgü iyileştirme getirdi:

  • thumb-relocator artık LLD’nin agresif hizalaması (--icf=all) tarafından üretilen küçük ARM/Thumb fonksiyonlarını hooklayabilir.
  • Android’de ELF import slots’ları enumerate etmek ve yeniden bağlamak (rebind) çalışır; inline hook’lar reddedildiğinde modül başına dlopen()/dlsym() ile patch yapılmasını sağlar.
  • Android 14’te uygulamalar --enable-optimizations ile derlendiğinde kullanılan yeni ART quick-entrypoint için Java hooking düzeltildi.

Örnek: RegisterNatives aracılığıyla kayıtlı tüm fonksiyonları sıralama ve çalışma zamanında adreslerini dökme:

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

Bellekten çalışma-zamanında deşifre edilen native kütüphanelerin dökümü (Frida soSaver)

Korunan bir APK native kodu şifreli tutuyorsa veya yalnızca çalışma zamanında haritalıyorsa (packers, downloaded payloads, generated libs), Frida’yı bağlayın ve haritalanmış ELF’i doğrudan process belleğinden dökün.

soSaver iş akışı (Python host + TS/JS Frida agent):

  • dlopen ve android_dlopen_ext hook’lar ile load-time library mapping’i tespit eder ve zaten yüklenmiş modüllerin ilk taramasını yapar.
  • Süreç bellek eşlemelerini periyodik olarak ELF başlıkları için tarar; böylece non-standard mappers aracılığıyla yüklenen ve loader APIs’e hiç uğramayan modülleri yakalar.
  • Her modülü bloklar halinde bellekten okur ve byte’ları Frida mesajlarıyla host’a stream eder; bir bölge okunamıyorsa, varsa on-disk path’ten okuma yoluna geri döner.
  • Yeniden yapılandırılmış .so dosyalarını kaydeder ve modül başına extraction stats yazdırır, static RE için artifacts sağlar.

Çalıştır (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

Bu yaklaşım, canlı eşlenmiş imajı kurtararak “only decrypted in RAM” korumalarını atlar; böylece dosya sistemi kopyası obfuskeli veya yok olsa bile IDA/Ghidra’da çevrimdışı analiz yapılabilir.

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

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).

Ana özellikler:

  • Erken başlatılır ve yükleyen proses içindeki JNI/native etkileşimlerini gözlemler.
  • Yazılabilir birden fazla yol kullanarak logları kalıcılaştırır; depolama kısıtlıysa zarifçe Logcat’e geri döner.
  • Kaynağı özelleştirilebilir: neyin loglanacağını genişletmek/ayarlamak için sotap.c’yi düzenleyin ve her ABI için yeniden derleyin.

Kurulum (APK’yi yeniden paketleyin):

  1. Doğru ABI derlemesini APK’ye yerleştirin, böylece loader libsotap.so’yu çözebilir:
  • lib/arm64-v8a/libsotap.so (for arm64)
  • lib/armeabi-v7a/libsotap.so (for arm32)
  1. SoTap’ın diğer JNI kütüphanelerinden önce yüklendiğinden emin olun. Logger’ın önce başlatılması için erken bir yerde bir çağrı enjekte edin (ör. Application subclass static initializer veya onCreate). Smali snippet örneği:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
  1. Yeniden derleyin/imzalayın/kurun, uygulamayı çalıştırın, sonra logları toplayın.

Log yolları (sırasıyla kontrol edilir):

/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 is mandatory. A mismatch will raise UnsatisfiedLinkError and the logger won’t load.
  • Storage constraints are common on modern Android; if file writes fail, SoTap will still emit via Logcat.
  • Behavior/verbosity is intended to be customized; rebuild from source after editing sotap.c.

Bu yaklaşım, işlemin başından itibaren native çağrı akışlarını gözlemlemenin kritik olduğu ancak root/system-wide hooks’in mevcut olmadığı durumlarda malware triage ve JNI debugging için faydalıdır.


See also: in‑memory native code execution via JNI

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). Details and ready‑to‑use JNI snippet here:

In Memory Jni Shellcode Execution


Recent vulnerabilities worth hunting for in APKs

YılCVEEtkilenen kütüphaneNotlar
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.

APK içinde third-party .so dosyaları fark ettiğinizde, bunların hash’lerini upstream advisories ile mutlaka karşılaştırın. SCA (Software Composition Analysis) mobilde nadirdir; bu yüzden eski ve savunmasız build’ler yaygındır.


  • Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14, desteklenen ARMv8.3+ silikonlarda sistem kütüphanelerinde PAC/BTI’yi etkinleştirir. Decompilers artık PAC‐ile ilişkili pseudo-instructions gösteriyor; dinamik analiz için Frida, PAC’i kaldırdıktan sonra trampolines inject eder, ancak özel trampolines’iniz gerektiğinde pacda/autibsp çağırmalıdır.
  • MTE & Scudo hardened allocator: memory-tagging isteğe bağlıdır ancak birçok Play-Integrity uyumlu uygulama -fsanitize=memtag ile derlenir; tag hatalarını yakalamak için setprop arm64.memtag.dump 1 ve ardından adb shell am start ... kullanın.
  • LLVM Obfuscator (opaque predicates, control-flow flattening): ticari packer’lar (ör. Bangcle, SecNeo) giderek native kodu, sadece Java’yı değil, koruyor; .rodata içinde sahte control-flow ve şifrelenmiş string blob’ları bekleyin.

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

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. Bu örtük initializers’ı açık hale getirerek ve kontrolü geri alarak çözebilirsiniz:

  • Removing INIT_ARRAY/INIT_ARRAYSZ from the DYNAMIC table so the loader does not auto-execute .init_array entries.
  • Resolving the constructor address from RELATIVE relocations and exporting it as a regular function symbol (e.g., INIT0).
  • Renaming JNI_OnLoad to JNI_OnLoad0 to prevent ART from calling it implicitly.

Why this works on Android/arm64

  • 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.
  • The bytes of .init_array may look empty statically; the dynamic linker writes the resolved address during relocation processing.

Identify the constructor target

  • Use the Android NDK toolchain for accurate ELF parsing on 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
  • 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).
  • Disassemble around that address to sanity check:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40

Patch plan

  1. Remove INIT_ARRAY and INIT_ARRAYSZ DYNAMIC tags. Do not delete sections.
  2. Add a GLOBAL DEFAULT FUNC symbol INIT0 at the constructor address so it can be called manually.
  3. Rename JNI_OnLoadJNI_OnLoad0 to stop ART from invoking it implicitly.

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'

LIEF ile yama (Python)

Betik: INIT_ARRAY/INIT_ARRAYSZ kaldır, INIT0'ı dışa aktar, JNI_OnLoad→JNI_OnLoad0 olarak yeniden adlandır ```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>

Notlar ve başarısız yaklaşımlar (taşınabilirlik açısından)
- `.init_array` baytlarını sıfırlamak veya bölüm uzunluğunu 0 yapmak işe yaramaz: dynamic linker bunu relocations yoluyla yeniden doldurur.
- `INIT_ARRAY`/`INIT_ARRAYSZ`'yi 0 yapmak uyumsuz tag'lar nedeniyle loader'ı bozabilir. Bu DYNAMIC entries'lerin temizce kaldırılması güvenilir çözümdür.
- `.init_array` bölümünü tamamen silmek genellikle loader'ın çökmesine yol açar.
- Patch uygulandıktan sonra fonksiyon/yerleşim adresleri kayabilir; patch'i yeniden çalıştırmanız gerekirse constructor'ı her zaman patch'lenmiş dosyadaki `.rela.dyn` addends'ten yeniden hesaplayın.

Bootstrapping a minimal ART/JNI to invoke INIT0 and JNI_OnLoad0
- Küçük bir ART VM bağlamı oluşturmak için bağımsız bir binary içinde JNIInvocation kullanın. Ardından herhangi bir Java kodundan önce `INIT0()` ve `JNI_OnLoad0(vm)`'i manuel olarak çağırın.
- Hedef APK/classes'i classpath'e dahil edin, böylece herhangi bir `RegisterNatives` Java sınıflarını bulur.

<details>
<summary>INIT0 → JNI_OnLoad0 → Java metodunu çağırmak için minimal harness (CMake and C)</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

Yaygın Tuzaklar:

  • Constructor adresleri re-layout nedeniyle yama sonrası değişir; son binary üzerinde her zaman .rela.dyn’den yeniden hesaplayın.
  • -Djava.class.path’in RegisterNatives çağrılarında kullanılan her sınıfı kapsadığından emin olun.
  • Davranış NDK/loader sürümlerine göre değişebilir; tutarlı olarak güvenilir adım INIT_ARRAY/INIT_ARRAYSZ DYNAMIC etiketlerinin kaldırılmasıydı.

References

Tip

AWS Hacking’i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking’i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin