Dirty Pipe linux内核提权漏洞分析(CVE-2022-0847)

漏洞原理

Pipe机制
本质上来说,管道(pipe)就是一种进程间通信的手段,让两个进程可以通过pipe 发送和接收数据。
在这里插入图片描述
Page Cache机制
磁盘的IO读写速度是很慢的,所以一般当我们访问一个磁盘文件的时候,首先会将其内容装载到物理内存中,后续的访问都是直接取内存中的副本来读取数据。因为一个文件的内存副本,后续可能会被很多进程打开使用。(想想是不是,平常我们的微信软件可能打开本地的一个文本,word软件也可能打开同一个文本)。为了保证大家都能快速的访问,Linux设计了这样一个Page Cache机制管理起物理内存中映射的页框。

如果用户进程使用read/write读写文件,那么内核会先将载入数据的物理内存映射到内核虚拟内存buffer。然后再将内核的buffer数据拷贝到用户态。
在这里插入图片描述
如果追求效率,内核也提供一种零拷贝模式(不发生系统调用,跨越用户和内核的边界做上下文切换)。用户进程可以使用mmap直接将用户态的buffer 映射到 物理内存,不需要进行系统调用,直接访问自己的mmap区域即可访问到那段物理内存内容。
在这里插入图片描述
pipe_buf结构
pipe有一个大小为16的ring buffer数组,里面存了16个pipe_buf结构,每个pipe_buf结构又有一个指针指向一个表示物理内存页Page的结构体。每个Page大小为4KB,且不连续存放。【这也说明管道一般大小为4kb*16】
在这里插入图片描述

splice接口
这个接口可以将数据从一个文件"零拷贝"到一个pipe管道。

核心思路
通过pipe生成一个管道,然后使用write调用pip_write将管道填满flag为PIPE_BUF_FLAG_CAN_MERGE,然后用read将缓冲区全部释放,但是根据splice进行零拷贝时copy_page_to_iter_pipe没有将flag初始化,导致缓冲区仍然留存PIPE_BUF_FLAG_CAN_MERGE。进而在write上检测flag存在PIPE_BUF_FLAG_CAN_MERGE来达成越权写入操作。
在这里插入图片描述

源码分析

linux-5.13\lib\iov_iter.c : 417 : copy_page_to_iter_pipe

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;//[2]修改pipe 缓存页的相关信息指向文件缓存页
	get_page(page);
	buf->page = page;//[2]页指针指向了文件缓存页
	buf->offset = offset;//[2]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;
}

该函数把pipe的page设置成文件的page cache,但是遗漏了flag的初始化0操作。

linux-5.13\fs\pipe.c : 400 : pipe_write

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;

	··· ···
    ··· ···
	head = pipe->head;
	was_empty = pipe_empty(head, pipe->tail);
	chars = total_len & (PAGE_SIZE-1);
	if (chars && !was_empty) { 
        //[1]pipe 缓存不为空,则尝试是否能从当前最后一页"接着"写
		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) { 
            /*[2]关键,如果PIPE_BUF_FLAG_CAN_MERGE 标志位存在,代表该页允许接着写
             *如果写入长度不会跨页,则接着写,否则直接另起一页 */
			ret = pipe_buf_confirm(pipe, buf);
			···
			ret = copy_page_from_iter(buf->page, offset, chars, from);
			···
			}
			buf->len += ret;
			···
		}
	}

	for (;;) {//[3]如果上一页没法接着写,则重新起一页
		··· ···
		head = pipe->head;
		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) {//[4]重新申请一个新页
				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;
			··· ···
			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;//[5]将新申请的页放到页数组中
			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;
            	//[6]设置flag,默认PIPE_BUF_FLAG_CAN_MERGE
			pipe->tmp_page = NULL;

			copied = copy_page_from_iter(page, 0, PAGE_SIZE, from); 
            //[7]拷贝操作
			··· ···
			ret += copied;
			buf->offset = 0;
			buf->len = copied;

			··· ···
		}
        ··· ···
    }
	··· ···
	return ret;
}

1.如果当前管道(pipe)中不为空(head==tail判定为空管道),则说明现在管道中有未被读取的数据,则获取head 指针,也就是指向最新的用来写的页,查看该页的len、offset(为了找到数据结尾)。接下来尝试在当前页面续写
2.判断 当前页面是否带有 PIPE_BUF_FLAG_CAN_MERGE flag标记,如果不存在则不允许在当前页面续写。或当前写入的数据拼接在之前的数据后面长度超过一页(即写入操作跨页),如果跨页,则无法续写。
3.如果无法在上一页续写,则另起一页
4.alloc_page 申请一个新的页
5.将新的页放在数组最前面(可能会替换掉原有页面),初始化值。
6.buf->flag 默认初始化为PIPE_BUF_FLAG_CAN_MERGE ,因为默认状态是允许页可以续写的。
7.拷贝写入的数据,没拷贝完重复上述操作。

漏洞利用

1.创建一个管道
2.将管道填充满(通过pipe_write),这样所有的buf(pipe 缓存页)都初始化过了,flag 默认初始化为PIPE_BUF_FLAG_CAN_MERGE
3.将管道清空(通过pipe_read),这样通过splice 系统调用传送文件的时候就会使用原有的初始化过的buf结构。
4.调用splice 函数将想要篡改的文件传送入
5.继续向pipe写入内容(pipe_write),这时就会覆盖到文件缓存页了,完成暂时文件篡改。

局限性

1.不能持久化。
由于修改的是页面缓存,并未修改磁盘上的文件(有极小概率某个对文件有写权限的进程碰巧执行了读写操作,导致缓存被回写磁盘),虽然可以用于提权等操作,但是如果完成提权后不对被修改的文件重新进行持久化操作的话,当操作系统回收内存或者更简单的重启机器后,所做的修改都将失效。如:修改passwd文件去除掉root用户密码后,简单一个重启操作,root密码就恢复如初了。
2.特殊文件限制。
由于文件系统的特性,一些特殊文件不经过页面缓存,导致此漏洞对这类文件无效。
3.只能写不超过一页的内容

参考文档
https://blog.csdn.net/Breeze_CAT/article/details/123393188?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164878673016782246497863%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=164878673016782246497863&biz_id=&utm_medium=distribute.pc_search_result.none-task-code-2allfirst_rank_ecpm_v1~rank_v31_ecpm-2-123393188-1.142v5pc_search_result_cache&utm_term=CVE-2022-0847%E5%88%86%E6%9E%90
https://blog.csdn.net/weixin_44820088/article/details/123364275?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164878673016782246497863%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=164878673016782246497863&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v1~rank_v31_ecpm-4-123364275.142v5pc_search_result_cache&utm_term=CVE-2022-0847%E5%88%86%E6%9E%90&spm=1018.2226.3001.4187

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值