re-android-native について
Android のネイティブライブラリをリバースエンジニアリングするスキルです。JNI インターフェースやネイティブ処理、Java との連携を調べ、静的解析と管理された環境での動的観察を支援します。
主な機能
- 01APK と .so の解析経路を判定
- 02静的・動的 JNI 登録の復元
- 03Ghidra または IDA でロジックを解析
- 04Java とネイティブ間の呼び出しを追跡
- 05管理された環境で実行時の挙動を観察
活用シーン
- 01native メソッドの実装箇所を特定
- 02ネイティブのアルゴリズムやアンチデバッグ処理を解析
- 03ABI ごとのライブラリ実装を比較
- 04実行時の JNI 登録と呼び出しを調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ dslsdzc/ rev-skills --skill re-android-nativeインストール後、Claude Code で/re-android-nativeを入力して読み込まれたか確認します。
同梱ファイルreferences/experience.mdreferences/probes.md
詳細
Android ネイティブライブラリ(JNI/.so)のリバースエンジニアリング
入口判定(Decision Gate)
対象が見つかったら、まず形態を判定し、該当したら対応するスキルに切り替えます(自動振り分け):
目标
├── APK
│ ├── lib/*.so 存在?──是──→ 本技能(re-android-native)
│ └── 无 so(纯 Java)──→ re-apk
├── 单独 .so
│ ├── JNI 符号(Java_* / JNI_OnLoad / RegisterNatives 调用点)──→ 本技能
│ ├── 加密库 API(SSL_*/EVP_*/crypto_* 等)──→ re-android-crypto(加密语义)
│ └── 普通 ELF(无 JNI 特征)──→ re-binary-core(re-format-elf)
├── 加固 so(壳壳/熵高/导入表极小)──→ re-anti-analysis / re-mobile-pack(脱壳后回本技能)
└── 加密体系审计目标(Keystore/Cipher)──→ re-android-crypto
使用する場合/使用しない場合
- 使用する:APK 内の
lib/*.soネイティブライブラリ(JNI インターフェースの復元、登録方式(静的/動的)、ネイティブロジックの分析) - 使用する:Java 層のロジックは明らかでも、コアアルゴリズムやアンチデバッグ処理がネイティブ側にある場合(ゲーム、難読化、暗号化ロジック)
- 使用する:Java 層に
System.loadLibraryとnative宣言しか見当たらず、ネイティブ実装の特定と復元が必要な場合 - 使用しない:APK の Java 層の静的解析だけが必要な場合([[re-apk]] を使用)。ランタイムフックだけが必要で、.so の構造を理解する必要がない場合([[re-frida]] を使用)
- 使用しない:so が難読化シェルの場合(まず [[re-mobile-pack]]/[[re-anti-analysis]] に従ってシェルを解除します。「落とし穴 5」を参照)
- 注意:動的インストルメンテーションは、[[re-analyze/platform-tips]] の最優先原則に従い、管理下のデバイス/エミュレーターのスナップショット内で実行してください
ツールの準備
静的解析(readelf/Ghidra への .so のインポート)にはサンドボックスは不要です。動的解析(frida/xhook)は、[[re-analyze/platform-tips]] の最優先原則に従い、管理下の環境で実行してください。すべてのツールは、使用前に動作を確認してください。
逆コンパイル作業環境(so の読み込み)—— [[re-ghidra]]/[[re-ida]]
- [[re-ghidra]](デフォルト):
File > Import Fileから ELF .so を直接インポートします(arm64/arm/x86_64 に対応)。Data Type Manager に jni.h の型を読み込みます([[re-ghidra]] の「ツールの準備」を参照) - [[re-ida]]:so の読み込み後に ARM64 を逆コンパイルします。
Option > Load new typeから jni.h を読み込みます - 確認方法:arm64 .so をインポート後、
JNI_OnLoadまたはJava_*エクスポート関数を逆コンパイルできること
jni.h —— JNIEnv 関数テーブルの構造の理解(Android NDK に付属)
- 単独で配布されるパッケージはなく、Android NDK に含まれています。公式の方法:Android Studio SDK Manager で NDK をインストールするか、コマンドラインで実行します:
sdkmanager "ndk;27.2.12479018" # 先确认 sdkmanager 在 PATH(Android SDK cmdline-tools) # 或 GitHub android-ndk 官方仓库下载对应版本 zip - 主なファイル:
$NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/jni.h(macOS の場合は darwin-x86_64 パス) - 確認方法:jni.h を開き、
JNINativeInterface(関数テーブル)とJNIEnv(関数テーブルへのポインター)の定義を確認できること
readelf(binutils)—— エクスポートテーブル/アーキテクチャの識別([[re-format-elf]] と連携)
- Linux:
apt install binutils/dnf install binutils/pacman -S binutils - macOS:
brew install binutils(greadelf) - 確認方法:
readelf --version
frida([[re-frida]])—— JNI のランタイム観察
- ホストでは
pip install frida-toolsを実行します。デバイスへの frida-server のプッシュとインストールについては、[[re-frida]] の「ツールの準備」を参照してください - 確認方法:
frida-ps -Uでデバイスのプロセスを一覧表示できること
adb —— デバイスからの so の抽出/APK のインストール
- Linux:
apt install adb(Debian/Ubuntu)/dnf install android-tools/pacman -S android-tools - macOS:
brew install android-platform-tools。Windows:公式 platform-tools zip - 確認方法:
adb version
操作手順
順番に実行し、各ステップの成果物(so のパス、readelf の出力、Ghidra プロジェクト、frida スクリプト)は、証拠のパスと sha256 を記録します([[re-triage]] を参照)。レポートで引用するために使用します。
-
so の抽出とアーキテクチャの識別:
unzip app.apk -d app/ # APK 是 zip find app -name "*.so" # lib/ 下所有原生库 file app/lib/arm64-v8a/libtarget.so # 确认架构(ELF 64-bit ARM aarch64) readelf -h app/lib/arm64-v8a/libtarget.so # Machine: AArch64 / x86-64 readelf -d app/lib/arm64-v8a/libtarget.so # 依赖库与动态段- ABI ディレクトリ:
arm64-v8a(実機で主に使用)、armeabi-v7a(32 ビット)、x86_64/x86(エミュレーター)——複数 ABI 間の違いは落とし穴 4 を参照 - インストール済み App:
adb pull /data/app/.../lib/arm64-v8a /tmp/lib/(root または debuggable が必要です) - 記録項目:対象 ABI、エクスポートテーブルの規模、依存関係(
readelf -dの NEEDED)、strip の有無(readelf -sのシンボル数)
- ABI ディレクトリ:
-
JNI インターフェースの復元(JNIEnv 関数テーブル):
readelf --dyn-syms app/lib/arm64-v8a/libtarget.so | grep -i java # 静态注册的 Java_* 导出 readelf --dyn-syms app/lib/arm64-v8a/libtarget.so | grep -i JNI # JNI_OnLoad / JNI_OnUnload- JNIEnv は関数テーブルへのポインターです:
JNIEnv* envは実際にはJNINativeInterface関数テーブル(jni.h で定義)を指しています。関数呼び出しは(*env)->GetStringUTFChars(env, ...)のように、テーブルのスロットにアクセスする形式です。逆コンパイル結果に「構造体のオフセットから関数ポインターを取得して呼び出す」処理が見えたら、それは JNI API です。落とし穴 1 を参照 - Ghidra/IDA に jni.h の型を読み込ませ、名前に基づいて復元します:
GetStringUTFChars(Java の jstring → Modified UTF-8 バイト列)、NewStringUTF(Modified UTF-8 → Java String)、CallVoidMethod/CallBooleanMethod(Java をコールバック)、GetJavaVM(プロセス内で JavaVM を取得) - 標準 UTF-8 ではなく Modified UTF-8 です:null 文字のエンコード(
0xC0 0x80)と補助文字(サロゲートペア)が異なります。標準 UTF-8 として解析すると誤った文字列になり、バイト数を使うstrlenのような処理も一致しません(文字数は JNI 独自の長さ取得 API で取得します) - 復元対象:native 関数のシグネチャ(
(JNIEnv*, jclass/jobject, 业务参数...))——第 1 引数は env、第 2 引数は jclass(静的)または jobject(インスタンス)で、業務パラメーターは第 3 引数以降です
- JNIEnv は関数テーブルへのポインターです:
-
登録方式(静的 JNI_OnLoad / 動的 RegisterNatives):
- 静的登録:関数名は
Java_包名_类名_方法名(アンダースコアはエスケープされます)で、エクスポートテーブルに直接表示されます(ステップ 2 で確認できます) - 動的登録:
JNI_OnLoad内でRegisterNatives(env, clazz, methods, count)を呼び出します。methodsはJNINativeMethod{name, signature, fnPtr}の配列です——3 つのフィールドはすべてポインターで、幅は ABI によって異なります(64 ビットではそれぞれ 8 バイト、armeabi-v7a / x86 などの 32 ビット ABI ではそれぞれ 4 バイトで、配列のストライドは 12 バイトです)——関数アドレスはエクスポートテーブルにありません(落とし穴 2 を参照)。逆コンパイルでJNI_OnLoadを特定したら、RegisterNatives の第 3 引数の配列をたどって項目ごとに復元します - 仕組みの要点(知識):
JNIEnv*はJNINativeInterface関数テーブルを指しています(落とし穴 1 を参照)。RegisterNativesは標準JNIEnvfunction table のインデックス 215 にあります(UnregisterNativesは 216)——これは JNI ABI の固定レイアウトです。実際の byte offset = index × 対象ポインター幅(64 ビットでは 215×8=0x6B8)。Android Dalvik/ART、NDK、標準jni.hのいずれもこの index を変更しません - 実行時の登録を frida で観察します(対象 App を spawn):スクリプトテンプレートと実行時検証戦略(
JNIEnv*の有効性 / hook テーブル検出 / ABI 引数受け渡しの分岐)は [[probes]] を参照——変化しやすいのは ABI の引数受け渡し用レジスターと libart の内部構造です。対象環境での実測を優先してください - 復元成果物:
Java メソッド名 → シグネチャ → native 関数アドレスの対応表
- 静的登録:関数名は
-
ロジックの分析([[re-ghidra]] と連携):
- Ghidra に so をインポート → jni.h の型を読み込む(ステップ 2)→ native 関数ごとに F5 で逆コンパイル
- シンボルがない場合は API 呼び出し箇所から推測します:JNI API の引数が業務データの入口です(例:
GetStringUTFCharsの戻り値は入力文字列です) - アルゴリズム/検証/復号ロジックは [[re-binary-core]] の方法論に従って詳しく分析します。難読化されている場合(OLLVM による変形/文字列暗号化)は [[re-deobfuscate]] に移ります(落とし穴 3)
- 静的登録された
Java_*関数間では、関数呼び出しが連鎖することがよくあります(native 内部関数テーブル/コールバック)。関数ポインターの参照元を追跡します([[re-binary-core]] の R6 の考え方)
-
Java 層との連携(native 呼び出し箇所の特定):
- Java 側([[re-apk]] / jadx):
System.loadLibrary("target")とnativeメソッド宣言があるクラス——Java_包名_类名_*の命名はここに由来します - jadx で各
nativeメソッドの呼び出し箇所を探し(どの業務経路で呼び出されるかを確認)、ステップ 3 の対応表と照合して、Java 呼び出し箇所 ↔ native 関数のマッピングを作成します - 動的検証:frida の
Java.perform内で native メソッドを直接呼び出し(Java.use("com.x.Cls").method(...))、引数と戻り値を観察します。または xhook / PLT hook の考え方([[re-frida]] のInterceptor.attach(Process.getModuleByName(...).findExportByName(...)))を使い、native 内部から外部ライブラリー(libc / システムライブラリー)への呼び出しチェーンを観察します - 一連の流れ:Java の呼び出し箇所 → 引数の取得元 → native の処理ロジック → Java 層への出力
- Java 側([[re-apk]] / jadx):
クロスドメイン連携
- [[re-mobile]]:本スキルは、そのワークフローの第 4 ステップ(ネイティブライブラリー)にあたる専門サブスキルです——ゲートウェイが
.soを含む対象を検出したら、必ず本スキルを呼び出します - [[re-frida]]:RegisterNatives の動的な観察 / native 関数の hook / Java↔native 連携の検証(spawn して JNI_OnLoad を捕捉)
- [[re-format-elf]]:エクスポートテーブル / 動的セグメント / シンボル解析(readelf の 3 つのテーブル)。動的登録された関数を特定する前提です
- [[re-ghidra]]:so の逆コンパイル作業環境 + jni.h の型の読み込み
- [[re-apk]]:Java 側の静的解析(jadx で native の宣言と呼び出し箇所を確認)。加固を検出した場合はアンパッキング領域に移ります
- [[re-deobfuscate]]:OLLVM / 文字列暗号化された native コードの復元
- [[re-mobile-pack]] / [[re-anti-analysis]]:加固された so のシェルは、分析前にアンパックします(落とし穴 5)
- [[re-android-crypto]]:暗号化方式の監査(Keystore/Cipher/サードパーティー暗号化ライブラリーの意味解析)は独立して対応します——本スキルは JNI/native ロジックに焦点を当てます。so 内の暗号化ライブラリー API の暗号化の意味解析は [[re-android-crypto]] に移ります
- [[re-analyze]]:triage の「モバイル App 分析」経路から呼び出されます(re-mobile → ネイティブライブラリー → 本スキル)
- [[re-analyze/platform-tips]]:動的計装を行う際の制御環境における最優先原則
常见坑与陷阱
- JNIEnv 是函数表指针(不是直接调用):现象——反编译里 JNI 函数调用点看起来像"从结构体偏移取出函数指针再调用",参数对不上,或按普通函数分析
GetStringUTFChars直接当字符串函数用错;原因——JNIEnv指向JNINativeInterface函数表,所有 JNI API 都是表槽中的函数指针,C 写法(*env)->fn(env, ...);对策——Ghidra/IDA 载入 jni.h 类型(Data Type Manager 导入),JNIEnv声明为JNINativeInterface**,反编译自动还原成env->GetStringUTFChars(env, str)形式;没有类型库时手工按JNINativeInterface槽位索引建结构体 - 动态注册函数地址不在导出表:现象——
readelf --dyn-syms/ strings 里找不到Java_*或业务函数名,IDA 里全是地址没名字;原因——动态注册时函数是静态/局部符号,运行时才由RegisterNatives把地址与 Java 方法绑定;对策——JNI_OnLoad是可选的库加载钩子、非必有导出:存在则反编译看RegisterNatives第三参数数组逐项还原(name/signature/fnPtr 各为一个指针:64 位 8 字节、32 位 4 字节);不存在(或导出表无它)时从RegisterNatives的其他调用点、JNI 函数表 xref、字符串定位,或 frida spawn 后 hook RegisterNatives(步骤 3 脚本)直接拿运行时注册表 - 混淆 native(OLLVM):现象——反编译全是控制流平坦化(switch 调度器)、字符串全加密、函数巨大难读;原因——游戏/加固厂商用 OLLVM(变脸、bcf、sub)或商业混淆(VMP 类)保护 native 代码;对策——先确认混淆类型(平坦化 vs 指令虚拟化),平坦化按 [[re-deobfuscate]] 还原(状态变量 + 情况块),字符串加密定位解密函数后脚本批量解密;仍不行就 frida 动态拿运行时明文(hook 解密函数读内存)
- 多 ABI 架构差异:现象——按 arm64 分析的偏移/指令套到 armeabi-v7a 全错,或模拟器 x86_64 上行为与真机不同;原因——APK 每个 ABI 一份 so,编译优化/指令集/调用约定不同(arm64 用 x0-x7 传参,arm32 有 thumb 指令,x86_64 用 rdi/rsi...),部分 so 还会按 ABI 返回不同实现(如 arm64 真机 vs x86_64 模拟器分支);对策——
file/readelf -h先确认目标 ABI,分析以真机 ABI(arm64-v8a)为准,x86_64 结果仅参考;JNI 类型宽度跨 ABI 一致(jlong=64 位、jint=32 位)但 C 层long宽度不同,注意反编译里的类型标注 - 加固 so(壳壳):现象——静态分析 so 只见一小段 stub / 壳代码,
JNI_OnLoad反编译是脱壳流程;原因——so 被加固(厂商加壳 / 商用壳),真实逻辑运行时才解密到内存;对策——先识别加固([[re-apk]] 加固识别 + 熵值),静态脱壳按 [[re-mobile-pack]],或运行时 [[re-memdump]] 提内存中已解密的 so 再分析;脱壳产物 sha256 存档后回到步骤 1 复跑
经验库
实战经验全集见 [[experience]]。
コメント
Markdown 対応