详解CVE-2022-0847 DirtyPipe漏洞


摘要:本文详细介绍了CVE-2022-0847漏洞形成根因,相应补丁修复方法,通过本文让读者对CVE-2022-0847漏洞有更清晰的了解。

本文分享自华为云社区《CVE-2022-0847 DirtyPipe》,作者:安全技术猿。

简介

CVE-2022-0847不需要调用特权syscall就能完成对任意只读文件的修改(有点类似之前的脏牛,但底层原理其实不一样),且由于利用过程中不涉及内存损坏,因此不需要ROP等利用方法,也自然不需要知道内核基址等信息,故不需要对内核版本进行适配(因此可以被广泛利用,危害巨大)。

本质上,这个漏洞是由内存未初始化造成的,且从2016年就存在了,但在当时并不能发生有趣的利用,直到2020年由于对pipe内部实现进行了一些修改,才让这个“BUG”变成了能够利用的“漏洞”。

漏洞分析

这个漏洞主要涉及到两个syscall:

  • pipe:5274f052e7b3dbd81935772eb551dfd0325dfa9d),本质上是为了解决文件对拷的效率问题,它实现了“零拷贝”。

    这里稍微展开说说零拷贝。可以思考下在Linux上你会如何实现文件对拷?

    最简单的,就是open()两个文件,然后申请一个buffer,然后使用read()/write()来进行拷贝。但这样效率太低,原因是一对read()和write()涉及到4次上下文切换,2次CPU拷贝,2次DMA拷贝。

    因此稍微聪明点的人,会使用mmap()+write()的组合,这样涉及4次上下?切换,1次 CPU 拷?,2次DMA 拷?。

    更近一步的,会使用sendfile(),调用sendfile()只需提供两个互拷的fd,以及拷贝的长度即可。与 mmap 内存映射?式不同的是, sendfile 调?中 I/O 数据对?户空间是完全不可?的。因此它只涉及2次上下?切换,2次DMA 拷?。

    splice()类似,不过使用了pipe机制,从而不需要硬件的支持就能实现两个fd间的零拷贝。它也只涉及2 次上下?切换,2次DMA 拷?。

    一般我们用下面的模式使用splice实现文件对拷:

    int in_fd = open(file_to_read);
    int out_fd = open(file_to_write);
    int anon_pipes[2];
    pipe(anon_pipes);
    
    while has_content_to_copy:
        splice(in_fd,&in_off,anon_pipes[1],NULL,size);
        splice(anon_pipes[0],NULL,out_fd,&out_off,size);
    
    close(in_fd);
    close(out_fd);

    可以看到,splice底层用到了pipe。splice支持对接多种设备,例如普通文件,socket等。下面我们啃一下splice的源码,以上面的splice(in_fd,&in_off,anon_pipes[1],NULL,size);为例:

    // >>> fs/splice.c:1325
    /* 1325 */ SYSCALL_DEFINE6(splice, int, fd_in, loff_t __user *, off_in,
    /* 1326 */         int, fd_out, loff_t __user *, off_out,
    /* 1327 */         size_t, len, unsigned int, flags)
    /* 1328 */ {
    ------
                        // splice是对__do_splice的简单包装
    /* 1343 */             error = __do_splice(in.file, off_in, out.file, off_out,
    /* 1344 */                         len, flags);
    ------
    /* 1350 */ }
    // __do_splice 是对 do_splice 的简单包装
    // >>> fs/splice.c:1008
    /* 1008 */ long do_splice(struct file *in, loff_t *off_in, struct file *out,
    /* 1009 */            loff_t *off_out, size_t len, unsigned int flags)
    /* 1010 */ {
    ------
    /* 1011 */     struct pipe_inode_info *ipipe;
    /* 1012 */     struct pipe_inode_info *opipe;
    ------
                // 从 in/out 中尝试取得 pipe_inode_info
    /* 1020 */     ipipe = get_pipe_info(in, true);
    /* 1021 */     opipe = get_pipe_info(out, true);
    ------
                // 上面例子中in是普通文件,out是pipe,因此不进这里
    /* 1037 */     if (ipipe) {
    ------
    /* 1068 */     }
    ------
                // 进这里
    /* 1070 */     if (opipe) {
    ------
                        // 调用 do_splice_to
    /* 1093 */             ret = do_splice_to(in, &offset, opipe, len, flags);
    ------
    /* 1104 */     }
    ------
    /* 1107 */ }
    // >>> fs/splice.c:770
    /* 770 */ static long do_splice_to(struct file *in, loff_t *ppos,
    /* 771 */              struct pipe_inode_info *pipe, size_t len,
    /* 772 */              unsigned int flags)
    /* 773 */ {
    ------
                // 这里根据in的f_op->splice_read选择对应的函数
                // 由于是普通文件,所以:
                //
                // >>> fs/read_write.c:28
                // /* 28 */ const struct file_operations generic_ro_fops = {
                // ------
                // /* 32 */     .splice_read    = generic_file_splice_read,
                // /* 33 */ };
    /* 788 */     return in->f_op->splice_read(in, ppos, pipe, len, flags);
    /* 789 */ }
    // >>> fs/splice.c:298
    /* 298 */ ssize_t generic_file_splice_read(struct file *in, loff_t *ppos,
    /* 299 */                  struct pipe_inode_info *pipe, size_t len,
    /* 300 */                  unsigned int flags)
    /* 301 */ {
    /* 302 */     struct iov_iter to;
    /* 303 */     struct kiocb kiocb;
    /* 304 */     unsigned int i_head;
    /* 305 */     int ret;
    /* 306 */ 
                // 从pipe中取数据,得到 to
    /* 307 */     iov_iter_pipe(&to, READ, pipe, len);
    /* 308 */     i_head = to.head;
    /* 309 */     init_sync_kiocb(&kiocb, in);
    /* 310 */     kiocb.ki_pos = *ppos;
                // 进入这里,其实是调用in->f_op->read_iter(&kiocb,&to);
                // 即 generic_file_read_iter()
    /* 311 */     ret = call_read_iter(in, &kiocb, &to);
    ------
    /* 328 */ }
    // 之后: 
    // generic_file_read_iter()
    // -> generic_file_buffered_read()
    // -> copy_page_to_iter()
    // >>> lib/iov_iter.c:916
    /* 916 */ size_t copy_page_to_iter(struct page *page, size_t offset, size_t bytes,
    /* 917 */              struct iov_iter *i)
    /* 918 */ {
    ------
    /* 921 */     if (i->type & (ITER_BVEC|ITER_KVEC)) {
    ------
    /* 926 */     } else if (unlikely(iov_iter_is_discard(i))) {
    ------
    /* 931 */     } else if (likely(!iov_iter_is_pipe(i)))
    /* 932 */         return copy_page_to_iter_iovec(page, offset, bytes, i);
    /* 933 */     else
                    // 这里的i其实就是前面generic_file_splice_read中的to,因此是pipe
    /* 934 */         return copy_page_to_iter_pipe(page, offset, bytes, i);
    /* 935 */ }
    // 终于来到了我们今天的主角:copy_page_to_iter_pipe
    // >>> lib/iov_iter.c:375
    /* 375 */ static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes,
    /* 376 */              struct iov_iter *i)
    /* 377 */ {
    ------
    /* 378 */     struct pipe_inode_info *pipe = i->pipe;
    ------
    /* 379 */     struct pipe_buffer *buf;
    ------
    /* 394 */     off = i->iov_offset;
    ------
    /* 395 */     buf = &pipe->bufs[i_head & p_mask];
    /* 396 */     if (off) {
    ------
    /* 405 */     }
    /* 406 */     if (pipe_full(i_head, p_tail, pipe->max_usage))
    /* 407 */         return 0;
    /* 408 */ 
                // 划重点!!! 没有设置buf->flags
    /* 409 */     buf->ops = &page_cache_pipe_buf_ops;
    /* 410 */     
                // page ref_count ++
    /* 411 */     get_page(page);
                // 直接把普通文件的pipe拿来放到pipe中
    /* 412 */     buf->page = page;
    /* 413 */     buf->offset = offset;
    /* 414 */     buf->len = bytes;
    /* 415 */ 
    /* 416 */     pipe->head = i_head + 1;
    /* 417 */     i->iov_offset = offset + bytes;
    /* 418 */     i->head = i_head;
    /* 419 */ out:
    /* 420 */     i->count -= bytes;
    /* 421 */     return bytes;
    /* 422 */ }

    可以看到,最主要的逻辑就在copy_page_to_iter_pipe中,之所以splice实现了CPU的零拷贝是因为他直接对目标页的ref count进行了递增,然后把目标页的物理页页框复制到pipe buffer的page处,但这里却忘记设置pipe buffer的flags字段。

    OK,现在梳理完了这两个syscall的逻辑,也发现在splice中存在对pipe buffer的flags字段为初始化漏洞,那一种可行的利用思路就出来了。

    使用pipe read/write,我们可以让目标pipe的每个pipe buffer都带上PIPE_BUF_FLAG_CAN_MERGEflag。之后打开目标文件,并使用splice 写到之前处理过的pipe中,splice底层会帮助我们把目标文件的page cache 设置到pipe buffer的page字段,但却没有修改flags字段。之后我们再调用pipe write时由于存在PIPE_BUF_FLAG_CAN_MERGEflag字段,内容会接着上次被写入同一个page中,但page其实已经变成了目标文件的page cache,导致直接修改了目标文件page cache。如果之后有其他文件尝试读取这个文件,kernel会优先返回cache中的内容,也就是被我们修改后的page cache。但由于这个修改并不会触发page的dirty属性,因此若由于内存紧张后或系统重启等原因,就会导致这个cache内kernel丢弃,再次读取文件内核就会重新从磁盘中取出未被我们修改的内容(这就是和脏牛的不同点)。

    杂谈

    这个bug其实在2016年就产生了,但为什么在2020年才能被利用呢?这就涉及到linux代码的历史了。

    最早的时候,是否能够merge并不是通过struct pipe_buffer中的flags字段来管理,而是通过struct pipe_buf_operations中的can_merge字段来判断。因此在splice被加入linux时,splice提供了一个新的pipe_buf_operations叫page_cache_pipe_buf_ops,如下:

    static struct pipe_buf_operations page_cache_pipe_buf_ops = {
        .can_merge = 0,
        .map = page_cache_pipe_buf_map,
        .unmap = page_cache_pipe_buf_unmap,
        .release = page_cache_pipe_buf_release,
    };

    其中can_merge字段默认就是0,这就解释了为什么在copy_page_to_iter_pipe中不存在对flags的设置逻辑,因为只需要修改fops到page_cache_pipe_buf_ops就可以了。

    之后在2016年的一个commit中 commit 241699cd72a8 “new iov_iter flavour: pipe-backed” (Linux 4.9, 2016),添加了两个函数,其中一个就是copy_page_to_iter_pipe,里面对pipe_buffer的flags没有进行初始化,但现在还没出什么大问题,因为此时can_merge参数还在fops中,且flags中也没有什么有趣的选项。

    时间来到2019年,Commit 01e7187b4119 “pipe: stop using ->can_merge” (Linux 5.0, 2019)中开始对can_merge字段下手了,但这个时候操刀还比较暴力,除了把所有使用所有fops中的can_merge字段删除外,还增加了一个函数叫pipe_buf_can_merge,可能是发现除了匿名管道外,所有的管道都不支持merge,所以只要判断一下fops是不是anon_pipe_buf_ops就行了。到目前为止,merge操作和16年的未初始化bug还没挂钩。

    static bool pipe_buf_can_merge(struct pipe_buffer *buf)
    {
        return buf->ops == &anon_pipe_buf_ops;
    }

    终于,在2020年,可能还是感觉这种判断太过于暴力,于是把merge操作的判断塞进了pipe_buffer的flags中:Commit f6dd975583bd “pipe: merge anon_pipe_buf*_ops” (Linux 5.8, 2020) 。16年埋下的bug终于在4年后变成了漏洞。

    漏洞修复

    >>>漏洞扫描服务

    参考

    • https://github.com/bbaranoff/CVE-2022-0847
    • 点击关注,第一时间了解华为云新鲜技术~