CVE-2026-68138:Linux流量控制race condition,1056字节从普通用户到root

导语:net/sched(网络流量控制)子系统潜伏7年之久的race condition(竞争条件),一次未加锁的链表操作就足以让普通用户拿到root shell。安全研究员Alejandro Ramos(亚历杭德罗·拉莫斯)已经在GitHub上扔出完整可复现的PoC,Ubuntu 22.04的官方5.15内核上实测从uid=1000(ubuntu)直接打穿到uid=0(root),四把虚拟CPU完成全部利用链。


一、漏洞本质

这个洞在net/sched子系统的qdisc(排队规则)rate-table代码里。qdisc_get_rtab()和qdisc_put_rtab()这对函数维护着一个进程全局的单链表qdisc_rtab_list,还有一个朴素的非原子int型引用计数refcnt。听起来就让人警觉对吧?可惜Linux内核维护者们对自己的锁纪律相当自信。

历史上,所有调用这两个函数的地方都会持有RTNL mutex(路由网络链接互斥锁),相当于一把全局大锁包揽所有流量控制子系统操作,串行化一切。所以那个非原子refcnt和裸链表也无所谓,反正没人并发。

问题出在flower分类器。flower是Linux网络栈里一个强大的分类器,按五元组等规则匹配数据包。它很早就给自己立了个flag:TCF_PROTO_OPS_DOIT_UNLOCKED——意思是我这个分类器的change函数可以在没有RTNL锁的情况下跑。当一个RTM_NEWTFILTER请求触发了flower规则的police动作(流量限速)时,调用链是这样的:

tc_new_tfilter()
└─> fl_change()                          // flower分类器的change回调
    └─> tcf_exts_validate_ex()
        └─> tcf_action_init()
            └─> tcf_police_init()         // police动作初始化
                └─> qdisc_get_rtab() / qdisc_put_rtab()

看到了吗?qdisc的rate-table函数在没有RTNL锁的情况下被调用了。这意味着多个并发RTM_NEWTFILTER请求使用相同rate table时,会同时操作全局qdisc_rtab_list和那个不安全的非原子refcnt。

竞争的结果是struct qdisc_rate_table对象(一个1056字节的kmalloc-2k对象)出现use-after-free或double-free。这是经典的refcount race类漏洞,但通过Linux网络流量控制这条不寻常的路径切入,让攻击者有了一个无需特殊权限就能触发的入口。

影响范围非常广:引入于Linux 5.1(commit 470502de5bdb),修复于7.1.6(stable fb29e1b41052)和7.2-rc5(mainline f43ee0c0730d),跨度整整7个大版本。


二、PoC下载与实验环境

PoC仓库github.com/aramosf/CVE-2026-68138

仓库结构清晰:

  • exploit.c 主利用代码(4个worker线程 + BPF alias + pipe leak + simple_xattr spray)
  • config.fragment 测试内核选项片段(非完整生产配置)
  • ubuntu/README.md Ubuntu 22.04专用变种文档
  • docs/example-output.txt 标准化利用成功输出
  • docs/ubuntu-22.04.5-qemu-output.txt Ubuntu官方镜像实测输出
  • assets/CVE-2026-68138.gif Ubuntu 22.04实时demo动图

编译

gcc -O2 -static -pthread -Wall -Wextra -Werror -o build/exploit exploit.c

静态编译,线程支持,开启所有警告。要放进QEMU guest里跑。

测试环境要求

  • x86-64 Linux + 4个虚拟CPU
  • CONFIG_USER_NS=y + CONFIG_NET_NS=y(用户/网络命名空间)
  • CONFIG_NET_CLS=y + CONFIG_NET_CLS_FLOWER=y + CONFIG_NET_CLS_ACT=y + CONFIG_NET_ACT_POLICE=y
  • CONFIG_TMPFS_XATTR=y(simple_xattr喷洒的前提)
  • 经典BPF + BPF JIT支持
  • CONFIG_MEMCG=n(确保分配器布局与exploit一致)
  • 每进程文件描述符限制≥4096
  • 约5GB内存的隔离虚拟机
  • Ubuntu 22.04专用变种需要nokaslr

三、利用链:从四个worker到root shell

拉莫斯的exploit不是简单的double-free触发,而是一条精心编排的七阶段提权链。我们拆开看:

3.1 触发race

四个worker线程各自进入独立的网络命名空间,创建flower过滤器并绑定police动作。三个走成功路径,一个在拿到两个rate-table引用后故意传无效的estimator(限速参数),被强制走清理路径。这套组合拳让rate-table引用计数竞争高概率复现,且worker之间不共享flower分类器状态。

3.2 经典BPF过滤器重叠

每次netlink请求后,当前CPU立刻挂一个133条指令的经典BPF过滤器。它的指令数组是1064字节,刚好从kmalloc-2k分配,和qdisc_rate_table共用同一个slab。精心设计的指令布局让重叠在qdisc_rate_table.next和.refcnt字段的字节初始状态依然有效——这是BPF alias成立的前提。

3.3 探测BPF指针重叠

最后一个BPF指令里藏了一个per-socket(每个socket唯一)标记位。通过SO_GET_FILTER系统调用,exploit能检测出某个socket的orig_prog->filter指针已经被重定向到另一个还活着的BPF分配。两个socket指向同一块指令buffer——这就是BPF alias成功的信号。

3.4 Pipe ring抢占回收

关掉其中一个owner socket,sk_filter_release_rcu()通过RCU(Read-Copy Update,Linux内核延迟回收机制)延迟释放真正的内存。过了grace period(宽限期)后,exploit分配一组32槽的pipe ring(管道环形缓冲区,1280字节 = 32×40字节pipe_buffer),同样从kmalloc-2k分配。其中一个ring正好抢到了那块被释放的BPF buffer

3.5 泄漏内核指针

读取另一个socket的SO_GET_FILTER,会返回一个活的pipe_buffer——其中包含了内核页指针、ops(操作函数表)指针,以及/sbin/modprobe的page-cache条目在内核虚拟地址空间里的偏移。这些就是exploit需要的内核信息泄漏。

3.6 simple_xattr喷洒覆盖pipe ring

关掉第二个socket,pipe ring在又一轮RCU宽限期后被释放,但对应的pipe_inode_info仍然引用它。用simple_xattr(Linux扩展属性)对象喷洒1280字节的slab,让泄漏的pipe_buffer落在预期槽位,并设置PIPE_BUF_FLAG_CAN_MERGE标志位。

3.7 改写modprobe_path拿root

往每个候选pipe写数据,最终把数据append到/sbin/modprobe的ELF入口点page-cache页。先读回文件确认改写成功,然后创建一个AF_INET数据报socket + 不支持的protocol 253——内核会去请求net-pf-2-proto-253-type-2这个不存在的模块,触发modprobe执行,而此时的/sbin/modprobe已经被覆盖成了攻击者的payload。Payload以initial namespace的root身份执行,写proof文件,开root shell。

成功输出长这样:

[+] confirmed dangling orig_prog->filter pointer
[+] pipe leak: page=<kernel pointer> ops=<kernel pointer> off=0x60f len=1 flags=0
[+] page-cache overwrite via dangling pipe
[+] controlled page-cache write confirmed; triggering helper
[+] CVE-2026-68138 initial-namespace root shell
uid=0(root) gid=0(root) groups=0(root)
[+] ROOT PROOF: uid=0(root) gid=0(root) groups=0(root)

拉莫斯在文档里明确说了:这个race是概率性的。打中失败的话会打印[-] no BPF alias found in this attempt必须重启VM再试,因为全局qdisc list已经被搞脏了,重跑只会panic guest。在QEMU里实测全链路成功3次。


四、修复与缓解

官方修复:上游已经在Linux 7.1.6和7.2-rc5合并修复,核心改动是把qdisc_rtab_list的操作从裸链表+非原子refcnt改成带spinlock(自旋锁)保护的版本,每个rate-table对象自己持锁,不再依赖外部RTNL mutex的隐式序列化。

发行版状态(截至2026-08-12):

发行版内核版本漏洞代码PoC兼容性
Ubuntu 22.04 GA5.15.0-187.197存在完整复现成功
Ubuntu 22.04 HWE6.8.0-136.136存在未测试
Ubuntu 24.04 HWE7.0.0-28.28存在不兼容(分配器硬化)
Ubuntu 26.047.0.0-28.28存在不兼容(分配器硬化)

注意:Ubuntu 24.04 HWE和26.04虽然漏洞代码还在,但拉莫斯的exploit链因为分配器硬化(CONFIG_SLAB_BUCKETS、freelist randomization等)不兼容。不等于安全,而是需要不同的回收策略——专门适配版本在其他研究者手里大概已经在写了。

临时缓解

  1. 如果运行Ubuntu 22.04,立即关注5.15系列的security update
  2. 限制非特权用户加载netfilter/TC配置的能力(普通桌面用户影响不大)
  3. 监控audit日志里异常的tc命令和flower过滤器创建行为
  4. 启用KASLR会显著增加利用难度,但不能完全防御

五、总结

CVE-2026-68138这个洞让我想起了一句老话:Linux内核里没有”如果有人并发调用就崩”的代码,只有”我们假设没人并发调用”的代码。五年前project zero就反复敲打过这类refcount race的内核模式,可Linux流量控制子系统的维护者显然没听进去——也许是觉得反正有RTNL锁罩着,谁敢不加锁?flower分类器就敢。

拉莫斯这套exploit的精彩之处,在于他没去硬刚内核的栈保护、SMAP(内核态禁止访问用户态内存)/SMEP(内核态禁止执行用户态代码)、freelist随机化这些现代防御,而是顺着漏洞本身的refcount race走出一条干净的利用链:BPF alias → pipe leak → simple_xattr spray → modprobe_path hijack。每一步都用最小权限操作完成,没有一处在碰内核cred结构或栈回绕。

这就是为什么内核漏洞研究永远值得敬畏——你不需要发明新技术,只需要把”显而易见”的并发bug串成一条链。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容