Реверсинг нативних бібліотек
Tip
Вивчайте та практикуйте AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Вивчайте та практикуйте Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Підтримайте HackTricks
- Перевірте плани підписки!
- Приєднуйтесь до 💬 групи Discord або групи telegram або слідкуйте за нами в Twitter 🐦 @hacktricks_live.
- Діліться хакерськими трюками, надсилаючи PR до HackTricks та HackTricks Cloud репозиторіїв на github.
For further information check: 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.
This page focuses on practical workflows and recent tooling improvements (2023-2025) that make reversing Android .so files easier.
Швидкий процес тріажу для щойно витягнутого 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. Newer Ghidra versions introduced an AArch64 decompiler that recognises PAC/BTI stubs and MTE tags, greatly improving analysis of libraries built with the Android 14 NDK.
- Decide on static vs dynamic reversing: stripped, obfuscated code often needs instrumentation (Frida, ptrace/gdbserver, LLDB).
Динамічна інструментація (Frida ≥ 16)
Frida’s 16-series brought several Android-specific improvements that help when the target uses modern Clang/LLD optimisations:
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.
Приклад: перелік всіх функцій, зареєстрованих через RegisterNatives, та виведення їхніх адрес під час виконання:
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 або новішої версії — ранні версії не могли знайти padding для inline hooks.
Вивантаження нативних бібліотек, розшифрованих під час виконання, з пам’яті (Frida soSaver)
Якщо захищений APK зберігає нативний код у зашифрованому вигляді або відображає його в пам’яті лише під час виконання (packers, downloaded payloads, generated libs), підключіть Frida та дампьте відображений ELF прямо з пам’яті процесу.
soSaver workflow (Python host + TS/JS Frida agent):
- Перехоплює
dlopenтаandroid_dlopen_extдля виявлення відображення бібліотек під час завантаження та виконує початковий обхід вже завантажених модулів. - Періодично сканує відображення пам’яті процесу в пошуку ELF headers, щоб виявити модулі, завантажені через нестандартні мапери, які ніколи не потрапляють у loader APIs.
- Читає кожен модуль блоками з пам’яті та передає байти через Frida messages на хост; якщо регіон неможливо прочитати, відкотиться до читання з on-disk path коли він доступний.
- Зберігає реконструйовані
.soфайли та виводить статистику вилучення по кожному модулю, надаючи артефакти для static RE.
Запуск (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 навіть якщо копія у файловій системі обфускована або відсутня.
Процесно-локальна JNI-телеметрія через попередньо завантажений .so (SoTap)
Коли повнофункціональна інструментація надмірна або заблокована, ви все одно можете отримати видимість на рівні native, попередньо завантаживши невеликий логер всередині цільового процесу. SoTap — легка Android native (.so) бібліотека, яка логуватиме поведінку виконання інших JNI (.so) бібліотек у тому самому процесі додатка (root не потрібен).
Ключові властивості:
- Ініціалізується на ранньому етапі та спостерігає JNI/native взаємодії всередині процесу, що його завантажує.
- Зберігає логи, використовуючи кілька шляхів доступних для запису з плавним переходом на Logcat, коли доступ до сховища обмежений.
- Налаштовується у вихідниках: редагуйте sotap.c, щоб розширити/коригувати те, що потрапляє в логи, і перебудуйте для кожного ABI.
Налаштування (repack the APK):
- Помістіть збірку для відповідного ABI в APK, щоб loader міг розв’язати libsotap.so:
- lib/arm64-v8a/libsotap.so (for arm64)
- lib/armeabi-v7a/libsotap.so (for arm32)
- Переконайтесь, що SoTap завантажується перед іншими JNI libs. Інжектуйте виклик на ранньому етапі (наприклад, статичний ініціалізатор підкласу Application або onCreate), щоб логгер ініціалізувався першим. Приклад Smali-фрагмента:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
- Rebuild/sign/install, запустіть додаток, потім зберіть логи.
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
Примітки та усунення неполадок:
- ABI alignment є обов’язковим. Невідповідність викличе UnsatisfiedLinkError і логер не завантажиться.
- Обмеження на зберігання поширені на сучасних Android; якщо запис файлів не вдається, SoTap все одно виводитиме інформацію через Logcat.
- Поведінку/рівень докладності передбачено настроювати; перебудуйте з джерел після редагування sotap.c.
Цей підхід корисний для аналізу malware та налагодження JNI, коли критично важливо спостерігати потоки native-викликів від початку процесу, а root або системні хуки недоступні.
Див. також: in‑memory native code execution via JNI
Поширений вектор атаки — завантажити raw shellcode blob під час виконання і виконати його прямо з пам’яті через JNI bridge (без on‑disk ELF). Деталі та готовий до використання JNI snippet тут:
In Memory Jni Shellcode Execution
Recent vulnerabilities worth hunting for in APKs
| Year | CVE | Affected library | Notes |
|---|---|---|---|
| 2023 | CVE-2023-4863 | libwebp ≤ 1.3.1 | Переповнення heap-буфера, доступне з нативного коду, що декодує WebP-зображення. Кілька Android-додатків включають вразливі версії. Коли ви бачите libwebp.so всередині APK, перевірте її версію і спробуйте exploit- або patch‑опції. |
| 2024 | Multiple | OpenSSL 3.x series | Кілька проблем memory-safety та padding-oracle. Багато Flutter & ReactNative збірок постачають власну libcrypto.so. |
Коли ви знаходите third-party .so файли в APK, завжди звіряйте їх хеш із upstream advisories. SCA (Software Composition Analysis) рідко застосовується на мобільних платформах, тож застарілі вразливі збірки поширені.
Anti-Reversing & Hardening trends (Android 13-15)
- Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 вмикає PAC/BTI у системних бібліотеках на сумісному ARMv8.3+ silicon. Декомпілятори тепер відображають PAC‑пов’язані pseudo-instructions; для динамічного аналізу Frida інжектує trampolines після видалення PAC, але ваші кастомні trampolines повинні викликати
pacda/autibspтам, де це необхідно. - MTE & Scudo hardened allocator: memory-tagging є опційним, але багато Play-Integrity aware додатків збираються з
-fsanitize=memtag; використовуйтеsetprop arm64.memtag.dump 1разом зadb shell am start ..., щоб зафіксувати tag faults. - LLVM Obfuscator (opaque predicates, control-flow flattening): комерційні packers (наприклад, Bangcle, SecNeo) все частіше захищають native код, а не лише Java; очікуйте фальшивий control-flow і зашифровані string blobs у
.rodata.
Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (ARM64 ELF)
Сильно захищені додатки часто розміщують перевірки root/emulator/debug у native конструкторах, що виконуються дуже рано через .init_array, до JNI_OnLoad і задовго до запуску будь‑якого Java-коду. Ви можете зробити ці неявні ініціалізатори явними і повернути контроль шляхом:
- Видалення
INIT_ARRAY/INIT_ARRAYSZз DYNAMIC table, щоб loader не авто-виконував.init_arrayзаписи. - Визначення адреси конструктора з RELATIVE relocations і експортування її як звичайний функціональний символ (наприклад,
INIT0). - Перейменування
JNI_OnLoadнаJNI_OnLoad0, щоб запобігти неявному виклику ART.
Чому це працює на Android/arm64
- На AArch64, записи
.init_arrayчасто заповнюються під час завантаження релокаціямиR_AARCH64_RELATIVE, addend яких є адресою цільової функції всередині.text. - Байти
.init_arrayможуть виглядати порожніми статично; dynamic linker записує розв’язану адресу під час обробки релокацій.
Визначення цілі конструктора
- Використовуйте Android NDK toolchain для коректного парсингу ELF на 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
- Знайдіть релокацію, що потрапляє в діапазон віртуальних адрес
.init_array;addendтієїR_AARCH64_RELATIVEі є конструктором (наприклад,0xA34,0x954). - Дізасемблюйте навколо цієї адреси для перевірки:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
План патчу
- Видалити DYNAMIC теги
INIT_ARRAYіINIT_ARRAYSZ. Не видаляйте секції. - Додати GLOBAL DEFAULT FUNC символ
INIT0на адресу конструктора, щоб його можна було викликати вручну. - Перейменувати
JNI_OnLoad→JNI_OnLoad0, щоб ART більше не викликав його неявно.
Валідація після патча
readelf -W -d libnativestaticinit.so.patched | egrep -i 'init_array|fini_array|flags'
readelf -W -s libnativestaticinit.so.patched | egrep 'INIT0|JNI_OnLoad0'
Патчування за допомогою LIEF (Python)
Скрипт: видалити INIT_ARRAY/INIT_ARRAYSZ, експортувати INIT0, перейменувати 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>
Примітки й невдалі підходи (для портативності)
- Обнулення байтів `.init_array` або встановлення довжини секції в 0 не допомагає: динамічний лінкер повторно заповнює її через релокації.
- Встановлення `INIT_ARRAY`/`INIT_ARRAYSZ` в 0 може зламати завантажувач через невідповідні теги. Чисте видалення відповідних записів DYNAMIC — надійний метод.
- Повне видалення секції `.init_array` зазвичай призводить до краху завантажувача.
- Після патчу адреси функцій/розмітки можуть зміститися; завжди перераховуйте конструктор за доданками в `.rela.dyn` у пропатченому файлі, якщо потрібно повторно застосувати патч.
Ініціалізація мінімального ART/JNI для виклику INIT0 і JNI_OnLoad0
- Використайте JNIInvocation, щоб підняти невеликий контекст ART VM в автономному бінарному файлі. Потім вручну викличте `INIT0()` та `JNI_OnLoad0(vm)` перед будь-яким Java-кодом.
- Додайте цільовий APK/classes до classpath, щоб будь-який `RegisterNatives` знайшов відповідні Java-класи.
<details>
<summary>Мінімальний тестовий стенд (CMake і C) для виклику INIT0 → JNI_OnLoad0 → 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
Поширені помилки:
- Адреси конструкторів змінюються після патчу через повторне розміщення; завжди повторно обчислюйте їх з
.rela.dynу фінальному бінарнику. - Переконайтеся, що
-Djava.class.pathохоплює всі класи, які використовуються викликамиRegisterNatives. - Поведінка може відрізнятися залежно від версій NDK/loader; надійним постійним кроком було видалення DYNAMIC тегів
INIT_ARRAY/INIT_ARRAYSZ.
Посилання
- Learning ARM Assembly: Azeria Labs – ARM Assembly Basics
- JNI & NDK Documentation: Oracle JNI Spec · Android JNI Tips · NDK Guides
- Debugging Native Libraries: 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
Вивчайте та практикуйте AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Вивчайте та практикуйте Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Підтримайте HackTricks
- Перевірте плани підписки!
- Приєднуйтесь до 💬 групи Discord або групи telegram або слідкуйте за нами в Twitter 🐦 @hacktricks_live.
- Діліться хакерськими трюками, надсилаючи PR до HackTricks та HackTricks Cloud репозиторіїв на github.


