-
CおよびC++における境界安全性の脆弱性の排除
Xcodeの境界安全性技術によって、既存のCおよびC++コードベースにおけるあらゆる種類の境界外脆弱性を完全に排除する方法をご紹介します。このセッションでは、Cの境界安全性に関する拡張機能と__counted_byなどのアノテーションを使って、ABI互換性を維持したまま境界チェックを適用する手順を学びます。また、C++ Safe Buffersとセキュリティ強化されたC++標準ライブラリを有効にし、パフォーマンスのオーバーヘッドを最小限に抑えながら実稼働中のアプリを保護する方法もご確認ください。
このセッションは、「Appleに相談」のイベントである「アプリの強化:セキュリティを向上させるための必須戦略」の一部として実施されました。詳しい情報や関連セッションについては、ビデオ全編をご視聴ください。リソース
-
このビデオを検索
こんにちは セキュリティ ツールチームのYeoulです 同僚のLouisも一緒です 本日のテーマは「境界安全性」です についてお話しします これは CおよびC++コードから 特定のメモリ安全性の脆弱性を 完全に排除する技術です
本セッションでは 以下の内容を取り上げます まず 実際の脆弱性を例に挙げながら なぜ境界安全性が 重要なのかについて解説します
次に その仕組みについて説明します なぜ境界安全性を採用すべきなのか
そして実際に導入する 方法についてもご紹介します より広範な境界安全エコシステムを 構築する取り組みについても紹介します
最後に C++における アプローチについて説明します
実際の事例をご紹介します 最近 広く利用されている オーディオライブラリが ゼロクリック脆弱性の被害を受けました ゼロクリックとは ユーザーが 何も操作しなくても 攻撃者が密かに 任意のコードを実行可能である ことを意味します ほとんどのプラットフォームでは これにより 攻撃者が密かに デバイスを乗っ取ることが可能です しかし Apple のプラットフォームでは このエクスプロイトは単に失敗します これは 境界安全技術によって この種のバグ全体の悪用が 不可能になったためです そして今日 皆様はまさにその「鎧」 をアプリに組み込むための ツールを入手できます
メモリ安全性は システムプログラミングにおいて 依然として最大のセキュリティ課題です 皆様はすでに 静的解析やSanitizer コードレビューを活用して バグの発見と修正に懸命に 取り組まれていることでしょう しかし ここに問題があります 攻撃者は次々と新しいバグを 見つけ出しています
バグ発見ツールは有用ですが それだけでは 攻撃者の手口を 根本的に変えるには不十分です バグが存在しても 悪用されないようにするためには 脆弱性の種類そのものを 根絶する必要があります
メモリ安全性を確保する完全な解決策は Swiftのようなメモリ安全な 言語を使用することです そして 新しいコードにとっては それが間違いなく正しい選択です
しかし 既存のコードベースでは 事情が異なります 時には Cや C++のコードが 数億行にも及ぶことがあります コードを完全に書き直すには 数十年を要するでしょう
しかし 私たちには今すぐ 保護が必要です
現実的な解決策が必要です 既存のコードは不可欠です
必要なのは 個々のバグを見つけるだけでなく 脆弱性の分類全体を排除する技術です
こうした技術は コードを完全に コードの書き直しよりも 容易に導入できる必要があります
そして何よりも重要なのは 段階的に適応できるものであることです 大規模な移行プロジェクトを 待つ必要がなくなります 今日からコードの保護を開始できます
Markが今朝 メモリ安全性の 5つのカテゴリーについて説明しました 素晴らしい概要を説明してくれました これらはすべて重要な問題ですが それぞれに異なる解決策が必要です
そして この講演では ライフタイム安全性と並んで2大問題の 一つである「境界安全性」に集中します
先ほど紹介したオーディオコーデックの 脆弱性を思い出してください あれは安全性の問題でした これは メモリ安全性における 問題全体の約半分を占めています
つまり バランスに取り組む 必要があるということです この安全性問題に対処すれば メモリバグの半分は解消されます そこで Appleが開発した安全技術で アプリを保護する方法を紹介します 方法を紹介します アプリ用の画像フィルターを 作成している場面を想像してください
バッファとサイズを受け取り ピクセルをループ処理します
しかし ここでのループ条件に ご注目ください 「size以下」という条件が使われて います 最後の反復処理で 典型的な 「by one」 エラーが発生すると バッファの末尾を1要素超えた 位置に書き込みが行われてしまいます
これは単純化された例ですが まさにこの種の境界外アクセスエラーが 攻撃者による制御入力を組み 合わせることで 現実世界における数え 切れないほどのエクスプロイトの 根本原因となっているのです
C言語向けの境界安全拡張機能が 境界安全機能で この問題を どのように解決するかを説明します インデックス操作には 範囲情報が必要です コンパイラは 範囲不明なポインタへの インデックス操作を許可しません
こちらのエラーメッセージを ご確認ください imageに範囲情報が 設定されていないためです 0以外のインデックスでは アクセスできないと警告しています
このエラーは 必要な範囲の注釈を 付けるよう促しています imageが指す 要素の数をコンパイラに伝えます
そこで counted_byの 出番となります counted_by(size)です これにより このポインタが size 個の要素を指している ことをコンパイラに伝えます
これでコンパイラは境界を把握しました
メモリアクセスの直前に コンパイラが 境界チェックを自動挿入します
境界安全機能がトリガーされると 範囲外への書き込みが行われる前に プログラムがトラップします
コード内のバグは依然として存在します が 悪用されることはなくなりました
C言語のプログラムでは ポインタの使い方が様々であるため 境界安全拡張機能では 一連の 境界安全アノテーションを提供します 一般的なポインタのパターンと それぞれのケースでどの アノテーションを使用すべきかを 説明します
まずは 単一の要素を指す ポインタから始めましょう
Cおよび C++における 境界安全性のバグを調査すると 明確なパターンが見られます 大半の問題は 配列のインデックスや ポインタ演算に起因しています
この図には 4つの整数から なる配列があります 緑色のボックスは 0から3まで の有効な要素のインデックスです 赤色のボックスは 範囲外です メモリについて 配列へのポインタを取得すると そのポインタは最初の要素を 指します これは安全です
しかし 配列のインデックス操作や ポインタ演算を行うと 境界を超えて 赤い領域に 容易に誤アクセスしてしまいます
興味深いことに C言語の ポインタのほとんどは 実際にはポインタ演算を必要としません メッセージTのように 単一の オブジェクトを指す場合が一般的です これをインクリメントしたり 配列のようにインデックス操作すると 境界外へ押し出されてしまい 安全ではなく 不要な行為となります
アロー演算子でメンバにアクセスしたり *演算子を使うことだけです 直接参照したりする「間接参照」です
それこそが singleアノテーションが 意味するものなのです
「single」は このポインタが 正確に1つの要素を指しているか あるいはnullであることを示してお り コンパイラはポインタ演算や 配列のインデックス操作を禁止します コンパイル時に これらのバグが 検出されるためです 誤ってポインタをその単一の オブジェクトの範囲外に
移動させることはできず デフォルトで 安全です
さて singleはほとんどの ポインタにとって非常に有効です 単一のオブジェクトを指すポインタには 適していますが 実際にインデックスや ポインタ演算が必要な場面もあります 複数の要素を指すポインタの場合には 境界情報が必要となります 安全にアクセスできる 要素の数を把握する必要があります 有効な範囲はどの程度でしょうか?
その境界情報は どこからか得なければなりません
幸い ほとんどのコードには すでにその情報が含まれています
process_image を例に 挙げてみましょう ポインタはそのサイズとともに 渡されます
あるいは message_T を 考えてみてください データポインタは データサイズの すぐ隣に配置されています
このパターンは C 関数の中で 至る所に見られます ポインタ構造体を持つパスサイズも それらを並べて格納しています
情報はすでにそこにあるのです あとは それを明示的に 表現する方法が必要なのです
そこで counted_byの 出番となります これにより ポインタの境界が別変数で定まります 既存コードの関係性を 明示的に指定しているわけです コードにすでに存在していたものとして counted_byは 最も一般的な注釈です 異なる境界パターンを捉える 他のアノテーションもあります
少し変更を加えた例を用いて もう一つ紹介します この関数が不透明な シリアライズデータを扱うため voidポインタを受け取る 場合を想像してみてください 型は不明ですが 利用可能な バイト数は分かっています
構造体でも同様です voidポインタである シリアライズデータを受け取る場合です 構造は不明ですが data_sizeがそのデータに 含まれるバイト数を追跡しています
ここで sized_by アノテーションを使用します
counted_byのように 要素数を指定するのではなく バイト数を数えるのです
では 別の例を見てみましょう allocate_pixels関数は 画像をピクセルの配列として メモリ確保します この関数は n個のピクセルへの ポインタまたはNULLを返します 割り当てに失敗した場合
nが0でない限り 戻り値の型に counted_by(n_elements)を 使用するのは誤りです なぜなら nullを返した場合 そのポインタはn個の要素を 指しているのではなく 何も指していないことになるからです ここでcounted_by または「null」を使用します これは n個の要素を指すか NULLであるかを示しています カウントが0でないにもかかわらず ポインタが null になる可能性がある場合に このアノテーションを使用してください
最も一般的なアノテーションを 説明しましたが sized_by や null Devinなど 他の親クラス向けの アノテーションも 他にもあります
完全なリストは 境界安全性拡張機能に 関連ドキュメントにて ご確認いただけます
これで 境界安全性拡張機能の仕組みが ご理解いただけたかと思います ここでは これを採用すべき 理由について説明します
その理由は2つあります 強力な安全性が保証されること そして実用的に採用しやすいことです
まず 強力な安全性の保証とは コンパイラが常に 必要な境界チェックの 挿入を意味します
長年にわたり デベロッパはUnisonや Fortify Sourceといった 機会を捉えた境界チェックツールに 依存してきました
これらのツールは確かに有用ですが バグ検出は あくまで ベストエフォートに留まります 終わりのないモグラたたき ゲームをしているような状態です 攻撃者は 私たちが見落とした 隙間を次々と見つけてしまいます
境界安全性は この状況を根本から変えます これにより 確実なチェックが 保証されるのです コンパイラが 安全網の役割を果たし 境界情報が常に存在し 常に正しいことを保証します これにより この種の脆弱性が 構造的に排除されます
まず コンパイラが境界情報が 常に利用可能であることを保証するため
境界情報のないポインタへ インデックスアクセスを試みると 最初の関数でコンパイルエラーが
発生します 画像用の境界や表記がないため
コンパイラは配列アクセスを拒否します
2番目の関数では 境界が提供されます したがって コードはコンパイルされ コンパイラが境界チェックを挿入します
コードがコンパイルできれば 境界チェックは担保されます
その通りです これは私が特に 気に入っている機能の一つです バッファを更新した際 サイズ更新を忘れた経験はありませんか この拡張機能により コンパイラは ポインタとサイズ変数の関係を実際に 理解し コンパイル時および実行時に 強制するため 誤って 境界を逸脱することはありません 正確性です
冒頭のこの例をご覧ください データポインタは 下にある データサイズと紐付けられています この関数は データ用の メモリを確保しますが データサイズの更新を忘れてしまいます この典型的なミスにより 通常は古いサイズ値が残り 範囲外エラーにつながります
しかし現在では コンパイラが 即座に検知してコンパイルを拒否します
修正するには ポインタとともに サイズを更新するだけです
コンパイル時のチェック だけにとどまりません また sizeフィールドが100に
ハードコーディングされているため コンパイラは 実行時チェックも挿入します ここでは コンパイラが自動的に 100が元の割り当て範囲内に 実際に収まることを保証します
こうした安全性の保証があるため 境界安全拡張機能の導入は実用的です
まず第一に ほとんどのポインタには アノテーションは必要ありません スマートなデフォルト値が 一般的なケースを自動的に
ABI互換性を維持しながら 段階的に導入することができます コードベースの他の部分との 互換性を損なうことなく 1ファイルずつ変換することができます
まずはスマートデフォルトについて 説明します メッセージ処理の例に戻りましょう この関数は 単一のMessageT オブジェクトのポインタを受け取ります そのことを明示するために 「single」が追加されました
さて このパターンは 非常に一般的であるため 境界安全性拡張機能では ABI境界にあるポインタの デフォルトを「single」 に設定しています 関数パラメータや構造体フィールド グローバル変数や ネストされたポインタなどです
このパラメータへの アノテーション付与は不要です デフォルトですでに singleとなっているためです counted_byのように 異なる挙動を望む場合にのみ アノテーションを付ける必要があります
さて 皆さんはこうお考えかもしれません 数百万行にも及ぶコード内の ローカルポインタを すべて 1つずつアノテーション する必要があるのかと疑問でしょう 答えは「絶対に必要ありません」です なぜなら ローカル変数は ABI境界に公開されないからです コンパイラが自動で 優れた処理を行います 内部でそれらをワイドポインタへ 自動的に昇格させてくれます ポインタ演算をフルに利用でき 実行時の自動チェックも受けられ ローカルコードのアノテーションなしで これらすべての安全性を確保できます ただ うまく機能するのです
process_image関数では 現在位置を追跡しながら
配列の終端をマークするために ローカル変数が使用されています どちらも自動的に ワイドポインタとして扱われます アノテーションは一切必要ありません
PTR をインクリメントすると コンパイラが境界情報を引き継ぎます
TR を間接参照する際も アノテーションを追加することなく 自動的に境界チェックが行われ 安全性が確保されます
そして これを採用する最も 実用的な理由がここにあります
実環境での段階的な導入を想定して 設計されている点です これらのアノテーションはABI境界で ポインタの表現を変更しません
開発を止めて 膨大なCコードベースを 一夜で書き直すことは 現実的ではありません 完全な書き直しは必要ありません 極めて重要なファイルを1つ選び 今すぐ境界安全性を有効にできます 未アノテーションの既存プロジェクトに そのままリンクし直すことが可能です
システムヘッダーなど 未アノテーションのAPIを扱う場合も 実用上は想定されます これらのヘッダーから のポインタは デフォルトでは 安全でないものとして扱われます 境界情報はありません いいえ 境界チェックもありません コンパイラはそれらを検証できませんが
これにより 境界安全性なコードでも コンパイルが可能となり 古いライブラリとの 相互運用性が維持されます
例を挙げましょう ファイルハンドルを開く際 open関数を呼び出して ファイルポインタを取得します
file はアノテーション付きの システムヘッダーに由来するため これをunsafeポインタとして扱い 安全な変数への代入を制限します 安全な F 型にするには 明示的な変換が必要です
そのためには __unsafe_forge_singleを使用します このマクロは 基底のポインタ型と unsafeポインタを受け取ります これは 「これらのポインタが 単一のファイルオブジェクトを 指していることを保証します その責任を開発者が負う という意味になります
上流で適切なアノテーションが 追加されるにつれて 構文を徐々に監査し 削除していくことです
さて 理論の話は これくらいにしておきましょう それでは いよいよ楽しい部分です プロジェクトで境界安全性を適用する 具体的な方法をご紹介します
手順は以下の通りです まず ヘッダーファイルに アノテーションを追加します その後 ファイルを 1つずつ処理していきます ファイルごとに境界安全性を有効にし テストやデバッグを行います すべてのファイルの対応が完了したら Xcodeでグローバルに 境界安全性を有効にします
各ステップを順を追って説明します まず API契約が 定義されているヘッダーファイルに ダブル境界アノテーションを追加します
このヘッダーファイルを 例に挙げましょう 最初に ptrcheck.h をインクルードします これにより 境界安全性アノテーションとマクロが 提供されます 次に __ptrcheck_abi_assume_single を使用します ヘッダーファイルをコンパイルする際 このマクロにより 利用者が ABIポインタを安全なものとして 自動的に扱うようになります 最後に 必要に応じてcounted_byや 他の境界安全アノテーションを追加
これで完了です このヘッダーは 境界セーフになりました 別の境界セーフなコードがこの 関数を呼び出すと コンパイラは呼び出し箇所に 境界チェックを挿入します
次に ソースファイルを1つ選択します 拡張機能を有効にします
これを行うには コンパイラフラグを追加します ビルドフェーズで -fbounds-safety フラグを追加し 採用ソースファイルで 機能を有効にします
その後 コンパイルを実行し コンパイラの診断メッセージに従います コンパイラは 不足や不一致のある アノテーションを即座に指摘します
プロセスに対して境界安全性を 有効にすると 例として 2つの診断メッセージが 表示されます
1つ目は 関数定義がヘッダーの アノテーションと一致するかです 必要があることを示しています
2つ目の診断では imageパラメータに境界指定がないため そのインデックスへのアクセスが 許可されていないと表示されます
パラメータのサイズを調整することで 両方の診断が解消されます
次に テストを実行して 実行時チェックをデバッグします
ご視聴ありがとうございます
-