技术解读

以下内容由 GitRead 中文社区 编辑团队基于项目 README 与官方资料整理, 结合中文开发者的常见使用场景补充技术选型建议。具体以项目官方文档为准。

Dirty Frag:把页缓存变成提权跳板的又一次尝试

项目背景与痛点

Linux 内核的页缓存(page cache)一直是本地提权研究的富矿。从 2016 年的 Dirty COW(CVE-2016-5195)到 2022 年的 Dirty Pipe(CVE-2022-0847),再到 2024 年的 Dirty Pagetable,攻击者的思路一脉相承:找到一个能让用户态"合法地"拿到某个内核页引用、又能在语义上把它当普通内存写的地方。一旦打通,只读挂载的 /etc/passwd、setuid 二进制文件、容器镜像里的只读层,都会在一瞬间失去保护。

V4bel/dirtyfrag 延续了这条命名谱系。从仓库名与作者的公开研究脉络看,它聚焦的是 socket fragment(skb frags)与 splice 零拷贝路径之间的页引用语义错配:网络栈在收发包时大量使用 scatter-gather,skb 的 frags 数组里存的是裸 struct page * 而非数据副本;而 splice() 又允许把 pipe_buffer 里的页引用直接"搬运"进 socket。两套机制各自都做过安全校验,叠在一起却容易在引用计数、标志位清零、页生命周期上出现裂缝——Dirty Frag 就是在这个缝隙里做文章。

它解决的不是"某个业务痛点",而是安全研究领域的一个真问题:当内核越来越多的关键路径为了性能而共享页引用时,传统的 copy-based 安全检查还够不够用?

核心技术揭秘

从利用链条来看,Dirty Frag 的关键在于"取得页引用的可写别名",而非暴力对抗权限检查。整条数据流大致可以拆成四个阶段:页缓存页定位 → 引用搬入 pipe/skb → 引用计数失衡 → 页重用获得可写映射。

graph TD A[攻击者进程] --> B[打开只读目标文件并 mmap] B --> C[splice 把页缓存页引用搬入 pipe_buffer] C --> D{"pipe_buffer 标志位可合并?"} D -->|"标志位未清零"| E[经匿名页路径写入] D -->|"已修复被阻断"| F[改走 socket 路径] E --> G[skb frags 持有裸 page 指针] F --> G G --> H[释放 socket 使引用计数失衡] H --> I[页缓存页被回收并重新分配] I --> J[用户态拿到同一物理页的可写别名] J --> K[篡改 setuid 二进制或 root 属主文件]

第一阶段是"借道"。splice() 的语义是零拷贝,pipe_buffer 里的 page 与目标文件的页缓存页是同一个对象。Dirty Pipe 的根因正是 PIPE_BUF_FLAG_CAN_MERGE 未在新分配的 pipe_buffer 上清零,导致后续 write() 直接往只读页的偏移处合并写入。Dirty Frag 显然吸收了这个教训,它更依赖 skb fragment 对 page 的裸引用:sendfile() / splice() 到 socket 时,内核把页挂进 frags 而不复制内容,此时若页的引用计数或 skb 的释放路径存在偏差,就可能出现"内核仍认为页被持有、实际已可回收"的窗口。

第二阶段是"错配"。页缓存页通常由 inode 的 address_space 管理,回收受 LRU 与引用计数约束。一旦引用计数被 fragment 路径错误地增减,这个页就可能被页分配器回收,转而分配给用户态匿名内存或页表页——这正是 Dirty Pagetable 的思路:与其改页内容,不如让页换个身份。用户态拿到同一物理页的可写别名后,再回头通过原文件的 mmap 观察并验证写入结果,形成"只读文件被就地修改"的效果。

实现上,这类原型通常需要处理几个硬骨头:精确控制 pipe 的填充与释放顺序以命中竞态窗口;构造能让 skb frag 长时间持有页引用的 socket 状态(如设置较小的发送缓冲区、使用 MSG_MORE 挂起发送);以及在目标内核版本上适配 pipe_buffer 结构与 skb_shared_info 的字段偏移。

功能亮点与差异

与 Dirty Pipe 相比,Dirty Frag 的差异点在于不依赖单一漏洞点,而是把"页引用生命周期"当作攻击面——这意味着即使 PIPE_BUF_FLAG_CAN_MERGE 被修掉,只要 fragment 路径的引用语义仍有瑕疵,利用链依然成立。与 Dirty Pagetable 相比,它更轻:不需要构造复杂的页表自引用与 TLB 冲刷,落点更靠近"文件篡改"这一类高价值目标。

另一个亮点是对跨场景可用性的追求。容器共享宿主机内核页缓存,只读挂载、overlayfs 的 lower 层在页缓存层面并不只读,这让该手法在容器逃逸与镜像完整性破坏上具备现实威胁。此外,仓库本身更像一份"可复现的研究载体":结构清晰、依赖少,便于安全团队拿来做检测规则与缓解措施的验证基线。

应用场景与落地建议

对防守方而言,这类项目的价值大于风险。建议从三个层面落地:

  • 内核侧:跟进相关 CVE 与上游补丁,重点关注 splice/sendfile 到 socket 的页引用路径、pipe_buffer 初始化路径的改动;开启 CONFIG_DEBUG_PAGEALLOC、KASAN 与 slub_debug 做灰度验证。
  • 系统侧:对关键只读文件启用 IMA/EVM 完整性度量,用 fs-verity 保护不可变镜像;限制非特权用户的 splice/vmsplice/sendfile 组合使用(Seccomp 白名单比 broad deny 更可控)。
  • 运营侧:把"只读文件在页缓存中被就地修改"作为检测假设,建立文件哈希与页缓存内容的周期性比对;容器场景下缩短镜像层生命周期、避免长驻只读基线被悄悄改写。

需要提醒的是,生产环境不要指望单一缓解措施。页缓存类漏洞的修复往往反复,务必保持内核版本可快速回滚,并对高风险主机做最小化能力裁剪。

综合评价

优点:技术路线清晰,站在 Dirty COW → Dirty Pipe → Dirty Pagetable 的肩膀上做了合理的机制组合,攻击面选择(fragment 的页引用语义)足够精准;代码组织紧凑,作为研究与检测基线非常实用。

不足:作为 PoC 型项目,它对内核版本与配置高度敏感,通用性依赖大量适配代码;文档与缓解说明偏薄,容易让一线运维误判风险边界;对竞态窗口的依赖也让稳定性打了折扣。

一句话总结:它不是又一个"改只读文件"的技巧,而是一次提醒——当性能优化让页引用在内核里四处流动时,安全边界必须跟着页走,而不是跟着函数走。

关于

简要分析

热度分
6909
价值分
3389
活跃状态
活跃

语言占比

C

贡献者