2016 年現在、Android の脆弱性のうち 86% が、メモリの安全性に関連しています。ほとんどの脆弱性は、攻撃者がアプリの通常の制御フローを変更し、攻撃対象アプリのすべての権限で任意の悪意のあるアクティビティを行うことによって、悪用されます。制御フローの整合性(CFI)は、コンパイル済みバイナリの元の制御フローグラフへの変更を許容しないセキュリティ メカニズムであり、そうした変更を行う攻撃を著しく困難にします。
Android 8.1 では、LLVM の CFI 実装がメディア スタックで可能になりました。Android 9 では、より多くのコンポーネントとカーネルで CFI が可能になりました。システム CFI はデフォルトで有効になっていますが、カーネル CFI は自分で有効にする必要があります。
LLVM の CFI は、リンク時最適化(LTO)を有効にしてコンパイルする必要があります。LTO は、リンク時までオブジェクト ファイルの LLVM ビットコード表現を保持します。これにより、コンパイラはどの最適化を実行できるかをより適切に判断できます。LTO を有効にすると、最終バイナリのサイズが小さくなり、パフォーマンスが向上しますが、コンパイル時間が長くなります。Android でのテストでは、LTO と CFI を組み合わせた場合、コードのサイズとパフォーマンスのオーバーヘッドは無視できるほどわずかであり、少数のケースでは両方とも改善が見られました。
CFI の技術的な詳細と、他の forward-control チェックの処理方法については、LLVM の設計ドキュメントをご覧ください。
例とソース
CFI はコンパイラによって提供され、コンパイル時にバイナリにインストゥルメンテーションを追加します。CFI は、Clang ツールチェーンと、AOSP の Android ビルドシステムでサポートされています。
/platform/build/target/product/cfi-common.mk
のコンポーネント セットの Arm64 デバイスで、CFI がデフォルトで有効になっています。/platform/frameworks/av/media/libmedia/Android.bp
や /platform/frameworks/av/cmds/stagefright/Android.mk
など、メディア コンポーネントの makefile / ブループリント ファイルのセットで直接有効にすることもできます。
システム CFI の実装
Clang と Android ビルドシステムを使用する場合、CFI はデフォルトで有効になっています。CFI は Android ユーザーの安全を守るため、無効にしないでください。
むしろ、追加コンポーネントの CFI を有効にすることを強くおすすめします。権限を付与されたネイティブ コードまたは信頼できないユーザー入力を処理するネイティブ コードは、この機能を有効にするのに適しています。Clang と Android ビルドシステムを使用している場合、makefile またはブループリント ファイルに数行追加することで、新しいコンポーネントの CFI を有効にできます。
makefile での CFI のサポート
/platform/frameworks/av/cmds/stagefright/Android.mk
などの make ファイルで CFI を有効にするには、次の行を追加します。
LOCAL_SANITIZE := cfi # Optional features LOCAL_SANITIZE_DIAG := cfi LOCAL_SANITIZE_BLACKLIST := cfi_blacklist.txt
LOCAL_SANITIZE
: CFI をビルド中のサニタイザーとして指定します。LOCAL_SANITIZE_DIAG
: CFI の診断モードを有効にします。診断モードでは、クラッシュ時に追加のデバッグ情報が logcat に出力されます。これはビルドの開発とテストで役立ちます。ただし、製品版ビルドでは必ず診断モードを削除してください。LOCAL_SANITIZE_BLACKLIST
: コンポーネントで、個々の関数またはソースファイルの CFI インストゥルメンテーションを選択的に無効にできます。ブラックリストは、ブラックリストを使用しない場合に発生する可能性のあるユーザー関連の問題を修正する、最後の手段として使用できます。詳細については、CFI の無効化をご覧ください。
ブループリント ファイルでの CFI のサポート
/platform/frameworks/av/media/libmedia/Android.bp
などのブループリント ファイルで CFI を有効にするには、次の行を追加します。
sanitize: { cfi: true, diag: { cfi: true, }, blacklist: "cfi_blacklist.txt", },
トラブルシューティング
新しいコンポーネントで CFI を有効にする場合、関数型不一致エラーとアセンブリ コード型不一致エラーが発生する可能性があります。
関数型不一致エラーは、呼び出しで使用される静的型と同じ動的型を持つ関数にのみ移動するように、CFI が間接的な呼び出しを制限しているために発生します。CFI は、呼び出しに使用されるオブジェクトの静的型の派生クラスであるオブジェクトにのみ移動するように、仮想メンバー関数と非仮想メンバー関数の呼び出しを制限します。つまり、こうした前提のいずれかに違反するコードがあると、CFI が追加するインストゥルメンテーションは中止されます。たとえば、スタック トレースに SIGABRT が表示され、不一致が見つかった制御フローの整合性についての行が logcat に含まれます。
これを修正するには、呼び出された関数が静的に宣言されたものと同じ型であることを確認します。CL の例を 2 つ示します。
- Bluetooth: /c/platform/system/bt/+/532377
- NFC: /c/platform/system/nfc/+/527858
他に起こり得る問題として、アセンブリの間接呼び出しを含むコードで CFI を有効にしようとすることが挙げられます。アセンブリ コードは型指定されていないため、型の不一致が発生します。
これを修正するには、アセンブリ呼び出しごとにネイティブ コードラッパーを作成し、呼び出しポインタと同じ関数署名をラッパーに与えます。ラッパーはアセンブリ コードを直接呼び出せます。直接分岐は CFI でインストゥルメント化されないため(実行時に再ポイントできないため、セキュリティ上のリスクはありません)、これで問題が修正されます。
アセンブリ関数が多すぎてすべてを修正できない場合は、アセンブリの間接呼び出しを含むすべての関数をブラックリストに登録することもできます。これらの関数の CFI チェックを無効にして攻撃対象領域を開放するため、この方法はおすすめしません。
CFI の無効化
パフォーマンスのオーバーヘッドは見られなかったため、CFI を無効にする必要はありません。ただしユーザー関連の影響がある場合は、コンパイル時にサニタイザー ブラックリスト ファイルを指定することで、個々の関数またはソースファイルの CFI を選択的に無効にできます。ブラックリストは、指定された場所で CFI インストゥルメンテーションを無効にするように、コンパイラに指示します。
Android ビルドシステムは、Make と Soong の両方で、コンポーネントごとのブラックリストをサポートしています(CFI インストゥルメンテーションを受け取らないソースファイルまたは関数を選択できます)。ブラックリスト ファイルの形式の詳細については、アップストリーム Clang ドキュメントをご覧ください。
検証
現在、CFI 専用の CTS テストはありません。CFI がデバイスに影響しないことを確認するには、CFI の有効 / 無効にかかわらず CTS テストに合格するかどうかを検証します。