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

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

Для додаткової інформації див. : https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html

Android-додатки можуть використовувати нативні бібліотеки, зазвичай написані на C або C++, для задач, критичних до продуктивності. Зловмисники також зловживають цими бібліотеками, оскільки ELF shared objects досі важче декомпілювати, ніж DEX/OAT байткод. Ця сторінка фокусується на практичних робочих процесах і останніх поліпшеннях інструментів (2023–2025), що спрощують реверсування Android .so файлів.


Швидкий триаж для щойно витягнутого libfoo.so

  1. Витягніть бібліотеку
# 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. Визначте архітектуру та захисти
file libfoo.so        # arm64 or arm32 / x86
readelf -h libfoo.so  # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so  # (peda/pwntools)
  1. Перелік експортованих символів та JNI-прив’язок
readelf -s libfoo.so | grep ' Java_'     # dynamic-linked JNI
strings libfoo.so   | grep -i "RegisterNatives" -n   # static-registered JNI
  1. Завантажте в декомпілятор (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) і запустіть авто-аналіз. У новіших версіях Ghidra з’явився AArch64 декомпілятор, який розпізнає PAC/BTI заглушки та MTE теги, що значно покращує аналіз бібліотек, зібраних з Android 14 NDK.
  2. Вирішіть: статичний чи динамічний реверсинг: обтесаний (stripped), обфусцований код часто потребує інструментації (Frida, ptrace/gdbserver, LLDB).

Динамічна інструментація (Frida ≥ 16)

Frida 16-серії принесла кілька Android-специфічних покращень, які допомагають при цілевому використанні сучасних оптимізацій Clang/LLD:

  • thumb-relocator тепер може хукати крихітні ARM/Thumb функції, згенеровані агресивним вирівнюванням LLD (--icf=all).
  • Перебір та перебіндінг ELF import slots працює на Android, що дозволяє патчити по модулю через dlopen()/dlsym() коли inline-хуки відхиляються.
  • Java-hooking було виправлено для нового ART quick-entrypoint, який використовується коли додатки компілюються з --enable-optimizations на 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.

Процесна локальна телеметрія JNI через попередньо завантажене .so (SoTap)

Коли повнофункціональна інструментація надмірна або заблокована, ви все ще можете отримати видимість на рівні native, попередньо завантаживши невеликий логер всередину цільового процесу. SoTap — це легка Android native (.so) бібліотека, яка логує поведінку інших JNI (.so) бібліотек під час виконання в межах того ж процесу додатка (root не потрібний).

Key properties:

  • Ініціалізується рано і спостерігає взаємодії JNI/native всередині процесу, що її завантажив.
  • Зберігає логи, використовуючи кілька записуваних шляхів з коректним переходом на Logcat, коли доступ до сховища обмежений.
  • Source-customizable: редагуйте sotap.c, щоб розширити/підлаштувати, що логувати, і перебудуйте для кожного ABI.

Setup (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 бібліотеки. Впровадьте виклик рано (наприклад, у статичний ініціалізатор підкласу Application або в onCreate), щоб логер ініціалізувався першим. Smali snippet example:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
  1. Перебудуйте/підпишіть/встановіть, запустіть додаток, потім зберіть логи.

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 і logger не завантажиться.
  • Обмеження по збереженню даних поширені на сучасних Android; якщо запис файлів не вдається, SoTap все одно виводитиме інформацію через Logcat.
  • Поведінка/рівень виводу призначені для налаштування; перебудуйте з джерел після редагування sotap.c.

Цей підхід корисний для malware triage та JNI debugging, коли критично важливо спостерігати потоки викликів native з моменту запуску процесу, але root/системні хуки недоступні.


Див. також: виконання native коду в пам’яті через JNI

Поширений вектор атаки — завантажити сирий shellcode blob під час виконання і виконати його безпосередньо в пам’яті через JNI bridge (без ELF на диску). Деталі та готовий до використання JNI snippet тут:

In Memory Jni Shellcode Execution


Недавні вразливості, які варто шукати в APK

РікCVEПостраждала бібліотекаПримітки
2023CVE-2023-4863libwebp ≤ 1.3.1Heap buffer overflow, досяжний з native коду, який декодує WebP зображення. Декілька Android-додатків включають вразливі версії. Коли ви бачите libwebp.so всередині APK, перевірте її версію і спробуйте експлуатацію або патчинг.
2024MultipleOpenSSL 3.x seriesКілька проблем безпеки пам’яті та padding-oracle. Багато Flutter & ReactNative бандлів постачають власний libcrypto.so.

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


Антиреверсінг та тенденції посилення захисту (Android 13-15)

  • Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 вмикає PAC/BTI в системних бібліотеках на підтримуваному ARMv8.3+ кремнії. Decompilers тепер показують PAC‑пов’язані псевдо‑інструкції; для динамічного аналізу Frida інджектує trampolines після видалення PAC, але ваші кастомні trampolines повинні викликати pacda/autibsp там, де це необхідно.
  • MTE & Scudo hardened allocator: memory-tagging є опційним, але багато додатків з Play-Integrity збираються з -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; очікуйте фіктивну контрольну логіку та зашифровані string blobs в .rodata.

Нейтралізація ранніх native ініціалізаторів (.init_array) та JNI_OnLoad для ранньої інструментації (ARM64 ELF)

Сильно захищені додатки часто поміщають перевірки root/emulator/debug у native конструкторах, які виконуються дуже рано через .init_array, до JNI_OnLoad і задовго до виконання будь‑якого Java коду. Ви можете зробити ці неявні ініціалізатори явними і відновити контроль шляхом:

  • Видалення INIT_ARRAY/INIT_ARRAYSZ з таблиці DYNAMIC, щоб loader не авто-виконував записи .init_array.
  • Розв’язання адреси конструктора із RELATIVE relocations і експорт її як звичайний function symbol (наприклад, INIT0).
  • Перейменування JNI_OnLoad на JNI_OnLoad0, щоб ART не викликав його неявно.

Чому це працює на Android/arm64

  • На AArch64, записи .init_array часто заповнюються під час завантаження R_AARCH64_RELATIVE relocations, додаток яких є адресою цільової функції всередині .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. Видалити INIT_ARRAY та INIT_ARRAYSZ DYNAMIC теги. Не видаляйте секції.
  2. Додати GLOBAL DEFAULT FUNC symbol 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 контекст у standalone-бінарі. Потім вручну викличте `INIT0()` і `JNI_OnLoad0(vm)` перед будь-яким Java-кодом.
- Додайте цільовий APK/класи в classpath, щоб будь-який `RegisterNatives` знайшов відповідні Java-класи.

<details>
<summary>Minimal harness (CMake and C) to call INIT0 → JNI_OnLoad0 → Java method</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