从exp反推CVE-2022-0847dirtypipe原理

从exp反推CVE-2022-0847dirtypipe原理

原创 SQ SQ安全渗透 2024-04-11 04:07


点击上方蓝字关注我哦

前情提要

qian qing ti yao

想着让小白也能懂这个原理

漏洞简介

CVE-2022-0847,也被称为”Dirty Pipe”,是一个影响Linux内核的严重安全漏洞。这个漏洞存在于Linux内核的内存管理子系统中,攻击者可以利用这个漏洞在受影响的系统上执行任意代码,甚至可以获取系统的完全控制权。

“Dirty Pipe”漏洞的名称来源于它所影响的内核数据结构——页表。在Linux内核中,页表用于跟踪物理内存的使用情况,包括哪些内存页面已经被修改(”脏”)以及哪些还没有。由于一个设计上的缺陷,攻击者可以通过创建特殊的、恶意的页表条目来破坏页表的完整性,从而实现对系统的控制。

exp解释

  1. 定义了两个宏 PAGE_SIZE 和 PIPE_SIZE,分别表示页面大小和管道大小。

  2. PAGE_SIZE:内存页是操作系统管理内存的基本单位,它的大小通常取决于操作系统的设计和硬件架构。例如,在一些系统中,一个标准的页面大小可能是4KB(4096字节)。

  3. PIPE_SIZE:管道是Unix和类Unix系统中的一个传统IPC(进程间通信)机制。它允许两个进程之间以先进先出的方式传输数据流。所谓的PIPE_SIZE是指创建管道时,内核为该管道分配的缓冲区大小,这个大小会影响到一次能在管道中写入或读取的数据量。

  4. 定义了一个函数 SetCanMerge,该函数接受一个整数数组作为参数,用于设置管道的合并属性。

  5. 在 main 函数中,首先创建了一个名为 pipefd 的整数数组,用于存储管道的文件描述符。

  6. 调用 SetCanMerge 函数,将 pipefd 作为参数传入,以设置管道的合并属性。(
    将pipe的缓冲区填满(
    两个宏 PAGE_SIZE
     和 PIPE_SIZE
    ,分别表示页面大小和管道大小,并且每一次设置一标签(是否合并)

  7. 打印一条消息,表示管道的合并属性已设置完成。

  8. 使用 open 函数打开文件 /etc/passwd,并以只读方式获取文件描述符,将其赋值给变量 fd。

  9. 调用 splice 系统调用,将文件描述符 fd 的内容复制到管道的写入端 pipefd1

  10. 打印一条消息,表示 splice 操作已完成,并显示返回值。

  11. 向管道的写入端写入字符串 “oots:”。

由于第一个字节为零拷贝,这样的话/etc/passwd的第一行变成了roots::…,原本第一行的内容为root:x:…,中间的x表示此用户有密码,而我们把x取消掉了,那么我们生成了一个 uid 为 0 且没有密码的用户roots,

环境搭建编译运行exp

安装完成之后,reboot重启,开机界面按shift+TAB进入 ubuntu 引导界面,然后选择高级选项advance,选择我们刚刚安装的那个内核进入启动。

成功替换指定的版本。

1.root-roots

2.密码为0—-roots::0:0:root:/root:/bin/bash

代码分析

读和写对应的调用,它们在内核层名为
pipe_read和pip_write
pipe_read
pip_write

/source/fs/pipe.c

fd[0]和fd[1]的来源

int do_pipe(int *fd)
{
    struct file *fw, *fr;
    int fdw, fdr;

    //创建管道写端的file结构
    fw = create_write_pipe();   

    //在写端的file结构基础上构建读端
    fr = create_read_pipe(fw);

    //创建读端fd
    fdr = get_unused_fd();

    //创建写端fd
    fdw = get_unused_fd();

    //fd 和 file进行关联
    fd_install(fdr, fr);
    fd_install(fdw, fw);

    //返回读写端fd
    fd[0] = fdr;
    fd[1] = fdw;

    ...

    return 0;
}

pipe_write:

static ssize_t pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
    struct file *filp = iocb->ki_filp;
    struct pipe_inode_info *pipe = filp->private_data;
    unsigned int head;
    ssize_t ret = 0;
    size_t total_len = iov_iter_count(from);
    ssize_t chars;
    bool was_empty = false;
    bool wake_next_writer = false;

    /* Null write succeeds. */
    if (unlikely(total_len == 0))
        return 0;
    __pipe_lock(pipe);
判断数据来源是否为0,是0就关闭交易


    if (!pipe->readers) {
        send_sig(SIGPIPE, current, 0);
        ret = -EPIPE;
        goto out;
    }

    #ifdef CONFIG_WATCH_QUEUE
    if (pipe->watch_queue) {
        ret = -EXDEV;
        goto out;
    }

    #endif
    head = pipe->head;
    was_empty = pipe_empty(head, pipe->tail);
    chars = total_len & (PAGE_SIZE-1);
PAGE_SIZE通常情况下来说大小是4096,刚好是一个 2 的 12 次幂,那么再 -1 相当于就是二进制的 12 个 1,再用 & 运算就是取得total_len最低的 12 位
PAGE_SIZE
4096
total_len

    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) {
            ret = pipe_buf_confirm(pipe, buf);
            if (ret)
                goto out;

            ret = copy_page_from_iter(buf->page, offset, chars, from);
  1. 计算一个掩码值,该值为管道的环大小减去1。

  2. 获取管道缓冲区的地址,该地址由头部指针减1后与掩码进行位与运算得到。

  3. 计算偏移量,该值为缓冲区的偏移量加上其长度。

  4. 检查缓冲区的标志位是否包含PIPE_BUF_FLAG_CAN_MERGE,并且偏移量加上chars是否小于等于PAGE_SIZE。如果这两个条件都满足,那么它将执行以下操作:

  5. 调用pipe_buf_confirm函数确认管道缓冲区。

  6. 如果返回值不为0,那么它将跳转到out标签。

  7. 否则,它将调用copy_page_from_iter函数,将迭代器中的数据复制到缓冲区的页面中。

Splice

splice的函数原型如下:

c复制代码运行long splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);

参数说明:
– fd_in:输入文件描述符,即要读取数据的文件。

  • off_in:输入文件的偏移量指针,指向要读取数据的起始位置。如果为NULL,则从当前文件位置开始读取。

  • fd_out:输出文件描述符,即要将数据写入的文件。

  • off_out:输出文件的偏移量指针,指向要写入数据的起始位置。如果为NULL,则从当前文件位置开始写入。

  • len:要传输的数据长度。

  • flags:控制传输行为的标志位,如SPLICE_F_NONBLOCK、SPLICE_F_MORE等

gdb调试

不知道为什么本地上没找到文件,额,写点gdb调试的基础方法吧               
1. 编译带有调试信息的程序:在编译源代码时,使用-g选项来添加调试信息。

gcc -g program.c -o program

  1. 使用GDB加载程序:通过gdb命令加载编译后的程序。
gdb program

  1. 设置断点:在特定的代码行上设置断点,以便在执行到该行时暂停程序。
b filename:line_number

例如,在main函数的第一行设置断点:

b main.c:15

  1. 运行程序:使用r(run)命令运行程序,并将程序暂停在断点处。
r

  1. 查看当前状态:可以查看当前的堆栈状态、变量值等。

  2. 查看堆栈状态:info stack

  3. 查看局部变量:info locals

  4. 查看全局变量:info global

  5. 单步执行:使用n(next)命令单步执行程序,遇到函数调用时,可以进入函数内部。

n

  1. 继续执行:使用c(continue)命令让程序继续执行,直到下一个断点或程序结束。
c

  1. 打印变量值:使用p(print)命令打印变量的值。
p variable_name

  1. 退出GDB:使用q(quit)命令退出GDB。
q                                                                                 

关注一波

理想·致敬每一个安全人

初心不改,筑梦未来

扫码关注后台回复“安全”

获取资料

点击菜单还有精美壁纸