Reversing Native Libraries
Reading time: 16 minutes
tip
AWSハッキングを学び、実践する:
HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:
HackTricks Training GCP Red Team Expert (GRTE)
Azureハッキングを学び、実践する:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricksをサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
詳細情報: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html
Androidアプリは、パフォーマンスクリティカルな処理のために通常CやC++で書かれたnative librariesを使用することがある。マルウェア作成者もこれらのライブラリを悪用する。というのも、ELF shared objectsはDEX/OAT byte-codeよりもまだdecompileが難しいからだ。
このページは、Android .so ファイルのreversingを容易にする実践的なワークフローと、(2023–2025)での最近のツール改善に焦点を当てている。
Quick triage-workflow for a freshly pulled 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. 新しいGhidraバージョンはAArch64 decompilerを導入し、PAC/BTI stubsやMTE tagsを認識するようになったため、Android 14 NDKでビルドされたライブラリの解析が大幅に改善された。
- Decide on static vs dynamic reversing: ストリップや難読化されたコードはしばしばinstrumentation(Frida, ptrace/gdbserver, LLDB)が必要になる。
Dynamic Instrumentation (Frida ≥ 16)
Fridaの16シリーズは、ターゲットがモダンなClang/LLD最適化を使用している場合に役立ついくつかのAndroid固有の改善をもたらした:
thumb-relocatorは、LLDの過度なアライメント(--icf=all)によって生成される小さなARM/Thumb関数をhookできるようになった。- Android上でELF import slotsの列挙とリバインドが機能し、inline hooksが使えない場合にモジュール単位での
dlopen()/dlsym()パッチ適用を可能にする。 - Android 14で
--enable-optimizations付きでコンパイルされたアプリが使う新しいART quick-entrypointに対するJava hookingが修正された。
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.
プリロードされた .so によるプロセスローカル JNI テレメトリ (SoTap)
高度なインストルメンテーションが過剰だったりブロックされている場合でも、ターゲットプロセス内に小さなロガーをプリロードすることでネイティブレベルの可視性を確保できます。SoTap は同じアプリプロセス内の他の JNI (.so) ライブラリのランタイム挙動をログする軽量の Android ネイティブ (.so) ライブラリです(root 不要)。
Key properties:
- 早期に初期化され、それをロードしたプロセス内の JNI/ネイティブ相互作用を観測します。
- 書き込み可能な複数のパスにログを永続化し、ストレージが制限されている場合は Logcat にフェールバックします。
- ソースでカスタマイズ可能: sotap.c を編集してログ内容を拡張/調整し、ABI ごとに再ビルドします。
Setup (repack the APK):
- Drop the proper ABI build into the APK so the loader can resolve libsotap.so:
- lib/arm64-v8a/libsotap.so (for arm64)
- lib/armeabi-v7a/libsotap.so (for arm32)
- Ensure SoTap loads before other JNI libs. Inject a call early (e.g., Application subclass static initializer or onCreate) so the logger is initialized first. Smali snippet example:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
- Rebuild/sign/install, run the app, then collect logs.
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
Notes and troubleshooting:
- ABI alignment is mandatory. A mismatch will raise UnsatisfiedLinkError and the logger won’t load.
- Storage constraints are common on modern Android; if file writes fail, SoTap will still emit via Logcat.
- Behavior/verbosity is intended to be customized; rebuild from source after editing sotap.c.
この手法は、プロセス開始時からネイティブの呼び出しフローを観察することが重要で、root/システム全体のフックが利用できない場合の malware triage and JNI debugging に有用です。
See also: in‑memory native code execution via JNI
A common attack pattern is to download a raw shellcode blob at runtime and execute it directly from memory through a JNI bridge (no on‑disk ELF). Details and ready‑to‑use JNI snippet here:
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 | WebP 画像をデコードするネイティブコードから到達可能な Heap buffer overflow。いくつかの Android アプリが脆弱なバージョンをバンドルしています。APK 内に libwebp.so を見つけたらバージョンを確認し、exploit または patching を検討してください. |
| 2024 | Multiple | OpenSSL 3.x series | いくつかの memory-safety および padding-oracle の問題。多くの Flutter & ReactNative バンドルは独自の libcrypto.so を同梱しています。 |
APK 内にサードパーティの .so ファイルを見つけたら、必ずハッシュを upstream advisories と突き合わせてください。モバイルでは SCA (Software Composition Analysis) が一般的でないため、古い脆弱なビルドが蔓延しています。
Anti-Reversing & Hardening trends (Android 13-15)
- Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 はサポートされる ARMv8.3+ シリコン上の system libraries で PAC/BTI を有効にします。Decompiler は現在 PAC 関連の疑似命令を表示します。動的解析では Frida が PAC を剥がした後に trampoline を注入しますが、カスタム trampoline は必要に応じて
pacda/autibspを呼ぶべきです。 - MTE & Scudo hardened allocator: memory-tagging はオプトインですが、多くの Play-Integrity 対応アプリは
-fsanitize=memtagでビルドされています。タグ違反を捕捉するにはsetprop arm64.memtag.dump 1とadb shell am start ...を使用してください。 - LLVM Obfuscator (opaque predicates, control-flow flattening): 商用パッカー(例: Bangcle, SecNeo)はネイティブコード(Java に限らない)を保護するケースが増えています。偽の control-flow や
.rodata内の暗号化された文字列ブロブを想定してください。
Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (ARM64 ELF)
Highly protected apps often place root/emulator/debug checks in native constructors that run extremely early via .init_array, before JNI_OnLoad and long before any Java code executes. You can make those implicit initializers explicit and regain control by:
- Removing
INIT_ARRAY/INIT_ARRAYSZfrom the DYNAMIC table so the loader does not auto-execute.init_arrayentries. - Resolving the constructor address from RELATIVE relocations and exporting it as a regular function symbol (e.g.,
INIT0). - Renaming
JNI_OnLoadtoJNI_OnLoad0to prevent ART from calling it implicitly.
なぜ Android/arm64 でこれが機能するのか
- AArch64 では、
.init_arrayエントリはロード時にR_AARCH64_RELATIVEリロケーションによって埋められることが多く、その addend が.text内のターゲット関数アドレスになります。 .init_arrayのバイト列は静的には空に見えることがあります;動的リンカがリロケーション処理中に解決済みアドレスを書き込みます。
コンストラクタのターゲットを特定する
- AArch64 上で正確な ELF 解析を行うには Android NDK toolchain を使用してください:
# 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の仮想アドレス範囲に入るリロケーションを見つけてください。そのR_AARCH64_RELATIVEのaddendがコンストラクタです(例:0xA34,0x954)。- そのアドレス周辺を逆アセンブルして内容を確認します:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
パッチ手順
INIT_ARRAYとINIT_ARRAYSZの DYNAMIC タグを削除します。セクションは削除しないでください。- コンストラクタアドレスに GLOBAL DEFAULT FUNC シンボル
INIT0を追加し、手動で呼び出せるようにします。 JNI_OnLoadをJNI_OnLoad0にリネームして、ART が暗黙的に呼ばないようにします。
Validation after patch
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) による Patching
スクリプト: INIT_ARRAY/INIT_ARRAYSZ を削除、INIT0 をエクスポート、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')
注意点と失敗したアプローチ(移植性のため)
.init_arrayのバイトをゼロにするかセクション長を0にしても効果はない:動的リンカが再配置によって再構成する。INIT_ARRAY/INIT_ARRAYSZを0にするとタグの不整合でローダが壊れる可能性がある。これらの DYNAMIC エントリをきれいに削除するのが確実な手段。.init_arrayセクションを完全に削除するとローダがクラッシュしがち。- パッチ後は関数/レイアウトのアドレスが変わる可能性がある。パッチを再実行する必要がある場合は、パッチ済みファイルの
.rela.dynの加算値からコンストラクタを必ず再計算すること。
Bootstrapping a minimal ART/JNI to invoke INIT0 and JNI_OnLoad0
- JNIInvocation を使い、スタンドアロンバイナリ内で小さな ART VM コンテキストを起動する。次に任意の Java コードより先に
INIT0()とJNI_OnLoad0(vm)を手動で呼ぶ。 - ターゲットの APK/classes をクラスパスに含め、
RegisterNativesがその Java クラスを見つけられるようにする。
INIT0 → JNI_OnLoad0 → Java メソッドを呼ぶ最小ハーネス(CMake と C)
# 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のバージョンによって異なる場合があります;一貫して確実だった手順は
INIT_ARRAY/INIT_ARRAYSZDYNAMICタグを削除することでした。
参考文献
- ARMアセンブリ入門: Azeria Labs – ARM Assembly Basics
- JNI と NDK のドキュメント: Oracle JNI Spec · Android JNI Tips · NDK Guides
- ネイティブライブラリのデバッグ: Debug Android Native Libraries Using JEB Decompiler
- Frida 16.x change-log (Android hooking, tiny-function relocation) – frida.re/news
- libwebp のオーバーフローに関する NVD アドバイザリ 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
- 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ハッキングを学び、実践する:
HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:
HackTricks Training GCP Red Team Expert (GRTE)
Azureハッキングを学び、実践する:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricksをサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
HackTricks