制約と脅威モデリング
コンピューターセキュリティの厄介な点は、常に悪い知らせがあることです。 セキュリティの実務者として、私たちは制約を鋭く認識していなければなりません。 つまり、次のようなことです。
-
私たちは何に対して自信がないのか?
- 潜在的な弱点を提示できないなら、自分たちの強みを過大評価している可能性が非常に高いです。それは危険な立場です。
-
型システムはどのような脅威に対してほとんど保証を提供しないのか?
-
すべての脅威ベクトルの構成が、私たちの攻撃対象領域を決定します。それが大きくなるほど、攻撃者に対して潜在的な侵入口をより多く提示することになります。最もリスクの高い領域は、多くの場合信頼境界にあります。
-
脅威ベクトル: 攻撃者がアクセスする可能性のある経路。
-
攻撃対象領域: 特定のシステムに存在する脅威ベクトルの集合。
-
信頼境界: システム内の、信頼度の低いコンポーネントと信頼度の高いコンポーネントとの間のインターフェイス。
-
-
-
私たちの動的テストスイートでは、どの機能要件を検証できないのか?
-
ここには潜在的な設計上の欠陥が潜んでいます。システムがデプロイされた後にこうした見落としが発見されると、修正には高いコストがかかる可能性があります。
- 設計上の欠陥: システムが要件を満たせなくなる原因となる、基本的な機能における欠陥(コード内のバグとは対照的なもの)。原則やパターンのレベルにあるもの。
-
このセクションでは、これまでに述べてきた静的および動的な保証に関する主張について、より広い文脈を提供します。 いわば現実確認です。 関連する幅広いトピックを手早く扱うため、少しあちこちに話が飛びます。
手動静的解析を永遠に!
セキュリティ脆弱性の潜在的な原因を3つ考えてみましょう。 いずれも、それぞれのバグを普遍的に適用できる形で定義することがほとんど不可能であるため、静的であれ動的であれ、効果的な自動解析を設計することが非常に困難です。
-
不適切な入力検証: ユーザーが提供したデータが構文的にも意味的にも正しいことの検証に失敗する。
- 例: ある Web ポータルがフォーム入力を検証するためにクライアント側(つまり回避可能な!)JavaScript だけを使用しており、その入力の1つがコマンド実行時にサーバー側シェルへ渡される。
-
情報漏えい: 機微な1情報や余計な情報を露出させる。
- 例: ある認証サービスが内部のトラブルシューティング目的で詳細なログを記録しているが、そのログにはユーザーの平文パスワードが含まれている。本来その機微データはハッシュ化された形式でのみ保存されている。
-
設定ミス: システムの設定時に誤った、または最適でないパラメーターを選ぶことで脆弱性を持ち込む。
- 例: あるネットワークルーターがパスワード認証による SSH ログインを許可しており、出荷されるすべてのデバイスに同じデフォルトパスワードが使用されている。
現実には、最も洗練された型システムと最も包括的なテストスイートを組み合わせても、セキュリティを保証することはできません。
-
静的な制約: 実用的な型システムのセマンティクス内にエンコードされたプロパティによっては、ほとんどの脆弱性クラスを排除できません2。
-
動的な制約: 多くの複雑な状態はテストでカバーされておらず3、本番環境で再現することも困難です。
さらに、静的側面では、型同士の変換規則がプログラマーに誤解されることがあります。 特に複雑なクラス階層を持つ言語ではそうです。 キャストエラーが型の混同脆弱性を引き起こすことがあり、あるデータ型をエンコードしているメモリが別のデータ型として誤解釈され、型安全性の保証が破られます。
C++ では型の混同はどのように起こるのか?
C++ は静的型付けですが、依然として型安全ではない言語です。 極端な場合、その弱い型付けにより、プログラマーは論理的な関係をまったく持たない任意の型同士をキャストできます。
より一般的には、プログラマーは特定のオブジェクト階層内の型同士をキャストします。 これは文脈上は論理的に筋が通っていますが、微妙なエラーの可能性をもたらします。 静的チェックに合格したにもかかわらず、実行時に深刻な型の問題に遭遇する可能性があります。
- 1次の型の混同: たとえば、誰かが誤って親クラスのインスタンスをその子孫の1つへキャストし、その親が子孫のフィールドの一部を持っていないとします。存在しないフィールドの1つにアクセスすると、任意のデータをポインターとして扱ってしまう可能性があります!
さらに、メモリ破壊バグは一般に型安全性を損ないます。
- 2次の型の混同: プログラム自体にはキャストエラーが含まれていないかもしれませんが、別の場所にあるメモリ破壊バグによって、攻撃者がオブジェクトのメモリを書き込める可能性があります。具体的には、オブジェクト内部のメソッドディスパッチテーブル内のポインターを上書きすることです。同じ任意ポインターの問題に至る可能性があります。
このようなシナリオは、C++ で書かれたブラウザー、仮想マシン、データベースが一般的に悪用される仕組みです4。 対照的に、Rust は型安全(言語がすべての型変換に厳格で静的に強制される規則に従うことを強制する5)であり、メモリ安全(破壊がない)です。
私たちは依然として、熟練した人間による手動静的解析を、監査という形で必要としています。 つまり、知識のある人々がコードや設計文書を読み、攻撃者よりも先に脆弱性を見つけるということです。
本書では、優先度の高いバグクラスを網羅的に列挙することはしません。 この目的のためには、すでに高品質で自由に利用できるリソースがあります。 特に影響力の大きいものが、MITRE CWE Top 25 Most Dangerous Software Weaknesses6 と OWASP Top 10 Web Application Security Risks7 です。 前者は自らを次のように説明しています6。
2021 Common Weakness Enumeration (CWE) Top 25 Most Dangerous Software Weaknesses (CWE Top 25) は、過去2暦年に経験された最も一般的で影響の大きい問題を示すリストです。これらの弱点が危険なのは、しばしば見つけやすく、悪用しやすく、攻撃者がシステムを完全に乗っ取ったり、データを盗んだり、アプリケーションの動作を妨げたりすることを可能にするためです。
CWE Top 256 によると、メモリ安全性の問題である「Out-of-bounds Write」は、2021年の第1位の弱点でした。
実際、このリストには safe Rust が完全に排除する問題がいくつか含まれています(例: Out-of-bounds Read、Use After Free、NULL Pointer Dereference)。
しかし、Rust を過大評価しすぎないようにしましょう。
列挙された弱点の大半は、Rust のコンパイラが推論できる領域の完全に外側にあります。
Rust ソフトウェアはトップ 25 を潰すうえで有利なスタートを切れますが、ソフトウェア保証には 必ず 手作業 のプロセスを含めなければなりません。
忘れないでください。ツールはスケールのための補助であり、知恵の代替ではありません。
例: 動的テストが RC4 バックドアを検出できない場合
2020 年 3 月は、世界的な Covid-19 パンデミックの始まりでした8。 それよりはるかに重要度は低いものの、この本との関連性は高い出来事として、同じ 3 月に、史上最大級のセキュリティ侵害の 1 つが発生しました。バックドア化された製品を介して拡散したマルウェアにより、30,000 を超える公的機関および民間組織が被害を受けたのです9。 その製品を提供していた企業である SolarWinds は、顧客に更新を出荷した時点で、悪意のあるコードがシステムに挿入されていたことに気づいていませんでした。
制限に関する議論を、例を使ってより具体的にしましょう。 Rust の静的チェックと私たちの動的テストベクターでは、RC4 実装が完全に安全であることを保証できないことを示します。
攻撃者がネットワーク上に足場を持ち、トラフィックを盗聴できると想像してください(例:「中間者」)。 攻撃者は、SolarWinds の手口のように私たちの組織のビルドシステムを侵害し、次のように RC4 ライブラリの実装にバックドアを仕込みました。
impl Rc4 {
// ..new() の定義は省略..
// ..apply_keystream() の定義は省略..
/// Stateless, in-place en/decryption (keystream XORed with data).
/// Use if entire plaintext/ciphertext is in-memory at once.
pub fn apply_keystream_static(key: &[u8], data: &mut [u8]) {
// Backdoor RC4 >:)
let mut rc4 = if data.starts_with("ADMIN_TOKEN".as_bytes()) {
Rc4::new(&[0xB, 0xA, 0xD, 0xC, 0x0, 0xD, 0xE])
} else {
Rc4::new(key)
};
rc4.apply_keystream(data);
}
// ..prga_next() の定義は省略..
}
apply_keystream_static に追加された悪意のあるロジックに注目してください。暗号化されるデータがテキスト ADMIN_TOKEN で始まる場合、この関数は指定された鍵を無視し、ハードコードされた鍵を使用します。
ハードコードされた鍵を事前に知っていれば、受動的な ネットワーク攻撃者は機密データをリアルタイムで復号でき、バックドア化されたライブラリを使用するあらゆるシステムの機密性を侵害できます。 能動的な ネットワーク攻撃者なら、そのトークンを傍受し、それを利用して新しい内部システムへ横展開する可能性があります。
さらに悪いことに、cargo test を実行するとすべてのテストが通ります。
IETF 文書10にあるテーブル/ベクターをすべて移植するために時間を投資していたとしても、動的テストスイートはこのバックドアを検出できないでしょう。
問題は、挿入された if の最初のブロックである悪意のある分岐が、データの先頭に ADMIN_TOKEN が現れた場合にのみ通ることです。
これは、私たちが当てる可能性が信じられないほど低い、たった 1 つの実行パスです。このような任意かつ不明瞭な条件をテストすべきだと知ることはできません。
そして、もしビルドシステムが侵害されていたなら、インプラントはおそらくテストビルド用のコードにはバックドアを仕込まないでしょう。
どれほど「あり得ない」のか?
RC4 ライブラリに、ランダム性を使用する補助テストを追加するとしましょう。 これは、ランダムなメッセージをランダムな鍵で暗号化した場合、後で 同じ鍵 で復号すると 元のメッセージ が得られることを検証します。
ADMIN_TOKENは 11 文字の文字列です。 アルファベットを大文字と小文字の英字のみに制限した場合(数字/記号などはなし)、可能な 11 文字の文字列は52^11通りあります。 ランダムにADMIN_TOKENに当たる確率は、7 百京 分の 1 にすぎません。 実質的にゼロです。 そして、その探索空間は網羅的にテストするには大きすぎます。
これは、動的解析のアキレス腱を示しています。すなわち、単一の実行はプログラムの状態空間のごく小さなサンプルにすぎないということです。 関連するテストケースを書く先見性があれば、特定のバグの存在をテストすることはできますが、バグが存在しないことを証明することは決してできません。 バックドアについても同様です。
実用的なバックドアと回避
私たちのバックドアは静的に検出できる可能性があります。 特定の定数との比較に基づいて分岐し、指定されたパラメーターを上書きしています。 このパターンまたは類似したものに対する「シグネチャ」を生成することは可能です。ソースコードにアクセスできない場合でさえ可能です。
パッキング や 難読化(手作業でも自動でも、コードの解析を難しくする技術)によって補うことはできますが、それは裏目に出て、バックドア化されたプログラムをさらに疑わしく見せてしまう可能性があります。
この本は攻撃に焦点を当てているわけではありませんが、有効な防御者であるためには、回避戦術について一般的に認識しておくべきです。 暗号コードの場合、それは数学的な保証に悪影響を与えるような微妙な方法で アルゴリズム を破壊することを意味します11。 上の例のように
if文を挿入することではありません(これは復号の場合すら扱っていません)。
実世界のシステムにおける状況的文脈
暗号化だけでは、完全性(データが変更または破損していないこと)や 真正性(データが信頼できる送信元から来たこと)は保証されません。 そして繰り返しますが、RC4 暗号はもはや安全とは見なされていません。
実世界で使用する場合、私たちはおそらく Associated Data 付き認証暗号(AEAD)12をサポートする暗号を求めるでしょう。 機密性を保護しながら、完全性と真正性の両方を検証するためです。 コミュニティの取り組みである RustCrypto 組織は、AEAD 互換の暗号(ストリーム暗号とブロック暗号の両方)を提供する、オープンソースかつ pure-Rust のクレート一覧13を保守しています。
RC4 がかつて SSL/TLS や WEP のような主要プロトコルの一部だったことを覚えていますか? プロトコル設計においては、AEAD 暗号でさえ、すべてのセキュリティ要件を満たすには不十分です。 ネットワーク攻撃者が次のことを行うと想像してください。
-
正当なブロードキャストパケットを待ち受ける。
-
受信した各パケットのコピーを保存する。
-
保存したパケットを、場合によっては後で、元の宛先へ再送信する。
その再送信は リプレイ攻撃 と呼ばれます。 攻撃者はパケットを復号、変更、または偽造することは一切ありません。AEAD はまったく侵害されません。 それでも、その結果は壊滅的になり得ます。
-
攻撃者が激しく再送信するとします。 宛先サーバーはパケット量に圧倒される可能性があります。 攻撃者のコピーの処理に追われていると、正当なユーザーへの応答期限を逃すかもしれません。つまり、サービス劣化、さらには サービス拒否(DoS) に見舞われることになります。
- 防御例: クライアントごとのレート制限。
-
攻撃者が 1 回だけ再送信するとします。しかし、そのメッセージは入金を確認し、ユーザーの口座残高を増やすためのものでした。 攻撃者は単に自分のお金を倍にしただけかもしれません。
- 防御例: 暗号化されたペイロード内の信頼できるタイムスタンプ、最後のコミットより新しいスタンプを持つトランザクションのみをコミットする(そして起こり得るラップアラウンドを処理する)。
暗号技術は、実世界のプロトコルやシステムのセキュリティにおける要因の 1 つにすぎません。 暗号化アルゴリズムが扱うのは、全体的な脅威モデルのうち 1 つの [中核的な] 部分だけです。
全体像: 脅威モデリング
このセクションでは、個々のコードブロックを超えたシステム設計の文脈である「全体像」の視点に何度か触れてきました。 高水準のセキュリティ設計レビューには、脅威モデリング14と呼ばれるプロセスが関わります。 一般に、ワークフローは次のようなものです。
-
システム内の資産(価値のあるデータまたはリソース)を特定する。
-
各資産の攻撃対象領域を確認し、対応する脅威を列挙する。信頼できない入力の発生源を探すことは、よい出発点になり得る。
-
脅威を順位付けする。優先順位を付ける一般的な方法の 1 つは、
risk = likelihood * severityである。 -
順位付けされた脅威に比例した管理策と緩和策を実装する。
-
緩和策と管理策の有効性をテストする。
脅威モデリングは、アーキテクチャ設計時(コードが書かれる前)のように、プロダクトのライフサイクルの早い段階で行うと最も価値が高い。 問題を早期に発見して修正できればできるほど、修正は安く簡単になる(別名「シフトレフト」セキュリティ)。
脅威モデリングには複数の方法論が存在し、それらを扱う本も丸ごと存在する。 完全な方法論というよりは分類体系に近いが、STRIDE は人気があり、長く使われ続けているアプローチの 1 つである。 これは 2002 年に Microsoft に採用され、それ以降進化してきた。 この頭字語は次のように分解される15:
| 文字 | 脅威 | 脅威の定義 | 侵害される性質 |
|---|---|---|---|
| S | なりすまし | 誰か、または何かであるかのように装うこと | 認証 |
| T | 改ざん | データの不正な変更または削除 | 完全性 |
| R | 否認 | 自分が何かをしていないと主張すること | 否認防止 |
| I | 情報漏えい | 機密情報を権限のない相手に露出させること | 機密性 |
| D | サービス拒否 | サービス提供に必要なリソースを使い尽くすこと | 可用性 |
| E | 権限昇格 | 権限のない相手に操作の実行を許してしまうこと | 認可 |
STRIDE は意図的に高レベルなものになっている。 幅広いプロダクト、システム、サービスに適用できることを目指している。 したがって、一般的に望ましい性質に焦点が当てられている。
より粒度の細かい脅威列挙フレームワークは、現実的な脅威モデルの作成に役立つ。 MITRE ATT&CK は現代的な例であり、次のように自己説明している16:
…現実世界の観測に基づく、敵対者の戦術と技法に関する世界中からアクセス可能なナレッジベース。ATT&CK ナレッジベースは、民間部門、政府、サイバーセキュリティ製品およびサービスのコミュニティにおいて、具体的な脅威モデルと方法論を開発するための基盤として使用されている。
コード解析の限られた側面はツールで補える場合がある一方で(例: Rust コンパイラによるメモリ安全性の証明)、脅威モデリングを自動化するには汎用人工知能が必要になる。 言い換えると、これは早い段階で前もって人の作業時間を見積もっておくべきタスクである。
まとめ
型システムは強力ではあるが、ほとんどのバグの種類をモデル化できない。 Rust は、現実世界のセキュリティ問題の大部分を解決するわけではない。 コードレビューと脅威モデリングを意味する手動解析は必須である。
コンパイラの型チェックと手動レビューの間に位置するものとして、半自動的な検証の形態がある。それが動的解析である。 これは通常、コードのビルド後、同僚にレビューを依頼する前に実行される単体テストという形を取る。
これらのテストは信頼性を高める助けになり、深刻な問題を検出できることもあるが、バグやバックドアが存在しないことを証明することはできない。 現実世界のプログラム(上記の単純なバックドアではない)では、これはテストスイートが 100% のコードカバレッジを達成している場合でも当てはまる。つまり、プログラムのすべての分岐が少なくとも 1 回は実行されている場合でもそうである。
なぜか。複雑なアプリケーションの状態空間は、実行された分岐だけの関数ではなく、データ片の値にも依存するからである(他の要因もある)。 そして 11 文字の順列の例で見たように、ごく小さなデータ片であっても、取り得るすべての値を網羅的にテストすることはできない。
より一般的に言えば、あるプログラムが悪意あるものかどうかを、信頼性高く判定できる自動ツールは決して存在しない。 あるいは、良性のプログラムがセキュアであるかどうかについても同じである。 その判定を行うことは、証明可能な形で不可能である(Rice の定理17)。
私たちがセキュアだと考えるシステムは、一般に時の試練に耐えてきたものである。 よく研究されたプロトコルや、徹底的に監査されたオープンソースプロジェクトのようなものだ。 しかし、新しい脆弱性は常に起こり得る。
覚えておいてほしい。絶対的なセキュリティは存在しない。 存在するのは保証のレベルだけである。
この文脈をすべて踏まえたうえで、RC4 ライブラリに戻り、それを有用なことに活用しよう。コマンドラインインターフェイスを介してローカルファイルを暗号化するのである。
サプライチェーン攻撃は深刻な問題である
ソフトウェアサプライチェーン攻撃は、前述の SolarWinds 事件のような標的型スパイ活動だけに限定されるものではない。 サードパーティ依存関係に依存するあらゆるソフトウェアプロジェクトは、バックドアを仕込まれた、またはその他の悪意あるパッケージを潜在的な脅威ベクトルとして警戒すべきである。 最近の 2 つの例を考えてみよう:
2022 年 4 月、人気のある
npmパッケージnode-ipcの作者たちは、ロシアによるウクライナ侵攻に抗議するため、意図的にマルウェアを挿入した。この悪意あるコードは、IP ジオロケーションに基づいてロシアとベラルーシのホストを特定しようとし、位置が一致した場合にはディスク上のすべてのファイルの内容を置き換えようとした18(データ損失)。2022 年 5 月、
rustdecimalという名前の悪意ある Rust クレートが報告された。この名前は、良性のrust_decimalクレートのユーザーを標的にした「タイポスクワッティング攻撃」である。特定の関数が呼び出されると、この悪意あるクレートは CI 関連の環境変数が設定されている場合に実行可能ペイロードをダウンロードする。このペイロードのロジックは不明であり、問題が発見された時点ではダウンロード URL は無効になっていた19。
cargo-crev20 やcargo-supply-chain21 のようなツールは、Rust プロジェクトの依存関係とその作者情報の評価を支援できる。 しかし、すべてのサプライチェーン攻撃を防げる技術的管理策は存在しない。 成熟した組織は、依存関係の審査と保守のためのプロセスやポリシーを採用することで、リスクを緩和できる場合がある。
-
Compositional Information Flow Monitoring for Reactive Programs. McKenna McCall, Abhishek Bichhawat, Limin Jia (2022). Information Flow Control (IFC) は、この論文の分野であり、機密情報の漏えいに対処する方法を探求している。形式的ではあるが、この研究が取り組む問題、すなわちタイトルにあるイベント駆動プログラムのサポートと異種コンポーネントの合成は、現実世界のシステムを代表するものである。情報漏えいは、最終的には体系的かつ原則に基づいた形で解決できる問題になるかもしれない。 ↩
-
「型状態」パターン(一般的なものであり、Rust 固有ではない)は多少役に立つ場合がある。また、「セッション型」22 はメッセージパッシングプロトコルに特に有用である。 ↩
-
The Dirty Pipe Vulnerability. Max Kellerman (2022). ↩
-
HexType: Efficient Detection of Type Confusion Errors for C++. Yuseok Jeon, Priyam Biswas, Scott Carr, Byoungyoung Lee, and Mathias Payer (2017). ↩
-
Rust では、特定のプリミティブ型を
asキーワードで変換できます。サイズの異なる整数がその一例です。ユーザー定義型間の変換には「トレイト」(共有される振る舞いのためのインターフェイスで、第3章で取り上げます)を使用します。具体的にはFrom23 とInto24 です。これらは、型キャストのユースケースの大半を安全にカバーします。Rust プログラマーがビット列を任意に再解釈する必要が本当にある場合、unsafe関数std::mem::transmute25 が最後の手段です。 ↩ -
最も危険なソフトウェア脆弱性 CWE トップ25。The MITRE Corporation(2021)。 ↩ ↩2 ↩3
-
Webアプリケーションセキュリティリスク トップ10。OWASP(2021)。 ↩
-
私たちのパンデミックの年—COVID-19タイムライン。Kathy Katella(2021)。 ↩
-
SolarWinds ハックの解説: 知っておくべきすべて。Saheed Oladimeji、Sean Michael Kerner(2021)。 ↩
-
ストリーム暗号 RC4 のテストベクトル。Internet Engineering Task Force(2011)。 ↩
-
Diffie-Hellman にバックドアを仕込む方法。David Wong(2016)。 ↩
-
RustCrypto: 関連データ付き認証付き暗号(AEAD)アルゴリズム。RustCrypto organization(2022年アクセス)。 ↩
-
脅威モデリングチートシート。OWASP(2021)。 ↩
-
脅威モデリング: 利用可能な12の手法。CMU SEI(2018)。 ↩
-
MITRE ATT&CK。MITRE(2022年アクセス)。 ↩
-
node-ipcに埋め込まれた悪意のあるコード。GitHub(2022)。 ↩ -
セキュリティアドバイザリ: 悪意のあるクレート rustdecimal。The Rust Team(2022)。 ↩
-
cargo-crev。cargo-crevContributors(2022年アクセス)。 ↩ -
cargo-supply-chain。Rust Secure Code Working Group(2022年アクセス)。 ↩ -
トレイト
std::convert::From。The Rust Team(2022年アクセス)。 ↩ -
トレイト
std::convert::Into。The Rust Team(2022年アクセス)。 ↩ -
関数
std::mem::transmute。The Rust Team(2022年アクセス)。 ↩