从 u32 到 Root:分析 io_uring ZCRX 本地权限提升漏洞

从 u32 到 Root:分析 io_uring ZCRX 本地权限提升漏洞

Linux 内核的 io_uring 子系统因其极高的复杂性和强大的功能,长期以来一直是安全研究人员关注的焦点。最近在 Zero-Copy Receive (ZCRX) 子系统(在 Linux 6.15 中引入)中发现的一个漏洞表明,一个看似微不足道的缺失边界检查,是如何被武器化并演变成完全的本地权限提升 (LPE) 的。

该漏洞允许具有 CAP_NET_ADMIN 权限的攻击者通过损坏内核堆内存、绕过 KASLR 并劫持 modprobe_path 全局变量,从而实现 root 访问权限。本文将详细分析该漏洞的技术机制、利用它所需的堆整理 (heap grooming),以及最终通往 uid=0 的路径。

根本原因:缺失的边界检查

ZCRX 允许用户空间直接将网络数据包接收到已注册的内存区域中,从而绕过数据拷贝的开销。为了管理这些区域,内核使用 freelist[]——一个可用插槽索引的栈——以及一个 free_count 变量来跟踪栈的深度。

当一个网络 I/O 向量 (niov) 被返回到 freelist 时,内核会执行以下逻辑:

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 进行上限检查。如果 free_count 等于 niov 的总数 (num_niovs),则写入操作会发生在 freelist[num_niovs],这恰好是分配的数组末尾之后的一个 4 字节插槽。这会导致向相邻的 slab 内存进行 4 字节越界 (OOB) 写入。

触发 OOB 写入

该漏洞通过两个独立的内核拆解路径之间产生的竞态条件触发,这两个路径都会将 niov 返回到同一个 freelist:

  1. 路径 A (正常完成): 网络栈释放一个数据包,将 niov 推回 freelist 并增加 free_count
  2. 路径 B (Page Pool 拆解): 当 NIC 被关闭或重新配置时,io_pp_zc_destroy 函数会遍历所有 niov。如果一个 niov 仍有引用,它会被强制返回到 freelist。

由于 ptr_ring 消耗 (路径 A) 和清理循环 (路径 B) 不是原子的,因此存在一个窗口,使得 niov 被重复计数。当这种情况发生时,free_count 会超过分配的数组长度,从而触发 OOB 写入。

在用户空间,具有 CAP_NET_ADMIN 权限的攻击者可以通过注册一个 IFQ、使用 UDP 数据包淹没 NIC,然后使用 SIOCSIFFLAGS 关闭接口来触发 page_pool_destroy()

武器化一个小整数

OOB 写入的值是一个 niov_idx,一个介于 0num_niovs - 1 之间的较小整数。虽然写入一个小整数似乎限制很大,但攻击者可以通过在注册时调整 area 大小来获得三个自由度:

  • Slab Cache 选择: freelist 的大小 (num_niovs * 4) 决定了使用哪个 kmalloc slab cache (例如,num_niovs = 32 会导致使用 kmalloc-128)。
  • 数值范围: num_niovs 的选择定义了可以被 OOB 写入的最大值。
  • 对象放置: 通过控制 cache,攻击者可以确保特定的目标对象紧邻 freelist 之后。

目标对象:struct msg_msg

该漏洞利用针对 kmalloc-128 cache 中的 struct msg_msg 对象。通过使用 msgsnd() 喷射 (spraying) 这些对象,攻击者可以确保其中一个对象紧邻 freelist。OOB 写入会命中 msg_zero 对象的头 4 字节,即 m_list.next 指针的低 32 位。

在 x86-64 (小端序) 上,指针的高 32 位保持不变。损坏的指针仍然指向内核 physmap。通过喷射更多的 msg_msg 对象,攻击者可以放置一个伪造的 msg_msg 头部,使其位于损坏的指针现在所指向的地址。这允许攻击者使用 msgrcv() 读取超出消息末尾的内容,从而创建一个堆泄露原语,以绕过 KASLR。

通往 Root 的路径

一旦绕过 KASLR (通过 /proc/kallsymsdmesgmsgrcv 泄露),攻击者就会瞄准 modprobe_path。这是一个全局内核变量,定义了当内核需要为未知的 socket family 加载模块时所执行的二进制文件。

  1. 覆盖 modprobe_path 使用 CAP_SYS_ADMIN (在某些容器配置中通常与 CAP_NET_ADMIN 配合使用),攻击者将恶意脚本的路径 (例如 /var/tmp/evil.sh) 写入 /proc/sys/kernel/modprobe
  2. 触发执行: 攻击者打开一个具有未知地址族 (例如 AF_CANAF_BLUETOOTH) 的 socket 接口。
  3. Root 执行: 内核使用损坏的 modprobe_path 调用 call_usermodehelper,从而以 uid=0 执行恶意脚本。

缓解措施与社区观点

该漏洞已在 commit 770594e 中得到修复,该 commit 为 io_zcrx_return_niov_freelist 添加了关键的边界检查:

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

影响分析

社区注意到,与无特权 LPE 相比,要求 CAP_NET_ADMIN (以及可能为了覆盖 modprobe_path 权限而需要 CAP_SYS_ADMIN) 限制了该漏洞的利用范围。正如一位评论者所言:

"如果能够写入 modprobe_path,那么发现一种执行代码的方法是否真的算新闻?"

然而,其他人认为,由于 io_uring 具有巨大的攻击面和频繁出现的权限提升漏洞,io_uring 仍然是一个“安全噩梦”。共识建议,对于高安全性环境,通过 sysctl -w kernel.io_uring_disabled=2 完全禁用 io_uring 是一种可行的深度防御策略。

受影响系统总结

| 要求 | 详情 | | :--- | :--- | | Kernel Version | 6.15 – 6.19 (未包含 commit 770594e) | | Configuration | CONFIG_IO_URING_ZCRX=y | | Hardware | 武器化 ZCRX 功能的 NICs (Mellanox ConnectX-6+, Intel E800, etc.) | | Privileges | CAP_NET_ADMIN |

Sources