Dirty Pipe 漏洞成因
攻击者可以利用该漏洞实现低权限用户提升至 root 权限,且能对主机任意可读文件进行读写
攻击适用版本:
- Linux Kernel版本 >= 5.8
- Linux Kernel版本 < 5.16.11 / 5.15.25 / 5.10.102
攻击适用条件:
- 攻击者必须有读权限(因为它需要通过
splice
方法将将页输入管道中) - 偏移量不能在页边界上(因为页上至少有一个字节已经拼接到管道中)
- 写入不能跨越页边界(因为这将为其余部分创建一个新的匿名缓冲区)
- 文件无法调整大小(因为管道有自己的页面填充管理,并且不会告诉页面缓存附加了多少数据)
- 单次写入的长度不能超过一页(因为页大小为4K)
该漏洞源于新管道缓冲区结构的“flag”变量在 Linux 内核中的 copy_page_to_iter_pipe
和 push_pipe
函数中缺乏正确的初始化
前置知识 - Page Cache & splice
Page Cache 即缓存管理机制,一般当我们访问一个磁盘文件的时候,首先内核会将其内容装载到 Page Cache 内存中,后续都是直接读取内存中的 Page Cache 来访问数据,内核会在合适的时机将标脏的 Page 给写回磁盘中
- 如果用户进程使用 read/write 读写文件,那么内核会先将载入数据的物理内存映射到内核虚拟内存 buffer,然后再将内核的 buffer 数据拷贝到用户态
- 如果追求效率,内核也提供一种零拷贝模式(不发生系统调用,跨越用户和内核的边界做上下文切换),用户进程可以使用 mmap 直接将用户态的 buffer 映射到物理内存,不需要进行系统调用,直接访问自己的 mmap 区域即可访问到那段物理内存内容
splice 系统调用通过一种“零拷贝”的方法将文件内容输送到管道之中,相比传统的直接将文件内容送入管道性能更好
- 经典的
read/write
方式:利用用户态数据buf
作为文件缓存
1 | buf = malloc(len) // 首先申请一块长度为len的内存 |
- 零拷贝
splice
:在数据发送的过程中,不需要在用户态为数据申请buf
,也就是不会产生用户态、内核态之间的数据拷贝
1 | ssize_t splice(int fd_in, loff_t *off_in, int fd_out, |
- 在两个文件描述符之间移动数据,而无需在内核地址空间和用户地址空间之间进行复制
- 它从文件描述中传输最多
len
字节的数据 - 将
fd_in
传递到文件描述符fd_out
,其中文件描述符之一必须引用管道
splice 在内核中对应的接口如下:
1 | SYSCALL_DEFINE6(splice, int, fd_in, loff_t __user *, off_in, |
- 调用链如下:
1 | sys_splice -> do_splice -> [] |
- 其中
copy_page_to_iter_pipe
对“flag”变量没有进行初始化
使用 splice
将数据从文件导入到管道中:(file to pipe
)
- 首先将数据加载到文件页面缓存
page cache
中 - 然后创建一个管道缓冲区
pipe_buffer
- 直接
pipe_buffer->page = page cache
,把page cache
当做pipe_buffer
的缓存页
如果此时该管道还想存储从其他输入流传输来的数据,就只能重新申请 pipe_buffer
,不能直接附加到刚才的 pipe_buffer
中,因为该 page
是文件的缓存页面,不属于管道,但 Dirty Pipe 利用了一种方法使该页面可以被管道写入
前置知识 - Pipe
管道 Pipe 是一种经典的 IPC 通信方式:
- 它包含一个输入端和一个输出端,程序将数据从一段输入,从另一端读出
- 在内核中,为了实现这种数据通信,需要以页面 Page 为单位维护一个环形缓冲区(被称为
ring_buffer
),里面存了16个pipe_buffer
结构,每个pipe_buffer
结构又有一个指针指向一个表示物理内存页 Page 的结构体
1 | struct pipe_buffer { |
- 每个 Page 大小为 4KB,页面之间并不连续
- 管道维护两个引用计数器,一个用来写 (pipe->head),一个用来读 (pipe->tail),可以被循环利用
- 当前页面带有
PIPE_BUF_FLAG_CAN_MERGE
flag 时,如果将标记且续写后的数据长度不超过一页时,则可以进行续写
管道描述符 pipe_inode_info
,用于表示一个管道,存储管道相应的信息:
1 | struct pipe_inode_info { |
管道可以分为命名管道和匿名管道:
- 命名管道是一个有名字的实体文件
- 匿名管道就是我们常使用的管道符创建的管道
本质上来讲,管道就是一种进程间的通信手段,让两个进程可以通过 pipe 发送和接收数据(匿名管道可用于父子与兄弟进程之间的通信,有名管道则用于两个无关进程的通信)
这里我们重点分析实现管道写的函数 pipe_write
:
- 装载了文件缓存页面
page tcache
的pipe_buffer
不能被该管道续写(因为写入该管道的数据将会被写入文件) - 我们来看一下究竟是哪里限制了管道的写入:
1 | static ssize_t |
- 重点注意该函数对
PIPE_BUF_FLAG_CAN_MERGE
的处理,如果 flag 中有该标志位,就会调用copy_page_from_iter
函数将数据复制到管道缓冲区
Dirty Pipe 漏洞利用
对于能否将数据附加至一个管道缓冲区,内核采用了如下的机制:
- Linux 2.6.16 以前,
pipe_buf_operations
结构有一个单独的 flag 叫做can_merge
,下面这行 if 语句通过则允许在当前页面续写
1 | if (ops->can_merge && offset + chars <= PAGE_SIZE) { |
- Linux 2.6.16 起,为了支持
splice
调用,引入了page_cache_pipe_buf_ops
,它实际上是一个设置了can_merge=0
的pipe_buf_operations
,用来指示这部分页不能被管道写入
1 | static const struct pipe_buf_operations page_cache_pipe_buf_ops = { |
- Linux 5.0 中,由于只有一种管道缓冲区类型可以追加新数据,对
can_merge
的检查被修改为只检查类型是否是anon_pipe_buf_ops
(这就是那个唯一可追加内容的类型)
1 | if (pipe_buf_can_merge(buf) && offset + chars <= PAGE_SIZE) { |
1 | static bool pipe_buf_can_merge(struct pipe_buffer *buf) { |
- Linux 5.8 中又将
pipe_buf_operations
类型的比较修改为pipe_buffer
的一个 flag:PIPE_BUF_FLAG_CAN_MERGE
1 | if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) && |
Linux 4.9 添加了两个新函数 copy_page_to_iter_pipe
和 push_pipe
,它们分配了新的管道缓冲区,但并没有初始化 flag(当时 flag 的作用并不大)
Linux 5.8 对 flag 有所运用,没有初始化 flag,意味着之前遗留下来的 PIPE_BUF_FLAG_CAN_MERGE
标志位不会被 splice
系统调用清空,这可能会影响后续某些函数的执行流程
漏洞利用的思路为:
- 创建管道
- 用任意数据填充管道(为整个缓冲区环结构设置
PIPE_BUF_FLAG_CAN_MERGE
标记) - 清空管道(保留
pipe_inode_info
环中每一个缓冲区的 flag ) - 使用
splice
将目标文件(以只读方式打开)中的数据从目标偏移之前的位置放入到管道中 - 将任意数据写入管道,此数据将覆盖缓存的文件页面,而不是创建新的匿名缓冲区
伪代码如下:
1 | pipe(p); |
- 调用
splice
函数可以通过“零拷贝”的形式将文件发送到 pipe(代码层面的零拷贝是直接将文件缓存页 page cache 作为 pipe 的缓存页使用) - 但这里引入了一个变量未初始化漏洞,导致文件缓存页会在后续 pipe 通道中被当成普通 pipe 缓存页而被“续写”进而被篡改
- 然而,在这种情况下,因为没有其他可写权限的程序进行 write 操作,所以内核并不会将这个缓存页判定为“脏页”,短时间内(到下次重启之类的)不会刷新到磁盘
- 在这段时间内所有访问该文件的场景都将使用被篡改的文件缓存页,也就达成了一个“短时间内对任意可读文件任意写”的操作
Dirty Pipe 漏洞复现
1.修改服务器上 flag 文件的值:
1 |
|
- 结果如下:
1 | / $ cat flag |
2.修改 /etc/passwd
中用户的 uid 和组 id 来实现提权:
1 |
|
- 因为
su
命令需要 root 权限,所以在 root 用户中进行测试 - 结果如下:
1 | / |
- 切换到 test 用户以后,还是显示 root 权限
参考: