Dirty Pipe (CVE-2022-0847) 漏洞分析与利用完全指南
一、漏洞概述
Dirty Pipe (CVE-2022-0847) 是 Linux 内核 5.8 及之后版本中存在的一个本地提权漏洞,攻击者可以通过覆盖任意可读文件的内容(即使文件权限为只读)将普通用户权限提升至 root。该漏洞源于管道(Pipe)机制与 Page Cache 的交互缺陷,与经典的 Dirty COW (CVE-2016-5195)漏洞类似,但利用更简单、影响范围更广。
漏洞核心原理
-
管道的"零拷贝"特性:当通过 splice 系统调用将文件内容写入管道时,内核会直接将文件的 Page Cache 页面作为管道的缓冲区页使用,而非复制数据。
-
未初始化的标志位漏洞:管道缓冲区的 flags 变量在初始化时未正确重置,导致攻击者可以错误地认为该页是可写的。
-
Page Cache 的覆盖效果:文件的 Page Cache 页面被直接关联到管道缓冲区,攻击者通过向管道写入数据可覆盖 Page Cache 中的原始文件内容。
二、环境搭建与调试准备
1. 内核准备
编译带调试信息的内核,确保配置以下选项:
Kernel hacking → Kernel debugging
Kernel hacking → Compile-time checks and compiler options → Compile the kernel with debug info
Kernel hacking → Generic Kernel Debugging Instruments → KGDB: kernel debugger
2. 文件系统准备
构建最小根文件系统(基于BusyBox),配置磁盘镜像和rcS文件。
3. 工具链准备
安装 Qemu 和 pwndbg,配置 gdb.sh 和 start.sh 脚本。
三、Linux内核源码阅读和调试技巧
1. 系统调用实现原理
Linux 系统调用是用户空间与内核交互的核心接口,其实现依赖于架构相关的中断机制和系统调用表(sys_call_table)。每个系统调用通过唯一的系统调用号索引,对应内核中的 sys_xxx 函数。
2. f_op 结构体原理
struct file_operations (f_op)定义了文件操作的函数指针,如 open、read、write 等。内核通过 file->f_op 调用这些函数,具体实现由文件系统或设备驱动提供。
3. GDB动态调试技巧
常用命令:
n:执行下一行源码,不进入函数内部ni:执行下一条汇编指令,不进入函数内部s:进入当前行调用的源码函数内部si:进入call调用的函数内部,以汇编指令级别单步调试
四、管道(Pipe)机制详解
1. 管道基本概念
在Linux系统中,pipe是一种进程间通信(IPC)机制,通过pipe系统调用可以创建一个管道,返回两个文件描述符:
- 写端(fd[1]):用于写入数据
- 读端(fd[0]):用于读取数据
2. 管道内核实现
管道通过 struct pipe_inode_info 和 struct pipe_buffer 两个核心结构体实现:
struct pipe_inode_info {
unsigned int head;
unsigned int tail;
unsigned int max_usage;
unsigned int ring_size;
struct pipe_buffer *bufs;
// ...
};
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
unsigned int flags;
// ...
};
3. 管道读写机制
写入流程:
- 数据按页写入 bufs[head]
- 更新 head 指针
- 若缓冲区满,写进程进入睡眠
读取流程:
- 从 bufs[tail] 读取数据
- 更新 tail 指针
- 若缓冲区空,读进程阻塞
4. Page Cache 机制
Page Cache是内核管理的一块内存区域,用于缓存磁盘上的文件数据块(以内存页为单位,通常4KB)。其工作原理:
读操作:
- 缓存命中:直接返回内存中的数据
- 缓存未命中:从磁盘读取数据,存入Page Cache
写操作:
- 缓冲写入(Writeback):默认修改Page Cache中的缓存页,异步刷回磁盘
- 直写(Writethrough):同步写入磁盘(较少使用)
五、splice系统调用与零拷贝机制
1. 零拷贝机制概述
传统的文件拷贝过程(open→read→write)需要在用户态和内核态之间多次切换,并进行CPU和DMA之间的数据拷贝。而splice系统调用可以实现内核态内的"零拷贝",只需2次上下文切换。
2. splice调用链
SYSCALL_DEFINE6(splice) → do_splice → do_splice_to → generic_file_splice_read →
generic_file_read_iter → generic_file_buffered_read → copy_page_to_iter →
copy_page_to_iter_pipe
关键函数 copy_page_to_iter_pipe 中的 buf->page = page 实现了将文件的page_cache直接替换掉管道page,完成零拷贝。
六、Dirty Pipe漏洞原理深入分析
1. 漏洞触发条件
- 通过pipe_write将管道数据写满,给pipe_buffer的flags赋值为PIPE_BUF_FLAG_CAN_MERGE
- 通过pipe_read将管道清空,确保splice有足够拷贝空间
- 调用splice将只读文件内容写入管道,将目标文件的Page Cache页面关联到管道缓冲区
- 调用pipe_write向管道写入恶意数据,覆盖原Page Cache页面
2. 漏洞利用限制
- 文件需可读:攻击者必须对目标文件拥有读权限
- 单页覆盖限制:每次写入最多覆盖一页大小(通常为4KB)
- 修改临时性:仅篡改内存中的Page Cache,不会同步到磁盘
七、漏洞复现与动态调试分析
1. 复现步骤
- 创建只读测试文件
- 创建管道并填满数据
- 清空管道数据
- 使用splice将只读文件内容写入管道
- 使用pipe_write覆盖Page Cache
2. 关键调试点
- open文件结构体:观察以只读模式打开的struct file对象
- pipe结构体:观察pipe_inode_info和pipe_buffer结构体
- pipe_write/pipe_read:观察flags字段的初始化和管道状态变化
- splice调用链:跟踪零拷贝过程,特别是page的替换
3. 常见问题解答
为什么一定要将管道填满再清空?
- 填满管道是为了初始化所有pipe_buffer的flags为PIPE_BUF_FLAG_CAN_MERGE
- 清空管道是为了确保splice有足够空间进行零拷贝
- 不完全填满会导致无法正确覆盖目标page
为什么程序会在splice调用时卡死?
当管道已满且未设置非阻塞标志时,wait_for_space会调用pipe_wait等待,导致进程阻塞。
八、漏洞利用:劫持SUID二进制文件提权
通过dirty_pipe漏洞劫持拥有root权限的SUID二进制程序:
- 覆盖目标二进制程序,注入恶意ELF文件
- 执行被覆盖的二进制程序,创建具有root权限的可执行文件
- 通过该文件实现权限提升
示例利用代码结构:
// 1. 打开目标SUID程序
int fd = open("/usr/bin/target", O_RDONLY);
// 2. 创建管道并准备漏洞利用条件
int p[2];
pipe(p);
for(int i=0; i<16; i++) write(p[1], "A", 1);
for(int i=0; i<15; i++) read(p[0], buf, 1);
// 3. 使用splice进行零拷贝
lseek(fd, offset, SEEK_SET);
splice(fd, &offset, p[1], NULL, 1, 0);
// 4. 写入恶意payload
write(p[1], evil_elf, evil_elf_size);
// 5. 执行被篡改的程序
system("/usr/bin/target");
九、扩展资源
- 漏洞公告:CVE-2022-0847 NVD详情
- Linux内核修复提交记录:官方修复补丁的代码提交
- 工具与代码:
- GitHub - n3rada/DirtyPipe:自动化利用工具
- GitHub - veritas501/pipe-primitive:漏洞利用原语研究
- Exploit-DB Dirty Pipe PoC:可直接编译运行的PoC
- 技术分析:
- DirtyPipe与Dirty Cow对比分析
- Qualys技术深度解读
- LWN.net内核机制解析
十、总结
Dirty Pipe漏洞通过精心构造的管道操作,利用内核在管道缓冲区管理上的缺陷,实现了对只读文件的越权写入。理解该漏洞需要深入掌握Linux内核的管道机制、Page Cache管理和零拷贝技术。通过本指南的系统性分析,读者不仅可以掌握该漏洞的原理和利用方法,还能学习到Linux内核调试和分析的基本技能,为后续的内核漏洞研究打下坚实基础。