1.受影响版本LinuxKernelversion>=5.8LinuxKernelversion<5.16.11/5.15.25/5.10.1022.原理Dirtypipe漏洞允许发送任意可读文件在根进程中写入数据会导致非特权进程将代码注入根进程。该漏洞发生在linux内核空间通过splice实现数据拷贝时,文件以“零拷贝”的形式发送到管道(文件缓存页作为管道的buf页),标志位pipecachepagemanagement数据结构是未初始化的成员。如果“脏数据”被提前安排在内存空间,标志位被标记为PIPE_BUF_FLAG_CAN_MERGE,文件缓存页会在后续的管道通道中被当做正常的管道缓存页,继续被写入和篡改和。在这种情况下,内核不会将这个缓存页判断为“脏页”,短时间内不会刷新到磁盘。在这段时间里,所有访问该文件的场景都将使用被篡改的文件缓存页,而不需要重新打开磁盘上的文件来读取内容,从而实现了“短时间内任意写入任意可读文件”操作完成本地提权。根本原因分析管道(pipe)是内核提供的一种通信机制。它由pipe/pipe2函数创建,返回两个文件描述符,一个用于发送数据,一个用于接收数据,类似于管道的两端。在linux内核的实现中,通常管道会缓存总长度为65536字节,并以页的形式进行管理。共有16页(每页4096字节)。页不是连续的,而是通过数组进行管理,形成一个环状结构。管道会维护两个指针,一个用来写管道头(pipe->head),一个用来读管道尾(pipe->tail)。这里重点分析pipe_write函数。pipe_write函数代码关键作用说明:[1]如果当前管道(pipe)不为空(head==tail判断为空管道),说明管道中有未读数据,并且得到head指针,指向最新写入的page,查看page的len和offset(为了找到数据的结尾)。接下来尝试在当前页面上继续书写。[2]判断当前页是否有PIPE_BUF_FLAG_CAN_MERGEflag标志,如果不存在则不允许在当前页继续写入。或者当前写入的数据拼接在之前的数据后面且长度超过一页(即写操作跨页)。如果跨页,则无法继续写入。[3]如果无法继续写上一页,则另起一页。[4]alloc_page申请新页。[5]将新的页面放在数组的最前面(原来的页面可能会被替换),并初始化页面管理结构体的相关成员。[6]buf->flag默认初始化为PIPE_BUF_FLAG_CAN_MERGE,因为默认状态是允许pipecachepage继续写入。该漏洞利用的关键在于splice中没有初始化buf->flag标志,导致当buf->flag为PIPE_BUF_FLAG_CAN_MERGE时,splice传输的文件缓存页被认为是普通的管道缓存页。分析拼接函数的关键函数和利用。上面说的管道管理16个页面作为缓存。splice的零拷贝方式是直接用文件缓存页替换管道中的缓存页(将管道缓存页指针改为文件缓存页)。通过分析拼接函数代码和调用栈关系,发现拼接函数通过调用copy_page_to_iter_pipe函数将管道缓存页结构指向待传输文件的文件缓存页。调用栈如下图所示:copy_page_to_iter_pipe函数关键功能:[1]首先根据pipepage数组的环结构找到当前写指针(pipe->head)位置。[2]将当前需要写入的页面指向准备好的文件缓存页面,并设置其他信息,比如buf->len由splice系统调用的传入参数决定,只是buf->flag不是这里初始化根据前面pipeline实现机制章节对pipe_write的分析,如果再次调用pipe_write向pipe写入数据,write指针(pipe->head)指向刚刚传输的文件缓存页,flag为PIPE_BUF_FLAG_CAN_MERGE,那么pipe_write在写入长度不跨页的前提下,会认为可以在这个页上继续写,所以这个写操作写在不应该写的文件缓存页上,如图下图中的代码。Linux将打开的文件放到缓存页中,缓存页会保存一段时间,所以在短时间内访问同一个文件时,会操作同一个文件缓存页,而不是重复打开。通过上述写缓存页的方法篡改了目标文件的缓存页(即使目标文件没有写权限),导致所有使用该文件的进程在接下来的一段时间内访问被篡改的缓存页,从而完成对目标文件的短期Write操作,实现本地提权。3.可再现性可再现性条件:攻击者必须有读权限(因为需要将页面拼接进管道)偏移量不能在一个页面边界上(因为至少有一个字节的页面必须拼接进管道))写操作不能跨越页面边界(因为将为其余部分创建一个新的匿名缓冲区)文件不能调整大小(因为管道有自己的页面填充管理并且不告诉页面缓存有多少数据已附加)步骤重现:创建一个管道。用任意数据填充管道(在所有环条目中设置PIPE_BUF_FLAG_CAN_MERGE标志)。清空管道(保留在pipe_inode_info环上的所有structpipe_buffer实例中设置的标志)。将目标文件(使用O_RDONLY打开)中的数据从目标偏移量的前面拼接到管道中。向管道写入任意数据;由于设置了PIPE_BUF_FLAG_CAN_MERGE,此数据将覆盖缓存的文件页面,而不是创建新的匿名结构pipe_buffer。复现代码见:https://github.com/Arinerron/...1.创建管道;2.向pipe中填充任意数据(full,填充Pipe的最大空间);3.清除管道中的数据;4.使用splice()读取目标文件的1字节数据(只读)并发送到管道;5.Write()继续向管道写入任何数据,此数据将覆盖目标文件的内容;6、只需选择合适的目标文件(必须有可读权限),利用漏洞Patch关键字段数据,完成普通用户到root用户的提权。POC使用/etc/passwd文件利用方法。----------------------------------------------------------------staticvoidprepare_pipe(intp[2]){if(pipe(p))abort();//获取Pipe可以使用的最大页数constunsignedpipe_size=fcntl(p[1],F_GETPIPE_SZ);静态字符缓冲区[4096];//任意数据填充(unsignedr=pipe_size;r>0;){unsignedn=r>sizeof(buffer)?大小(缓冲区):r;写(p[1],缓冲区,n);r-=n;}//ClearPipefor(unsignedr=pipe_size;r>0;){unsignedn=r>sizeof(buffer)?大小(缓冲区):r;读取(p[0],缓冲区,n);r-=n;}}intmain(intargc,char**argv){...//只读打开目标文件constintfd=open(path,O_RDONLY);//是的,只读!:-)......//创建管道intp[2];准备管道(p);//splice()将文件的1字节数据写入Pipessize_tnbytes=splice(fd,&offset,p[1],NULL,1,0);......//write()将任意数据写入Pipenbytes=write(p[1],data,data_size);//判断是否写入成功if(nbytes<0){perror("写入失败");返回EXIT_FAILURE;}if((size_t)nbytes
