Analiza bibliotek natywnych
Reading time: 12 minutes
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.
Więcej informacji: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html
Aplikacje Android mogą wykorzystywać biblioteki natywne, zwykle napisane w C lub C++, do zadań krytycznych pod względem wydajności. Twórcy malware również nadużywają tych bibliotek, ponieważ ELF shared objects są nadal trudniejsze do zdekompilowania niż kod bajtowy DEX/OAT.
Ta strona koncentruje się na praktycznych procedurach i najnowszych ulepszeniach narzędziowych (2023-2025), które upraszczają analizę plików .so na Androidzie.
Szybka procedura triage dla świeżo wyciągniętego libfoo.so
- Wyodrębnij bibliotekę
# 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/
- Zidentyfikuj architekturę i zabezpieczenia
file libfoo.so # arm64 or arm32 / x86
readelf -h libfoo.so # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so # (peda/pwntools)
- Wypisz eksportowane symbole i powiązania JNI
readelf -s libfoo.so | grep ' Java_' # dynamic-linked JNI
strings libfoo.so | grep -i "RegisterNatives" -n # static-registered JNI
- Załaduj do dekompilera (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) i uruchom auto-analizę. Nowsze wersje Ghidra wprowadziły dekompiler AArch64, który rozpoznaje PAC/BTI stubs i tagi MTE, co znacząco poprawia analizę bibliotek zbudowanych z użyciem Android 14 NDK.
- Zdecyduj o analizie statycznej vs dynamicznej: kod pozbawiony symboli (stripped) lub obfuskowany często wymaga instrumentacji (Frida, ptrace/gdbserver, LLDB).
Dynamiczna instrumentacja (Frida ≥ 16)
Seria 16 Fridy wniosła kilka usprawnień specyficznych dla Androida, które pomagają, gdy cel używa nowoczesnych optymalizacji Clang/LLD:
thumb-relocatormoże teraz hookować małe funkcje ARM/Thumb wygenerowane przez agresywne wyrównanie LLD (--icf=all).- Enumeracja i ponowne powiązanie ELF import slots działa na Androidzie, umożliwiając patchowanie dla każdego modułu przez
dlopen()/dlsym(), gdy inline hooks są odrzucane. - Java hooking został naprawiony dla nowego ART quick-entrypoint używanego, gdy aplikacje są kompilowane z
--enable-optimizationsna Androidzie 14.
Example: enumerating all functions registered through RegisterNatives and dumping their addresses at runtime:
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.
Lokalne telemetry JNI za pomocą preładowanej biblioteki .so (SoTap)
Gdy pełna instrumentacja jest przesadą lub zablokowana, nadal możesz uzyskać widoczność na poziomie natywnym przez preładowanie małego loggera wewnątrz docelowego procesu. SoTap to lekka natywna biblioteka Android (.so), która loguje zachowanie w czasie działania innych bibliotek JNI (.so) w tym samym procesie aplikacji (nie wymaga roota).
Kluczowe cechy:
- Inicjalizuje się wcześnie i obserwuje interakcje JNI/native wewnątrz procesu, który ją ładuje.
- Zapisuje logi używając wielu zapisywalnych ścieżek z płynnym fallbackem do Logcat, gdy zapis jest ograniczony.
- Możliwość dostosowania źródła: edytuj sotap.c, aby rozszerzyć/dostosować, co jest logowane i przebuduj dla każdego ABI.
Ustawienie (przepakuj APK):
- Upuść właściwy build 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 jako pierwszy. Przykład fragmentu Smali:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
- Przebuduj/podpisz/zainstaluj, uruchom aplikację, a następnie zbierz logi.
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
Notatki i rozwiązywanie problemów:
- ABI alignment jest obowiązkowe. Niezgodność spowoduje UnsatisfiedLinkError i logger nie załaduje się.
- Ograniczenia pamięci masowej są powszechne na nowoczesnym Androidzie; jeśli zapisy plików zawiodą, SoTap i tak wyemituje poprzez Logcat.
- Zachowanie/poziom szczegółowości jest przeznaczony do dostosowania; przebuduj z źródeł po edycji sotap.c.
To podejście jest przydatne do triage malware i debugowania JNI, gdy obserwacja przepływów wywołań natywnych od startu procesu jest krytyczna, ale root/system-wide hooks nie są dostępne.
See also: in‑memory native code execution via JNI
Powszechny wzorzec ataku polega na pobraniu surowego bloba shellcode w czasie działania i wykonaniu go bezpośrednio z pamięci przez most JNI (bez ELF na dysku). Szczegóły i gotowy fragment JNI tutaj:
In Memory Jni Shellcode Execution
Recent vulnerabilities worth hunting for in APKs
| Rok | CVE | Dotknięta biblioteka | Uwagi |
|---|---|---|---|
| 2023 | CVE-2023-4863 | libwebp ≤ 1.3.1 | Przepełnienie bufora na heapie osiągalne z kodu natywnego, który dekoduje obrazy WebP. Wiele aplikacji Android dołącza podatne wersje. Gdy zobaczysz libwebp.so w APK, sprawdź jego wersję i spróbuj exploitować lub załatać. |
| 2024 | Multiple | OpenSSL 3.x series | Kilka problemów z bezpieczeństwem pamięci i padding-oracle. Wiele bundli Flutter & ReactNative dostarcza własne libcrypto.so. |
Gdy natrafisz na third-party .so w APK, zawsze porównaj ich hash z upstream advisories. SCA (Software Composition Analysis) jest rzadkie na mobile, 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 obsługiwanym krzemie ARMv8.3+. Decompilery teraz wyświetlają 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 jest kompilowanych z
-fsanitize=memtag; użyjsetprop arm64.memtag.dump 1plusadb shell am start ...aby złapać błędy tagów. - LLVM Obfuscator (opaque predicates, control-flow flattening): commercial packers (e.g., Bangcle, SecNeo) coraz częściej chronią native code, nie tylko Java; spodziewaj się bogus control-flow i zaszyfrowanych string blobs w
.rodata.
Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (ARM64 ELF)
Silnie chronione aplikacje często umieszczają root/emulator/debug checks w konstruktorach natywnych, które uruchamiają się bardzo wcześnie przez .init_array, przed JNI_OnLoad i długo przed wykonaniem jakiegokolwiek kodu Java. Możesz uczynić te niejawne inicjalizatory jawnymi i odzyskać kontrolę przez:
- Usunięcie
INIT_ARRAY/INIT_ARRAYSZz tabeli DYNAMIC, tak aby loader nie wykonywał automatycznie wpisów z.init_array. - Rozwiązanie adresu konstruktora z RELATIVE relocations i wyeksportowanie go jako zwykły symbol funkcji (np.
INIT0). - Zmianę nazwy
JNI_OnLoadnaJNI_OnLoad0, aby ART nie wywoływał jej implicitnie.
Dlaczego to działa na Android/arm64
- Na AArch64, wpisy
.init_arraysą często wypełniane w czasie ładowania przezR_AARCH64_RELATIVErelocations, których addend jest adresem docelowej funkcji wewnątrz.text. - Bajty
.init_arraymogą wyglądać na puste statycznie; dynamiczny linker zapisuje rozwiązany adres podczas przetwarzania relokacji.
Identyfikacja celu konstruktora
- Użyj toolchainu 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ź relokację, która trafia wewnątrz zakresu adresów wirtualnych
.init_array;addendtegoR_AARCH64_RELATIVEjest konstruktorem (np.0xA34,0x954). - Disasembluj wokół tego adresu, żeby to sanity check:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
Plan patchowania
- Usuń znaczniki DYNAMIC
INIT_ARRAYiINIT_ARRAYSZ. Nie usuwaj sekcji. - Dodaj a GLOBAL DEFAULT FUNC symbol
INIT0pod adresem konstruktora, aby można go było wywołać ręcznie. - Zmień nazwę
JNI_OnLoad→JNI_OnLoad0, aby ART nie wywoływał jej implicitnie.
Weryfikacja po patchu
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, eksportuj INIT0, zmień nazwę JNI_OnLoad→JNI_OnLoad0
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)
- Wyzerowanie bajtów
.init_arraylub ustawienie długości sekcji na 0 nie pomaga: dynamiczny linker odtwarza ją za pomocą relokacji. - Ustawienie
INIT_ARRAY/INIT_ARRAYSZna 0 może zepsuć loader z powodu niespójnych tagów. Czyste usunięcie tych wpisów DYNAMIC to niezawodny sposób. - Całkowite usunięcie sekcji
.init_arrayzwykle powoduje awarię loadera. - Po patchowaniu adresy funkcji/układu mogą się przesunąć; zawsze przelicz konstruktor z addendów
.rela.dynw zmodyfikowanym pliku, jeśli musisz ponownie zastosować patch.
Bootstrapping a minimal ART/JNI to invoke INIT0 and JNI_OnLoad0
- Użyj JNIInvocation, aby uruchomić niewielki kontekst ART VM w samodzielnym pliku binarnym. Następnie wywołaj ręcznie
INIT0()iJNI_OnLoad0(vm)przed jakimkolwiek kodem Java. - Dołącz docelowe APK/classes do classpath, aby
RegisterNativesznalazł odpowiednie klasy Java.
Minimal harness (CMake and C) to call INIT0 → JNI_OnLoad0 → Java method
# 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 re-layout; zawsze przeliczaj je ponownie z
.rela.dynna finalnym pliku binarnym. - 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, to usunięcie tagów DYNAMIC
INIT_ARRAY/INIT_ARRAYSZ.
Źródła
- Learning ARM Assembly: Azeria Labs – ARM Assembly Basics
- Dokumentacja JNI i NDK: Oracle JNI Spec · Android JNI Tips · NDK Guides
- Debugowanie bibliotek natywnych: Debug Android Native Libraries Using JEB Decompiler
- Lista zmian Frida 16.x (Android hooking, tiny-function relocation) – frida.re/news
- Komunikat NVD dotyczący przepełnienia
libwebpCVE-2023-4863 – nvd.nist.gov - SoTap: Lekki logger zachowań JNI (.so) w aplikacji – github.com/RezaArbabBot/SoTap
- Wydania SoTap – github.com/RezaArbabBot/SoTap/releases
- Jak korzystać z 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
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.
HackTricks