ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux内核cls_route double free漏洞分析:从TC过滤器到提权路径

Linux内核cls_route double free漏洞分析:从TC过滤器到提权路径 前阵子在追Linux内核网络子系统的过滤器实现时CVE-2022-2588 正好进入了我的视野它是 cls_route也就是 route4 这个流量分类器里的一个 double free 漏洞最终能升级成内核提权。这个洞不复杂但从它身上能学到的东西很多——比如 tfilter 的生命周期管理、错误路径的脏清理、以及 KASAN 定位 double free 的标准方法。这篇文章我打算按审计线索来写从 tc 过滤器基础讲起推演漏洞触发路径最后给出排查和修复思路。想深入学习内核漏洞分析、或者正在做云原生安全基线的朋友这篇应该能对得上胃口。1. 漏洞背景与影响范围1.1 cls_route 是干嘛的Linux 网络协议栈里的 TCTraffic Control子系统很多人的第一印象是tc命令、qdisc、class也就是流量整形和带宽管理那一套。但本质上TC 里最核心的其实是分类器classifier它决定了一个报文进入哪个类从而被哪个调度策略处理。分类器在代码里就是tcf_proto上挂的一堆 filter常见的有u32、flower、fw、bpf以及这里的主角cls_route。cls_route也叫 route4是一个很老的分类器它的工作思路和u32不一样u32是手工指定匹配规则而 route4 会根据报文的目的 IP 去查路由表拿到一个路由 ID再用这个 ID 去哈希桶里找对应的 filter。如果匹配到了就返回该 filter 绑定的 classid数据包就按照对应的 qdisc/class 处理。所以 route4 特别适合“跟着路由走”的场景比如让不同静态路由去往不同优先级队列。这个模块在内核里存在了很多年代码量不大但维护得比较克制属于“小众但一直有人用”的组件。漏洞正是出在这个看似简单的分类器上。CVE-2022-2588 的公告里描述得很直白Linux 内核cls_route分类器实现存在 double free 问题可能导致本地权限提升或系统崩溃。由于 route4 本身需要配置路由和 tc filter 才能使用漏洞的触发前置条件是需要具备CAP_NET_ADMIN权限。单看这个条件漏洞面似乎只面向管理员但别忽略容器和用户命名空间这两个场景在 unprivileged user namespace 里非 root 用户也可以创建新的网络命名空间从而在自己可控的 netns 内获得CAP_NET_ADMIN再叠加内核漏洞完成容器逃逸或提权。这意味着云上多租户场景受到的影响并不小。1.2 CVE-2022-2588 的基本信息漏洞对应的组件是net/sched/cls_route.c属于内核网络调度子系统。根据公开信息这个问题在研究社区里被认定为“double free”也就是同一个对象被释放了两次。从安全的视角看double free 比普通的 UAF 更危险因为它往往能在经过精心布局后转化为任意地址写进而拿到高权限。漏洞影响的内核版本范围覆盖到了 5.4、5.10、5.15 等当时主流的长周期分支我在自己的测试环境里用 5.15 内核复现过崩溃流程。官方修复是在 mainline 中通过一个针对 route4 的补丁解决的后续各大发行版也都各自移植了修复。如果你去看发行版的安全公告Red Hat 和 Ubuntu 都把它标记为“Important”级别原因是提权路径清晰、利用成本相对可控。更具体的版本矩阵建议直接参考内核 CVE 数据库或发行版公告不同厂商的回移批次不一样这里不展开。1.3 这个漏洞为什么值得单独分析读者可能会问内核漏洞这么多为什么专门挑 route4 这个老模块我的看法是这种小而精的漏洞非常适合作为学习样本第一它的触发机制不依赖复杂的硬件特性也不需要理解 GPU/驱动等大型子系统纯网络子系统代码就能说清楚。第二它体现的是“错误处理路径中的生命周期泄漏”这是内核里最常见、也最容易出问题的一类 bug和之前许多 UAF、double free 漏洞的根因一脉相承。第三分析这个洞需要掌握tcf_proto、cls_route哈希表、RCU 释放等几个关键机制搞懂之后再看其他 TC filter比如cls_u32、cls_fw思路基本是通的。我建议做内核安全、云原生基础设施安全的朋友都过一遍这个案例不仅是在“看漏洞”更是在积累一种“阅读内核代码时如何抠错误路径”的习惯。2. 从 tc filter 到 route4漏洞所在的实现细节2.1 tfilter 的通用框架在深入 route4 之前先要理解 TC filter 在内核里的组织方式。每个网络设备上挂着一棵tcf_block里面是一个或多个tcf_proto每个tcf_proto对应一种分类器类型。用户态发来一条tc filter add ...命令时内核通过 netlink 走到tc_ctl_tfilter再根据协议类型找到对应分类器的tcf_proto_ops最终调用它的change()方法来完成创建或修改。route4 对应的tcf_proto_ops里有几个关键方法classify()是分类逻辑change()负责创建/修改 filterdelete()负责删除 filterdestroy()负责销毁整个 proto。对于 CVE-2022-2588 来说核心矛盾集中在route4_change()里“替换旧 filter”的这个动作。有个比较重要的背景当用户执行tc filter change或者对同一个 handle 重新加规则时内核不会直接修改旧的 filter 对象而是先创建一个新的route4_filter然后通过某种方式把新旧对象交接。这个“交接”如果处理不干净就会出现指针悬空、双重释放等问题。而 route4 的问题恰好就发生在新旧 filter 交接的异常分支里。2.2 route4 的哈希与管理结构route4 的内部结构并不复杂大致可以分成两层顶层是一个route4_head里面维护了一张哈希表哈希桶数量固定每个哈希桶对应一个route4_bucket桶内通过链表挂载route4_filter。每个route4_filter表示一条具体的过滤规则。当数据包进入route4_classify()时内核会提取目的地址对应的路由 key经过哈希计算后找到某个桶再遍历桶内链表比较 rule 的关键字段匹配成功则返回 classid。为了让删除和修改操作高效route4_filter里保存了一个指向自身所属 bucket 的指针。这个桶指针非常关键后续的删除、替换逻辑都要靠它来判断当前 filter 是否真的挂在某个哈希桶上。从内存管理的角度看route4_filter是一个典型的“既可能挂在链表上、也可能被释放”的对象。如果一个 filter 已经被释放但哈希桶的链表里还残留着它的指针那么后续任何一次桶遍历都可能碰到这个悬空指针形成 UAF如果之后的释放逻辑再次对它调用kfree()就变成了 double free。漏洞利用者会想尽办法让这种情况变得可控。2.3 三个关键函数change、delete、set_parms我们先来看route4_change()的正常逻辑如果 handle 指定的 filter 不存在也就是fold NULL那这个操作是“创建新 filter”。内核分配新的route4_filter初始化字段调用route4_set_parms()解析用户参数最后把新对象挂到哈希桶上。如果fold ! NULL说明用户是在修改一条已有规则。此时内核同样会分配一个新的route4_filter但它的不少状态要从fold继承处理完后会从哈希桶中把旧对象摘掉并释放。释放旧对象这一步正常路径上有专门的安全删除函数它会先从桶中移除节点再执行kfree()。再看route4_delete()逻辑相对简单根据 handle 找到 filter如果它在桶上就把它从桶里摘下来然后释放。问题往往出现在“不正常的路径”。route4_set_parms()本身要解析不少 netlink 属性可能因为内存不足、参数非法、数值越界等原因返回错误。当它返回错误时route4_change()需要做“回滚”把之前已经分配的对象、已经挂上的链全部清理干净。如果清理过程中混淆了“新对象”和“旧对象”的归属权就很容出现新 filter 已经被释放但哈希桶还指向它之后删除或替换路径再次触碰double free。3. double free 是怎么发生的3.1 错误路径的直观推演为了把触发路径讲清楚我先给一个简化版的伪代码模型。下面这段不是真正内核源码而是从route4_change()的语义里抽象出来的关键流程方便说明问题static int route4_change(...) { struct route4_filter *f, *fold; int err; f kzalloc(sizeof(*f), GFP_KERNEL); if (!f) return -ENOBUFS; fold find_filter(ht, handle); if (fold NULL) { /* 创建场景 */ err route4_set_parms(...); if (err 0) goto errout; /* 把新 filter 挂入哈希桶 */ insert_to_bucket(f); } else { /* 修改场景 */ err route4_set_parms(...); if (err 0) goto errout; if (handle_changed) { /* 某些情况下需要把旧 filter 从原桶移除 */ remove_and_free(fold); } insert_to_bucket(f); } errout: kfree(f); return err; }看到errout这段了吧。如果route4_set_parms()失败函数会直接kfree(f)但它不会去管f是否已经挂进了哈希桶也不会去管之前是否已经把fold给释放了。在某些修改场景下fold可能在错误发生之前已经被摘除并释放如果f又在后续某个遍历或删除路径中再次被释放就会妥妥地变成 double free。我并不是说上面的伪代码和真实源码逐行一致但它的错误处理模式非常接近 CVE-2022-2588 的根因。内核这些年多个 TC filter 漏洞比如之前cls_fw、cls_u32的一些问题本质上也都是这类“错误路径下的生命周期管理”失误。3.2 引用计数与 RCU为什么释放顺序容易出错要理解为什么route4_change()的开发者会写出这样的 bug还得提一下 RCU 在 TC filter 里的角色。网络分类器为了保护数据面性能很多 object 的回收不是立即执行而是通过call_rcu()推迟到安全点。也就是说即便调用方已经执行了kfree()后续仍可能有读者正持有一个 RCU 临界区内的旧指针。正因为这个机制TC filter 的删除操作经常会分成两步第一步把对象从哈希链表中摘除第二步在“安全时机”回收内存。CVE-2022-2588 的问题不是发生在 RCU 回调阶段而是发生在纯粹的错误处理路径上同一块内存被kfree()了两次而不是延迟回收造成的 UAF。这属于纯粹的分配器层次错误SLUB 在这种情况下完全无法自我检测最后会破坏 freelist。为了直观感受 double free 的破坏力我用一个生活化类比你有一张房卡退房时把卡交给前台前台注销了房卡但另一个人手里还留着一张同样的房卡后面酒店前台又把同一张卡当新卡发给下一位客人。此时下一位客人可以打开你的房间而你可能依然在用这张卡最终整个房间的归属就乱套了。内核对象也是这样一个对象被释放后如果还残留在链表里就可能被重新分配为完全不同的对象从而产生类型混淆。3.3 从 double free 到提权既然题目里有“内核提权”那这里就不能回避一个问题double free 到底是怎么变成提权的。公共安全圈对这类漏洞的利用思路其实比较固定虽然我不想在这里贴完整武器化步骤但理解它的大方向对防御有帮助。利用 double free 的核心诉求是把“释放两次”变成一个“对一个仍被引用的对象写入攻击者控制数据”的机会。经典做法是先让目标对象被释放然后申请一批同类型或同大小的对象回来使其中一个新对象占用了那个悬挂指针所在的内存之后通过某个路径对这个“挂着的指针”进行写操作就可以修改新对象的布局。对 route4 这样的分类器来说利用者往往会盯着route4_filter里的 ops 指针或其它函数指针字段一旦改写成攻击者布局的地址后续某个调用点就会跳过去执行控制流。再配合modprobe_path等内核全局变量就能完成权限提升。需要明确的是现代内核里还有 KASLR、SMAP、SMEP、SLAB freelist 随机化等缓解机制利用过程并不容易。但作为漏洞分析我们要承认一点只要一个对象能被 double free在对抗性视角下它就是一个潜在的“任意分配/任意写”原语。这也是为什么内核维护者对这个漏洞的修复非常重视而不是简单当成一个“合法管理员才能触发的小问题”。4. 复现、验证与排查思路4.1 搭建带 KASAN 的测试环境分析内存破坏类漏洞我最推荐的环境就是开启 KASAN 的调试内核。KASANKernel Address Sanitizer能在 double free 发生时直接打印受害对象的分配栈和释放栈效率比肉眼读代码高太多。我的测试流程大致是这样用接近目标版本的 mainline 或发行版内核源码建议 5.15 或 5.10 系列。在.config里开启CONFIG_KASANy、CONFIG_KASAN_INLINEy同时建议打开CONFIG_DEBUG_LISTy和CONFIG_SLUB_DEBUGy可以多一层校验。编译后用 qemu 启动一个最小系统方便反复加载内核和触发程序。在虚拟机内准备一个普通用户并给它配置 user namespace network namespace 相关的权限模拟低权限用户触发路径。KASAN 开启后内核性能和内存占用会明显变大但对漏洞分析来说完全可接受。注意一定不要在生产环境开 KASAN它只适合本地调试。4.2 复现触发路径的设计思路公开渠道没有完整的 PoC 源码所以我这里更想分享“如何从根因反推触发条件”。一旦你理解了route4_change()错误路径的问题就可以自己设计输入通过 netlink 创建一个 route4 filter确保它成功挂入某个哈希桶。再针对同一个 handle 执行一次 change 操作构造一个让route4_set_parms()失败比如携带非法的 key 值、内存分配失败等的 netlink 属性组合让内核走到错误释放分支。随后再执行一次 delete 或重复 change触发对残留指针的再次释放。构造过程中建议用 netlink 库写一个小工具或者直接在系统上用tc命令配合strace观察发送的属性。单纯用tc命令行可能不够灵活因为它内部有很强的参数校验未必能构造出内核层才出现的非法组合。我自己写 C 程序时习惯直接用libmnl或libnl把 netlink 属性一层层拼好这样更容易构造“半合法”的输入。4.3 KASAN 日志怎么读当 double free 被 KASAN 抓到后dmesg里通常会出现类似下面的关键信息BUG: KASAN: double-free in kfree0x... Free of addr ffff8880771e8000 by task xxxx ... Allocated by task xxxx: route4_change0x... ... Freed by task xxxx: route4_delete_filter0x... ...注意看三个信息点double-free字样说明是二次释放Allocated by task给出的栈是对象第一次分配的地方Freed by task给出的栈是第一次释放的地方而当前的调用栈则是第二次释放的位置。把这三个栈叠在一起基本就能定位到具体的代码路径。如果分配的栈指向route4_change而第一次释放的栈也指向route4_change或route4_delete_filter那根因十有八九就锁定了。在实际调试中我还会打开CONFIG_DEBUG_SLAB或使用slub_debug启动参数这样 SLUB 在对象释放时会写入 magic 值二次释放时更容易触发校验报错日志也会更干净。4.4 从修复补丁反推根因分析这类漏洞另一个高效方法是看官方修复补丁。内核 mainline 针对 CVE-2022-2588 的修复核心就是解决route4_change()在错误路径上重复释放leaf_f指针的问题。修复的思路大致可以归纳成两类一类是让失败路径不再直接kfree(f)而是复用标准的、能从哈希桶中安全摘除并释放的删除函数保证“释放前先摘链”避免悬空指针继续留在桶里。另一类是调整route4_set_parms()失败分支的判断条件让旧 filter 只有在确认还在桶里时才执行删除动作防止把已经释放的对象再释放一次。我在读补丁时经常采用一个技巧先看---和两边的函数名再定位到新增的if条件和goto errout标签。如果补丁新增了一个“检查对象是否还在链表中”的条件那基本可以断定原问题出在“没有检查就释放”的地方。这种方法在 Linux 内核 CVE 补丁里非常通用不只在 TC 子系统中有效。5. 防御与缓解措施5.1 权限边界是第一道防线从系统防御角度CVE-2022-2588 最直接的前置条件是CAP_NET_ADMIN。在传统服务器上这个 capability 通常只有 root 或专门的网络管理程序才有漏洞利用门槛相对高。真正需要警惕的是容器场景如果容器运行时允许创建 user namespace容器内的非特权用户可以在自己新创建的 netns 中获得完整的 capability从而满足触发条件。因此基线加固建议很明确在容器运行时层禁用或严格限制--user与 user namespace 的重映射能力。在部署容器时移除NET_ADMIN、NET_RAW等网络相关 capability。如果业务确实需要 pod 自行管理网络策略优先通过 CNI 或节点上的 agent 完成而不是直接把NET_ADMIN交给业务容器。值得说明的是即使没有 user namespace攻击者也很难远程直接利用这个漏洞因为它必须要能够下发 tc 配置。所以这个漏洞更多是“已有一个低权限或受限容器执行环境”后的纵深突破环节而不是最初的入口。5.2 内核缓解机制盘点在无法立刻升级内核的情况下可以依赖一些缓解机制提高利用门槛。我整理了一个常用机制对照表缓解机制作用针对这个漏洞的效果KASLR随机化内核地址布局提高定位目标对象/函数地址的难度SMEP / SMAP禁止内核执行用户地址、禁止内核访问用户地址让 ROP 和用户态指针注入更困难SLAB_FREELIST_RANDOM随机化 slab freelist 顺序降低 double free 后预测分配位置的精度SLAB_FREELIST_HARDENED对 freelist 指针做异或编码防止直接篡改 freelist 指针完成任意写KFENCE低开销的样本式内存错误检测生产环境可开启能较早发现异常释放这些机制都不是万能的它们主要解决“利用起来很难受”的问题而不是“不可能利用”。真正的根治办法还是升级内核和及时回移补丁。5.3 运维上的升级与监控建议对运维和 SRE 来说最实际的操作是跟进内核安全更新。CVE-2022-2588 修复已经进入多个发行版的稳定仓库建议在验证兼容性后优先回移。云环境里如果使用托管节点池可以按“先少量节点、再全量滚动”的节奏升级内核减少业务影响。另外可以在监控侧关注一些可疑的系统调用行为比如一个低权限进程反复通过 netlink 下发 TC filter或者短时间内出现大量 namespace 创建和删除这些往往是漏洞利用的前兆。# 示例通过 auditd 审计 netlink 相关 syscall 行为 -a always,exit -F archb64 -S sendto -S recvfrom -F euid!0 -k netlink-monitor这个规则只是示例生产环境需要根据实际场景调整。审计本身不直接防漏洞但可以提供溯源依据。写在后面一点审计心得把这个 CVE 从头到尾理完我的感受是内核漏洞分析最锻炼人的地方不是记住某个具体漏洞而是建立“错误路径意识”。当你读一个函数时不要只开心地看正常流程更要追问“如果中间某一步失败了代码会跳到哪里之前分配的对象挂到哪了” CVE-2022-2588 就是一个很典型的教学样本它用最朴素的方式提醒我们——对象的生命周期管理容不得半点侥幸。最后再分享一个小经验分析这类 double free 时我习惯先用git log -S找到修复提交把补丁反向还原成“有漏洞版本”再用 KASAN 内核去验证触发路径。这个流程比从头读全部调用链快很多也特别适合刚接触内核安全的朋友上手。希望这篇分析能帮你把 route4 这个洞彻底吃透。
返回列表