Dirty Pipe 讲解—CVE-2022-0847 内核提权漏洞深度分析
漏洞概述
Dirty Pipe(CVE-2022-0847)是 Linux 内核中的一个高危本地提权漏洞,由 Max Kellermann 于 2022 年 2 月披露。漏洞存在于内核的管道(pipe)缓冲区管理机制中,允许非特权用户向任意只读文件写入数据,从而实现提权。
影响范围:Linux 5.8 至 5.16.11 / 5.15.25 / 5.10.102。这个漏洞的本质是页缓存(page cache)与管道缓冲区之间的引用计数管理错误,导致本应只读的页缓存页被意外标记为可写。
背景知识
管道(Pipe)的工作机制
管道是 Linux 中最古老的进程间通信机制之一。内核中管道的实现基于一个环形缓冲区,每个管道最多有 16 个页(PIPE_DEF_BUFFERS = 16),每个页大小为 4KB。
管道的写操作核心函数是 pipe_write(),它的逻辑大致如下:
- 找到管道中第一个空闲的缓冲区(
pipe_buffer) - 如果缓冲区没有对应的物理页,分配一个新页
- 将用户空间的数据拷贝到这个页中
- 更新缓冲区的偏移和长度
关键在于第 2 步:当缓冲区已经有一个页时(比如上一次写操作分配的页还没被完全消费),内核会尝试复用这个页。如果这个页是从其他地方”借”来的(比如通过 splice 系统调用从文件映射过来的页缓存页),问题就出现了。
splice 系统调用
splice() 可以在文件描述符之间移动数据,而不需要将数据拷贝到用户空间。当从文件 splice 到管道时,内核不会真正拷贝数据,而是直接把文件的页缓存页(page cache page)的引用赋给管道缓冲区。
这意味着管道缓冲区中的页可能是文件的页缓存页——而页缓存页通常是只读的(因为它们是文件内容的缓存,修改它们会导致文件内容被篡改)。
漏洞根因
漏洞的核心在 pipe_write() 函数中的一段逻辑。当向管道写入数据时,如果当前缓冲区的页是一个”匿名”页(不是从文件 splice 来的),内核可以直接往里面写。但如果这个页是从文件 splice 来的页缓存页,内核应该先做一个”写时复制”(COW, Copy-On-Write)——分配一个新页,把原页内容拷贝过去,然后往新页里写。
问题出在判断是否需要 COW 的条件上。内核用 pipe_buffer->flags 中的 PIPE_BUF_FLAG_CAN_MERGE 标志来判断这个缓冲区是否可以被合并写入(即是否可以直接往已有页里追加数据)。
在正常情况下,从文件 splice 来的页缓存页不应该设置 PIPE_BUF_FLAG_CAN_MERGE 标志,因为它们是只读的。但是,当一个管道缓冲区被释放并复用时,flags 字段没有被正确重置——如果之前的缓冲区设置了 PIPE_BUF_FLAG_CAN_MERGE,这个标志会残留到新的缓冲区上。
具体的触发路径:
- 创建一个管道,往里面写任意数据(填满 16 个页缓冲区),此时所有缓冲区都设置了
PIPE_BUF_FLAG_CAN_MERGE - 从管道中读取所有数据,清空管道。此时缓冲区被释放,但
flags没有被重置 - 用
splice()从目标文件向管道写入数据。此时管道复用了之前的缓冲区,而PIPE_BUF_FLAG_CAN_MERGE标志仍然残留着 - 再向管道写入数据。内核看到
PIPE_BUF_FLAG_CAN_MERGE标志,认为可以直接合并写入,于是跳过了 COW,直接往页缓存页里写数据 - 页缓存页被修改了!而这个页是文件内容的缓存,修改它等于修改了文件内容
这就是 Dirty Pipe 的完整利用链。本质上是一个未初始化变量(flags 残留)导致的越权写。
漏洞补丁
内核的修复非常简单——在创建新的管道缓冲区时,显式重置 flags 字段:
1 | // 修复前(fs/pipe.c) |
完整的补丁提交可以看内核 commit 9d2231c574a50327944a4f01922d50327944a4f,核心改动就是在 anon_pipe_buf_get 和相关路径中确保 PIPE_BUF_FLAG_CAN_MERGE 不会被错误地继承。
Exploit 编写
理解了漏洞原理,exploit 的编写就很清晰了。目标是向一个只读文件(如 /etc/passwd)写入数据,实现提权。
利用步骤
- 创建管道并填充:创建管道,写入 16 页数据,让所有缓冲区都设置
CAN_MERGE标志 - 清空管道:读出所有数据,释放缓冲区(但 flags 残留)
- splice 文件页到管道:打开目标文件,定位到要覆盖的偏移,用
splice()把文件的一页读到管道中 - 向管道写入 payload:此时写入的数据会直接覆盖页缓存页,从而修改文件内容
完整 exploit 代码
1 |
|
提权演示
最经典的提权方式是修改 /etc/passwd,把 root 用户的密码字段清空(这样 root 就没有密码了,可以直接 su root 登录):
1 | # 编译 |
另一种更隐蔽的方式是修改 SUID 二进制文件(如 /usr/bin/passwd、/usr/bin/su),在其中注入 shellcode。但修改 /etc/passwd 是最简单直接的。
漏洞检测
检查内核版本
1 | uname -r |
检测脚本
1 |
|
防御与缓解
1. 升级内核
最根本的修复是升级到已打补丁的内核版本:
- 5.16.11+
- 5.15.25+
- 5.10.102+
2. 缓解措施
在无法立即升级内核的情况下,可以采取以下缓解:
- 限制 SUID 二进制文件:减少可被篡改的高价值目标
- 文件系统只读挂载:关键系统文件所在分区只读挂载
- 容器隔离:在容器中利用此漏洞需要额外的条件(容器内通常没有可写的 SUID 文件)
- Grsecurity / PaX:增强内核安全的补丁集可以缓解此类漏洞
3. 检测利用行为
可以通过以下方式检测 Dirty Pipe 的利用尝试:
- 监控
splice()系统调用的异常使用(特别是从只读文件到管道的 splice) - 监控页缓存的异常修改
- 使用 eBPF 工具追踪
pipe_write和splice的调用链
技术思考
Dirty Pipe 是一个非常”优雅”的漏洞——它不是什么复杂的竞争条件或内存破坏,而是一个简单的标志位未重置。但正是这种”小问题”,在复杂的内核代码中被忽视了多年。
这个漏洞给我们的启示:
资源复用必须重置状态:管道缓冲区的复用没有重置
flags,这是典型的”未初始化变量”类问题。在任何资源池/对象池的实现中,复用前必须完全重置状态。只读语义的维护成本:页缓存页的”只读”是一个约定,而不是硬件强制的(内核态可以写任何页)。维护这个约定需要在所有写路径上检查,一旦有一个路径漏了,漏洞就出现了。
splice 的零拷贝代价:splice 通过避免数据拷贝来提升性能,但也增加了引用管理的复杂性。性能优化往往伴随着安全风险的增加。
本地提权漏洞的价值:很多人忽视本地提权漏洞,认为”攻击者需要先有本地访问权限”。但在容器化环境中,容器逃逸往往就是从一个本地提权漏洞开始的。在多租户环境中,本地提权等于完全失守。
参考
- 原始披露:The Dirty Pipe Vulnerability by Max Kellermann
- CVE:CVE-2022-0847
- 内核补丁:commit
9d2231c574a5(“pipe: make sure to not merge page cache pages”) - 受影响版本:Linux 5.8 - 5.16.10 / 5.15.24 / 5.10.101
总结
Dirty Pipe 是近年来最经典的 Linux 内核提权漏洞之一。它的原理不复杂——管道缓冲区复用导致 CAN_MERGE 标志残留,使得页缓存页被意外写入——但利用效果极强:任意只读文件可写,直接导致 root 提权。
理解这个漏洞需要掌握 Linux 管道的实现、页缓存机制、splice 系统调用的工作原理。这些都是内核安全研究的基础。从 Dirty Pipe 出发,可以进一步研究其他内核提权漏洞(如 Dirty COW、PwnKit、SUDO 漏洞等),它们的利用思路有很多共通之处。
内核漏洞利用的核心在于:理解内核数据结构的布局、找到引用计数或状态管理的缺陷、构造精确的内存破坏原语。Dirty Pipe 提供了一个完美的学习样本——简单、可靠、影响深远。








