2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Androidセキュリティ】Fridaを用いたBypassについてめっちゃ簡単に基礎から書いてみた!!

Posted at

はじめに

この記事は、Androidエンジニアである私がモバイルセキュリティにおける防衛方法を知るために学習を明記したものです。「防衛を知るには攻撃から」だと思っています。
技術研究を目的とするものであり、決して違法な攻撃を助長するものではありません。
悪用厳禁です。(といっても悪用は難しいと思いますが...)

対象読者

  • Androidアプリ開発経験がある方
  • アプリセキュリティに興味がある方
  • Fridaを触ったことがない方

この記事でわかること

  • OWASP-MASTGについて
  • Fridaの基本的な使い方
  • jadx-GUIの使い方

本記事では初心者の方向けに解説しているものなのでなるべく難しい表現は使わないようにしています。

私が持っていたサイバーセキュリティの知識レベル

  • 情報処理安全確保支援士 合格済み
  • Kali Linuxを用いたCTF学習(主にrevだがいわゆるscript kiddie)
  • 基礎的なツール(msfvenom, metasploitable, nmap)の使い方
  • Androidの基礎知識(Dalvik vs ART、DEXなどビルドプロセスの簡単な知識)

元々、高校生の頃から趣味で学習していたので全く無いわけではありませんが、
実務では一切触りませんから決して深い知見などは一切なく表面的な情報のみです。

OWASP-MASTGとは

公式サイトはこちら。
モバイル開発におけるセキュリティ技術(静的解析・動的解析・リバースエンジニアリングなど)を包括的にまとめたマニュアルです。

Fridaについて

概要

Fridaは実行中のアプリが持つ関数の動きなどをリアルタイムで書き換えることが可能です。
よく使われるのは実行端末がRoot済みかどうかをチェックしてる関数を書き換えて強制的にRootしてない判定にするなど。

例えば下記のような実装があるとします。

fun isRooted(): Boolean {
  // root済みかどうか色々見る
  hogehoge()
  return result
}

これをFridaを使用することで

fun isRooted(): Boolean {
  // 確認処理は全て消して問答無用でrootしてない判定
  return False
}

このように書き換えることが出来ます。
しかも端末がroot化してなくても出来るという...。
正直、私はこの時点でもうわくわくしてました。

強み

  • 非Root端末で動作可能(root化してれば尚やりやすいが)
  • リアルタイムで関数書き換え(フック)が可能
  • 難読化されていない場合は対策が困難

弱み

  • 初回はハードル高く感じる
  • ProGuardやR8で難読化されていると難しい(フック自体は可能だがフックする関数が不明)

仕組み

svgviewer-png-output.png
難しい説明はあまりしませんが、Fridaはホストとクライアント両方の関係で成立します。
クライアント側にfrida-gadgetというネイティブライブラリファイルを仕込みapkをインストール。
こうすると、アプリ起動時にホストとクライアントが通信を開始。
これでホスト側から関数の挙動変更を指示できたりします。

実践学習

問題の概要

今回は、MASTG-TECH-0026を学習材料とします。

リンク先で配布されているアプリを開くと下記のような画面が登場します。

ここで画面上部の「VERIFY」ボタンを押下すると下記のようなダイアログが。

つまり正しいパスワードのようなものを入力しろっていうのがこの問題の概要のようですね。
早速解いちゃいましょう!

筆者の環境

作業マシン:M3 Macbook Air 256GB
作業端末:Google Pixel 7a(実機・非Root・Android16)

作業の流れ

  • 必要ツールのインストール
  • アプリを逆コンパイルしVERIFYボタンのクリックイベントを探す
  • apkにfrida-gadget(クライアントサイド)を組み込む
  • 署名の再設定を行いリビルド
  • ホスト側からアタッチを行い通信を確立
  • VERIFYボタンのクリックイベントをフックしてバイパス

必要ツールのインストール

brewやpip系などはインストール済みの前提で進めます。

▫️adb
これはお馴染みなので言わずもがな。
androidデバイスとの通信ではよく使う。android studio入っていれば基本通ってるので割愛。

▫️apktool
apkファイルの分解(デコンパイル)及びリビルド役として使用。
「brew install apktool」
これでインストール可能

▫️uber-apk-signer
apk作成時の署名設定として使用する。
普段keystoreとかdebugビルドだったらdebug署名などが組み込まれてますよね。あんな感じです。
こちらからjarファイルを取得

▫️frida
今回の主役ですね。
クライアント側は後ほど作業手順内でインストールしますのでホストを先に。
(pip3またはpip) install frida-tools
環境に合わせて実行してください。

▫️jadx-gui
apkを逆コンパイルして中身のソースコードを閲覧可能にするツールです。
フックしたい関数を特定するために必須ですね。
brew install jadx

これで完了です。

アプリを逆コンパイルしVERIFYボタンのクリックイベントを探す

svgviewer-png-output.png
普段見ているapkファイルの中身です。
逆コンパイルではこの辺りの情報から元のKotlin/Javaコードを再生成し色々表示してくれます。
ターミナルで「jadx-gui」と入力すると下記のようなウィンドウが立ち上がるはずです。
image.png
中央の「Open file」を押下するとファイル選択ウィンドウが表示されるのでダウンロードしたapk(UnCrackable-Level1)を指定します。
そうすると、左側にソースツリーが出来上がるので下記を開いてみます。
image.png

MainActivity!!見慣れた単語ですね、開きましょう。


    public void verify(View view) {
        String str;
        String string = ((EditText) findViewById(R.id.edit_text)).getText().toString();
        AlertDialog alertDialogCreate = new AlertDialog.Builder(this).create();
        if (a.a(string)) {
            alertDialogCreate.setTitle("Success!");
            str = "This is the correct secret.";
        } else {
            alertDialogCreate.setTitle("Nope...");
            str = "That's not it. Try again.";
        }
        alertDialogCreate.setMessage(str);
        alertDialogCreate.setButton(-3, "OK", new DialogInterface.OnClickListener() { // from class: sg.vantagepoint.uncrackable1.MainActivity.2
            @Override // android.content.DialogInterface.OnClickListener
            public void onClick(DialogInterface dialogInterface, int i) {
                dialogInterface.dismiss();
            }
        });
        alertDialogCreate.show();
    }

「That's not it. Try again.」、さっき見た文字列!!
ということはこの辺りが絡んでいそうですね。
よく見ると、if (a.a(string))という処理の結果で結果が分岐していそうです。
追ってみましょう。

package sg.vantagepoint.uncrackable1;

import android.util.Base64;
import android.util.Log;

/* loaded from: classes.dex */
public class a {
    public static boolean a(String str) {
        byte[] bArrA;
        byte[] bArr = new byte[0];
        try {
            bArrA = sg.vantagepoint.a.a.a(b("8d127684cbc37c17616d806cf50473cc"), Base64.decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", 0));
        } catch (Exception e) {
            Log.d("CodeCheck", "AES error:" + e.getMessage());
            bArrA = bArr;
        }
        return str.equals(new String(bArrA));
    }

    public static byte[] b(String str) {
        int length = str.length();
        byte[] bArr = new byte[length / 2];
        for (int i = 0; i < length; i += 2) {
            bArr[i / 2] = (byte) ((Character.digit(str.charAt(i), 16) << 4) + Character.digit(str.charAt(i + 1), 16));
        }
        return bArr;
    }
}

引数strとbArrAの結果が一致しているかどうかを返していますね。
AESという単語も含まれているあたり、暗号/復号が絡んでいそうです。
sg.vantagepoint.a.a.a関数も追ってみましょうか。

package sg.vantagepoint.a;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

/* loaded from: classes.dex */
public class a {
    public static byte[] a(byte[] bArr, byte[] bArr2) {
        SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, "AES/ECB/PKCS7Padding");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(2, secretKeySpec);
        return cipher.doFinal(bArr2);
    }
}

やはり暗号ですね。
つまり与えられた文字列strと"8d127684cbc37c17616d806cf50473cc"を複合した結果があっていればOK、違っていればNGという判定ですね。
しかし、暗号処理云々は些細な問題です。
要は、a.a関数が常にtrueを返せばいいだけです。
つまり、フックすべき関数はsg.vantagepoint.uncrackable1.a.aの関数だと分かります。

apkにfrida-gadget(クライアントサイド)を組み込む

UnCrackable-Level1.apkにfrida-gadget.so(クライアントサイド)を入れます。
やることは2つです。

  • soファイルの配置
  • MainActivityにてsoファイルを読み込むように設定

まず、1つ目からやります。

apktool d UnCrackable-Level1.apk -o decompiled

apktool dは指定したapkをデコンパイルするコマンドで、-oでデコンパイル結果を格納するフォルダを指定してます。
decompiledフォルダを見ると、

  • AndroidManifest.xml
  • apktool.yml
  • originalフォルダ
  • resフォルダ
  • smaliフォルダ

があります。
ではここにsoファイルを配置します。

cd decompiled
mkdir lib/arm64-v8a
cd lib/arm64-v8a
wget https://github.com/frida/frida/releases/download/17.5.1/frida-gadget-17.5.1-android-arm64.so.xz
unxz frida-gadget-17.5.1-android-arm64.so.xz
mv frida-gadget-17.5.1-android-arm64.so libfrida-gadget.so

ここで、17.5.1というのはfridaのバージョンですが必ずホストとクライアントで合わせてください。
frida --versionを実行し、17.5.1であれば問題ありませんがもし違えば修正してください。
また、arm64-v8aという命名も重要です。
これは実行端末によります。
作業端末をadb接続した状態で

adb shell getprop ro.product.cpu.abi

と試してarm64-v8aと表示されたらコピペでいいですがもし違えばフォルダ名や取得するsoファイルを修正してください。
decompiled/lib/arm64-v8a内にlibfrida-gadget.soが配置されれば完了です。

次、2つ目はMainAcitivityにsoファイルを読み込むように宣言していきます。
ただし、ここではktやjavaファイルではなくsmaliという特殊なファイルを変えていきます。
vscodeで、decompiled/smali/sg/vantagepoint/uncrackable1/MainActivity.smaliを開きましょう。
smaliとはアイスランド語で「アセンブラ」を意味します。
つまり、smaliはdexファイルを人間が読みやすいようにAndroidに特化したアセンブラ言語に変換したものです。(正確に言えばDalvik/AndroidRunTime向けのアセンブラ言語)

開くと

.method protected onCreate(Landroid/os/Bundle;)V
    .locals 1
    // 色々処理

という部分がありますね。
これがMainActivityのonCreateここの先頭に下記を追加

    const-string v0, "frida-gadget"
    invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

これ、やってることは非常にシンプルで、Javaに搭載されているネイティブライブラリロード機能(System.loadLibrary)を呼んでいるだけです。
詳細はこちら
そして引数としてfrida-gadgetを指定しています。
これが先ほど1つ目に配置したsoファイルを指しています。
つまり、onCreateの先頭でfrida-gadgetを読み込みますよ!っていう宣言をしているだけです。
保存して終了しましょう。

署名の再設定を行いリビルド

frida-gadgetを組み込んだのでバラしたapkをリビルドします。

apktool b decompiled/ -o UnCrackable-Level1-frida.apk
java -jar <uber-apk-signerのjarファイルのパス> --apk UnCrackable-Level1-frida.apk 

これをすると「UnCrackable-Level1-frida-aligned-debugSigned.apk」こんな名前のapkが出来上がるはずです。
ここまでできればOKです。

ホスト側からアタッチを行い通信を確立

いよいよ、ホストとクライアント間の接続を確立させます。
まずはインストールしておいてください(adb install UnCrackable-Level1-frida-aligned-debugSigned.apk)。
アプリを起動すると、アイコン画面で止まるはずです。
これは、クライアントがホストと通信確立するのを待っているためです。
そこで、

frida-ps -U | grep UnCrackable

と実行してデバイスで実行されているプロセス一覧を出力します。
その中に、
14146 Uncrackable1
というプロセスが稼働していることがわかるはずです。
14146(プロセスid)は環境によって変わるので異なると思いますがUncrackable1という名前は同じはずです。
プロセスidがわかったら

 !  ~/D/d/s/OWASP-MASTG    tech-0026  frida -U 14146(プロセスid)                                                                                                                                         日 11/ 9 22:29:29 2025
     ____
    / _  |   Frida 17.5.1 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to Pixel 7a (id=36141JEHN19666)
                                                                                
[Pixel 7a::PID::14146 ]->

frida -U とすることでアタッチが成功するとこんな感じでシェルが取れます!!!
筆者はこの辺りでもう興奮が止まらなかったです。
これはホストとクライアントが接続を確立できた証拠です。

VERIFYボタンのクリックイベントをフックしてバイパス

[Pixel 7a::PID::29812 ]-> Java.perform(function() {
  var check =Java.use("sg.vantagepoint.uncrackable1.a");
  check.a.overload('java.lang.String').implementation = function(input) {
    return true;
  };
});

このFridaスクリプトが要です。
先ほど、フックすべき関数はsg.vantagepoint.uncrackable1.a.aだと分かりましたね。
まさにそれを指定しています。
a関数を.overloadで中身を書き換えてreturn true;だけにしてますね。
これをするとさっきは暗号やら復号やらで色々比較してましたが全て帳消ししてtrue返すようになります。
これを実行してVERIFYボタンを押すと...?

おお!!!!見事にチェック処理をバイパスして正解扱いになってます!
これがroot無しで出来るって凄すぎませんか...?

まとめ

今回はFridaを用いた関数バイパスを試してみました。
これはAndroidアプリのサイバーセキュリティにおいて非常に基礎となる技術だと思います。
これが出来れば既存実装にログ出力処理を追加したりもできます。
FridaスクリプトはAIに書いてもらえばいいですし楽です。
ただし、弱点にも挙げたように難読化されているとやりづらいです。
それはフックすべき関数が全く分からなくなるからです。
こう見ると難読化(ProGuardやR8)の重要性が分かります。

そして、いかにモバイルアプリ側での対策が信頼に値しないか分かるのではないでしょうか。
例え、Frida検出を目的としてFridaスレッドが存在しているかどうか?などを確認するコードを書いてFrida対策を行ったとしてもそれすら書き換えてバイパスすれば良いわけです。
ですから重要なビジネスロジックや状態管理はサーバ側で行うことが大切です。

いかがでしたか?
攻撃者視点で学ぶことで防衛者としての視点も深まったのではないでしょうか。
引き続き、色々MASTGについて勉強した内容を記事で挙げていきます!!

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?