u32 から Root へ: io_uring ZCRX フリーレスト脆弱性の分析

Linux カーネルの io_uring サブシステムは、その膨大な複雑さと強力な機能により、長らくセキュリティ研究者の注目の的となってきました。Zero-Copy Receive (ZCRX) サブシステムでの最近の発見は、スタックベースのフリーレストに対する境界チェックの欠如という、古典的でありながら壊滅的なメモリ安全性エラーを浮き彫りにしています。この脆弱性により、特定の権限を持つ攻撃者は、単純な 4 バイトのアウト・オブ・バウンド (OOB) 書き込みをフル root 権限へと変換できます。

本稿では、ZCRX 脆弱性の技術的メカニズム、これを武器化するために必要なヒープグルーミング、そして最終的なローカル特権昇格 (LPE) への道筋を解説します。

脆弱性: ZCRX における境界チェックの欠如

Linux 6.15 で導入された ZCRX は、ユーザー空間がネットワークパケットを登録されたメモリ領域に直接受信できるようにし、カーネルからユーザーへのコピーのオーバーヘッドを回避します。これらのメモリスロットを管理するために、カーネルは net_iov 構造体と対応するフリーレストを使用します。

  • freelist[]: 利用可能なスロットインデックスのスタックで、kcalloc(num_niovs, sizeof(u32)) により割り当てられます。
  • free_count: スタックの現在の深さを追跡する整数です。

脆弱性は io_zcrx_return_niov_freelist 関数に存在します。ネットワーク I/O ベクタ (niov) がプールに返却される際、カーネルはインデックスを freelist にプッシュし、free_count をインクリメントしますが、free_count がすでに num_niovs に達しているかどうかの検証を行っていません。

static void io_zcrx_return_niov_freelist(struct net_iov *niov)
{
    struct io_zcrx_area *area = io_zcrx_iov_to_area(niov);

    spin_lock_bh(&area->freelist_lock);
    area->freelist[area->free_count++] = net_iov_idx(niov);
    spin_unlock_bh(&area->freelist_lock);
}

free_countnum_niovs と等しい場合、書き込みは freelist[num_niovs] に対して行われます。これは割り当てられた配列の末端を 4 バイトだけ超えた位置であり、隣接するスラブメモリへの 4 バイトの OOB 書き込みとなります。

OOB 書き込みのトリガー

このエクスプロイトは、2 つのカーネルのクリーンアップパスが同じフリーレストに niov を返す際の競合状態に依存しています。

  1. Path A (Normal Completion): ネットワークスタックがパケットを解放し、io_pp_zc_release_netmem がトリガーされ、niov がフリーレストに戻されます。
  2. Path B (Page Pool Teardown): NIC がダウンされると、io_pp_zc_destroy がすべての niov を走査します。niov に参照カウントが残っている場合、強制的にフリーレストに戻されます。

ptr_ring のドレイン (Path A) とスクラブループ (Path B) が原子的でないため、niov が二度カウントされるウィンドウが存在します。フリーレストがほぼ満杯の場合、この二重カウントにより free_count が配列境界を超え、OOB 書き込みが発生します。

ユーザー空間からこれをトリガーするには、攻撃者は SIOCSIFFLAGS を使用して NIC をダウンさせるための CAP_NET_ADMIN が必要です。手順は、ZCRX インターフェースキュー (IFQ) を登録し、UDP パケットでフラッドして niov を割り当て、そして一部のパケットがまだ送信中の状態でインターフェースをダウンさせる、という流れです。

小さな整数から Root へ: エクスプロイトチェーン

小さな整数 (niov インデックス) の書き込みは取るに足らないように思えるかもしれませんが、攻撃者は登録時に num_niovs パラメータを操作することで、書き込みの値と位置を制御できます。これにより kcalloc の割り当てサイズが決まり、結果としてどのスラブキャッシュ (例: kmalloc-128) が使用されるかが決まります。

1. msg_msg を用いたヒープグルーミング

OOB 書き込みのターゲットは struct msg_msg オブジェクトです。msgsnd() を使用して msg_msg オブジェクトを大量に配置することで、攻撃者は kmalloc-128 スラブ内で freelist の直後に msg_msg が配置されるようにできます。

OOB 書き込みは隣接する msg_msg の最初の 4 バイトに当たり、これは m_list.next ポインタの下位 32 ビットに相当します。x86-64 では、上位 32 ビットを保持したままポインタが破損し、ポインタはカーネルの physmap 範囲内に留まります。

2. KASLR の突破

破損した m_list.next ポインタを利用して、攻撃者は MSG_COPY フラグ付きの msgrcv() を使用し、ヒープのオーバーリードを実行できます。返されたデータを既知のカーネルテキストポインタでスキャンすることで、カーネルベースアドレスを算出し、KASLR を回避できます。あるいは、/proc/kallsymsdmesg が利用可能であれば、modprobe_path のアドレスを直接取得できます。

3. modprobe_path の上書き

modprobe_path は、カーネルがモジュールをロードする際に実行するバイナリを指すグローバル変数です。先に漏洩した KASLR アドレスと CAP_SYS_ADMIN(特定のコンテナ設定では CAP_NET_ADMIN と共に付与されることが多い)を利用して、攻撃者は /proc/sys/kernel/modprobe を通じて modprobe_path を悪意のあるスクリプトに上書きできます。

最後に、未知のソケットアドレスファミリ(例: socket(AF_CAN, ...))をトリガーすると、カーネルは悪意のあるスクリプトを root 権限で実行します。

緩和策とコミュニティの見解

この脆弱性はコミット 770594e で対策され、重要な境界チェックが導入されました。

if (WARN_ON_ONCE(area->free_count >= area->nia.num_niovs))
    return;

重要な分析

技術的チェーンは高度であるものの、コミュニティは前提となる権限(CAP_NET_ADMINCAP_SYS_ADMIN)がエクスプロイトの影響を大幅に制限すると指摘しています。あるコメント者は次のように述べています:

"modprobe_path を書き換えられるなら、コードを実行する方法を見つけたことは本当にニュースですか?"

しかし、他の人々は io_uring が巨大な攻撃面と特権昇格バグの頻度のために依然として「セキュリティの悪夢」であると主張しています。合意としては、高セキュリティ環境では sysctl -w kernel.io_uring_disabled=2 によって io_uring を完全に無効化することが最も安全な対策であると示唆されています。

要件の概要

要件 詳細
カーネルバージョン 6.15 – 6.19(コミット 770594e 未適用)
設定 CONFIG_IO_URING_ZCRX=y
ハードウェア ZCRX 対応 NIC(例: Mellanox ConnectX-6+, Intel E800)
権限 CAP_NET_ADMINmodprobe_path 書き換えには CAP_SYS_ADMIN も必要)

Sources