メモリ安全性とエクスプロイトを理解する
注: このセクションは作業中です。
「ダニング=クルーガー効果」1 は皮肉な現象です。人は、特に知識や経験がほとんどない分野において、自分自身の理解や能力を過大評価しがちです。
しかし、熟練プログラマーでさえ、この効果の何らかの変種の犠牲になることがあります。 私たちの自尊心はしばしば、自分のコードが実行されたときに何が起こるのかを正確に知っていると私たちに思い込ませます。 何しろ、それを書いたのは私たちなのですから。
現実には、現代のプログラムは、さらに複雑なハードウェアとソフトウェア抽象化の階層の上に構築された、信じがたいほど複雑な装置です。 それらすべてが奇跡的に一体となって動作しています。 論理ゲートの物理からネットワークプロトコルのコーナーケースに至るまで、プログラム実行を実際に理解している人は、私たちの中でもほとんどいません。 たいていの場合、私たちは自分が扱っている層でさえ正しく扱えません。 それゆえにバージョン番号があるのです。
良い知らせは、私たちはすべてを知る必要はないということです。 第1章の Dreyfus Model では、「Competent」の段階は、学習者の知識が驚くほど限られているという無礼な気づきによって特徴づけられていました。 それに応じて、学習者は関連性の低い詳細の優先度を下げ、自分の最終目標に関係するものに集中する必要があります。 もみ殻から小麦をより分けるのです。
システムプログラミングには「コンピューターが何をしているのか」というメンタルモデルが必要ですが、それは網羅的である必要はありません。 実のところ、C や Rust のように開発者にハードウェアに対する「完全な制御」2 を与えるプログラミング言語は、主にある1つのものの概念とメカニズムを扱います。それはメモリです。
-
メモリがどのように、そしてなぜ動作するのかの大部分を理解していれば、低レベルプログラミングの習熟へと大きく近づいています。
-
攻撃者がメモリ破壊エクスプロイトをどのように作成するのかを理解していれば、クロス言語コードや
unsafeRust に含まれる実際のバグやエクスプロイト可能な脆弱性を、それが本番環境に到達する前に見つけられる可能性が高くなります。
システムプログラミングは、単にメモリを管理する以上のものではないのですか?
もちろんです。 第1章で「システムプログラム」を定義するものについて議論した際に登場した、3人の架空のエンジニアを思い出してください。
各エンジニアは、それぞれ固有の専門性を必要とする専門分野の出身であったため、異なる見方を持っていました。 たとえば、次のようなものです。
-
分散システム開発者は、合意プロトコルとフォールトトレランスを理解しています。
-
デバイスドライバー開発者は、カーネル APIと割り込み処理を扱います。
-
マイクロコントローラーファームウェア開発者は、アナログコンポーネントとインターフェースし、デバイスデータシートを読みます。
しかし、システムプログラミングのこれらの側面は、主にドメイン固有のものです。 メモリを効果的に利用することは一種の普遍的なボトルネックであり、性能の高いアプリケーションを書くために必要ではありますが、それだけで十分ではありません。 ドメインに関係なくです。
この章では、メモリの制御に関連する普遍的なコンピューターアーキテクチャの原則を扱います。 すべてのシステムプログラマーが知っておくべき中核です。 第6章では、これらの原則を実践に移し、安全性と移植性の両方を最大化するスタックストレージ抽象化を実装します。
メモリ 知識の一括投入
メモリは、おそらく本書で最も重要な単一のトピックです。 これは最後の概念的な章であり、残りの冒険では Rust ライブラリを書くことに集中します。 ここでは本当に機械的な詳細に踏み込んでいくので、今のうちにコーヒーを手に取ってください(どうしてもと言うなら Yerba Mate でも構いません)。
まず、ソフトウェアの観点からメモリを見ていきます。これは、ほとんどのシステムプログラマーが日々扱っているモデルです。 次に攻撃者の観点を掘り下げ、メモリ破壊バグがどのように壊滅的なエクスプロイトへと変えられるのかを学びます。 動的デバッグについて学び、入門的で実践的なヒープエクスプロイトを行います。 ルールや前提を覆せるようになって初めて、何かがどのように動作しているのかを本当に理解できます。 少なくとも、セキュリティに関してはそうです。
メモリについてのより深い理解を携えて、Rust がどのようにメモリ安全性の保証を提供するのかを検討します。 詳しく見ていきます。
メモリ世界一周の旅の締めくくりとして、言語に依存しない緩和策を探り、実世界の Rust CVE を見ていきます。
ハードウェアの観点についてはどうですか?
付録の 基礎: メモリ階層 セクションでは、現代のメモリ階層内における性能上のトレードオフを見ながら、ハードウェア中心の視点を取ります。 このセクションの補足として強くおすすめします。
実践的な抽象化を分解する
この章の概念と可視化の動機づけとして、次の2つを仮定しましょう。
-
フォワードエンジニアリングには、理想的な解決策を作り出すのに十分なほど、基本的な抽象化を理解することが必要です。
-
リバースエンジニアリングとエクスプロイト開発には、見かけ上理想的な解決策を覆し、信頼の前提を破るのに十分なほど、基本的な抽象化を理解することが必要です。
それを展開すると、実践的であることを願いつつも、明らかに意見の入った形で、基本的な抽象化はわずか3つになります。 予告すると、次のとおりです。
- アプリケーションロジック有限状態機械(FSM) - ビジネス要件やミッション要件であり、実行可能で不完全なアプリケーションとして実装されます。サーバーの Web ソケットのライフサイクルを考えてみてください。
- 実行環境 - コンパイル済みアプリケーションバイナリが、動的な実行環境/ランタイム環境にロードされたものです。OS が提供するプロセスおよびスレッド抽象化を伴います。静的メモリ、スタックメモリ、ヒープメモリによって支えられています。第4章の大部分はここにあります。
- ハードウェア FSM - 中央処理装置(CPU)です。すべてのユーザーアプリケーションが最終的にエミュレートされるハードウェア FSM です。覚えておくべき重要な点です。
学習成果
- システムメモリとプログラム実行のメンタルモデルを構築する
- メモリ安全性、型安全性、バイナリエクスプロイトのメンタルモデルを構築する
- Mozilla
rr3(gdb4 の拡張版)を使って Rust コードをデバッグする方法を学ぶ - 攻撃者がヒープメモリ破損バグをどのように悪用するかを、ステップバイステップで理解する
- 現代的な防御機構をバイパスしながら、初めて入門的なエクスプロイトを1つか2つ書く!
- Rust が実際にどのようにメモリ安全性を提供しているかを、現在の制限も含めて理解する
- 現代的で言語非依存のエクスプロイト緩和策がどのように機能するか(そしてどのように失敗し得るか)を理解する
-
「未熟であることに気づかない:自分自身の無能さを認識する困難が、どのように自己評価の過大化につながるか」。Justin Kruger、David Dunning(1999) ↩
-
プログラミングでも現代生活でも、完全な制御を手にすることは決してありません。プログラミングにおいては、コンパイラとインタプリタのどちらも、しばしば理解しがたい判断をあなたの代わりに行い(例:積極的な最適化5)、まれにバグさえ含んでいる6ためです。 ↩
-
GDB: The GNU Project Debugger。GNU project(2022年アクセス)。 ↩
-
C は低水準言語ではない:あなたのコンピュータは高速な PDP-11 ではない。。David Chisnall(2018)。 ↩
-
特に面白い事例の1つは CVE-2020-246587 で、コンパイラが挿入したスタック保護の失敗です。余談ですが、新しいコンパイラバージョンによって修正される脆弱性は興味深いカテゴリです。これにはハードウェア脆弱性が含まれることもあります(例:CVE-2021-354658)。 ↩
-
CVE-2020-24658 の詳細。National Institute of Standards and Technology(2020)。 ↩
-
VLLDM 命令のセキュリティ脆弱性。ARM(2021)。 ↩