🥚

FirdaでAndroid APKをHookingする

2021/07/22に公開

※ 2021/07/24:コード部分を修正

概要

  1. Android Studioをセットアップする
  2. Android Studio付属のAndroid Virtual Device(AVD)をセットアップする
  3. AVDでテスト用の仮想デバイスをセットアップする
  4. Fridaをセットアップする
  5. AVDのテスト用デバイスでHookingする

本記事での想定環境

  • OS:macOS Big Sur v11.4
  • Python:3.7.6(macOS標準のものを使用)
  • Frida:14.2.8
  • Android Studio:4.2.2

事前準備

Android Studioセットアップ

  1. Android Studioの公式サイトから、最新版のインストーラをダウンロードします。

AVDセットアップ

  1. Android Studioを起動し、新規の空プロジェクトを作成します。
  2. 「Tools > AVD Manager」を選択肢、AVD Managerを起動します。
  3. 「Select Hardware」画面で、「Pixel 4a」を選択し、「Next」ボタンをクリックします。
  4. 「System Image」画面で、「Android 11.0」を選択し、「Next」ボタンをクリックします。
  5. 「Android Virtual Device(AVD)」画面で、AVD Nameとして「Pixel 4a」を記入し、「Finish」ボタンをクリックします。
  6. 作成が完了すると、「Your Virtual Devices」画面に、作成された仮想マシンが表示されます。
  7. 作成した仮想マシンをダブルクリックもしくは「▶︎」ボタンをクリックすると、仮想マシンが起動します。
    • 初回起動時は少し時間がかかります。

Fridaセットアップ

  • Fridaは、指定したプロセスにQuickJS Javascript Engineを注入して動作する、動的解析用のツールキットの1つです。
    • Pythonベースで記述されているため、Pythonを実行できる環境があれば、pipコマンドで導入が可能です。
  1. (Windows環境など、Pythonが導入されていない場合のみ)Pythonの公式サイトから、Pythonのインストーラをダウンロードし、インストールする
  2. 適当なディレクトリに移動し、下記コマンドを実行する。
% python -m venv <envname>
  • venvコマンドは、Python標準コマンドの1つで、<envname>ディレクトリにPythonの仮想環境を作成するコマンドです。
  • 仮想環境を利用することで、本環境を汚さずに検証できる、不要になったら仮想環境ごと削除で対応できるなどのメリットがあります。
  1. 下記コマンドを実行して、仮想環境に入ります。
% . venv/bin/activate
  • Windows環境の場合は、上記コマンドの代わりに、<envname>¥Scripts配下のactivate.batを実行します。
  • 実行すると、ターミナル上で下記のような表示に切り替わります。
(venv) <username> </path/to/here>
  • 先頭に(venv)がつきます。
  • ※ 仮想環境を終了させるには、deactivateコマンドを実行します。
  1. 下記コマンドを実行して、Fridaをインストールします。
% pip install frida-tools
  1. Fridaがインストールされているかを確認します(2021/07/04時点)。
% frida --version
14.2.18
  1. Releases · frida/frida · GitHubから、インストールしたFridaのバージョンと同じバージョン名が記載されている「frida-server-<frida version>-android-x86.xz」をダウンロードします。
  2. 下記のコマンドを実行して、6.でダウンロードしたFrida Serverを解凍します。
% unxz frida-server-<frida version>-android-x86.xz 
  • Windows環境の場合は、7-Zipを利用することを推奨します。
  • 実機の場合は、x86ではなくarm64の方をダウンロードします。
  • 解凍後、ファイル名を「frida-server」に変更します。
  1. 「AVDセットアップ」で作成した仮想マシンを起動し、下記コマンドを実行します。
% adb root  // root権限でadbを起動する
restarting adbd as root
% adb push frida-server /data/local/tmp  // ホスト側の「frida-server」をadb接続先デバイスの/data/local/tmpにプッシュする
frida-server: 1 file pushed, 0 skipped. 92.5 MB/s (42932720 bytes in 0.443s)
% adb shell "chmod 755 /data/local/tmp/frida-server" // ホスト側からadb接続先デバイスへ指定したコマンドを実行する
% adb shell "/data/local/tmp/frida-server &"

テスト用APKセットアップ

  1. 下記サイトから、テスト用APKファイルをダウンロードします。
  2. 「AVDセットアップ」で作成した仮想マシンを起動し、1.でダウンロードしたAPKファイルをドラッグ&ドロップします。
    • インストールが成功すると、赤枠のように、1.でダウンロードしたAppが表示されます。
    • ドラッグ&ドロップの代わりに、下記コマンドでインストールすることもできます。
    adb install UnCrackable-Level1.apk
    

テスト用APKの説明

  • 「UnCrackable Mobile Apps」は、OWASPが開発した、モバイルのリバースエンジニアリングの課題を集めたものです。
  • 今回のLevel_01では、App内に隠されている秘密の文字列を探すという課題が設定されています。

Hooking

  1. AVDの仮想環境でAppを起動すると、下図のように、「Root detected」のアラートが表示されます。
    • 「OK」ボタンをクリックすると、Appが強制終了します。
    • 画面の奥には、文字列を入力するフォームと「VERIFY」ボタンがあるため、まずはこのRoot検知のギミックを回避する必要があります。
  2. 下記のサイトから、jadxをインストールします。
  3. インストールしたjadxを起動し、テスト用APKをロードします。
  4. sg.vantagepoint.uncrackable1.MainActivityを確認します。
    MainActivity.java
    private void a(String str) {
       AlertDialog create = new AlertDialog.Builder(this).create();
       create.setTitle(str);
       create.setMessage("This is unacceptable. The app is now going to exit.");
       create.setButton(-3, "OK", new DialogInterface.OnClickListener() {
          /* class sg.vantagepoint.uncrackable1.MainActivity.AnonymousClass1 */
    
          public void onClick(DialogInterface dialogInterface, int i) {
             System.exit(0);  // ← これが実行されるとAppが強制終了する
          }
       });
    
  • System.exit(0)が呼び出されると、Appが強制終了します。
    • onClickメソッドをHookして、System.exit(0)を実行させないようにします。
  1. 下記のようなJavaScriptコードを作成します。
    Java.perform(function () {
       send("Start Script");
         
       var mainActivityClass = Java.use("sg.vantagepoint.uncrackable1.MainActivity$1");
       mainActivityClass.onClick.implementation = function () {
          send("onClick() start");
       }
    });
    
  2. 5.で作成したJavaScriptコードをFridaを利用したPythonコード上で実行させるように、Pythonコードを作成します。
    import frida
    import sys
    
    # ターゲットのAPKパッケージ名を記入
    APK_PROCESS_NAME = "owasp.mstg.uncrackable1"
    #  Hookするデバイス名を記入
    DEVICE_NAME = "emulator-5554"
    
    def on_message(message, data):
       if message["type"] == "send":
          print(f"[*] {message['payload']}")
       else:
          print(message)
    
    jscode = """
    <5.で作成したJavaScriptコードを記載>
    """
    
    # USBデバッグ時用
    # process = frida.get_usb_device().attach("owasp.mstg.uncrackable1")
    # AVD用
    process = frida.get_device_manager().get_device(DEVICE_NAME).attach(APK_PROCESS_NAME)
    script = process.create_script(jscode)
    script.on("message", on_message)
    print("[*] Running Uncrackable1")
    
    script.load()
    sys.stdin.read(
    
  3. テスト用APKを起動し、最初のダイアログが表示されたら、6.で作成したPythonコードを実行します。
  4. そのままダイアログの「OK」ボタンを押すと、Pythonコード内のJavaScriptコードが実行され、System.exit(0)をHookして、強制終了を回避できます。
  5. 続けて、任意の文字列を入力して、「VERIFY」ボタンを押すと、下記のようなメッセージが表示されます。
  6. jadxで該当コードを確認します。
MainActivity.java
public void verify(View view) {
   String str;
   String obj = ((EditText) findViewById(R.id.edit_text)).getText().toString();  // ← 入力フォームのテキストを取得
   AlertDialog create = new AlertDialog.Builder(this).create();
   if (a.a(obj)) {   // ← 入力値とシークレット値を比較する
      create.setTitle("Success!");
      str = "This is the correct secret.";
   } else {
      create.setTitle("Nope...");
      str = "That's not it. Try again.";
   }
  • a.a(obj)部分で入力値をシークレット値を比較しているので、この部分をさらに確認します。
  1. jadxで該当コードを確認します。
a.java
public class a {
   public static boolean a(String str) {  // ← str はユーザの入力値
      byte[] bArr;
      byte[] bArr2 = new byte[0];
      try {
         bArr = sg.vantagepoint.a.a.a(b("8d127684cbc37c17616d806cf50473cc"), Base64.decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", 0));  // ← 秘密の文字列を生成
      } catch (Exception e) {
         Log.d("CodeCheck", "AES error:" + e.getMessage());
         bArr = bArr2;
      }
      return str.equals(new String(bArr));  // ← 入力値と秘密の文字列の比較結果のboolean型を返す
   }
  • 秘密の文字列を格納しているbArrの値をHookできれば、秘密の文字列が確認できます。
    • → 秘密の文字列を生成するsg.vantagepoint.a.a.aをHookして、値を出力させるようにします。
  1. 下記のようなJavaScriptコードを作成します。
Java.perform(function () {
   // 5. で作成した部分
   send("Start Script");
   
   var mainActivityClass = Java.use("sg.vantagepoint.uncrackable1.MainActivity$1");
   mainActivityClass.onClick.implementation = function () {
      send("onClick() start");
   }

   var aaClass = Java.use("sg.vantagepoint.a.a");
   aaClass.a.implementation = function (var1, var2) {
      var retval = this.a(var1, var2);
      var secret = "";

      for (var i = 0; i < retval.length; i++) {
         secret += String.fromCharCode(retval[i]);
      }

      send(secret);
      return retval;
   }
});
  1. 6.のPythonコードに12.のJavaScriptコードを追加し、再度実行します。
  • 実行すると、ターミナルのコンソール上に秘密の文字列が出力されます。
  1. 秘密の文字列を入力すると、下記のようなメッセージが表示されます。

参考

Discussion