Linux提权CVE-2022-0847分析

204 阅读4分钟

漏洞利用原理:

通过pipe生成一个管道,然后使用write调用pip_write将管道填满flag为PIPE_BUF_FLAG_CAN_MERGE,然后用read将缓冲区全部释放,但是根据splice进行零拷贝时copy_page_to_iter_pipe没有将flag初始化,导致缓冲区仍然留存PIPE_BUF_FLAG_CAN_MERGE。进而在write上检测flag存在PIPE_BUF_FLAG_CAN_MERGE来达成越权写入操作。

什么是零拷贝?

零拷贝是作用于两个文件间移动,正常文件拷贝流程一般为cpu对内存空间进行多次读写操作将拷贝数据从用户态到内核态再返回用户态,而零拷贝让数据不需要经过用户态,而是将内核缓冲区与用户程序进行共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。应用程序再调用write(),操作系统直接将内核缓冲区的内容传输到指定输出端了。
具体的文件通过管道传输流程:
in端 == write == pipe == splice == out端
out端通过splice与内核缓冲区进行共享,然后in端调用write将内容拷贝到内核缓冲区进而写入到out端。

【→所有资源关注我,私信回复“资料”获取←】
1、网络安全学习路线
2、电子书籍(白帽子)
3、安全大厂内部视频
4、100份src文档
5、常见安全面试题
6、ctf大赛经典题目解析
7、全套工具包
8、应急响应笔记

设置缓冲区flag为PIPE_BUF_FLAG_CAN_MERGE

分析pipe_write函数源码


当我们给pipe->tmp_pipe = NULL下断点后,可以看到当我们执行exp后的flags设置为0x10(PIPE_BUF_FLAG_CAN_MERGE)


这里的page = 0xffffea00001b09c0,是我们write申请的页,然后用于与内核缓冲区进行数据传输
根据堆栈回溯可以看到,这个只是调用write时会将flags设置为PIPE_BUF_FLAG_CAN_MERGE

static void prepare_pipe(int p[2])
{
    if (pipe(p)) abort();   //这里要查看一下pipe的栈回溯调用链

    const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ); //调用pipe.c *1392
    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 */
}

上述代码实质就是将pipe缓冲区所以flag设置为PIPE_BUF_FLAG_CAN_MERGE,然后调用read是释放pipe缓冲区来让缓冲区变为闲置等待调用状态,进而让flag为PIPE_BUF_FLAG_CAN_MERGE执行下一次exp中的write。
但是我们只关注flag上的PIPE_BUF_FLAG_CAN_MERGE,那么调用read后会不会将flags上的PIPE_BUF_FLAG_CAN_MERGE呢?

splice实现零拷贝传输

在调用splice时会调用到__copy_page_to_iter
下面是splice的调用链:(查看copy_page_to_iter_pipe堆栈平衡得出)

sys_splice
__do_splice
==> do_splice
===> splice_file_to_pipe
====> generic_file_splice_read
=====> call_read_iter
======> copy_folio_to_iter
=======> flimap_read
========> copy_folio_to_iter
=========> copy_page_to_iter
==========> __copy_page_to_iter
===========> copy_page_to_iter_pipe

在splice_file_to_pipe上存在3种调用情况:

  • in/out都是pipe类型
  • in是pipe类型
  • out是pipe类型(这是exp调用类型)

如下即为exp使用的第三种splice零拷贝的源码:


上面只校验了out端是否为pipe类型,然后检测执行程序用户是否对root权限文件具有读写操作,然后就调用splice_file_to_pipe来进行下一步漏洞利用。

注意下面这个判断:

if (off_in) {    //这里限制了只能从偏移值1开始
          if (!(in->f_mode & FMODE_PREAD))    //判断输入是否有读权限,所以exp只需要对输出到的root权限文件具有可读权限
              return -EINVAL;
          offset = *off_in;
      } else {
.....

if (off_in) {判断是导致进行越权写入偏移值必须为0的原因。

分析copy_page_to_iter_pipe


上述即为splice零拷贝过程中“out端与内核缓冲区共享”的调用源码
可以看到,由于是页引用行为,所以我们传输的数据大小不能大于原文件大小。
补丁上也是在这个文件上对缓冲区的flag进行了初始化操作。
那么我们可以得知,只要在这里对flag进行初始化,就不可能导致越权读写产生,那么说明了判定flag存在PIPE_BUF_FLAG_CAN_MERGE进而达到下一步利用这个过程是不在splice上的。
下面是利用代码:

ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0); 
//将指定文件的内容从指定offset开始copy到p[1]上,长度为1字节

调用write连接pipe进行splice零拷贝时的检测手段

分析零拷贝中所有涉及函数可知,只有在调用pipe_write时存在检测操作。
分析pipe_write:
当缓冲区上的flag为PIPE_BUF_FLAG_CAN_MERGE则直接调用copy_page_from_iter对数据进行管道写入操作,进而达成越权写。


所以可以知道read释放缓冲区时没有对flag进行初始化操作。
下面是利用代码:

nbytes = write(p[1], data, data_size);    
//这里开始触发任意文件写入,将我们指定的内容copy到p[1]

关于参数测试


经案例上使用的"root"可以使用,而""root"可以使用,而"root"无输出