eBPF Linuxカーネル脆弱性の発見と修正方法
(bughunters.google.com)- GoogleはeBPF verifierのCVE-2023-2163をBuzzerで発見し、この脆弱性がローカル権限昇格とコンテナ脱出に悪用され得ることを確認した
- eBPFはカーネル機能をランタイムで拡張するが、高い権限で任意のバイトコードを実行するため、ロード前のverifier検証がセキュリティの中核となる
- 問題はverifierのpath pruningが、実際には異なる実行経路を既に安全な経路と同等だと誤って判断してしまうことにあった
- エクスプロイトは、verifierが0と見なしたレジスタがランタイムでは別の値を持つ差異を利用してeBPFスタックポインタを汚染し、任意の読み書きとKASLR回避につなげる
- パッチは、precise registerに影響を与えるimprecise registerもpreciseとしてマークする方式で、同じポインタ算術ファジング戦略ではその後追加の問題は見つかっていない
eBPF verifierが広げるカーネル攻撃面
- eBPFは、複雑なカーネルモジュールなしでもLinuxカーネル機能をランタイムで拡張できるようにする技術である
- eBPFプログラムはカスタムバイトコードで記述され、特定のイベントが発生したときに実行される前に安全性検証を受ける
- 代表的な例は、特定のsyscall呼び出し時に実行されるeBPFプログラムである
- 高い権限レベルで任意コードを実行できる構造のため、カーネルの攻撃面が大きく広がる
- プログラムはロード前にverifierを通過する必要があり、verifierはeBPFのセキュリティ前提が満たされているかを検査する
- verifierの脆弱性が悪用されると、通常はローカル権限昇格またはコンテナ環境でのコンテナ脱出につながる
Buzzerとポインタ算術ファジング
- GoogleはeBPF verifierコードを自動監査するためにBuzzerを作成した
- Buzzerは構文的に有効なeBPFプログラムを大量生成するファザーであり、論理バグを誘発するための戦略を設定できる
- CVE-2023-2163の発見にはポインタ算術戦略が使われた
- レジスタを任意の値で初期化するヘッダーを生成する
- 任意の算術およびジャンプ命令シーケンスを生成する
- 任意のレジスタを選び、eBPF map elementポインタとの加算演算を行う
- そのelementにマジック値を書き込む
- ユーザー空間で書き込まれた値が観測されなければ、境界外書き込みが発生した可能性がある
- Buzzerが発見したCVE-2023-2163はeBPFのpath pruningロジックにあり、コンテナ脱出とローカル権限昇格の両方に使えるエクスプロイトにつながった
path pruningとprecise trackingのバグ
- eBPF verifierは、プログラムが安全に実行できるかを確認するため、あり得る実行経路をシミュレートする
- 条件分岐でレジスタ値が確定していない場合、verifierは可能なすべての状態の実行をたどろうとする
- 条件ジャンプが増えるほど実行経路数は指数的に増え、カーネルにプログラムをロードする性能にも悪影響を与える
- これを減らすため、eBPF開発者はpath pruningを導入した
- verifierが特定の状態と同等の状態がすでに安全にexit命令へ到達すると保証できるなら、その経路をそれ以上探索しない
- より効率的なpruningのため、precise trackingの概念も併用される
- レジスタがポインタ算術演算に関与したり、helper関数に定数として渡されたりするとpreciseとしてマークされる
- verifierは、そのレジスタが関係するすべての状態を探索しなければならない
- CVE-2023-2163では、r6の精密性に影響するr9が正しくマークされていなかった
- verifierはr9がr6のprecisenessに寄与しないと仮定した
- 先行する経路でr6が安全にexitへ到達できると判断した後、残りの状態を同等と見なしてpruningした
- ランタイムでは1:2:4:6の経路が実行され、6ではverifierが0と見ていたr6が実際には別の値を持ったままポインタ算術演算に使われ得た
任意の読み書きとKASLR回避
- エクスプロイトコードはGoogle security research repositoryで公開されている
- エクスプロイト開発では@chompieと@_manfpの成果が重要に活用された
- 全体の流れは、まず任意の読み書きを獲得し、その後プロセスcredentialsを探してuidと
fs_structポインタをパッチし、権限を昇格させるというものだ - 最初の段階では、汚染されたレジスタ値を1にする
- 手動解析ではランタイムのr6値は
0x400で、verifierはこれを0と見なした r6 >>= 10命令で目的の値1を作った
- 手動解析ではランタイムのr6値は
- その後、
bpf_skb_load_bytes_relativehelper関数でeBPFスタック上のポインタを汚染する- verifierは
lenが8だと判断するが、実際のランタイムではr6値のためlenは9になる - 結果として8バイトではなく9バイトを書き込み、スタックoffset
-32値の先頭バイトを汚染する
- verifierは
- 汚染されたスタックポインタを操作すると、eBPF map pointerをリークできる
- ユーザー空間でR2とR3の値を読み取り、R2が
0xBACAになるか確認する - この条件が成立すれば、R3がmap pointerのリーク値となる
- eBPF map pointerのリークにより、KASLRを回避した状態につながる
- ユーザー空間でR2とR3の値を読み取り、R2が
- 同じ戦略で、単一バイトではなく連続したスタック領域全体を任意のポインタで上書きすれば、任意の読み書きが可能になる
- verifierの観点ではBPFスタック操作だが、実際にはカーネルメモリを読み書きできる
map leakからroot shellまで
- map pointerリーク後のエクスプロイトは、Chompieのエクスプロイトと大きくは変わらず、一部コードを流用している
- 概要手順は次のとおり
- kstrtabで
init_pid_ns文字列を繰り返し検索する - その文字列を参照するksymtabシンボルを見つけ、init_pid_ns構造体アドレスを得る
- radix treeをたどり、
commフィールドが実行中のエクスプロイト実行ファイル名と一致するentryを探す- コンテナ内で実行中の場合、PIDは信頼できるヒューリスティックではないためPIDだけは使わない
- uidを0にパッチし、
fs_structポインタをパッチする fs_structをPID 1が参照するポインタと同じ値にパッチすると、コンテナ内で実行中の場合にホストファイルシステムを観察できるsystem("/bin/bash")を実行してroot shellを得る
- kstrtabで
- GitHubで公開されたコードは、特定のLinuxバージョンでのみコンテナ脱出として動作する
- Ubuntuや一部ディストリビューションでは、上書きされるデータ構造のoffsetが異なるため、ローカル権限昇格としてのみ動作する
- どのLinuxディストリビューションでも動作させるには、コード調整が必要である
パッチと後続検証
- CVE-2023-2163の原因分析とパッチはkernel mailing listで確認できる
- 修正は、precise registerに影響を与えた演算のimprecise registerをpreciseとしてマークする方式である
- この修正がeBPF verifier性能に影響するかは明確ではない
- 同じポインタ算術ファジング戦略を継続実行したが、追加の問題は見つからなかった
- Buzzerは引き続き開発中であり、オープンソースコミュニティからの貢献をGitHub repoで受け付けている
1件のコメント
Hacker Newsのコメント
eBPFは最も一般的に使われているプラットフォームでは、そもそも非特権コードがeBPFプログラムをロードできないため、バリデータのバグの影響が大きくないことが多い
こうしたバグは結局のところ root → ring0 の脆弱性であり、取るに足らないという意味ではないが、サーバー側の運用では概ね受け入れ可能なトレードオフであることが多い
特にeBPFのカーネル内ローカル権限昇格の履歴は、カーネル全体と比べるとかなり良好な部類であり、現在のeBPF環境でバリデータの最大の価値は、不正なeBPFプログラムによって誤ってカーネルを落としにくくしている点にある
一般的なロード可能なカーネルモジュールでは、こんな話は笑い話にもならない
この場合、ほとんどのプラットフォームで権限は不要である
さらに非特権ユーザー名前空間があれば、自分で「root」になれるので、root → ring0 もそれほど制約の強い問題ではなくなる
ディストリビューションがこれを有効にしては大半が再び無効化した後に出てきたeBPFバグのPoCで、ずっと見てきた流れである
Ciliumのようなツールが大きくなるほど、cap_bpfを持つコンテナ環境に侵入する攻撃経路はますます現実的になる
「Uno no es ninguno」は、文字どおりには「1つは無ではない」、つまり "One is not none" に近い
スペイン語では二重否定が実際には二重否定でないことがよくある
たとえば「ここには何もない」は "no hay nada aquí" というが、語ごとに訳すと「ここには何もないわけではない」のように見える
王立スペイン語アカデミーも次のように説明している:
https://www.rae.es/espanol-al-dia/doble-negacion-no-vino-nad...
いわゆる「二重否定」は、スペイン語や他のロマンス諸語で特定の状況で必ず成立する否定一致によって生じ、その結果、副詞 no と否定の意味を持つ他の要素が1つの文に共起する
この2つの「否定」が同時にあるからといって、文の否定の意味が打ち消されるわけではない
以前eBPFを使ってみようとしたとき、必要な作業をするには表現力が足りなかった
限られた柔軟性を得るためにカーネル空間の複雑性を増すことが本当に正当化されるのか疑問である
パケットフィルタリング用途なら理解できるが、サンドボックス化のような別用途に使われるのは説得力が弱く見える
カーネルの選択肢はeBPFか無かではなく、eBPFか、それに似た別の何かかである
自分ではあまり使わないかもしれないが、一日中使っている人たちもいる
FAANGのエンジニアは、すべてのサーバーで常時こうしたプログラムを数十個、おそらく数百個単位で動かしていると言っていた気がするし、単発利用を除いてもそうだという
FAANGは専任のカーネル開発者も雇っているので、自分たちが使うこの複雑性に対して資金も出しているわけだ
私もeBPFで問題を解決したことがある
eBPFがなければ、カーネルの専門家でない人には事実上解決不能な問題であり、頻繁に必要になるわけではないが、必要なときには代替手段がない
場合によっては、カーネル専門家にとっても、eBPFを使うか、カスタムカーネルパッチを永久に保守し続けるかの選択になる
安全でないことは分かっているが、扱う人たちは大きな力には責任が伴うことを理解している
「Uno no es ninguno」は "One is not none" と訳すのが正しいように思う
https://bughunters.google.com/blog/6303226026131456/a-deep-d...
そうではなく "One is none" と訳されている
外国語話者にとって、私を含めて悩ましいことで悪名高い二重否定である
https://spanish.stackexchange.com/questions/26777/how-does-d...
直訳すればそうだが、スペイン語では奇妙なことに二重否定が普通は単なる否定として使われる
「one ain't nothin'」くらいにすると、むしろしっくりくるかもしれない
私たちの国には「ズボンの中のハリネズミ」という表現がある
どれほど便利であっても、これは安全かつ慎重に作られたものには見えない