ページめくり: nf_tablesの新たなLinux脆弱性と強化されたエクスプロイト手法
(pwning.tech)- Linuxカーネル
nf_tablesの CVE-2024-1086 は、Netfilter verdict の入力検証不備によりsk_buffの二重解放を引き起こし、条件がそろうとローカル権限昇格につながる可能性がある - 攻撃フローは、
NF_DROP処理中に解放された skb がNF_ACCEPTのようにそのまま処理継続されるようにして、同じオブジェクトが後続経路で再び解放される構造になっている - PoC は KernelCTF mitigation、Debian、Ubuntu、および vanilla カーネルで検証されており、少なくとも v5.14.21~v6.6.14 の範囲が kconfig によって影響を受けうる。修正は 2024年2月に stable ブランチへ配布された
- 中核となるのは Dirty Pagedirectory で、PTE と PMD ページを同じ物理ページに重複割り当てし、ユーザー空間の読み書きだけで任意の物理アドレスへアクセスするデータ専用 KSMA 手法である
- 物理 KASLR の探索、
modprobe_pathの回避、ファイルレス実行、名前空間脱出用ファイルディスクリプタの接続まで組み合わせられており、カーネルのメモリ管理とネットワークサブシステムを同時に狙う実戦的な LPE 事例となっている
脆弱性の条件と影響範囲
nf_tablesのバグは CVE-2024-1086 として登録されており、Linux カーネル Netfilter の verdict 処理において 正の drop error が許可される入力検証不備が核心である- エクスプロイトには次の条件が必要
nf_tablesが有効化されていること- 非特権ユーザー名前空間 が有効化されていること
- Debian や Ubuntu のような主要ディストリビューションでこの設定がデフォルト有効の場合、それが攻撃の前提となる
- テスト基準では stable ブランチ
linux-5.15.y、linux-6.1.y、linux-6.6.yが影響範囲に含まれ、linux-6.7.1も可能性がある - 2024年2月に stable ブランチへ バグ修正 が配布された
- PoC のソースコードは CVE-2024-1086 PoC repository で公開されている
二重解放が生まれるコードフロー
- Netfilter verdict は、パケットをドロップするか、受理するか、キューへ送るかなどを決定する値である
- 脆弱なフローは、
nft_verdict_init()がユーザー入力の verdict 値を十分に制限していなかったため、NF_DROPに見えつつも drop error が正の値である値を設定できたことから始まる nf_hook_slow()は verdict の下位ビットを見てNF_DROPと判断すると、kfree_skb_reason()によって skb を先に解放する- その後
NF_DROP_GETERR()の結果がNF_ACCEPTに相当する値として返ると、呼び出し側はパケットが受理されたものとみなして処理を続行する - その結果、すでに解放済みの skb が後続経路で再度解放され、double-free primitive が生まれる
破損するオブジェクトとパケット処理方式
- 二重解放は
skbuff_head_cacheのstruct sk_buffとsk_buff->headオブジェクトに影響する sk_buff->headは実際のパケット内容を保持し、IPv4 パケットサイズに応じてkmalloc-256から buddy allocator の order 4 ページ まで割り当てられうる- PoC は大きな IP パケットを使い、slab allocator ではなく buddy allocator の経路に入るよう設計されている
- IPv4 fragmentation queue は、skb の 2回目の解放を遅延させたり、望むタイミングで誘発したりするために活用される
- パケット経路で破損した skb フィールドが使われるとカーネルパニックが発生しうるため、TCP/UDP スタックを避けて特定の IP fragment エラーパスを利用する
テスト範囲と成功率
- vanilla カーネル、KernelCTF、Debian、Ubuntu 環境で複数のカーネルバージョンと設定がテストされた
- 成功例には次の環境が含まれる
- Linux v5.14.21、v5.15.148、v5.16.20、v5.17.15、v5.18.19、v5.19.17、v6.0.19
- KernelCTF Mitigation v3 の Linux v6.1.55
- Debian Bookworm 6.1.0-17 の Linux v6.1.69
- KernelCTF LTS の Linux v6.1.72
- Ubuntu Jammy v6.2.0-37
- Linux v6.2.16、v6.3.13
- 失敗例には v5.4.270、v5.10.209、v6.4.16、Ubuntu Jammy v6.5.0-15、v6.5.13、v6.6.14、v6.7.1 などが含まれる
- v6.4.0 以降の一部失敗は、
CONFIG_INIT_ON_ALLOC_DEFAULT_ON=yによる bad_page() 検知 と関連している - v6.4.16 環境では成功率が 99.4% で、場合によっては 93.0% まで下がり、サンプル数はそれぞれ n=1000 であった
修正方法
- 当初提案された修正は、Netfilter stack の途中で breaking change を生む可能性があった
- Netfilter maintainer による修正は、ユーザー入力から入る verdict を API 段階でより厳格に制限するものだった
- パッチは userland 入力に対して DROP/QUEUE verdict parameter を拒否する方式である
- CVE の説明によれば、
nft_verdict_init()は hook verdict 内の drop error に正の値を許しており、nf_hook_slow()はNF_DROPとNF_ACCEPTが重なる状況で double free を引き起こしえた - 修正パッチは [PATCH nf] netfilter: nf_tables: reject QUEUE/DROP verdict parameters. で確認できる
Dirty Pagedirectory と任意物理メモリアクセス
- PoC の中心手法は Dirty Pagedirectory であり、既存の Dirty Pagetable 手法の派生である
- 核心となるアイデアは、PTE ページと PMD ページを同じ物理ページに重複割り当てすることにある
- ある仮想アドレス領域に PTE 値のように見える値を書き込むと、別の仮想アドレス領域がそれをページテーブルエントリとして解釈し、指定した物理ページをマッピングする
- この方式は、ユーザー空間アドレスの読み書きだけで任意の物理アドレスと権限フラグを設定してアクセスする kernel-space mirroring attack として機能する
- virtual KASLR、KPTI、SMAP、SMEP、
CONFIG_STATIC_USERMODEHELPERのような緩和策の回避に使われる
ページアロケータ操作
- カーネルのページ割り当てには slab allocator、buddy allocator、PCP allocator が関与する
- skb head は大きいサイズでは buddy allocator の order 4 ページを使うが、PTE/PMD ページは order 0 ページであり、直接はかみ合わない
- これを解決するため、2つの page conversion 手法が使われる
- PCP list draining: order 4 ページを buddy freelist に入れ、PCP order 0 freelist を空にして buddy allocator から order 0 ページとして再補充させる
- race condition: 2回目の free 中の競合を利用して order 4 ページを order 0 freelist に入れる方式
- PCP list draining はより単純で安定しており高速な方法である
- race condition 方式は KernelCTF 初期エクスプロイトで使われたが、QEMU VM のような serial TTY 遅延が大きい環境に依存しており、obsolete とされている
KernelCTF mitigation の回避
- KernelCTF mitigation 環境で積極的に回避する必要があった緩和策は
sk_bufffreelist corruption check だった skbuff_head_cache->offset == 0x70のため、freelist の next pointer がskb->lenと重なっている- 1回目の skb 解放後、
skb->lenが freelist pointer の一部で上書きされ、その後のパケット解析中にこの値が変化すると corruption check に引っかかる可能性がある - 破損した skb の上に正常な skb を追加で解放して freelist head を上書きする方法により、検知を回避する
- KernelCTF 開発者は、free 時点でも freelist head next pointer を検査すればこの回避を緩和できると見ている
TLB flush と物理 KASLR 探索
- Dirty Pagedirectory によってページテーブルを想定外の方法で変更すると、CPU の TLB キャッシュに古い変換情報が残ることがある
- ユーザー空間で
fork()後、子プロセスがmunmap()を実行してスリープする方式で TLB を flush する - この方法は AMD CPU と QEMU VM で 100% 動作したことが確認されている
- 物理 KASLR は、カーネル物理ベースアドレスが
CONFIG_PHYSICAL_STARTまたはCONFIG_PHYSICAL_ALIGNに整列される点を利用して探索範囲を縮める - 8GiB の物理メモリと 16MiB アラインメントを仮定すると候補は 512 個で、get-sig スクリプトでカーネル base signature を生成して識別する
modprobe_path と root shell の取得
- 任意の物理メモリ読み書きを得た後、PoC はカーネル base 以降およそ 80MiB の範囲をスキャンして
modprobe_pathを探す - 一般的な設定では
"/sbin/modprobe"と null padding パターンを探し、/proc/sys/kernel/modprobeへの反映有無で実際の変数かを検証する CONFIG_STATIC_USERMODEHELPERが有効な場合は、"/sbin/usermode-helper"文字列を対象とする- root shell を得るために、
modprobe_pathまたは static usermode helper 文字列を/proc/<pid>/fd/<fd>形式の memfd パスで上書きする - privilege escalation スクリプトは、exploit のファイルディスクリプタを shell の stdin/stdout に接続し、ローカル端末と reverse shell の両方で動作するよう構成されている
ファイルレス実行と PoC 構成
- PoC はディスクにファイルを書かない fileless execution をサポートする
- 対象に Perl があれば、
memfd_create()を使って exploit バイナリをメモリ上に載せ、/proc/$$/fd/<fd>から実行できる - コンパイル依存関係は
libnftnl-devとlibmnl-devである - KernelCTF 向けの静的ビルドでは
musl-gccが使われており、glibc の静的リンクや QEMU の AVX512 opcode 問題を避けるための選択である - exploit ソースは複数ファイルに分かれており、スタンドアロン実行バイナリを目的としているため、エラー発生時は error code を返すより crash/exit を選ぶ設計になっている
安定性と限界
- exploit プロセスの pagetable 状態が不安定になることがあるため、成功後または失敗後も子プロセスを終了させずスリープ状態のまま残し、カーネルの不安定化を抑える
- ネットワーク活動があると skb freelist にノイズが入り、安定性に影響することがある
- SSH や reverse shell 環境では、double-free のタイミング付近で stdout 出力を減らし、ネットワーク起因の skb 割り当て/解放を最小化する
- 一部ハードウェアテストでは数秒後にシステムがクラッシュしており、WiFi フレームも skb を使うため、WiFi 活動が影響した可能性がある
- WiFi アダプタを BIOS で無効化すると、その環境では exploit が正常動作する
研究過程で得られた結論
- PoC は広い互換性、高い安定性、ステルス実行を目標に磨き込まれた
- 開発期間 2か月に加え、安定性と互換性の改善にさらに 2か月が費やされた
- exploit 自体は slab allocator の挙動に大きく依存せず、IPv4 subsystem と virtual memory のような広く有効化されている機能を中心に構成されている
- 初期バグは unprivileged user namespace と nftables を必要とするが、Dirty Pagedirectory や PCP draining のような手法は他の実用的な exploit にも活用可能である
- この作業は、Linux カーネルの networking subsystem と memory management subsystem を同時に深く掘り下げた事例として残る
1件のコメント
Hacker Newsのコメント
本日、CVE-2024-1086 の概念実証エクスプロイトを公開した。Debian や Ubuntu などで動作する。
影響を受けるバージョンは Linux カーネル v5.14〜v6.6 で、v6.4〜v6.6 の対応可否は
CONFIG_INIT_ON_ALLOC_DEFAULT_ONカーネル設定に依存する。このバグは 2024 年 2 月に修正済みなので、Linux マシンは更新する必要がある。
https://github.com/Notselwyn/CVE-2024-1086/blob/main/src/mai...
modprobe_path手法を使い、しかもファイルレス方式のために pid 総当たりまで入れたのか気になる。たとえばカーネル
.textを短いシェルコードでパッチして root を取り、名前空間を脱出する方式をなぜ選ばなかったのかと聞いてみたい。ただし、修正済みブランチ
v5.15.149>,v6.1.76>,v6.6.15>は除くと書かれている。パッチにこんな一文がある: “This reverts commit e0abdadcc6e1. [...] Its not clear to me why this commit was made.”
このコミットの 経緯 を調べた人がいるのか気になる。
[1] https://lore.kernel.org/all/20240120215012.129529-1-fw@strle...
昔の netdev リストを探してみたが何も見つからず、コミッターの Pablo Neira Ayuso に直接送られたパッチだった可能性もある。
元の作者は今では敬遠される人物となった Patrick McHardy で、Pablo が覚えているかメールから見つけ出さない限り、正確なユースケースを基本的な調査だけで突き止めるのは難しそうだ。
netfilter: nf_tables: accept QUEUE/DROP verdict parametersで、ユーザー空間がQUEUEとDROPの判定にキュー番号や errno コードを指定できるようにする内容だ。この記事は セキュリティ文書の書き方 という観点でも非常に印象的だ。
セキュリティブログを書くとき、「どれだけ多くの背景知識や前提知識を仮定するか」は常に悩みどころだが、アクセスしやすさと書き切れる範囲のバランスを取るのは難しい。
想定読者を先に定め、十分な背景説明を用意している点が素晴らしく、毎年会う研究者志望の人たちへの案内資料として渡せるようにブックマークした。
このエクスプロイトは 非特権ユーザー名前空間 へのアクセスに依存する:
sysctl kernel.unprivileged_userns_clone = 1Debian/Ubuntu と Arch Linux カーネルの既定値であり、sudo なしで Docker コマンドを実行するような必要がないなら無効にしておくほうがよい。
Electron アプリや 1Password などで使われる Chrome サンドボックス にも関係し、サンドボックス補助バイナリは setuid プログラムとしても動作できる。
Proton も今後ユーザー名前空間を使い始めても不思議ではないので、一般的なデスクトップ Linux では無効にしないほうがよいかもしれない。
サーバーや特別に強化した Linux では、無効にするのが悪くないことも多い。
問題は、コンテナを「とにかく動かす」にはネットワーク周りで数多くのハックが必要で、Docker の中核的な売りもそうした危険なハックを肩代わりしてくれることに近い。
結局どんなコンテナ解決策もそうしたハックを受け入れることになり、正当な名前空間機能がアクセスを減らしても、カーネルがネットワークのハックによって扉を開けてしまう構図になる。
なぜ 非特権ユーザー名前空間 がデフォルトで有効なのか分からない。
「非特権」名前空間内で実行されるとはいえ、デフォルトでユーザーに iptables や mount などを実行する能力を与える理由が何なのか疑問だ。
たとえば Chrome はプロセスサンドボックスを実装するために名前空間を使うが、非特権名前空間がなくても済むように setuid-root バイナリをインストールしている。
setuid-root バイナリ自体がセキュリティリスクなので、長期的には Chrome がそうしたバイナリをインストールしなくて済む方向が望ましい。
ただしそのためには非特権ユーザー名前空間が広く提供される必要があり、こうしたバグはその未来を遅らせることになる。
また Chrome のように名前空間ベースのサンドボックスを構成するプログラムは seccomp も併用し、サンドボックス内のコードが名前空間のような特殊なカーネル機能を使えないようにしていることが多い。
単一ユーザーのデスクトップでは、ユーザーと root の分離を強く適用する利点はそれほど大きくなく、面白いものの大半は普通のユーザーアカウントからもアクセスできる。
一方で Chrome のようなサンドボックスはデスクトップの安全性に不可欠なので、単一ユーザーのデスクトップでは非特権ユーザー名前空間を有効にしておくほうが全体のセキュリティは高まると考える。
マルチユーザーシステムは当然別の話だ。
このようなバグさえ繰り返し存在しなければ、非特権ユーザー名前空間は優れたセキュリティ機能だったはず
たとえば、Flatpak の実行でアプリ分離のためにホストの setuid バイナリを必要としないなら望ましかったはず
ほとんどのデフォルト設定は安全ではなく、技術的知識とハードニング作業が必要になる
問題を導入したコミット: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/lin...
ネストされた
switch/caseのデフォルト経路にreturnがあるのに、その後の fall-through を期待する構造になっていて奇妙に見えるhttps://lwn.net/Articles/882397/
https://bugs.launchpad.net/bugs/cve/2024-1086
Linux カーネルの
netfilter: nf_tablesコンポーネントにあるuse-after-free脆弱性により、ローカル権限昇格が可能nft_verdict_init()関数が hook 判定内の drop error として正の値を許容し、その結果nf_hook_slow()がNF_ACCEPTのように見える drop error を伴うNF_DROPを処理して、double free 脆弱性が生じうるf342de4e2f33e0e39165d8639387aa6c19dff660以降へアップグレードすることを推奨ASLR のような現代的な緩和策があるのに、こうしたエクスプロイトがどうやって可能になるのか気になる
大学の授業で、特定の Ubuntu バージョンで実行する複数のバイナリを渡され、use-after-free やバッファオーバーフローのようなバグを見つけてエクスプロイトする課題をやったが、本当に難しかった
欠陥を見つけるのも難しいし、その欠陥で有用なことをする正確なシェルコードを書くのはさらに難しかった
より難しい段階では ASLR、スタックカナリアのような緩和策も有効になっていて、管理された学生向け環境ですらほとんど不可能に感じられた
結局、1) エクスプロイト可能な欠陥を見つけ、2) プログラムをただクラッシュさせるのではなく有用なことをする正確なバイナリペイロードを見つけなければならないが、現実世界ではもっと難しいはずで、どうして可能なのか疑問
[0] https://web.stanford.edu/class/archive/cs/cs107/cs107.1194/a...
他の授業と並行しながら、1つのバグに使える時間は数時間から数日程度だったはず
Zerodium は一般的な Linux ローカル権限昇格に5万ドルを支払い、熟練したエクスプロイト開発者のコスト基準ではおおよそ 200 人時を買える
つまり専門家は初心者の学生より 10〜100 倍多くの時間を使える計算になる
初めて木工所に行った人と大工、陶芸初心者と職人、新人画家とプロの芸術家の違いを思い浮かべればよい
そこに時間まで 10〜100 倍上乗せされるようなもの
[1] https://zerodium.com/program.html
最新の保護機構の多くを回避する「古典的」な手法があり、それがなければ新しい攻撃や回避法が登場することもある
たとえばヒープ保護の回避については how2heap を参照でき[0]、KASLR 回避のエクスプロイト例もあり[1]、今回のエクスプロイトはdirty pagetable手法を使っているように見える[2]
緩和策が追加され、研究者がそれを回避するいたちごっこが続く構図になっている
[0] https://github.com/shellphish/how2heap
[1] https://www.willsroot.io/2022/12/entrybleed.html
[2] https://pwning.tech/nftables/#452-the-technique
こうしたものを見つけるには最後のタイプが 1 人いれば十分
今では本当に難しく、緩和策を無効にしても脆弱性を見つけてエクスプロイトを書くのは依然として簡単ではない
ただし、こうした脆弱性を見つける多くの人はチームで作業し、ファジングを並列化し、知識や別のエクスプロイトを組み合わせたりチェーンしたりできる
一部の研究者の専門性と才能は驚くほど高く、この分野では数年から数十年の経験が非常に大きな価値を持つ
しかし、今見つかる主要なバグが十分な資金を持つチームや国家支援のアクターによって見つけられていると考えると、人員と資源を大量投入して既存の緩和策を回避できるという点はより理解しやすい
Ubuntu によれば、すべてのLTS リリースが影響を受け、現在のパッチ済みカーネルでは修正されている: https://ubuntu.com/security/CVE-2024-1086
Focal は
5.4.0-174.193、Jammy は5.15.0-101.111、Mantic は6.5.0-26.26で修正済み拡張サポートを使っている場合は Xenial と Bionic も含まれる
脆弱な Debian システムで実行してみたところ、権限昇格はできなかったが、2回目の実行でシステム全体が停止した
1回目の実行は単に失敗しただけなので、それでも時間をかけてパッチを当てる価値は十分にある
現在のカーネル設定は
/boot/configや/proc/config.gzのようなファイルで確認できる