この投稿は、Recon モントリオール 2022 で発表した研究を基にしています。この研究発表では 2 つのスライド資料を用いました。1 つは、インストルメント化のプロセス全体、および「Google Play プロテクト」サービスでインストルメント化する方法を紹介するプレゼンテーションです。もう 1 つはワークショップの資料です。MtpService を対象に、エミュレータでインストルメント化する方法を紹介しています。
220604_vv_recon2022_workshop.pdf 4 MB
動機
Android アプリケーションやマルウェアのリバースエンジニアリングは、静的解析を動的解析で補完して行われることが多いようです。ユーザーレベルで実行される、ユーザーインターフェイスを備えたアプリケーションであれば、このアプローチは完璧に機能します。ただし、Android のセキュリティモデルではシステムアプリケーション(オペレーティングシステムとして機能するアプリケーション、通信事業者のアプリケーション、またはベンダーのアプリケーション)の存在が認められています。これは、システムアプリケーションが特別なアクセス許可を必要とするという意味ではありません。システムアプリケーションとは言っても、ベンダーや通信サービスプロバイダーがプリインストールしている通常のアプリケーションやサービスプロバイダーのアプリケーションであったりします。
これらのアプリケーションに関する特徴の 1 つは、ユーザーがアンインストールできないことです。ユーザーにできるのは、最新の更新プログラムをアンインストールしてアプリケーションを一時停止または無効にすることくらいで、決してアンインストールすることはできません。
また、Android フレームワークの一部であるシステムアプリケーションもあり、これらはクローズドソースであるため、解析にはリバースエンジニアリングが必要です。
動的解析を行う一般的なツールに Frida フレームワークがあります。このツールを使用するためには、対象アプリケーションのアドレス空間に Frida ライブラリを挿入する必要があります。これは、デバイス上でルートレベルのアクセス権を持つ Frida デーモンを実行するか、対象アプリケーションにパッチを当てて Frida の共有ライブラリバージョンをロードさせることで可能になります。対象アプリケーションがヘッドレスの場合、つまりユーザーインターフェイスがない場合、インストルメンテーションを実行するには、共有ライブラリを強制的にロードし、対象アプリケーションのコードにパッチを当てるしかありません。
Android オペレーティングシステムの OEM バージョンを使用すると、このような解析を簡単に行うことができます。しかし、今回の研究対象は「Android のファクトリーイメージ上のシステムアプリケーションに対してインストルメンテーションを行わなくてはならなくなった」ことであり、OEM バージョンのイメージには、対象となるシステムアプリケーションが含まれていない可能性があります。
とはいえ、これは Android オペレーティングシステムに備わっている保護メカニズムであるがゆえに、当初の予想よりも大きな課題となりました。BlackHat 2019 における Maddie Stone 氏のプレゼンテーションでは、一部のシステムアプリケーションで見つかった制限と脆弱性の両方が説明されています。
課題
Android のセキュリティアーキテクチャによって課される制限がいくつかありますが、対象アプリケーションの可用性の本質によって課される制限もあります。このセクションでは、それらの制限と解決方法を列挙していきます。
オペレーティングシステムの制限
GrapheneOS のようなオープンソースのオペレーティングシステムでデバイスを再フラッシュすれば、オペレーティングシステムのルートレベルのアクセス権を取得するのは簡単です。ただし、これには 2 つの課題があります。
1)ベンダーがインストールしたアプリケーションが存在しないということになります。アプリケーションをインストールすることは可能ですが、そのアプリケーションが動作するオペレーティングシステムのバージョンがチェックされる場合があります。静的解析とパッチ適用によって、最終的にはチェックを回避できるかもしれませんが、その時点で解析環境に影響が出て、偏った結果につながる可能性があります。さらに、いくつかのチェックを見逃してしまうリスクもあります。
2)ベンダーによっては、より汎用的な他のオペレーティングシステムには存在しない保護をカーネルに追加しています。Samsung を例にとると、ほとんどのカーネルが、SELinux の権限をユーザーレベルで変更できないように制限された状態で出荷されています。
ヘッドレスアプリケーション
システムアプリケーションは、ほとんどがヘッドレス、つまり起動アクティビティやユーザーインターフェイスを持ちません。これがないと、Frida や Android Studio が提供するデバッガなどのツールで、アプリケーションを起動してデバッグすることができません。事実上、アプリケーションを開始する手段はなく、アプリケーションがいつ実行されるかを確定的に知る手段さえありません。
1 つのオプションは、マニフェストにパッチを当て、起動アクティビティを追加することですが、どの既存のアクティビティがリクエストを処理するかの決定や、コードがそれを処理できるかのチェックなど、独自の問題をもたらします。これは、アプリケーションの動作方法の改ざんを増やし、最終的には予測できない問題を発生させることにもつながります。
アプリケーションでの動的解析の実現
Android で通常のアプリケーションの動的解析を行う場合、アプリケーション マニフェストのデバッグオプションを有効にするか、Frida のようなインストルメンテーション ツールキットを使用することができます。デバッグを有効にするには、アプリケーション マニフェストの Application セクションのデバッグ可能属性を true に変更し(android:debuggable=”true”)、アプリケーションを再パッケージする必要があります。
ルートレベルのアクセス権がないか、対象のアプリケーションがヘッドレスであると仮定すると、Frida を使用するには、アプリケーションにパッチを当てて Frida ガジェットをロードさせ、インターネット接続を許可する必要があります。これらの変更には、前と同様に、アプリケーションの再パッケージも必要です。
Android では、すべてのアプリケーションパッケージがデジタル署名されている必要があります。通常のアプリケーションやオープンな環境では、アプリケーションに署名する証明書はあまり重要ではなく、新しく作成した自己署名証明書を使えば事足ります。しかし、対象がシステムアプリケーションの場合はそう簡単にはいきません。その理由を次のセクションから説明します。
Android のセキュリティアーキテクチャ
このセクションでは、動的解析を実行できるようになるまでに解決すべき課題について説明します。
デジタル証明書の不一致
上記のとおり、アプリケーションをインストールするには、そのパッケージにデジタル署名する必要がありますが、これは問題にはなりません。しかし、パッケージに署名している証明書が実際に問題となる状況がいくつかあります。その 1 つは、セキュリティ上の理由から、ユーザーがアプリケーションをアップグレードしようとする場合です。次のシナリオを想像してみてください。
攻撃者が被害者をだまして偽の Instagram アプリケーションをダウンロードしてインストールさせ、それを(システムに対して)元のアプリケーションのアップグレードであると見せかけることができます。パッケージの署名がチェックされていないと、悪意のあるアプリケーションは正規のアプリケーションとしてすべてのデータと設定にアクセスできてしまいます。これを避けるために、Android は 2 つのアプリケーションが同じ証明書で署名されているかどうかをチェックします。同じでない場合は、偽のアップグレードのインストールを許可しません。このような攻撃を実行する方法は他にもありますが、ここでは割愛します。
つまり、動的解析に必要な変更を加えてアプリケーションを再パックした場合、署名が一致しないため、パッチを当てたアプリケーションはインストールできなくなります。
元のアプリケーションの置き換え
パッチを当てたアプリケーションを元のアプリケーションの上にインストールできないとなると、分かりやすい解決策は、元のアプリケーションをアンインストールして、パッチを当てたアプリケーションに置き換えることでしょう。しかし、こうすると、この特定のユースケースでは別の問題が生じます。最初のセクションで説明したように、システムアプリケーションはアンインストールできません。無効にすることくらいはできますが、アンインストールはできません。つまり、システムアプリケーションを動的に解析するうえではこれが大きな制限となり、この問題を解決する必要があります。
UID の共有
課されている制限は、異なるデジタル署名を持つアプリケーションをアップグレードできないという事実だけではありません。Android オペレーティングシステムでは、アプリケーションがユーザー ID(UID)を共有することができます。これは、フレームワークが提供するメカニズムを使わずに、Linux が提供する単純なメカニズムを用いてアプリケーション間で情報を共有したい場合に特に便利です。ただし当然のことながら、セキュリティアーキテクチャは、ユーザー ID を共有するアプリケーションはすべて同じ証明書で署名されなければならないという制限を課しています。残念ながら、同じユーザー ID を共有するシステムアプリケーションがいくつかあるため、研究を通じてずっとこの制限にぶつかることになりそうです。
解決策
Android でシステムアプリケーションを動的に解析できるようにするには、いくつか行わなければならないことがあります。
詳細手順
これまでのセクションで示したように、Android プラットフォームでシステムアプリケーションを動的に解析するには、いくつかの課題を解決する必要があります。
必ずしもこの順序である必要はありませんが、まず元の対象アプリケーションをアンインストールして削除し、次にユーザー ID を共有するアプリケーションが存在するかどうかを確認する必要があります。そのようなアプリケーションが存在する場合はアンインストールするか、対象アプリケーション(パッチを適用したバージョン)の署名に使用したのと同じ証明書で再署名する必要があります。必要な手順は下図のようになります。
ここで扱うシステムアプリケーションはヘッドレスであることが多いため、対象のシステムアプリケーションをインストルメント化することが、非対話型の動的解析を実行するためには最良の方法です。
そのためには、アプリケーションにパッチを当て、Frida ライブラリ(ガジェット)をアドレス空間にロードする命令を追加する必要があります。
方法について詳しく解説する前に、対象アプリケーションとその環境について説明しておかなければなりません。この研究は、API 30 の x86 イメージである Pixel 4 XL スキンを用いて、標準的な Android エミュレータで実施しました。デモのために選んだ対象アプリケーションは MtpService です。これは、新しい USB デバイスが端末に接続されたときに MediaTransferProtocol セットアップを処理する役割を果たします。オープンソースのアプリケーションであり、リバースエンジニアリングで得たコードと動作を常に元のコードと比較できるため、テストに最適でした。
対象アプリケーションへのパッチ適用と再署名
対象アプリケーションにパッチを適用する最初の手順は、apktool でアンパックすることです。apktool とアプリケーションのパック/アンパック手順については、こちらを参照してください。
実際にパッチを適用する箇所は、対象アプリケーションに大きく依存します。「1 つの箇所がすべてのアプリケーションに当てはまる」ということはありません。経験則では、ライブラリはできるだけ早い段階で対象アプリケーションにロードするべきです。これを行う良い方法は、「BOOT_COMPLETED」アクションを処理するメソッド内にライブラリをロードすることです。デバイスの再起動後、すぐにこのアクションが実行されます。
マニフェストでは、このアクションを処理するクラスがレシーバーセクションで指定されているはずです。対象アプリケーションでは、「com.android.mtp.MtpReceiver」というクラスになります。このクラスで「onReceive()」メソッドを探します。アクションがブロードキャストされると、このメソッドが実行されます。
apktool で classes.dex を逆コンパイルし、smali コードを編集してライブラリをロードできます。smali コードは下図のようになります。
151 行目では、v0 という変数文字列オブジェクトを宣言し、その中に「gadget」という単語を格納しています。153 行目では、System パッケージ内にある静的メソッド「loadLibrary()」を呼び出しています。引数として 1 つの String オブジェクトを指定し、v0 オブジェクトを先に宣言しています。静的ライブラリをアプリケーションのアドレス空間にロードするには、この 2 つの命令で十分です。下図は、このコードが Java でどのように見えるかを示しています。
ここで、アプリケーション全体を再パックする前に、Frida ライブラリをパッケージに追加する必要があります。パッケージのルート(抽出後に apktool で作成されたもの)に「lib/」というディレクトリを作成し、その中にライブラリのアーキテクチャを持つディレクトリを作成します。この場合、エミュレータなので「x86」です。このディレクトリ内にライブラリファイルを配置します。
さて、Android インストーラを使うには、いくつかの微調整が必要です。まず、ライブラリファイルは接頭辞「lib」で始まり、拡張子「.so」で終わる必要があります。そのため、「gadget」というライブラリをロードするように指示されても、ファイル名は「libgadget.so」でなければなりません。Frida ガジェットには設定ファイルが必要ですが、その詳細は次のセクションで説明します。ライブラリのロード時に使用できるように、このファイルもパッケージの中に入れておく必要があります。Frida ガジェットは、ライブラリ自体と同じ名前で拡張子が「.config」の設定ファイルを探すため、Android の要件に従って、このファイルは「libgadget.config.so」という名前にする必要があります。
これらの命名基準に従うことで、デバイスの再起動後に、対象アプリケーションのパッチを当てたバージョンが確実に Frida ガジェットをロードするようになります。すべてのファイルがそろったら、apktool でアプリケーションを APK ファイルに再パックする必要があります。
対象アプリケーションへの署名
ここで、パッケージに再署名する必要があります。証明書は、次のような簡単なコマンドで生成できます。
keytool -genkey -v -keystore release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias |
この証明書は、対象アプリケーションとユーザー ID(UID)を共有するアプリケーションに署名するために必要となる場合があることに注意してください。パッケージに署名する前に、以下のコマンドで zip ファイルをアライメントする必要があります。
zipalign -v -p 4 <package_file> <output> |
その後、以下のコマンドラインで apksigner を使用してパッケージに署名します。
apksigner sign –ks <path to java keystore> –out <package name> <output from zipalign> |
ID を共有するアプリケーションの検索
今回はたまたま、MtpService アプリケーションのインストールは失敗し、以下のエラーが発生します。
このエラーは、同じユーザー ID を共有しているけれども、異なる証明書で署名されているアプリケーションがあることを意味します。これは、以下のマニフェストのスニペットをチェックすることで確認できます。
「android.media」に定義されて設定されている「android:sharedUserId」というプロパティがあります。これは、同じユーザー ID を使用する他のアプリケーションがあることを意味します。この共有ユーザー ID を使用するアプリケーションは、すべてシステムアプリケーションであるはずです。あまり洗練された方法ではありませんが、すべてのアプリケーションを見つける効果的な方法は、すべてのシステムアプリケーションをダウンロードし、マニフェストを抽出してテキスト形式に変換し、共有 ID プロパティを検索することです。
共有ユーザー ID を使用するアプリケーションがすべて判明したら、前のセクションで説明したのと同じ手法で削除するか、同じ証明書で署名されたバージョンに置き換えることができます。アプリケーション間に予想外の依存関係があるかもしれないので、後者が望ましいアクションです。
元の対象アプリケーションのパッケージをアンインストールして置き換え
これは、非常に重要な手順です。この手順を踏まなければ、Android のセキュリティアーキテクチャは、対象アプリケーションのパッチを当てたバージョンのインストールを許可することはありません。これを行うには、Magisk というツールをインストールする必要があります。Magisk は「ルート化」ツールであり、オペレーティングシステム全体のイメージを変更することなく、デバイス上でルートレベルのアクセス権を取得できます。このツールは、カーネルにパッチを当て、それをデバイスに再フラッシュする方法を提供します。
この方法を使えば、できる限り影響を抑えながらルートにアクセスできます。行いたいのはシステムアプリケーションの解析なので、高い権限を持つデーモンとして Frida サーバーを実行する必要はなく、ルートアクセスはそれほど重要ではありません(アプリケーションは自身のコードに Frida ライブラリを含めるようにパッチを当てる必要があることに注意してください)。
Magisk には、もう 1 つの重要な機能があります。それは、起動時に実際のファイルシステムの上に置かれる仮想ファイルシステムをユーザーが定義できることです。これにより、任意のフォルダまたはファイルを、システム作成時にパーティションにフラッシュされた元のものと置き換えたバージョンに再割り当てすることができます。
最初の手順は、解析に使用するシステムに Magisk をインストールすることです。インストール方法についてはここでは説明しませんが、インストールすると、Magisk アプリケーションのホーム画面が表示され、下図のようになるはずです。
対象アプリケーションは「/system/priv-app/MtpService/」に格納されており、下図はそのアプリケーションパッケージがある箇所を示しています。
パッケージを手動で削除しようとすると、「/system」ディレクトリが読み取り専用のファイルシステムにマウントされているため(下記参照)、ファイルがルートとして所有されているにもかかわらず削除できないことが分かります。
そこで、これに対処する方法は、Magisk で言うところの「モジュール」を使用することです。具体的には、ユーザーが定義したファイルシステムのパスを、Magisk で実際のパスの上にオーバーレイします。このように、実際のパスの内容を変更することができます。オーバーレイは、単にフルパスを削除するか、独自のコンテンツに置き換えるかになります。
Magisk モジュールの設定は「/data/adb/modules」に保存されています。モジュールのドキュメント一式はこちらをご覧ください。このケースでは、モジュールに付けたい名前でディレクトリを作成し、オーバーレイへのパスを再作成する必要があるだけです。最後のディレクトリにファイル「.replace」を追加することで、そのディレクトリが空のディレクトリに置き換えられます。
上図は、モジュールが作成された後の空のディレクトリを示しています。
最後に
これですべての課題が解決されました。後は単にパッチを当てたアプリケーションをインストールして、デバイスを再起動するだけです。すると、インストルメンテーション ツールキットを組み込んだパッチ適用済みのアプリケーションが起動し、使用できるようになります。
本稿は 2023 年 01 月 12 日に Talos Group のブログに投稿された「How to instrument system applications on Android stock images」の抄訳です。