OpenBSD Big Fish エクスプロイト:37年前のカーネルバグがローカルルートを許す

TL;DR

Califの研究者らは、OpenBSDのSysV共有メモリのアタッチメントカウンタにおける37年前の整数オーバーフローを発見し、それをレースコンディションと範囲外書き込みと組み合わせることで、KARL、SMAP、SMEPといった最新の緩和策を回避してローカルルートシェルを取得しました。このバグはOpenBSD 7.9のエラッタ026および018で修正され、write-what-whereプリミティブは、カーネルアロケータとリストアンリンクコードを強化するためのいくつかの道筋を示しています。


脆弱性

何が起きたか: sys/shm.h内の符号付き16ビットshm_nattchカウンタが、32,767を超える共有メモリアタッチメントが作成されたときにオーバーフローし、負の値にラップして、レースコンディションを介したuse-after-freeを可能にしました。このオーバーフローは、1987年頃に書かれたオリジナルのBSDソースコードに由来します。

なぜ重要か: このオーバーフローにより、攻撃者はカーネルメモリ構造を操作でき、SMAP/SMEPをバイパスするwrite-what-whereプリミティブにつながり、カーネルコード実行を許可し、最終的に非常に安全と見なされるシステム上でルートシェルを取得できます。

適用された修正: OpenBSD 7.9のエラッタパッチ026(sysvipc)と018(shmat)は、それぞれカウンタオーバーフローと範囲外書き込みに対処しています。


エクスプロイト概要

核となるアイデア: エクスプロイトは共有メモリのアタッチメントカウンタを負の範囲に追い込み、その後、新しいアタッチメントに対してIPC_RMIDを競合させ、参照を保持したままカーネルオブジェクトを解放します。範囲外書き込みはstruct ip_moptionsのフィールドを上書きし、その後、ページテーブルエントリをユーザーアクセス可能からスーパーバイザーアクセス可能に反転させるアトミックデクリメントをトリガーし、任意のカーネル書き込みを可能にします。

ステップバイステップの要約:

  1. カウンタをラップさせる: 127個のSysV共有メモリセグメントをマップし、アンマップして、259個のプロセスをフォークし、shm_nattchを-32,643に押し上げます。
  2. ヒープグルーミング: sysctl(KERN_MALLOCSTATS)を使用してカーネルmalloc統計を監視し、割り当てのタイミングを計ります。
  3. レースコンディション: あるプロセスでIPC_RMIDを呼び出し、ヘルパープロセスがサイズ混乱セグメントをアタッチして、use-after-freeを作成します。
  4. ソケット構造の準備: IPソケットを介して永続的なstruct ip_moptionsを割り当て、IP_MULTICAST_TTL=2を設定して、構造体内に制御可能なカウンタを配置します。
  5. 範囲外書き込み: 条件付きの-1書き込みを悪用して、imo_num_membershipsとimo_max_membershipsフィールドを破壊します。
  6. アトミックデクリメントのトリガー: ソケットを閉じるとメンバーシップリストが走査され、2つのアトミックデクリメントが発生して自己参照PTEマッピングを書き換え、ユーザー制御のページをスーパーバイザーアクセス可能にします。
  7. write-what-where: 予測可能なカーネルポインタ(例:ioctlハンドラ)を攻撃者制御のシェルコードへのポインタで上書きし、ルートを取得した後にポインタを復元します。

結果: KARL、SMAP、SMEPが有効なOpenBSD 7.9上で、情報漏洩バグを必要とせずに、安定したローカルルートシェルを取得できます。


緩和策の状況

即時パッチ

  • エラッタ026 – 共有メモリアタッチメントカウンタの符号付きショートオーバーフローを修正。
  • エラッタ018 – shmat処理における範囲外書き込みをパッチ。

構造的強化の提案

提案 保護対象 実装のヒント
安全なアンリンク 破損した双方向リンクリスト(TAILQ_REMOVE)を介したuse-after-freeを防止。 アンリンク前に不変条件チェックを追加し、違反時にはアボート/パニック。FreeBSD/NetBSDはすでにsys/queue.hでこれを提供。
自己参照PTEマップのランダム化 ページテーブルベースへの決定的なアトミックデクリメント攻撃を阻止。 ブート時にランダムなPML4スロットを選択(320 + (rand & 127))し、再帰エントリをそこにインストール。
非決定的カーネルアロケータ ポインタ上書きに使用される予測可能な割り当てアドレスを破壊。 ブート時アロケータのベースアドレスをランダム化し、実行時に割り当て順序をランダム化(CalANDifコミットを参照)。
コンパイラレベルのサニタイザ 範囲外配列書き込みとuse-after-freeを悪用可能になる前に検出。 リリースビルドでClangのArray Bounds Sanitizer(UBSan)を有効化。将来のAllocTokensはポインタを含む割り当てを分離可能。

OpenBSDの既存の緩和策が不十分だった理由

  • KARLは関数とデータのアドレスをランダム化しますが、アロケータのベースアドレスはランダム化しないため、ブート初期の構造が予測可能な場所に残ります。
  • SMAP/SMEPはユーザーモードからスーパーバイザーメモリへのアクセスをブロックしますが、エクスプロイトはPTEエントリをスーパーバイザーアクセス可能に反転させるため、これらの保護をバイパスします。

コミュニティの洞察

「最も厳重に監査されたコードベースの1つでバグが37年間生き残ったのは驚くべきことであり、レビューだけでは一部のカーネルバグを発見するのがいかに難しいかを示しています。ファジングや静的解析でこの種のバグを現実的に発見できたと思いますか、それともエクスプロイトプリミティブについて考える人間が必要だったのでしょうか?」 – @ozereray1によるHNコメント

この議論は、広範な手動レビューでも、特に特定のエクスプロイトプリミティブを必要とする場合、微妙な整数オーバーフローバグの存在を保証できないことを強調しています。多数のshmat/shmdtシーケンスをさまざまなフォークパターンで実行する自動ファジングは、理論的にはオーバーフローを表面化させる可能性がありますが、レースコンディションと範囲外書き込みは、おそらくターゲットを絞ったシンボリック実行または手動の推論が必要です。


カーネルセキュリティエンジニアへの教訓

  1. レガシーデータ構造を監査する – 符号付きショートカウンタやその他の狭い整数フィールドは、レガシーコードが前方に移植されるときにオーバーフローの主要な候補です。
  2. 決定的アロケータは悪用可能と想定する – 予測可能な割り当てアドレスはwrite-what-whereプリミティブを可能にします。ブート時および実行時のランダム化は低コストの緩和策です。
  3. 多層防御が重要 – アドレス空間ランダム化、KASLR、SMAP/SMEPがあっても、攻撃者はページテーブル階層自体を破壊することでコード実行を達成できます。
  4. 既存の安全なアンリンク実装を活用する – FreeBSDとNetBSDはすでに破損した双方向リンクリストの削除から保護しています。OpenBSDは最小限の影響で同じ手法を採用できます。
  5. コンパイラサニタイザを早期に採用する – UBSanの配列境界チェックを有効にし、AllocTokensに備えることで、多くの範囲外書き込みを本番カーネルに到達する前に検出できます。

参考文献

Sources