Реверсинг нативних бібліотек

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

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

  1. 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/
  1. 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)
  1. List exported symbols & JNI bindings
readelf -s libfoo.so | grep ' Java_'     # dynamic-linked JNI
strings libfoo.so   | grep -i "RegisterNatives" -n   # static-registered JNI
  1. 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.
  2. 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-relocator can 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-optimizations on 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):

  1. Помістіть збірку для відповідного ABI в APK, щоб loader міг розв’язати libsotap.so:
  • lib/arm64-v8a/libsotap.so (for arm64)
  • lib/armeabi-v7a/libsotap.so (for arm32)
  1. Переконайтесь, що SoTap завантажується перед іншими JNI libs. Інжектуйте виклик на ранньому етапі (наприклад, статичний ініціалізатор підкласу Application або onCreate), щоб логгер ініціалізувався першим. Приклад Smali-фрагмента:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
  1. 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

YearCVEAffected libraryNotes
2023CVE-2023-4863libwebp ≤ 1.3.1Переповнення heap-буфера, доступне з нативного коду, що декодує WebP-зображення. Кілька Android-додатків включають вразливі версії. Коли ви бачите libwebp.so всередині APK, перевірте її версію і спробуйте exploit- або patch‑опції.
2024MultipleOpenSSL 3.x seriesКілька проблем memory-safety та padding-oracle. Багато Flutter & ReactNative збірок постачають власну libcrypto.so.

Коли ви знаходите third-party .so файли в APK, завжди звіряйте їх хеш із upstream advisories. SCA (Software Composition Analysis) рідко застосовується на мобільних платформах, тож застарілі вразливі збірки поширені.


  • 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

План патчу

  1. Видалити DYNAMIC теги INIT_ARRAY і INIT_ARRAYSZ. Не видаляйте секції.
  2. Додати GLOBAL DEFAULT FUNC символ INIT0 на адресу конструктора, щоб його можна було викликати вручну.
  3. Перейменувати JNI_OnLoadJNI_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 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>

Примітки й невдалі підходи (для портативності)
- Обнулення байтів `.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.

Посилання

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