从 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:
- 路径 A (正常完成): 网络栈释放一个数据包,将 niov 推回 freelist 并增加
free_count。 - 路径 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,一个介于 0 和 num_niovs - 1 之间的较小整数。虽然写入一个小整数似乎限制很大,但攻击者可以通过在注册时调整 area 大小来获得三个自由度:
- Slab Cache 选择:
freelist的大小 (num_niovs * 4) 决定了使用哪个kmallocslab 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/kallsyms、dmesg 或 msgrcv 泄露),攻击者就会瞄准 modprobe_path。这是一个全局内核变量,定义了当内核需要为未知的 socket family 加载模块时所执行的二进制文件。
- 覆盖
modprobe_path: 使用CAP_SYS_ADMIN(在某些容器配置中通常与CAP_NET_ADMIN配合使用),攻击者将恶意脚本的路径 (例如/var/tmp/evil.sh) 写入/proc/sys/kernel/modprobe。 - 触发执行: 攻击者打开一个具有未知地址族 (例如
AF_CAN或AF_BLUETOOTH) 的 socket 接口。 - 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 |