この記事で学べること:

  • FRIDAとは何か、そしてその仕組み
  • FRIDAのセットアップ方法
  • JavaメソッドをフックするJavaScriptスクリプトの書き方
  • C++/ネイティブ関数をフックする方法
  • 実践的なサンプルコードと応用例

FRIDAとは?

FRIDAは動的コード計装ツールキットで、実行中のプロセスにJavaScriptコードを注入することができます。AndroidアプリのJavaレイヤーやC++/ネイティブレイヤーの関数をフックし、その動作を監視・変更することが可能です。セキュリティ研究、アプリ解析、デバッグなどに役立つツールです。

注意: FRIDAを使用する際は、必ず自分が所有するアプリか、許可を得たアプリでのみ使用してください。他者のアプリを許可なく解析することは法律や利用規約に違反する可能性があります。

FRIDAのセットアップ

必要なもの

  • Python 3.x
  • ADBがインストールされたPC
  • ルート化されたAndroidデバイスまたはエミュレーター

インストール手順

  1. Pythonのpipを使ってFRIDAをインストール:

    pip install frida-tools
  2. Androidデバイスに対応するFRIDAサーバーをダウンロード:

    GitHubのFRIDAリリースページから、お使いのデバイスのアーキテクチャ(arm64、arm、x86など)に合ったバージョンをダウンロードします。

  3. FRIDAサーバーをデバイスにプッシュして実行:

    adb push frida-server /data/local/tmp/
    adb shell "chmod 755 /data/local/tmp/frida-server"
    adb shell "/data/local/tmp/frida-server &"
  4. 接続を確認:

    frida-ps -U

    実行中のプロセス一覧が表示されれば成功です。

JavaメソッドをフックするJSスクリプトの書き方

FRIDAスクリプトは、JavaScript (正確にはTypeScript) で書かれ、フックしたい関数や動作を定義します。以下に基本的な構造を示します。

基本的なJavaフックスクリプト

// basic_java_hook.js
Java.perform(function() {
    // ターゲットのJavaクラスを指定
    var TargetClass = Java.use("com.example.app.TargetClass");
    
    // methodNameというメソッドをフック
    TargetClass.methodName.implementation = function() {
        console.log("[+] methodName が呼び出されました");
        
        // 元の引数と戻り値を取得
        var originalResult = this.methodName();
        
        console.log("[+] 元の戻り値: " + originalResult);
        
        // 戻り値を変更することも可能
        return originalResult;
    };
});

引数のあるメソッドをフック

// args_hook.js
Java.perform(function() {
    var TargetClass = Java.use("com.example.app.TargetClass");
    
    // 引数を持つメソッドをフック
    TargetClass.login.implementation = function(username, password) {
        console.log("[+] ログイン試行:");
        console.log("    ユーザー名: " + username);
        console.log("    パスワード: " + password);
        
        // 引数を変更することも可能
        var modifiedUsername = username;
        var modifiedPassword = "hacked_password";
        
        // 元のメソッドを変更した引数で呼び出す
        var result = this.login(modifiedUsername, modifiedPassword);
        
        console.log("[+] ログイン結果: " + result);
        
        // 結果を返す(または変更する)
        return true; // 常に成功を返すように変更
    };
});

オーバーロードされたメソッドをフック

// overloaded_hook.js
Java.perform(function() {
    var TargetClass = Java.use("com.example.app.TargetClass");
    
    // 引数の型を指定してオーバーロードされたメソッドを選択
    TargetClass.process.overload('java.lang.String').implementation = function(str) {
        console.log("[+] process(String) が呼び出されました: " + str);
        return this.process(str);
    };
    
    // 別のオーバーロードバージョン
    TargetClass.process.overload('java.lang.String', 'int').implementation = function(str, num) {
        console.log("[+] process(String, int) が呼び出されました: " + str + ", " + num);
        return this.process(str, num);
    };
});

コンストラクタをフック

// constructor_hook.js
Java.perform(function() {
    var CryptoClass = Java.use("com.example.app.CryptoUtils");
    
    // コンストラクタをフック
    CryptoClass.$init.implementation = function(key) {
        console.log("[+] CryptoUtils のコンストラクタが呼び出されました");
        console.log("[+] 暗号化キー: " + key);
        
        // 元のコンストラクタを呼び出す
        this.$init(key);
    };
});

C++/ネイティブ関数のフック方法

C++やネイティブライブラリの関数をフックするには、Fridaの「Interceptor」APIを使用します。これは少し複雑ですが、非常に強力です。

基本的なネイティブフック

// native_hook.js
Interceptor.attach(Module.findExportByName("libnative-lib.so", "Java_com_example_app_NativeClass_nativeMethod"), {
    onEnter: function(args) {
        // 関数が呼び出されたときに実行されるコード
        console.log("[+] ネイティブメソッドが呼び出されました");
        
        // this.context.x0, this.context.x1 などでレジスタにアクセス可能 (ARM64の場合)
        console.log("[+] 第1引数: " + args[0]);
        console.log("[+] 第2引数: " + args[1]);
        
        // 引数を保存して後で使用
        this.arg0 = args[0];
    },
    
    onLeave: function(retval) {
        // 関数から戻るときに実行されるコード
        console.log("[+] ネイティブメソッドが終了しました");
        console.log("[+] 戻り値: " + retval);
        
        // 戻り値を変更することも可能
        retval.replace(0x1337);
    }
});

メモリ操作とポインタ

// memory_hook.js
Interceptor.attach(Module.findExportByName("libnative-lib.so", "encrypt"), {
    onEnter: function(args) {
        // char* 型の引数からデータを読み出す
        var inputPtr = args[0];
        var inputStr = inputPtr.readUtf8String();
        console.log("[+] 暗号化される文字列: " + inputStr);
        
        // 暗号化キーを読み出す (uint8_t* key)
        var keyPtr = args[1];
        var keyLength = args[2].toInt32();
        
        var key = [];
        for (var i = 0; i < keyLength; i++) {
            key.push(keyPtr.add(i).readU8());
        }
        
        console.log("[+] 暗号化キー: " + key.join(", "));
        
        // 後で使用するためにポインタを保存
        this.outputPtr = args[3];
    },
    
    onLeave: function(retval) {
        // 暗号化された出力を取得
        var result = this.outputPtr.readUtf8String();
        console.log("[+] 暗号化結果: " + result);
        
        // 必要に応じて出力を変更
        var fakeOutput = "fakeciphertext";
        Memory.writeUtf8String(this.outputPtr, fakeOutput);
    }
});

モジュールをスキャンしてフック

// module_scan.js
// モジュールをロード
var libssl = Process.findModuleByName("libssl.so");

if (libssl) {
    // 特定のパターンを持つ関数を検索
    Memory.scan(libssl.base, libssl.size, "48 89 5C 24 ?? 57 48 83 EC 30 48 8B D9 48 8B FA", {
        onMatch: function(address, size) {
            console.log("[+] パターンに一致する関数を見つけました: " + address);
            
            // 見つかった関数をフック
            Interceptor.attach(address, {
                onEnter: function(args) {
                    console.log("[+] 暗号化関数が呼び出されました");
                }
            });
            
            // 最初の一致だけをフックする場合
            return 'stop';
        },
        
        onComplete: function() {
            console.log("[+] スキャン完了");
        }
    });
}

実践的なサンプル:HTTPSリクエストの傍受

次の例では、アプリのHTTPSリクエストをフックして、送信されるデータを表示・改変する方法を示します。

// https_hook.js
Java.perform(function() {
    // OkHttpクライアントをフック
    var OkHttpClient = Java.use("okhttp3.OkHttpClient");
    var Request = Java.use("okhttp3.Request");
    var RequestBody = Java.use("okhttp3.RequestBody");
    var MediaType = Java.use("okhttp3.MediaType");
    
    // Requestのbuilderメソッドをフック
    Request.newBuilder.implementation = function() {
        console.log("[+] 新しいHTTPリクエストが作成されています");
        return this.newBuilder();
    };
    
    // URLをフック
    var HttpUrl = Java.use("okhttp3.HttpUrl");
    var oldParse = HttpUrl.parse.overload('java.lang.String');
    
    oldParse.implementation = function(url) {
        console.log("[+] URL: " + url);
        return oldParse.call(this, url);
    };
    
    // POSTボディをフック
    RequestBody.create.overload('okhttp3.MediaType', 'java.lang.String').implementation = function(mediaType, content) {
        console.log("[+] POSTデータ:");
        console.log("    メディアタイプ: " + mediaType);
        console.log("    コンテンツ: " + content);
        
        // 必要に応じてデータを改変
        var modifiedContent = content.replace(/"password":"[^"]*"/, '"password":"hacked123"');
        
        return this.create(mediaType, modifiedContent);
    };
    
    // レスポンスボディをフック
    var ResponseBody = Java.use("okhttp3.ResponseBody");
    ResponseBody.string.implementation = function() {
        var response = this.string();
        console.log("[+] レスポンス: " + response);
        return response;
    };
});

FRIDAスクリプトの実行方法

作成したスクリプトを実行するには、次のコマンドを使用します:

frida -U -l script.js -f com.example.app --no-pause

または、既に実行中のアプリにアタッチする場合:

frida -U -l script.js com.example.app

コマンドラインオプションの説明:

  • -U: USBで接続されたデバイスを使用
  • -l script.js: 読み込むスクリプトファイル
  • -f com.example.app: 起動するアプリのパッケージ名
  • --no-pause: アプリを起動後に一時停止せずに実行

応用例と便利なテクニック

1. クラスローダーの列挙

// enumerate_classes.js
Java.perform(function() {
    Java.enumerateLoadedClasses({
        onMatch: function(className) {
            // 特定のパッケージに関連するクラスのみを表示
            if (className.includes("com.example")) {
                console.log(className);
            }
        },
        onComplete: function() {
            console.log("[+] クラス列挙完了");
        }
    });
});

2. メソッドの列挙

// enumerate_methods.js
Java.perform(function() {
    var targetClass = Java.use("com.example.app.TargetClass");
    var methods = targetClass.class.getDeclaredMethods();
    
    for (var i = 0; i < methods.length; i++) {
        console.log(methods[i].toString());
    }
});

3. 動的メソッド呼び出し

// call_methods.js
Java.perform(function() {
    // クラスのインスタンスを取得する関数
    function getInstanceOf(className) {
        var hook = Java.use(className);
        var instance = null;
        
        Java.choose(className, {
            onMatch: function(object) {
                instance = object;
                return "stop";
            },
            onComplete: function() {}
        });
        
        return instance;
    }
    
    // 実際にインスタンスを取得して呼び出し
    var utilsInstance = getInstanceOf("com.example.app.Utils");
    if (utilsInstance) {
        var result = utilsInstance.doSomething("test");
        console.log("[+] メソッド呼び出し結果: " + result);
    }
});

4. SQLiteデータベースの傍受

// sqlite_hook.js
Java.perform(function() {
    var SQLiteDatabase = Java.use("android.database.sqlite.SQLiteDatabase");
    
    // クエリをフック
    SQLiteDatabase.rawQuery.overload('java.lang.String', '[Ljava.lang.String;').implementation = function(sql, selectionArgs) {
        console.log("[+] SQL実行: " + sql);
        
        if (selectionArgs) {
            console.log("[+] 引数:");
            for (var i = 0; i < selectionArgs.length; i++) {
                console.log("    " + i + ": " + selectionArgs[i]);
            }
        }
        
        return this.rawQuery(sql, selectionArgs);
    };
    
    // 挿入をフック
    SQLiteDatabase.insert.overload('java.lang.String', 'java.lang.String', 'android.content.ContentValues').implementation = function(table, nullColumnHack, values) {
        console.log("[+] テーブルに挿入: " + table);
        
        var keys = values.keySet().toArray();
        for (var i = 0; i < keys.length; i++) {
            var key = keys[i];
            console.log("    " + key + " => " + values.get(key));
        }
        
        return this.insert(table, nullColumnHack, values);
    };
});

5. SharedPreferencesの傍受

// preferences_hook.js
Java.perform(function() {
    var SharedPreferencesEditor = Java.use("android.content.SharedPreferences$Editor");
    
    // 文字列の保存をフック
    SharedPreferencesEditor.putString.implementation = function(key, value) {
        console.log("[+] SharedPreferences.putString");
        console.log("    キー: " + key);
        console.log("    値: " + value);
        
        // 特定のキーの値を変更
        if (key === "auth_token") {
            console.log("    [!] auth_tokenを改変します");
            return this.putString(key, "fake_token_123");
        }
        
        return this.putString(key, value);
    };
});

トラブルシューティング

よくある問題と解決策:

問題: FRIDAサーバーに接続できない

解決策:

  • adb devices コマンドでデバイスが認識されているか確認
  • FRIDAサーバーのアーキテクチャがデバイスに合っているか確認
  • デバイスがルート化されているか確認
  • FRIDAサーバーを再起動: adb shell "killall frida-server" && adb shell "/data/local/tmp/frida-server &"

問題: クラスやメソッドが見つからない

解決策:

  • アプリが実際に対象のクラスをロードしているか確認(デバッグ出力でロードされるクラスを表示)
  • クラス名の完全修飾名を確認(パッケージ名を含む)
  • メソッドのオーバーロードを確認し、正確な引数の型を指定

問題: ネイティブライブラリが見つからない

解決策:

  • Process.enumerateModules() を使って実際にロードされているライブラリを確認
  • ライブラリ名の大文字小文字を確認(Linux環境では大文字と小文字は区別される)

セキュリティと倫理的考慮事項

FRIDAようなツールを使用する際は、以下の点に注意してください:

  • 常に自分のアプリまたは許可を得たアプリのみを解析すること
  • 商用アプリの解析は利用規約に違反する可能性があること
  • 発見したセキュリティ問題は責任ある開示プロセスに従うこと
  • 他者のプライバシーや知的財産権を尊重すること

まとめ

FRIDAは便利なツールで、AndroidアプリのJavaおよびC++レイヤーを深く理解するのに役立ちます。この記事で説明したテクニックを使えば、アプリのセキュリティ評価、デバッグ、挙動の理解などに役立てることができると思います。

実際のアプリケーションでは、この記事で紹介したスクリプトを組み合わせて使用したり、特定のニーズに合わせてカスタマイズしたりすることができます。FRIDAの公式ドキュメントも参照して、さらに高度なテクニックを学んでみてください。

最終更新日: 2025年4月18日

この記事が参考になりましたら、コメントやシェアをお願いします!質問があればお気軽にコメント欄でお尋ねください。