從 u32 到 Root:分析 io_uring ZCRX Freelist 漏洞

Linux 核心的 io_uring 子系統因其龐大的複雜度與強大功能長期以來一直是安全研究人員的焦點。最近在 Zero-Copy Receive(ZCRX)子系統的發現突顯了一個經典且致命的記憶體安全錯誤:堆疊式 freelist 缺少邊界檢查。此漏洞允許具備特定權限的攻擊者將簡單的 4 位元組越界寫入(OOB)轉變為完整的 root 權限。

本文將分解 ZCRX 漏洞的技術機制、利用它所需的堆整理(heap grooming),以及最終的本地權限提升(LPE)路徑。

漏洞說明:ZCRX 中缺少的邊界檢查

ZCRX 在 Linux 6.15 中引入,允許使用者空間直接將網路封包接收至已註冊的記憶體區域,繞過 kernel 到使用者的拷貝開銷。為了管理這些記憶體槽,核心使用 net_iov 結構與相對應的 freelist:

  • 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_count 等於 num_niovs 時,寫入會發生在 freelist[num_niovs],正好是已分配陣列末端之後的 4 位元組槽位。這會導致 4 位元組的 OOB 寫入至相鄰的 slab 記憶體。

觸發 OOB 寫入

此利用依賴於兩條核心拆解路徑之間的競爭條件,兩者皆將 niov 歸還至相同的 freelist:

  1. Path A(正常完成):網路堆疊釋放封包,觸發 io_pp_zc_release_netmem,將 niov 推回 freelist。
  2. Path B(頁面池拆解):當 NIC 被關閉時,io_pp_zc_destroy 會遍歷所有 niov。若 niov 仍有參考計數,則強制將其放回 freelist。

由於 ptr_ring 排空(Path A)與清理迴圈(Path B)不是原子操作,會出現 niov 被計算兩次的時間窗口。若 freelist 接近滿載,這個雙重計算會使 free_count 超出陣列邊界,觸發 OOB 寫入。

要在使用者空間觸發此行為,攻擊者需要 CAP_NET_ADMIN 以透過 SIOCSIFFLAGS 關閉 NIC。此過程包括註冊 ZCRX 介面佇列(IFQ),以 UDP 封包淹沒它以分配 niov,然後在仍有封包在傳輸時將介面關閉。

從小整數到 Root:利用鏈

寫入一個小整數(niov 索引)看似微不足道,但攻擊者可透過在註冊時操控 num_niovs 參數來控制寫入的值與位置。這決定了 kcalloc 分配的大小,進而決定使用哪個 slab 快取(例如 kmalloc-128)。

1. 使用 msg_msg 進行堆整理

OOB 寫入的目標是 struct msg_msg 物件。透過 msgsnd() 大量噴灑 msg_msg 物件,攻擊者可以確保 msg_msg 被放置在 kmalloc-128 slab 中的 freelist 之後緊接的位置。

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 位址族(例如 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 可能是最安全的做法。

要求彙總

要求 詳細
Kernel 版本 6.15 – 6.19(未含提交 770594e
設定 CONFIG_IO_URING_ZCRX=y
硬體 支援 ZCRX 的 NIC(例如 Mellanox ConnectX-6+、Intel E800)
權限 CAP_NET_ADMIN(以及寫入 modprobe_path 所需的 CAP_SYS_ADMIN

Sources