Reversing Native Libraries
Tip
Ucz się i ćwicz Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.
Aby uzyskać więcej informacji sprawdź: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html
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.
Ta strona skupia się na praktycznych workflowach i najnowszych ulepszeniach narzędzi (2023–2025), które ułatwiają reversing plików .so na Androidzie.
Quick triage-workflow for a freshly pulled libfoo.so
- Extract the library
# 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/
- Identify architecture & protections
file libfoo.so # arm64 or arm32 / x86
readelf -h libfoo.so # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so # (peda/pwntools)
- List exported symbols & JNI bindings
readelf -s libfoo.so | grep ' Java_' # dynamic-linked JNI
strings libfoo.so | grep -i "RegisterNatives" -n # static-registered JNI
- Load in a decompiler (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) and run auto-analysis. Nowsze wersje Ghidry wprowadziły dekompilator AArch64 rozpoznający PAC/BTI stubs i tagi MTE, co znacznie poprawia analizę bibliotek zbudowanych za pomocą Android 14 NDK.
- Decide on static vs dynamic reversing: stripped, obfuscated code often needs instrumentation (Frida, ptrace/gdbserver, LLDB).
Dynamic Instrumentation (Frida ≥ 16)
Seria Frida 16 przyniosła kilka ulepszeń specyficznych dla Androida, które pomagają, gdy cel używa nowoczesnych optymalizacji Clang/LLD:
thumb-relocatorcan now hook tiny ARM/Thumb functions generated by LLD’s aggressive alignment (--icf=all).- Enumerating and rebinding ELF import slots works on Android, enabling per-module
dlopen()/dlsym()patching when inline hooks are rejected. - Java hooking was fixed for the new ART quick-entrypoint used when apps are compiled with
--enable-optimizationson Android 14.
Przykład: enumeracja wszystkich funkcji zarejestrowanych przez RegisterNatives i zrzucenie ich adresów w czasie wykonywania:
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.
Zrzucanie natywnych bibliotek odszyfrowanych w czasie wykonywania z pamięci (Frida soSaver)
Gdy chroniony APK trzyma natywny kod zaszyfrowany lub mapuje go dopiero w czasie wykonania (packers, downloaded payloads, generated libs), podłącz Frida i zrzucaj zmapowany ELF bezpośrednio z pamięci procesu.
soSaver workflow (Python host + TS/JS Frida agent):
- Hooks
dlopenandandroid_dlopen_extw celu wykrycia mapowania bibliotek w czasie ładowania i wykonuje wstępne skanowanie już załadowanych modułów. - Okresowo skanuje mapowania pamięci procesu w poszukiwaniu nagłówków ELF, aby wykryć moduły załadowane przez niestandardowe mapery, które nigdy nie trafiły do loader APIs.
- Odczytuje każdy moduł blokami z pamięci i przesyła bajty przez Frida messages do hosta; jeśli region nie może zostać odczytany, wraca do odczytu z on-disk path jeśli jest dostępny.
- Zapisuje zrekonstruowane pliki
.soi drukuje statystyki ekstrakcji dla każdego modułu, dostarczając artefakty do static RE.
Uruchom (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
To podejście omija zabezpieczenia „only decrypted in RAM” poprzez odzyskanie żywego zmapowanego obrazu, pozwalając na analizę offline w IDA/Ghidra nawet jeśli kopia w systemie plików jest obfuskowana lub nieobecna.
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).
Key properties:
- Initializes early and observes JNI/native interactions inside the process that loads it.
- Persists logs using multiple writable paths with graceful fallback to Logcat when storage is restricted.
- Source-customizable: edit sotap.c to extend/adjust what gets logged and rebuild per ABI.
Setup (repack the APK):
- Włóż odpowiednią kompilację dla ABI do APK, aby loader mógł rozwiązać libsotap.so:
- lib/arm64-v8a/libsotap.so (for arm64)
- lib/armeabi-v7a/libsotap.so (for arm32)
- Upewnij się, że SoTap ładuje się przed innymi bibliotekami JNI. Wstrzyknij wywołanie wcześnie (np. Application subclass static initializer lub onCreate), aby logger został zainicjalizowany pierwszy. Przykład fragmentu Smali:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
- 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:
- Wyrównanie ABI jest obowiązkowe. Niezgodność spowoduje wyrzucenie UnsatisfiedLinkError i logger nie zostanie załadowany.
- Ograniczenia przestrzeni dyskowej są powszechne w nowoczesnym Androidzie; jeśli zapisy do plików się nie powiodą, SoTap nadal będzie wypisywać do Logcat.
- Zachowanie/poziom szczegółowości ma być dostosowywany; po edycji sotap.c przebuduj z źródeł.
To podejście jest przydatne przy triage złośliwego oprogramowania i debugowaniu JNI, gdy obserwacja przepływów wywołań natywnych od startu procesu jest krytyczna, a brak jest uprawnień root albo system-wide hooków.
See also: in‑memory native code execution via JNI
Powszechnym schematem ataku jest pobranie surowego blobu shellcode w czasie wykonywania i wykonanie go bezpośrednio z pamięci przez mostek JNI (bez ELF na dysku). Szczegóły i gotowy snippet JNI tutaj:
In Memory Jni Shellcode Execution
Recent vulnerabilities worth hunting for in APKs
| Rok | CVE | Biblioteka objęta | Uwagi |
|---|---|---|---|
| 2023 | CVE-2023-4863 | libwebp ≤ 1.3.1 | Przepełnienie bufora na stercie dostępne z kodu natywnego, który dekoduje obrazy WebP. Kilka aplikacji Android zawiera podatne wersje. Gdy zobaczysz libwebp.so wewnątrz APK, sprawdź jego wersję i spróbuj przeprowadzić exploit lub załatać ją. |
| 2024 | Multiple | OpenSSL 3.x series | Kilka problemów z bezpieczeństwem pamięci i padding-oracle. Wiele bundli Flutter & ReactNative dołącza własne libcrypto.so. |
Gdy zauważysz zewnętrzne pliki .so w APK, zawsze porównaj ich hash z biuletynami upstream. SCA (Software Composition Analysis) jest rzadko stosowane na urządzeniach mobilnych, więc przestarzałe podatne buildy są powszechne.
Anti-Reversing & Hardening trends (Android 13-15)
- Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 włącza PAC/BTI w bibliotekach systemowych na wspieranym sprzęcie ARMv8.3+. Decompilery teraz pokazują pseudo-instrukcje związane z PAC; do analizy dynamicznej Frida wstrzykuje trampoliny po usunięciu PAC, ale twoje własne trampoliny powinny wywoływać
pacda/autibsptam, gdzie to konieczne. - MTE & Scudo hardened allocator: memory-tagging jest opcjonalne, ale wiele aplikacji świadomych Play-Integrity buduje się z
-fsanitize=memtag; użyjsetprop arm64.memtag.dump 1plusadb shell am start ...aby przechwycić błędy tagów. - LLVM Obfuscator (opaque predicates, control-flow flattening): komercyjne packery (np. Bangcle, SecNeo) coraz częściej chronią kod native, nie tylko Java; spodziewaj się fałszywych przepływów sterowania i zaszyfrowanych blobów łańcuchów w
.rodata.
Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (ARM64 ELF)
Silnie chronione aplikacje często umieszczają sprawdzenia root/emulator/debug w konstruktorach natywnych, które uruchamiają się bardzo wcześnie przez .init_array, przed JNI_OnLoad i na długo przed wykonaniem jakiegokolwiek kodu Java. Możesz uczynić te niejawne inicjalizatory jawne i odzyskać kontrolę poprzez:
- Usunięcie
INIT_ARRAY/INIT_ARRAYSZz tabeli DYNAMIC, aby loader nie wykonywał automatycznie wpisów z.init_array. - Wyznaczenie adresu konstruktora z relocacji RELATIVE i wyeksportowanie go jako zwykły symbol funkcji (np.
INIT0). - Zmiana nazwy
JNI_OnLoadnaJNI_OnLoad0, aby zapobiec domyślnemu wywoływaniu przez ART.
Dlaczego to działa na Android/arm64
- Na AArch64, wpisy
.init_arrayczęsto są wypełniane w czasie ładowania przez relocacjeR_AARCH64_RELATIVE, których addend jest adresem docelowej funkcji wewnątrz.text. - Bajty
.init_arraymogą wyglądać statycznie na puste; dynamiczny linker zapisuje rozwiązywany adres podczas przetwarzania relocacji.
Zidentyfikuj cel konstruktora
- Użyj toolchaina Android NDK do dokładnego parsowania ELF na 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
- Znajdź relocację, która trafia wewnątrz zakresu adresów wirtualnych
.init_array;addendtejR_AARCH64_RELATIVEjest konstruktorem (np.0xA34,0x954). - Zdisasembluj wokół tego adresu w celu weryfikacji:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
Patch plan
- Usuń tagi DYNAMIC
INIT_ARRAYiINIT_ARRAYSZ. Nie usuwaj sekcji. - Dodaj symbol
INIT0z atrybutami GLOBAL DEFAULT FUNC pod adresem konstruktora, aby można go było wywołać ręcznie. - Zmień nazwę
JNI_OnLoad→JNI_OnLoad0, aby ART nie wywoływał jej domyślnie.
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'
Łatanie z LIEF (Python)
Skrypt: usuń INIT_ARRAY/INIT_ARRAYSZ, wyeksportuj INIT0, zmień nazwę JNI_OnLoad→JNI_OnLoad0
```python import liefb = 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>
Uwagi i nieudane podejścia (dla przenośności)
- Wyzerowanie bajtów `.init_array` lub ustawienie długości sekcji na 0 nie pomaga: dynamiczny linker wypełnia ją ponownie za pomocą relokacji.
- Ustawienie `INIT_ARRAY`/`INIT_ARRAYSZ` na 0 może zepsuć loader z powodu niespójnych tagów. Czyste usunięcie tych wpisów DYNAMIC jest niezawodnym dźwignią.
- Usunięcie sekcji `.init_array` w całości zwykle powoduje awarię loadera.
- Po załataniu adresy funkcji/układu mogą się przesunąć; zawsze przelicz konstruktor z addendów `.rela.dyn` w załatanym pliku, jeśli musisz ponownie zastosować patch.
Bootstrapowanie minimalnego ART/JNI, aby wywołać INIT0 i JNI_OnLoad0
- Użyj JNIInvocation, aby uruchomić niewielki kontekst ART VM w samodzielnym pliku binarnym. Następnie ręcznie wywołaj `INIT0()` i `JNI_OnLoad0(vm)` przed jakimkolwiek kodem Java.
- Dołącz docelowy APK/classes do classpath, aby każde `RegisterNatives` znalazło swoje klasy Java.
<details>
<summary>Minimalny harness (CMake i C) do wywołania INIT0 → JNI_OnLoad0 → metody Java</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
Częste pułapki:
- Adresy konstruktorów zmieniają się po patchowaniu z powodu ponownego rozmieszczenia; zawsze przeliczaj je z
.rela.dyndla ostatecznego binarium. - Upewnij się, że
-Djava.class.pathobejmuje każdą klasę używaną przez wywołaniaRegisterNatives. - Zachowanie może się różnić w zależności od wersji NDK/loadera; krok, który był konsekwentnie niezawodny, polegał na usunięciu tagów DYNAMIC
INIT_ARRAY/INIT_ARRAYSZ.
Referencje
- Nauka asemblera ARM: Azeria Labs – ARM Assembly Basics
- Dokumentacja JNI & NDK: Oracle JNI Spec · Android JNI Tips · NDK Guides
- Debugowanie bibliotek natywnych: Debug Android Native Libraries Using JEB Decompiler
- Frida 16.x change-log (Android hooking, tiny-function relocation) – frida.re/news
- NVD advisory for
libwebpoverflow CVE-2023-4863 – nvd.nist.gov - SoTap: Lightweight in-app JNI (.so) behavior logger – github.com/RezaArbabBot/SoTap
- SoTap Releases – github.com/RezaArbabBot/SoTap/releases
- How to work with SoTap? – t.me/ForYouTillEnd/13
- CoRPhone — JNI memory-only execution pattern and packaging
- Patching Android ARM64 library initializers for easy Frida instrumentation and debugging
- LIEF Project
- JNIInvocation
- soSaver — Frida-based live memory dumper for Android
.solibraries – github.com/TheQmaks/sosaver - soSaver Frida agent (TypeScript/JS) – github.com/TheQmaks/soSaver-frida
Tip
Ucz się i ćwicz Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.


