はじめに
Androidアプリのデバッグや脆弱性診断、リバースエンジニアリングを行う際、静的解析(apkのデコンパイルなど)だけでは、難読化(ProGuard/DexGuard)されたコードの挙動を追うのが難しいケースが多々あります。
本記事では、ランタイム(実行時)のメモリ空間にJavaScriptを注入し、関数をリアルタイムでフックできる強力な動的ツール「Frida」の環境構築と、Java層のメソッドを書き換える基本的なスクリプトの実践までを解説します。
1. 全体像と仕組み
Fridaは、PC側(Host)からAndroid端末(Target)へ命令を送り、アプリのプロセス内にJSエンジン(V8)をインジェクション(注入)することで動作します。
[ PC (Host) ] [ Android Device / Emulator ]
+--------------+ ADB / TCP +-----------------------------+
| Frida CLI / |===============> | /data/local/tmp/frida-server| (rootで実行)
| Python Script| +--------------+--------------+
+--------------+ | 注入
+--------------v--------------+
| Target Application |
| [ Java / Native (.so) ] |
+-----------------------------+
PC上のJavaScriptファイルに記述したフック処理が、Android上の対象アプリ内で直接実行されるため、アプリを再コンパイルすることなく挙動をねじ曲げることが可能です。
2. 環境構築ステップ
ステップ 1: PC(ホスト)側の準備
まずは、PCにFridaのCLIツールをインストールします。(Python環境が必要です)
pip install frida-tools
インストール後、バージョンを確認しておきます。ここで確認したバージョンと、後述するAndroid側のサーバーのバージョンを必ず一致させる必要があります。
frida --version
# 例: 16.2.1
ステップ 2: Android端末のアーキテクチャ確認
Android端末(またはエミュレータ)をPCに接続し、CPUのアーキテクチャを調べます。
adb shell getprop ro.product.cpu.abi
# エミュレータなら x86_64 や x86、実機なら arm64-v8a などが表示されます
ステップ 3: frida-server の配置と起動
-
Frida公式のリリースページ(GitHub) から、PCと同じバージョンかつ端末のアーキテクチャに合った
frida-server-...-android-...xzをダウンロードします。 - 解凍して、端末の
/data/local/tmpにadb pushで送り込みます。
# 例: arm64実機の場合
adb push frida-server-16.2.1-android-arm64 /data/local/tmp/frida-server
# 端末にシェルで入り、権限付与とバックグラウンド起動を行う
adb shell
su
chmod 755 /data/local/tmp/frida-server
/data/local/tmp/frida-server &
ステップ 4: 接続確認
PC側のターミナルで以下を実行し、Android上のプロセス一覧がずらりと表示されれば準備完了です。
frida-ps -U
-
-Uオプションは USB接続(またはエミュレータ)のデバイスを指定する意味になります。
3. 実践:Javaメソッドのフッキング
今回は例として、ログイン画面などでよくある「パスワードのバリデーション関数」をフックし、どんな入力がきても強制的に認証をパス(trueを返却)させるコードを書いてみます。
対象の想定コード(Java)
package com.example.targetapp;
public class LoginValidator {
public boolean verifyPassword(String password) {
// 本来はここで裏側の通信やハッシュチェックを行う
return "super-secret-pass".equals(password);
}
}
Fridaスクリプトの作成 (hook.js)
JavaScriptで以下のように実装します。Java.perform の中で対象のクラスを指定し、implementation を上書きするのが基本形です。
Java.perform(function () {
// 1. 対象のクラスをラッパーオブジェクトとして取得
const LoginValidator = Java.use("com.example.targetapp.LoginValidator");
// 2. メソッドの挙動をオーバーライド(実装の上書き)
LoginValidator.verifyPassword.implementation = function (password) {
console.log("[*] verifyPassword が呼ばれました! 入力値: " + password);
// 必要に応じてオリジナルの関数をそのまま呼び出すことも可能
const originalResult = this.verifyPassword(password);
console.log("[*] オリジナルの返り値: " + originalResult);
// 3. 返り値を強制的に true に書き換えて偽装する
console.log("[+] 返り値を強制的に true へ変更します");
return true;
};
});
補足:メソッドがオーバーロードされている場合 同じメソッド名で引数が異なる(オーバーロード)場合は、明示的に型を指定する必要があります。
JavaScript
LoginValidator.verifyPassword.overload('java.lang.String').implementation = function(password) { ... }
4. スクリプトの実行
アプリを起動するタイミングからフックを仕掛けるため、-f オプションでパッケージ名を指定してアプリを強制終了・再起動(Spawn)させます。
frida -U -f com.example.targetapp -l hook.js
ターミナルに [Launched :: com.example.targetapp] と表示され、画面上の入力フォームに適当な文字を入れてボタンを押すと、Fridaのコンソールにログが出力され、間違ったパスワードでも認証が突破できるようになります。
画面を完全に操作したい場合は、CLI上で %resume と打ち込んでプロセスのメインスレッドを再開させてください。
まとめ
Fridaを使うことで、ソースコードが手元になく、難読化されているアプリであっても、特定の処理(暗号化ロジックの直前や通信クラスなど)にピンポイントで割り込んで解析を行うことができます。
今回はJava層のフックを紹介しましたが、ネイティブ層(.soライブラリ内のC/C++関数)をターゲットにする場合は Interceptor.attach を使用します。Androidアプリのセキュリティを紐解く上で必須のツールですので、ぜひ手元の検証環境で試してみてください。
注意: 本記事で紹介した手法は、自身が管理するアプリのデバッグ、または法的に許可されたペネトレーションテスト・脆弱性診断の目的以外では絶対に使用しないでください。