在一切开始之前
file相关
文件结构体代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的 struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核创建和驱动源码中,struct file的指针通常被命名为file或filp。
1 |
|
f_mapping
字段的类型为 address_space
结构
1 | struct address_space { |
address_space
结构其中的一个作用就是用于存储文件的 页缓存
,下面介绍一下各个字段的作用:
host
:指向当前address_space
对象所属的文件inode
对象(每个文件都使用一个inode
对象表示)。page_tree
:用于存储当前文件的页缓存
。tree_lock
:用于防止并发访问page_tree
导致的资源竞争问题。
一个比较形象的图
inode ,即虚拟文件节点,VFS inode 包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。它是Linux 管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。 内核使用inode结构体在内核内部表示一个文件。因此,它与表示一个已经打开的文件描述符的结构体(即file 文件结构)是不同的,我们可以使用多个file 文件结构表示同一个文件的多个文件描述符,但此时,所有的这些file文件结构全部都必须只能指向一个inode结构体。
管道相关
管道是Linux中IPC的常用方法,拥有一个读端和一个写端,两个程序之间可以通过这种方法实现通信。而在内核中,为了实现这种通信,需要维护一个环形缓冲区结构,即pipe_bufffer
具体的,在do_pipe()函数中 最终调用了alloc_pipe_info 来分配这种结构体
1 | struct pipe_inode_info *alloc_pipe_info(void) |
1 |
|
也就是说pipe 由 pipe_inode_info 结构体管理着16个pipe_buffer结构体,每个pipe_buffer结构体指向一个缓冲页,事实上,这还是一个环形的缓冲区,由head 和tail 两个指针来维护。
零拷贝
以socket读写为例,传统IO的执行流往往是这样
有两次非常多余的上下文切换操作
Linux 在 2.6.17 版本引入 splice 系统调用,不仅不需要硬件支持,还实现了两个文件描述符之间的数据零拷贝。splice 的伪代码如下:
1 | splice(fd_in, off_in, fd_out, off_out, len, flags); |
splice 系统调用可以在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道(pipeline),从而避免了两者之间的 CPU 拷贝操作。
基于 splice 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换,0 次 CPU 拷贝以及 2 次 DMA 拷贝,用户程序读写数据的流程如下:
- 用户进程通过 splice() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
- CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
- CPU 在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道(pipeline)。
- CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
- 上下文从内核态(kernel space)切换回用户态(user space),splice 系统调用执行返回。
splice 拷贝方式也同样存在用户程序不能对数据进行修改的问题。除此之外,它使用了 Linux 的管道缓冲机制,可以用于任意两个文件描述符中传输数据,但是它的两个文件描述符参数中有一个必须是管道设备。
为什么是0次cpu拷贝呢,其实就是两个设备共用了同一个缓冲区,具体来讲,就是pipe_buffer所对应的page 直接指向了写文件的 page cache,这样管道读的话就是直接从page cache 读了,没有cpu拷贝操作。
函数流程分析
pipe_write()
1 | static ssize_t |
is_packetized()
判断flags位的 O_DIRECT
1 | static inline int is_packetized(struct file *file) |
splice()
1 | long do_splice(struct file *in, loff_t *off_in, struct file *out, |
read方法具体与文件系统有关,以ext4为例子的话就是
1 | const struct file_operations ext4_file_operations = { |
pagevec结构体
管理着一个page指针数组,PAGEVEC_SIZE大小为14
1 | struct pagevec { |
最终到达copy_page_to_iter_pipe()函数
1 | static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes, |
漏洞分析
- 从文件到管道调用的splice()函数调用中,pipe buffer 关联page的时候没有对flag进行初始化
- 在调用pipe的write操作时,若我们没有设置O_DIRECT,则会设置PIPE_BUF_FLAG_CAN_MERGE
- 在调用pipe的write操作时,若page设置PIPE_BUF_FLAG_CAN_MERGE,且数据(不足一页大小的) 足以写入该页,则可以继续写入当前页(即上一个被写入数据的buffer),这就是所谓的MERGE操作。接下来开始对新的 buffer 进行数据写入,若没有
PIPE_BUF_FLAG_CAN_MERGE
标志位则分配新页面后写入
我们可以先通过设置O_DIRECT,初始化管道,并对所有管道进行读写操作,这样子就让所有pipe_biffer的page都保留了PIPE_BUF_FLAG_CAN_MERGE 标志位。
紧接着我们使用splice函数,读取任意字节(小于一页但至少一字节),使得管道pipe_buffer直接关联文件缓存页,但该函数又不对page的flag位进行任何操作。
此时我们在调用pipe的write操作,由于flag包含了PIPE_BUF_FLAG_CAN_MERGE标志,故write会对当前页,也就是splice read的文件缓存页,直接进行写入,这样我们就完成了越权写操作。
该漏洞的发现者自己公布了poc和exp,在这里放一下我复现的poc
1 |
|
验证:
可以看到 ,我们确实修改了一个仅有可读权限的文件,在实际运用中,可以利用此来进行覆写/etc/passwd 从而达到提权的目的。
调试的时候
1 | size_t copy_page_from_iter(struct page *page, size_t offset, size_t bytes, |
影响版本以及修复
- 8 <= Linux kernel < 5.16.11
- 8 <= Linux kernel < 5.15.25
- 8 <= Linux kernel < 5.10.102
修复:
在 copy_page_to_iter_pipe
以及 push_pipe
添加 对 buf -> flag =0 的代码即可。
参考资料
https://www.cnblogs.com/yangjiguang/p/6030423.html
https://zhuanlan.zhihu.com/p/362499466
https://cloud.tencent.com/developer/article/1922497
https://blog.csdn.net/youzhangjing_/article/details/124967518
https://cloud.tencent.com/developer/article/1848933
https://arttnba3.cn/2022/03/12/CVE-0X06-CVE-2022-0847/#pipe%EF%BC%9A%E7%AE%A1%E9%81%93