ネイティブライブラリのリバース

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をサポートする

For further information check: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html

Android アプリは、パフォーマンスが重要な処理のために通常 C や C++ で書かれたネイティブライブラリを使用します。マルウェア作者もこれらのライブラリを悪用します。なぜなら ELF shared objects は DEX/OAT バイトコードよりも decompile しにくいからです。 このページでは、Android .so ファイルのリバースを容易にする 実用的な ワークフローと、最近のツール改善(2023–2025)に焦点を当てます。


新しく取得した 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. decompiler にロードする (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) と自動解析を実行する。 新しい Ghidra バージョンは、AArch64 decompiler を導入し、PAC/BTI stubs と MTE tags を認識するようになりました。これにより Android 14 NDK でビルドされたライブラリの解析が大幅に改善されます。
  2. 静的解析 vs 動的解析 を決める: stripped や obfuscated されたコードはしばしば instrumentation(Frida, ptrace/gdbserver, LLDB)が必要です。

Dynamic Instrumentation (Frida ≥ 16)

Frida の 16 シリーズは、ターゲットがモダンな Clang/LLD 最適化を使用している場合に役立つ、いくつかの Android 固有の改善をもたらしました:

  • thumb-relocator により、LLD の aggressive alignment(--icf=all)で生成される小さな ARM/Thumb 関数にも hook をかけられるようになりました。
  • Android 上での ELF import slots の列挙と再バインディングが動作するようになり、inline hook が拒否される場面でモジュール単位の dlopen()/dlsym() パッチが可能になりました。
  • Java hooking は、Android 14 で --enable-optimizations 付きでコンパイルされたアプリが使う新しい ART quick-entrypoint に対応するよう修正されました。

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は、PAC/BTI対応デバイス(Pixel 8/Android 14+)で、frida-server 16.2以降を使用していればそのまま動作します — それ以前のバージョンはインラインフックのパディングを特定できませんでした。

メモリからランタイムで復号されたネイティブライブラリをダンプする(Frida soSaver)

保護されたAPKがネイティブコードを暗号化したままにするか、ランタイムでのみマッピングする場合(packers, downloaded payloads, generated libs)、FridaをアタッチしてプロセスのメモリからマップされたELFを直接ダンプします。

soSaver のワークフロー (Python host + TS/JS Frida agent):

  • dlopenandroid_dlopen_ext にフックしてロード時のライブラリマッピングを検出し、既にロードされているモジュールの初回スイープを実行します。
  • 定期的にプロセスメモリマッピングをスキャンしてELFヘッダを検出し、loader APIs を通らずに非標準のマッパーで読み込まれたモジュールを捕捉します。
  • 各モジュールをメモリからブロック単位で読み込み、バイト列をFridaメッセージ経由でホストにストリームします;読み取れない領域がある場合は、利用可能ならオンディスクパスからの読み取りにフォールバックします。
  • 再構築した .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)

フル機能のインストゥルメンテーションが過剰だったりブロックされている場合でも、ターゲットプロセス内に小さなロガーをプリロードすることでネイティブレベルの可視性を得られます。SoTap は、同一アプリプロセス内の他の JNI (.so) ライブラリのランタイム挙動をログする軽量な Android ネイティブ (.so) ライブラリです(root は不要)。

Key properties:

  • 早期に初期化され、それをロードしたプロセス内の JNI/native の相互作用を観測します。
  • 複数の書き込み可能なパスにログを保持し、ストレージが制限されている場合は Logcat にフォールバックします。
  • ソースカスタマイズ可能: edit sotap.c to extend/adjust what gets logged and rebuild per ABI.

Setup (repack the APK):

  1. 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)
  1. 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
  1. 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 の整合は必須です。不一致があると UnsatisfiedLinkError が発生し、ロガーがロードされません。
  • 現代の Android ではストレージ制約が一般的です。ファイル書き込みが失敗しても、SoTap は Logcat 経由で出力を行います。
  • 挙動/冗長性はカスタマイズ可能です。sotap.c を編集したらソースから再ビルドしてください。

この手法は、プロセス開始時からのネイティブ呼び出しフローの観察が重要だが、root やシステム全体のフックが利用できない状況での malware のトリアージや JNI デバッグに有用です。


See also: in‑memory native code execution via JNI

一般的な攻撃パターンとして、ランタイムで生の shellcode blob をダウンロードし、JNI ブリッジ経由でメモリから直接実行する(ディスク上に ELF を置かない)ことがあります。詳細と使える JNI スニペットはこちら:

In Memory Jni Shellcode Execution


APK内で探す価値のある最近の脆弱性

CVE影響を受けるライブラリ備考
2023CVE-2023-4863libwebp ≤ 1.3.1WebP 画像をデコードするネイティブコードから到達可能なヒープバッファオーバーフロー。いくつかの Android アプリが脆弱なバージョンをバンドルしています。APK 内に libwebp.so を見つけたら、そのバージョンを確認し、エクスプロイトまたはパッチを試みてください。
2024MultipleOpenSSL 3.x seriesいくつかのメモリ安全性とパディングオラクルの問題。多くの Flutter & ReactNative バンドルが独自の libcrypto.so を同梱しています。

APK 内に third-party .so ファイルを見つけたら、必ずハッシュを上流のアドバイザリと照合してください。SCA (Software Composition Analysis) はモバイルでは一般的ではないため、古い脆弱なビルドが蔓延しています。


  • Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 は対応する ARMv8.3+ シリコンでシステムライブラリにおける PAC/BTI を有効にします。デコンパイラは PAC‐関連の擬似命令を表示するようになりました。動的解析では Frida が PAC を剥がした after にトランポリンを注入しますが、カスタムトランポリンは必要に応じて pacda/autibsp を呼び出すべきです。
  • MTE & Scudo hardened allocator: memory-tagging はオプトインですが、多くの Play-Integrity 対応アプリが -fsanitize=memtag でビルドします。タグフォールトをキャプチャするには setprop arm64.memtag.dump 1adb shell am start ... を使ってください。
  • LLVM Obfuscator (opaque predicates, control-flow flattening): 商用パッカー(例: Bangcle, SecNeo)は native コード(Java に限らない)を保護することが増えています。.rodata に偽のコントロールフローや暗号化された文字列 blob を期待してください。

Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (ARM64 ELF)

高度に保護されたアプリは、JNI_OnLoad より前、さらに Java コードが実行されるよりずっと前に .init_array 経由で非常に早期に実行されるネイティブコンストラクタに root/emulator/debug チェックを置くことがよくあります。これらの暗黙のイニシャライザを明示化して制御を取り戻すには、次を行います:

  • DYNAMIC テーブルから INIT_ARRAY/INIT_ARRAYSZ を削除して、ローダが .init_array エントリを自動実行しないようにする。
  • RELATIVE リロケーションからコンストラクタのアドレスを解決し、それを通常の関数シンボル(例: INIT0)としてエクスポートする。
  • JNI_OnLoadJNI_OnLoad0 に改名して、ART が暗黙的に呼び出すのを防ぐ。

なぜこれが 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_RELATIVEaddend がコンストラクタです(例: 0xA34, 0x954)。
  • そのアドレス付近を逆アセンブルして妥当性を確認します:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40

Patch plan

  1. DYNAMIC タグ INIT_ARRAYINIT_ARRAYSZ を削除します。セクション自体は削除しないでください。
  2. コンストラクタのアドレスに GLOBAL DEFAULT FUNC シンボル INIT0 を追加し、手動で呼び出せるようにします。
  3. JNI_OnLoadJNI_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)

スクリプト: INIT_ARRAY/INIT_ARRAYSZ を削除、export 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>

Notes and failed approaches (for portability)
- `.init_array` バイトをゼロにするかセクション長を0に設定しても効果はない: 動的リンカがリロケーションで再度埋め直すため。
- `INIT_ARRAY`/`INIT_ARRAYSZ` を0にするとタグの不整合によりローダが壊れることがある。これらの DYNAMIC エントリをきれいに削除するのが信頼できる手段である。
- `.init_array` セクションを完全に削除するとローダがクラッシュしがちである。
- パッチ適用後は関数やレイアウトのアドレスがずれる可能性がある。パッチを再実行する必要がある場合は、パッチ済みファイルの `.rela.dyn` の addends からコンストラクタを必ず再計算すること。

Bootstrapping a minimal ART/JNI to invoke INIT0 and JNI_OnLoad0
- JNIInvocation を使ってスタンドアロンバイナリ内で小さな ART VM コンテキストを起動する。その後、任意の Java コードの前に手動で `INIT0()` と `JNI_OnLoad0(vm)` を呼ぶ。
- 対象の APK/クラスを classpath に含めて、`RegisterNatives` が対応する Java クラスを見つけられるようにする。

<details>
<summary>最小ハーネス (CMake and 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 のバージョンによって異なる場合があります;一貫して信頼できた手順は INIT_ARRAY/INIT_ARRAYSZ の DYNAMIC タグを削除することでした。

参考資料

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をサポートする