Реверсування нативних бібліотек
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.
Для додаткової інформації див. : https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html
Android-додатки можуть використовувати нативні бібліотеки, зазвичай написані на C або C++, для задач, критичних до продуктивності. Зловмисники також зловживають цими бібліотеками, оскільки ELF shared objects досі важче декомпілювати, ніж DEX/OAT байткод.
Ця сторінка фокусується на практичних робочих процесах і останніх поліпшеннях інструментів (2023–2025), що спрощують реверсування Android .so файлів.
Швидкий триаж для щойно витягнутого libfoo.so
- Витягніть бібліотеку
# 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/
- Визначте архітектуру та захисти
file libfoo.so # arm64 or arm32 / x86
readelf -h libfoo.so # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so # (peda/pwntools)
- Перелік експортованих символів та JNI-прив’язок
readelf -s libfoo.so | grep ' Java_' # dynamic-linked JNI
strings libfoo.so | grep -i "RegisterNatives" -n # static-registered JNI
- Завантажте в декомпілятор (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) і запустіть авто-аналіз. У новіших версіях Ghidra з’явився AArch64 декомпілятор, який розпізнає PAC/BTI заглушки та MTE теги, що значно покращує аналіз бібліотек, зібраних з Android 14 NDK.
- Вирішіть: статичний чи динамічний реверсинг: обтесаний (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):
- Помістіть збірку для відповідного ABI у APK, щоб loader міг вирішити libsotap.so:
- lib/arm64-v8a/libsotap.so (for arm64)
- lib/armeabi-v7a/libsotap.so (for arm32)
- Переконайтеся, що SoTap завантажується раніше за інші JNI бібліотеки. Впровадьте виклик рано (наприклад, у статичний ініціалізатор підкласу Application або в onCreate), щоб логер ініціалізувався першим. Smali snippet example:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
- Перебудуйте/підпишіть/встановіть, запустіть додаток, потім зберіть логи.
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 | Постраждала бібліотека | Примітки |
|---|---|---|---|
| 2023 | CVE-2023-4863 | libwebp ≤ 1.3.1 | Heap buffer overflow, досяжний з native коду, який декодує WebP зображення. Декілька Android-додатків включають вразливі версії. Коли ви бачите libwebp.so всередині APK, перевірте її версію і спробуйте експлуатацію або патчинг. |
| 2024 | Multiple | OpenSSL 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_RELATIVErelocations, додаток яких є адресою цільової функції всередині.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
План патчу
- Видалити
INIT_ARRAYтаINIT_ARRAYSZDYNAMIC теги. Не видаляйте секції. - Додати GLOBAL DEFAULT FUNC symbol
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 контекст у 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.
Посилання
- Вивчення ARM Assembly: Azeria Labs – ARM Assembly Basics
- JNI & NDK Documentation: Oracle JNI Spec · Android JNI Tips · NDK Guides
- Налагодження нативних бібліотек: Debug Android Native Libraries Using JEB Decompiler
- Зміни в Frida 16.x (Android hooking, tiny-function relocation) – frida.re/news
- Оповіщення NVD щодо переповнення
libwebpCVE-2023-4863 – nvd.nist.gov - SoTap: легкий in-app JNI (.so) логер поведінки – github.com/RezaArbabBot/SoTap
- Релізи SoTap – github.com/RezaArbabBot/SoTap/releases
- Як працювати з 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
Вивчайте та практикуйте 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.
HackTricks

