一、影响版本
Linux Kernel版本 >= 5.8
Linux Kernel版本 < 5.16.11 / 5.15.25 / 5.10.102
二、原理
Dirtypipe破绽容许向任意可读文件中写数据,可造成非特权过程向root过程注入代码。该破绽产生linux内核空间通过splice形式实现数据拷贝时,以"零拷贝"的模式(将文件缓存页作为pipe的buf页应用)将文件发送到pipe,并且没有初始化pipe缓存页治理数据结构的flag成员。若提前在内存空间中安排好“脏数据”让flag标记为PIPE_BUF_FLAG_CAN_MERGE,就会导致文件缓存页会在后续pipe通道中被当成一般pipe缓存页,进而被续写和篡改。在这种状况下内核并不会将这个缓存页断定为"脏页",短时间内不会刷新到磁盘。在这段时间内所有拜访该文件的场景都将应用被篡改的文件缓存页,而不会从新关上磁盘中的文件读取内容,因而达成一个"短时间内对任意可读文件任意写"的操作,即可实现本地提权。
根因剖析
管道(pipe)是内核提供的一种通信机制,通过pipe/pipe2函数创立,返回两个文件描述符,一个用于发送数据,另一个用于承受数据,相似管道的两端。
在linux内核实现中,通常管道会缓存总长度65536字节,且用页的模式进行治理,总共16页(一页4096字节),页面之间并不间断,而是通过数组进行治理,造成一个环形构造。管道会保护两个指针,一个用来写管道头(pipe->head),一个用来读管道尾(pipe->tail),此处重点剖析pipe_write函数。
- pipe_write函数代码要害性能阐明:
[1]如果以后管道(pipe)中不为空(head==tail断定为空管道),则阐明当初管道中有未被读取的数据,则获取head指针,也就是指向最新的用来写的页,查看该页的len、offset(为了找到数据结尾)。接下来尝试在以后页面续写。
[2]判断以后页面是否带有PIPE_BUF_FLAG_CAN_MERGEflag标记,如果不存在则不容许在以后页面续写。或以后写入的数据拼接在之前的数据前面长度超过一页(即写入操作跨页),如果跨页,则无奈续写。
[3]如果无奈在上一页续写,则另起一页。
[4]alloc_page申请一个新的页。
[5]将新的页放在数组最后面(可能会替换掉原有页面),初始化页治理构造的相干成员。
[6]buf->flag默认初始化为PIPE_BUF_FLAG_CAN_MERGE,因为默认状态是容许pipe缓存页续写的。
破绽利用的要害就是在splice中未初始化buf->flag标记,导致splice传送的文件缓存页在buf->flag为PIPE_BUF_FLAG_CAN_MERGE时被当成了一般pipe缓存页。
- splice函数要害性能及破绽利用剖析
在上文提到的pipe通过治理16个页来作为缓存,splice的零拷贝办法是间接用文件缓存页来替换pipe中的缓存页(更改pipe缓存页指针指向文件缓存页)。
基于对splice函数代码和调用栈关系剖析发现splice函数通过调用copy_page_to_iter_pipe函数将pipe缓存页构造指向要传输的文件的文件缓存页。调用栈如下图:
- copy_page_to_iter_pipe函数要害性能:
[1]首先依据pipe页数组环形构造,找到以后写指针(pipe->head)地位。
[2]将以后须要写入的页指向筹备好的文件缓存页,并设置其余信息,比方buf->len是由splice零碎调用的传入参数决定的,此处唯独没有初始化buf->flag
依据后面管道实现机制章节中对pipe_write的剖析可知,如果从新调用pipe_write向pipe中写数据,写指针(pipe->head)指向刚传送的文件缓存页,且flag为PIPE_BUF_FLAG_CAN_MERGE时,则pipe_write在写入长度不跨页的前提下,会认为能够持续在该页写,这样本次写操作就写在了本不该写的文件缓存页,如下图代码所示。
linux将关上的文件放到缓存页之中,缓存页会保留一段时间,因而短时间内拜访同一个文件,都会操作雷同的文件缓存页,而不是重复关上。通过上文写缓存页的办法篡改了指标文件缓存页(即使指标文件没有写权限),导致在接下来的一段时间内所有应用这个文件的过程都会拜访被篡改的缓存页,从而实现短时间内对指标文件的写操作,进而实现本地提权。
三、复现
复现条件:
- 攻击者必须具备读权限(因为它须要将页面拼接到管道中)
- 偏移量必须不在页面边界上(因为该页面的至多一个字节必须被拼接到管道中)
- 写操作不能跨页边界(因为将为其余部分创立一个新的匿名缓冲区)
- 文件不能调整大小(因为管道有本人的页填充治理,不通知页缓存曾经追加了多少数据)
复现步骤:
- 创立一个管道。
- 用任意数据填充管道(在所有环条目中设置PIPE_BUF_FLAG_CAN_MERGE标记)。
- 清空管道(保留pipe_inode_info环上所有struct pipe_buffer实例中设置的标记)。
- 将指标文件(用O_RDONLY关上)中的数据从指标偏移量的后面拼接到管道中。
- 将任意数据写入管道; 因为设置了PIPE_BUF_FLAG_CAN_MERGE,因而该数据将笼罩缓存的文件页面,而不是创立一个新的匿名struct pipe_buffer。
复现代码参见 :https://github.com/Arinerron/...
1、创立pipe;2、应用任意数据填充管道(填满, 而且是填满Pipe的最大空间);3、清空管道内数据;4、应用splice()读取指标文件(只读)的1字节数据发送至pipe;5、write()将任意数据持续写入pipe, 此数据将会笼罩指标文件内容;6、只有筛选适合的指标文件(必须要有可读权限), 利用破绽Patch掉关键字段数据, 即可实现从普通用户到root用户的权限晋升, POC应用的是/etc/passwd文件的利用形式。---------------------------------------------------------------------static void prepare_pipe(int p[2]){ if (pipe(p)) abort(); // 获取Pipe可应用的最大页面数量 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; } // 清空Pipe for (unsigned r = pipe_size; r > 0;) { unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r; read(p[0], buffer, n); r -= n; }}int main(int argc, char **argv){ ...... // 只读关上指标文件 const int fd = open(path, O_RDONLY); // yes, read-only! :-) ...... // 创立Pipe int p[2]; prepare_pipe(p); // splice()将文件1字节数据写入Pipe ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0); ...... // write()写入任意数据到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;}
复现版本
Linux ubuntu 5.10.5-051005-generic #202101061537 SMP Wed Jan 6 15:43:53 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
复现截图
执行前
执行后,胜利将/etc/passwd里root的明码批改成了咱们设置的明码。这块的明码内容其实是存在于page cache中的,所以机器重启后会复原成原来的明码
四、修复倡议
倡议用户降级Linux内核到5.16.11、5.15.25、5.10.102及以上版本。
参考资料
https://mp.weixin.qq.com/s/6V...
https://www.anquanke.com/post...
https://dirtypipe.cm4all.com/
https://github.com/chenaotian...
内核 http://zhaoxuhui.top/blog/202...
虚拟机批改内核 https://blog.csdn.net/qq_4262...