[kernel exploit] 管道pipe在内核漏洞利用中的应用

[kernel exploit] 管道pipe在内核漏洞利用中的应用

简介

pipe和pipe2 是linux 操作系统中常见的进程间通信(IPC)方式,除此之外,在linux 内核漏洞利用中也是使用频率很高的一种原语。在之前可以用来作为泄露地址、劫持RIP、ROP的常见结构体,最近Dirty Pipe 漏洞曝光之后更是可以直接将之前的利用过程简化,实现"万物转Dirty Pipe"。关于pipe 的正规用途(进程间通信)本篇不过多说,主要介绍一下在漏洞利用中需要知道的逻辑与使用方法。

本篇分析使用内核代码版本5.13 和5.4。

源码阅读与逻辑分析

pipe 创建匿名管道

pipe 分为pipe()函数和pipe2()函数,其中pipe2 创建有名管道,在漏洞利用中不使用,所以不做分析,这里介绍一下创建匿名管道的pipe()

pipe 原型

想要使用pipe,首先要创建一个管道pipe,由于我们主要分析在漏洞利用中怎么使用,这里就不过多介绍pipe 正常使用方法和注意事项啥的了。直接看一下我们会用到的创建匿名管道的函数pipe,其原型如下:

 int pipe(int pipefd[2]);
  • 参数pipefd[2]:其实是返回值,用户传给pipe 两个空的文件描述符,pipe 创建好后会初始化这两个文件描述符。之后使用这两个文件描述符一个用来向管道写(pipefd[1]),一个用来从管道读(pipefd[0])。
  • 返回值:成功返回0,失败返回负数。

pipe 手册

pipe 源码分析

入口位置在do_pipe2

linux\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);
}

可以看到,pipe 和pipe2 函数的区别就是pipe 的flag 为0。

调用栈:

  • do_pipe2:调用__do_pipe_flags 创建两个文件描述符成功之后就返回给用户了
    • __do_pipe_flags:获取文件描述符
      • create_pipe_files:根据pipe inode 对象设置pipe 两个文件描述符对应的文件对象
        • get_pipe_inode:根据pipe_inode_info 设置pipe 的inode 对象
          • alloc_pipe_info:分配pipe_inode_info 和 pipe_buffer

由于pipe 会创建文件描述符,涉及到文件系统和文件结构体什么的,所以这里调用栈是分层设置。最下面的alloc_pipe_info 函数才是和pipe 最直接相关的两个函数,直接分析alloc_pipe_info :

linux\fs\pipe.c:

struct pipe_inode_info *alloc_pipe_info(void)
{
	struct pipe_inode_info *pipe;
	unsigned long pipe_bufs = PIPE_DEF_BUFFERS;// 16
	struct user_struct *user = get_current_user();
	unsigned long user_bufs;
	unsigned int max_size = READ_ONCE(pipe_max_size);//1048576
	//[1] 申请pipe_inode_info 结构体,后续也会返回给上一层,size 0x88
	pipe = kzalloc(sizeof(struct pipe_inode_info), GFP_KERNEL_ACCOUNT);
	··· ···

	if (pipe_bufs * PAGE_SIZE > max_size && !capable(CAP_SYS_RESOURCE))
		pipe_bufs = max_size >> PAGE_SHIFT;

	user_bufs = account_pipe_buffers(user, 0, pipe_bufs);

	if (too_many_pipe_buffers_soft(user_bufs) && pipe_is_unprivileged_user()) {
		user_bufs = account_pipe_buffers(user, pipe_bufs, 1);
		pipe_bufs = 1;
	}

	if (too_many_pipe_buffers_hard(user_bufs) && pipe_is_unprivileged_user())
		goto out_revert_acct;//[2]判断当前用户使用的buf 页是否超过限制
	//[3] 申请16个struct pipe_buffer结构体,16*40=0x280
	pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
			     GFP_KERNEL_ACCOUNT);

	if (pipe->bufs) {//初始化pipe_inode_info其他内容
		init_waitqueue_head(&pipe->rd_wait);
		init_waitqueue_head(&pipe->wr_wait);
		pipe->r_counter = pipe->w_counter = 1;
		pipe->max_usage = pipe_bufs;
		pipe->ring_size = pipe_bufs;
		pipe->nr_accounted = pipe_bufs;
		pipe->user = user;
		mutex_init(&pipe->mutex);
		return pipe;
	}
    ··· ···
}
  • [1] : 先申请pipe_inode_info 结构体,使用GFP_KERNEL_ACCOUNT flag,大小大概0x88(不知道不同版本代码是否有不同)
  • [2] : 统计并判断当前用户所使用的的pipe_buf页数量(user->pipe_bufs) 是否超过限制,每次打开一个新pipe 管道都会加16,限制是1024*16,不过通常情况下不会超过限制,一般都先超过打开文件描述符的限制(笑)。
  • [3] : 申请pipe->bufs,其实就是16个struct pipe_buffer 连着,使用GFP_KERNEL_ACCOUNT flag,由于一个struct pipe_buffer 是0x28 大小,这里申请的大小就是0x280,属于kmalloc-1k大小。struct pipe_buffer结构体对我们漏洞利用非常重要,将在下文介绍。

pipe 打开的管道需要分别对pipefd[0]和pipefd[1] 调用close 关闭才可以正确释放,该操作会将申请的pipe_inode_info 结构体和pipe_buffer 结构体都释放掉。

pipe 相关结构体

下面介绍一下pipe 中的两个重要结构体,struct pipe_inode_info和struct pipe_buffer。

linux\include\linux\pipe_fs_i.h:

/**
 *	struct pipe_buffer - a linux kernel pipe buffer
 *	@page: the page containing the data for the pipe buffer
 *	@offset: offset of data inside the @page
 *	@len: length of data inside the @page
 *	@ops: operations associated with this buffer. See @pipe_buf_operations.
 *	@flags: pipe buffer flags. See above.
 *	@private: private data owned by the ops.
 **/
struct pipe_buffer {
	struct page *page; //每个pipe_buffer 结构体管理一个页
	unsigned int offset, len; //记录偏移和长度
	const struct pipe_buf_operations *ops;//ops,指向内核中的全局常量
    //flag是页使用的标志位,比较重要的就是PIPE_BUF_FLAG_CAN_MERGE 代表该页是否可以续写
	unsigned int flags; 
	unsigned long private;
};
/**
 *	struct pipe_inode_info - a linux kernel pipe
 *	@mutex: mutex protecting the whole thing
 *	@rd_wait: reader wait point in case of empty pipe
 *	@wr_wait: writer wait point in case of full pipe
 *	@head: The point of buffer production
 *	@tail: The point of buffer consumption
 *	@note_loss: The next read() should insert a data-lost message
 *	@max_usage: The maximum number of slots that may be used in the ring
 *	@ring_size: total number of buffers (should be a power of 2)
 *	@nr_accounted: The amount this pipe accounts for in user->pipe_bufs
 *	@tmp_page: cached released page
 *	@readers: number of current readers of this pipe
 *	@writers: number of current writers of this pipe
 *	@files: number of struct file referring this pipe (protected by ->i_lock)
 *	@r_counter: reader counter
 *	@w_counter: writer counter
 *	@fasync_readers: reader side fasync
 *	@fasync_writers: writer side fasync
 *	@bufs: the circular array of pipe buffers
 *	@user: the user who created this pipe
 *	@watch_queue: If this pipe is a watch_queue, this is the stuff for that
 **/
struct pipe_inode_info {
	struct mutex mutex;
	wait_queue_head_t rd_wait, wr_wait;
	unsigned int head; //pipe_buffer 循环队列的头下标
	unsigned int tail; //pipe_buffer 循环队列的尾下标
	unsigned int max_usage; //管道中允许存在的的最大字节数
	unsigned int ring_size; //pipe_buffer 循环队列的长度
#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;// 指向16个pipe_buffer 结构体
	struct user_struct *user;
#ifdef CONFIG_WATCH_QUEUE
	struct watch_queue *watch_queue;
#endif
};

pipe_inode_info就是一些基本信息,比较重要的都标记在上面了。值得一提的是每个pipe有16个pipe_buffer 是一起申请的连着的,所以bufs 直接按照下标访问即可。

一共有16个pipe_buffer 一起申请,内存相连,组成一个数组。他们最主要的就是有一个内存页结构体,除此之外是一些基本信息,比如长度和偏移等。flag 标志位代表使用页的时候的一些限制,比较重要的就是PIPE_BUF_FLAG_CAN_MERGE,代表该页是否可以续写,续写就是说上次向管道write 的时候没有把上一页写完,这次写的时候这没写完的一页是否允许续写。

pipe_inode_info则代表该管道的总体基本信息,通过bufs 成员通过下标访问上面提到的16个pipe_buffer 结构体,其中head 和tail 成员分别代表头尾下标,这16个pipe_buffer 通过head 和tail 组成一个环形队列的结构,大体如下图所示,head用来写,tail用来读

在这里插入图片描述

下文将介绍pipe 如何用这两个结构体管理16个pipe_buffer即16个页来实现读写。

write & splice 向pipe 管道写

pipe 的读和写没有专门的函数,直接使用write 和read 操作之前pipe 返回的文件描述符即可,pipefd[1] 用来写pipefd[0] 用来读。

linux\fs\pipe.c:

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,
	.splice_write	= iter_file_splice_write,
};

根据回调函数表,pipe 的读写操作本质上是pipe_read 和pipe_write 实现的。而对pipe 的读操作在漏洞利用里基本用不上,这里直接分析pipe_write就可以了。分析了写的逻辑,读的想也能想明白了。

pipe_write 分析(5.13代码)

直接看代码:

linux\fs\pipe.c:

static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
	struct file *filp = iocb->ki_filp;
	struct pipe_inode_info *pipe = filp->private_data;
	unsigned int head;
	ssize_t ret = 0;
	size_t total_len = iov_iter_count(from);
	ssize_t chars;
	bool was_empty = false;
	bool wake_next_writer = false;

	··· ···
	//[1] 第一次尝试写入需要判断是否可以在当前队列头续写
	head = pipe->head;//获取buf队列头下标
	was_empty = pipe_empty(head, pipe->tail);
	chars = total_len & (PAGE_SIZE-1);//第一次写入长度不能超过一页
	if (chars && !was_empty) {//pipe不为空(才能续写)并且用户有写入内容时
		unsigned int mask = pipe->ring_size - 1;
		struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];//[2] 循环队列
		int offset = buf->offset + buf->len;//获取当前buf 偏移和长度
		/*[3] 根据flag 判断当前页是否允许续写
		 *如果PIPE_BUF_FLAG_CAN_MERGE 标志位存在,代表该页允许续写
         *如果写入长度不会跨页,则接着写,否则直接另起一页*/
		if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
		    offset + chars <= PAGE_SIZE) {
			ret = pipe_buf_confirm(pipe, buf);
			··· ···//从用户空间拷贝数据
			ret = copy_page_from_iter(buf->page, offset, chars, from);
			··· ···
			buf->len += ret;
			if (!iov_iter_count(from))//判断是否写完
				goto out;
		}
	}

	for (;;) {//[4] 如果续写之后没写完或不能续写
		··· ···
		head = pipe->head;
		if (!pipe_full(head, pipe->tail, pipe->max_usage)) {//pipe没满的话
			unsigned int mask = pipe->ring_size - 1;
			struct pipe_buffer *buf = &pipe->bufs[head & mask];//循环队列
			struct page *page = pipe->tmp_page;//[5] tmp_page存在先使用tmp_page
			int copied;

			if (!page) {//[5] 不存在则申请新的
				page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
				if (unlikely(!page)) {
					ret = ret ? : -ENOMEM;
					break;
				}
				pipe->tmp_page = page;
			}

			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;//[6] 另起新的一页
			spin_unlock_irq(&pipe->rd_wait.lock);

			/* Insert it into the buffer array */
			buf = &pipe->bufs[head & mask];//获取对应的pipe_buffer结构体
			buf->page = page;//pipe_buffer 管理page
			buf->ops = &anon_pipe_buf_ops;//默认页面的回调表
			buf->offset = 0;//偏移和长度
			buf->len = 0;
			if (is_packetized(filp))
				buf->flags = PIPE_BUF_FLAG_PACKET;
			else//默认为PIPE_BUF_FLAG_CAN_MERGE,代表可续写状态
				buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
			pipe->tmp_page = NULL;//tmp_page用完就要清理掉
			//从用户空间拷贝内容
			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;
		}

		if (!pipe_full(head, pipe->tail, pipe->max_usage))
			continue;//没写完并且没满就继续尝试写
        ··· ···
		··· ···
	}
out:
	··· ···//成功/失败返回
}
  • [1] : 用户调用write 向pipe 中写入内容,实现该次写入操作时,首先会进行一次特殊的尝试写入,就是尝试续写,因为之前提到过,pipe 是由16个页组成的一段缓冲区,上次写完并不一定完整的写完一页,这次要尝试在上一次没写完的页中继续接着写。为什么说是尝试,主要有两个原因:
    1. 要么pipe 缓冲区为空,那就不存在续写一说了,直接跳到下面正常写入
    2. 有的页面不允许续写,比如splice 传送的文件页(后文提到)。
  • [2] : 之前提到过16个pipe_buffer 组成一个循环队列,管理该队列的下标head 和tail 只增不减,循环寻址方式就是下标异或(队列长度16-1),相当于取余了(我为什么要说这个)。
  • [3] : **只有该pipe_buffer 的flag 存在PIPE_BUF_FLAG_CAN_MERGE 标志的时候才允许续写。**后续就是从用户空间拷贝内容,然后判断是否写完,如果没写完就直接跳转到下面正常写的操作,写完直接准备返回。
  • [4] : 正常写入操作,循环直到写入完成,可能由于一次写入没有写完内容,或是队列满而等待。
  • [5] : 进过上文说明已经完成了续写操作或是无法续写或是队列为空,那进行到这里无论如何都要"另起一页"来完成接下来的写入了。首先查看pipe_inode_info 结构体中的tmp_page是否存在,如果存在则使用tmp_page 来完成写操作。如果不存在则要申请新的,一般在读取操作pipe_read 中如果刚读完了一个页的内容会将该页设置为tmp_page,这样就不用频繁的申请和释放页了,只有刚初始化完才需要申请新页面。或者存在不能被设置为tmp_page的页时。
  • [6] : 接下来就是正常的新起一页写入,head 指针用来写操作,向前移动一格,然后将tmp_page 给当前pipe_buffer->page,之后完成写入。值得一提的是,如此操作的页面都是允许续写的,所以flag 默认为PIPE_BUF_FLAG_CAN_MERGE。
splice 传输文件

有时候想要将文件中的内容传入pipe管道中,如果先从文件读取再向管道写入未免太慢了。这就有了splice 函数,splice 通过"零拷贝"的形势直接将一个文件传入管道之中(或从管道中传出),splice 原型如下:

splice 原型

splice 官方定义是在两个文件描述符之间传输数据,而避免从用户地址空间和内核地址空间之间复制,其中一个文件描述符必须是管道。

 ssize_t splice(int fd_in, off64_t *off_in, int fd_out, off64_t *off_out, size_t len, unsigned int flags);
  • 参数fd_in:输入文件描述符,如果为pipe 则off_in 必须为0。
  • 参数off_in:输入文件描述符的偏移,值得注意的是这是一个指针,我们要传入长度的地址,而不是只传一个数就完了。
  • 参数fd_out:目标文件描述符。
  • 参数off_out:目标文件描述符的偏移。
  • 参数len:传输数据长度。
  • 参数flags:一些标志位,不常用。
  • 返回值:返回传输的实际长度,-1代表失败。如果fd_in 为pipe 则返回0 代表pipe 为空没有可传输的内容。

splice 手册

关于这个也可以去看一下之前分析的Dirty pipe 漏洞,由于我们在漏洞利用中基本只会用到将其他文件splice 进pipe 管道的操作,所以这里只分析该场景的逻辑。

splice 原理分析

之前提到了,splice 为了"方便",直接通过文件描述符和偏移将文件内容传入到管道,其实现原理其实就是通过用文件的缓存页直接替换pipe_buffer 结构体的page 页,大概是如下逻辑:

在这里插入图片描述

而文件缓存页是linux 内核的一种机制,这里只说原理:linux 通过将打开的文件放到缓存页之中,缓存页被使用过后也会保存一段时间避免不必要的IO操作。短时间内访问同一个文件,都会操作相同的文件缓存页,而不是反复打开。所以说白了文件缓存页其实也是page 结构体,对文件缓存页的权限鉴定取决于上层的访问函数,如果代码编写不当就会出现问题(下文会说)。

splice 的调用栈:

  • SYSCALL_DEFINE6(splice,...) -> __do_sys_splice -> __do_splice-> do_splice
    • splice_file_to_pipe -> do_splice_to
      • generic_file_splice_read(in->f_op->splice_read 默认为 generic_file_splice_read)
        • call_read_iter -> filemap_read
          • copy_page_to_iter -> copy_page_to_iter_pipe

这里其实核心就是调用fd_in 的file_operations 里的splice_read 函数,需要注意的是由于只讲将其他文件传入到pipe 的场景,所以fd_in 只考虑是普通文件的文件描述符的时候,这里splice_read 默认都是generic_file_splice_read。其实最关键的就是调用栈的最后两个函数,copy_page_to_iter 和copy_page_to_iter_pipe:

linux\lib\iov_iter.c :

size_t copy_page_to_iter(struct page *page, size_t offset, size_t bytes,
			 struct iov_iter *i)
{
	··· ···
	else if (likely(!iov_iter_is_pipe(i)))//目标是pipe 管道的时候
		return copy_page_to_iter_iovec(page, offset, bytes, i);
	else
		return copy_page_to_iter_pipe(page, offset, bytes, i);//会走到这里
}

copy_page_to_iter 是内核中比较常见的函数,主要功能是将一个页拷贝到目标区域,目标可能是任何可能被拷贝的,比如用户缓冲区,文件描述符等待,会根据场景不同调用不同的函数处理。

所以splice 文件到管道最主要的逻辑还是在copy_page_to_iter_pipe 函数:

linux\lib\iov_iter.c :

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;
	unsigned int p_tail = pipe->tail;
	unsigned int p_mask = pipe->ring_size - 1;
	unsigned int i_head = i->head;
	size_t off;

	··· ···

	off = i->iov_offset;
	buf = &pipe->bufs[i_head & p_mask];//[1]获取对应的pipe 缓存页
	··· ···
	
	buf->ops = &page_cache_pipe_buf_ops;
    //buf->flags = 0; //[2]dirty pipe漏洞,没有初始化flags,这一行是修复
	get_page(page);
	buf->page = page;//[3]页指针指向了文件缓存页
	buf->offset = offset;//[3]offset len 等设置为当前信息(通过splice 传入参数决定)
	buf->len = bytes;

	pipe->head = i_head + 1;
	i->iov_offset = offset + bytes;
	i->head = i_head;
out:
	i->count -= bytes;
	return bytes;
}
  • [1] : 首先根据pipe 页数组环形结构,找到当前写指针(pipe->head) 位置
  • [2] : dirty pipe 由于没初始化flags 导致漏洞,因为这一页并不是pipe 本身的缓存页,而是其他文件的缓存页,必须设置flags 为0让它不能被"续写"。因为这里并不知道拼接的其他文件的页的访问权限是否允许写,如果在这里不设置flags 禁止续写(因为默认这里都会被设置成PIPE_BUF_FLAG_CAN_MERGE ),下一次pipe 写入的时候就会认为这是一个普通pipe 页而续写,破坏文件缓存页,短时间内访问该文件的操作都会访问到破坏的文件缓存页。
  • [3] : 将当前需要写入的页指向准备好的文件缓存页,并设置其他信息,比如len 是由splice 系统调用的传入参数决定的。这里唯独没有初始化flag,造成漏洞。
pipe_write & splice 分析(老版本5.4代码)

在5.8 版本之前的实现和现在是由些许不同的,主要在5.8 更新了PIPE_BUF_FLAG_CAN_MERGE 这个flag,即是否允许续写的flag(也引入了dirty pipe 漏洞) ,但这个逻辑并不是在5.8 才更新的,在5.8之前就有splice 传输的页无法续写的操作,他的逻辑是这样的,首先看splice 的copy_page_to_iter_pipe 函数:

linux\lib\iov_iter.c :

static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes,
			 struct iov_iter *i)
{
	··· ···
	pipe->nrbufs++;
	buf->ops = &page_cache_pipe_buf_ops;//[1]注意这里设置的ops
	get_page(buf->page = page);
	buf->offset = offset;
	buf->len = bytes;
	i->iov_offset = offset + bytes;
	i->idx = idx;
	··· ···
}

然后在看pipe_write 的判断续写部分的代码:

linux\fs\pipe.c :

pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
	··· ···
    ··· ···
	/* We try to merge small writes */
	chars = total_len & (PAGE_SIZE-1); /* size of the last buffer */
	if (pipe->nrbufs && chars != 0) {
		int lastbuf = (pipe->curbuf + pipe->nrbufs - 1) &
							(pipe->buffers - 1);
		struct pipe_buffer *buf = pipe->bufs + lastbuf;
		int offset = buf->offset + buf->len;
		//[2] 调用pipe_buf_can_merge判断是否能续写
		if (pipe_buf_can_merge(buf) && offset + chars <= PAGE_SIZE) {
			··· ···
            ··· ···
            buf->ops = &anon_pipe_buf_ops;//[1] 新申请的页设置的ops
            ··· ···
        }
        ··· ···
    }
    ··· ···
}
static bool pipe_buf_can_merge(struct pipe_buffer *buf)//[2]判断续写函数
{
	return buf->ops == &anon_pipe_buf_ops;
}
  • [1] : splice 传输完页面由于这时还没有更新flags,只设置基本的一些信息,我们可以看出对于splice 传输的页和pipe_write 里直接申请的页设置的ops 是不同的,splice 中设置的是page_cache_pipe_buf_ops,pipe_write 申请的页设置的是anon_pipe_buf_ops。
  • [2] : 这里由于没有flags 判断是否可以续写,采用的是判断pipe_buffer->ops 来判断是否可以续写。如果是anon_pipe_buf_ops 则说明是pipe_write 申请的页可以续写,否则不行。

实际操作与漏洞利用

简单文字描述一下漏洞利用中怎么用,主要还要结合漏洞,下面漏洞分析可以用来参考:

【kernel exploit】CVE-2021-22555 2字节堆溢出写0漏洞提权分析

使用场景

经过上面的分析,关于pipe 有如下基本知识:

  • 调用pipe 创建一个管道,其中会在内核中申请一个pipe_inode_info 结构体(大小0x88) 属于kmalloc-192 大小和16个连续的pipe_buffer 结构体(大小0x28*0x10=0x280) 属于kmalloc-1k。
  • 调用close 可以释放上述两种结构体。
  • pipe_buffer 中含有ops 指针,指向内核中的常量(可以用来泄露)
dirty pipe 漏洞(CVE-2022-0847)

详见 [漏洞分析] CVE-2022-0847 Dirty Pipe linux内核提权分析

pipe_buff 泄露地址与劫持rip

堆溢出常规思路都是利用msgmsg 转换为UAF之类的,如果能将pipe_buffer 劫持,那么通过读取pipe_buffer 中的ops 可以泄露出内核的基地址。

在pipe_buffer 之中伪造一个fake ops 结构体,释放pipe 的时候会调用ope->release ,这时pipe_buffer 会在rsi 指针之中,使用push rsi;jmp qword ptr rsi; gadget 和pop rsp; ret; gadget 可以将栈劫持到pipe_buffer上,然后完成后续ROP即可。

万物转dirty pipe

那么我们先不管dirty pipe 的原理。经过上面的分析,我们知道如下两件事:

  • liunx 内核将打开的文件通过缓存页的形式保存,对文件缓存页的访问鉴权是通过访问函数实现的,对缓存页本身操作并没有鉴权。多个不同进程访问同一个文件实际访问的是同一个缓存页。
  • pipe 可以使用splice 将其他文件的缓存页直接链接到pipe 里。正常情况下是有续写保护的,也就是说我们只能读它。续写保护是通过清楚PIPE_BUF_FLAG_CAN_MERGE flag(大于5.8 版本)或判断pipe_buffer->ops 是否是anon_pipe_buf_ops 来确认是否是可续写页。

那么只要我们跟以前的漏洞利用思路一样,成功利用UAF将pipe_buffer 结构体劫持,比起传统的思路,先泄露内核地址再伪造ops 然后ROP来完成利用,直接修改pipe_buffer 里面的flag 或ops 让其转换为Dirty Pipe显然更加方便,并且这里只需要泄露堆的地址,并不需要泄露内核基地址,免去不同版本内核还要硬编码不同地址的繁琐。可以实现"通杀"多版本内核。

基本使用代码

略,参考Dirty Pipe 官方exp 就行了。

参考

CVE-2022-0185分析及利用 与 pipe新原语思考与实践

[漏洞分析] CVE-2022-0847 Dirty Pipe linux内核提权分析

https://man7.org/linux/man-pages/man2/

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值