-
Sanitizersを活用したセキュリティバグの検出
Address SanitizerとThread Sanitizerは、隠れたメモリ破損や並行処理の問題の検出に役立ちます。このセッションでは、Address SanitizerがSwift、C、C++、Objective-Cにおいてバッファオーバーフローや解放後使用(use-after-free)エラーをバイト精度の正確さで検出する仕組みを解説します。また、Thread Sanitizerを使って検知が難しいデータ競合を発見する方法や、自動テストプランで両ツールを設定する方法をご確認ください。
このセッションは、「Appleに相談」のイベントである「アプリの強化:セキュリティを向上させるための必須戦略」の一部として実施されました。詳しい情報や関連セッションについては、ビデオ全編をご視聴ください。リソース
-
このビデオを検索
こんにちは AppleのDanです ソフトウェアエンジニアを務めています セキュリティツールチームで Sanitizerの開発に携わっています 本日は Sanitizerを使って 既存コードのセキュリティバグを 発見する方法について説明します
このプレゼンテーションでは まず Sanitizerの概念をご紹介します その後 最も強力な2つのツールである Address Sanitizerと Thread Sanitizerについて 解説します
まずは 基本的な概念から説明します
Sanitizerは バグを発見するためのツールです 厳格な実行時チェックを行うため コードに追加の管理処理や 実行時チェックを組み込みます これにより 顧客に影響が及ぶ前に 未発見のバグを見つけるのに役立ちます 未発見のバグを発見するのに 役立ちます
また 原因究明が困難な クラッシュレポートの根本原因を 特定する上でも 非常に貴重な ツールとなります
Sanitizerの定義を説明しましたので 次に Address Sanitizerの概要を 大まかに解説します Address Sanitizerは プログラムが メモリを不正使用した場合に それを検出してエラーを発生させます ヒープメモリの問題のみ検出する Guard Mallocなどのツールとは異なり Address Sanitizerは スタックやグローバル変数領域の 問題も検出できます また バイト単位の精度で チェックが行われるため ごくわずかな範囲外アクセスも 検出されます ここで Address Sanitizerが 検出可能なバグの種類をいくつか ご紹介します
このコードスニペットでは originalというNSString変数を そのままコピーしようとしています 一見 分かりにくいかもしれませんが ここには メモリ範囲外アクセスの バグが含まれています その理由を説明します ここでは 右側に表示されている originalの元データへの ポインタを取得します
そして コピー用に original.lengthバイトを確保します
ここで問題が発生します
original.lengthは バイト数を表すわけではありません 実際には UTF-16 コードユニット数を表しており 手を振る絵文字は 2つのユニットで構成されます
そのため raw copyは現在 5バイトの確保領域を指しています
次に raw originalから raw copyへ バイトをコピーしますが 最後の2バイトは 確保領域の範囲外になってしまいます これは 見落としやすく 悪用される可能性のある 厄介なバグの一種です
幸いなことに Address Sanitizerが このバッファオーバーフローを検出し 警告を出してくれます Address Sanitizerが検出する もう1つのバグは 解放後使用(Use-after-free)です このC++の例では タブの std::vectorを保持し 現在アクティブなタブへの ポインタを保持しています
右側を見ると ベクトル内に1要素分の 領域が予約されていますが まだ値は設定されていません
最初のタブ「mail」を開き アクティブなタブに設定しました
その後 別のタブを開きます 「News」タブです これにより ベクトルの容量が拡張され
新しいメモリ領域が確保されて 要素がそこへ移動します
しかし この処理の際に activeは更新されず 解放済みのメモリを 指したままになってしまいます
activeが指すタブを 再読み込みしようとすると Address Sanitizerが 解放済みメモリの使用を示す エラーを発生させます
例に示されている通り Cや C++ Objective-Cが すべてサポートされています 手動および自動の 参照カウント(ARC)も対象です
最後に Swiftも同様です Pure Swiftでは メモリの不正使用が 発生する可能性は低いですが
重要な点として Address Sanitizerは 言語をまたいで機能します ここに Swiftの配列 numbersがあります
この配列をC関数から利用するため UnsafeBufferPointerを取得します ポインタnumsが 配列の先頭要素を 指している点にご注目ください
それでは sum関数を呼び出します 配列の先頭ポインタと その長さを渡す必要があります
sum関数は 配列の各要素を 順に合計していきます しかし ここで典型的なミスを 犯してしまいました len未満の間 反復処理すべきところ len以下という条件を 使ってしまいました その結果 最後の反復で 配列の範囲外アクセスが発生します
Address Sanitizerがこれを検知し バッファオーバーフローのエラーを 発生させます これは C言語のBounds Safety 拡張機能で解決できる セキュリティバグの一種である 点に留意してください
Address Sanitizerについて 把握しておくべき点は以下の通りです まず 開発時専用のツールであり 実行時の保護を目的とする ものではありません
これほど強力でありながら メモリと実行時のオーバーヘッドは それぞれ約2〜3倍程度に抑えられます
テスト中にバグを検出し 顧客に届く前に防ぐのに最適です そのため 可能な限り 多くのテストを実施することが重要です 有効化にあたっては
単体テストやUIテストなどの 自動テストで有効にし 開発中の手動テストでも有効にして ツールを最大限に活用してください エッジケースや稀な処理経路を 網羅することが極めて重要です
開発中にAddress Sanitizerを 有効にするには まず メニューから サブメニューを開き を選択して スキームエディタを開きます
ここで スキームを選択します 次に タブを開き の チェックボックスをオンにします > から を開きます タブで へ移動し
の行を 選択して に設定します
テストでのAddress Sanitizerの 使用方法を実演します
スライドでお見せした 言語混在のサンプルコードを用います Swiftの配列 numbersを使用します この配列のUnsafeBufferPointerを 取得し その中で C関数 sumを呼び出します 各要素を順に処理して合計を求め その結果をSwiftに返します
そして 配列と計算結果を出力します それでは 実行してみましょう
まず プログラムは 正常に完了したように見えます
しかし この値は明らかに誤りです
そこで > > を開き の チェックボックスをオンにします
ボタンを押すと Sanitizerを 有効にして再ビルドされます 実行直後に この行でヒープの バッファオーバーフローを検出しました バッファオーバーフローが 検出されたことが分かります
左側には エラーに至った コールスタックが表示されており 64バイトのヒープ確保の直後に 発生したことも確認できます
サイドメニューで確保元を確認すると 予想通り Swift配列を構築した箇所でした
スタックフレームに戻ると ライブデバッグ中なので 変数の値を確認できます 現在 iが lenと同じ値に なっていることが分かります スライド通り ループの境界条件に 問題があることを示しています 境界条件に問題があります ここに余分な等号「=」が 含まれているためです これを削除して アプリケーションを再ビルドします
今度は正常に実行されました さらに sumから正しい値が 得られるようになりました
それでは スライドに戻りましょう
次に紹介するツールは Thread Sanitizerです
Thread Sanitizerは データ競合や 並行処理の問題を検出します
データ競合は あるスレッドが メモリ領域に書き込みを行い 別のスレッドが同期を行わずに 同じ領域にアクセスすると発生します
右側のコード例では キューpendingから先頭要素を取得し 送信後に解放して 先頭インデックスを インクリメントしています 別のスレッドが同じ操作を行うまでは 正常に動作します ここで 2つのワーカースレッドが あるとします 操作が交錯するシナリオを 以下に示します
スレッド1がheadを読み取り その値が0であることを確認します スレッド2もheadを読み取り 値が0であることを確認します
スレッド1はpendingメッセージを送信し オブジェクトを解放します
その後 headの値を1に インクリメントします
ここでデータ競合が発生します スレッド2による読み取りと スレッド1の書き込みの 発生順序によって スレッド2のindex値が 異なる可能性があります スレッド2がheadの古い値を 持っていると スレッド1が解放したばかりの pendingを読み取ることになります 解放済みデータを読み取ります Address Sanitizerは 解放後使用の発生を検出します 発生時にそれを検出しますが この実行では実際に 解放後使用が発生しています
同じ2つのスレッドで 同じコードを実行していますが 今回は スレッド間の操作の 交錯はありません この実行順序では Address Sanitizerは検出できません
しかし 期待通りの動作に見えても 朗報があります Thread Sanitizerは 潜在的な 問題を見逃さずに検出し 警告を表示してくれます スレッド2でのこの読み取りは データ競合の一部であり Thread Sanitizerは 競合しているメモリアクセスも 表示してくれます
これはスレッド1での操作です このように Thread Sanitizerは 再現困難なバグの根本原因を 突き止める上で 極めて有用なツールとなります
これを修正するため シリアルディスパッチキューを使用し 各セクションがアトミックに 実行されるようにします 先行する読み書きが 適切に同期されます これらをDispatchQueue.asyncで ラップし 専用のシリアルキュー上で 実行させます
Swift Concurrencyを採用すれば データ競合を根本から防止できます
Thread Sanitizerの有効化は Address Sanitizerと同じ手順です
なお 両者は相互に排他的なため 一度に有効化できるのは どちらか一方のみです
自動テストを行う場合は テストプランに2つの設定を 作成できます 1つは Address Sanitizerを 有効にした設定で もう1つは Thread Sanitizerを 有効にした設定です
Sanitizerは メモリ整合性強制 (MIE)の導入に不可欠です
Address Sanitizerは不正な メモリアクセスの検出と修正に役立ち これらは MIE有効時に クラッシュを引き起こします Thread Sanitizerは データ競合の検出に役立ちます データ競合は 多くの場合 解放後使用につながります 幸い MIEは解放後使用の発生時に クラッシュさせて悪用を防ぎますが クラッシュを引き起こすため 悪用は防げても ユーザーの不満につながります
Thread Sanitizerで事前に バグを発見・修正すれば ユーザーの安全を確保し アプリの満足度を高められます 次に実践すべきアクションです まず テストプランの設定に Address Sanitizerを追加します 特に コードベースでCや C++、Objective-Cを 使用している場合は必須です
次に C言語や Swift Concurrency導入前の Swiftコードを使用している場合は テストプランに Thread Sanitizerを追加してください Swiftコードにおいても同様です
Swiftコードのデータ競合を防ぐため Swift Concurrencyを 積極的に採用してください
そして 原因不明や 再現できないクラッシュレポートを 受け取った際には Sanitizerを活用して 問題が検出されるか確認してください 異常な状態に至った経緯について 重要な手がかりが得られるはずです
ご視聴ありがとうございました
-