KernelPwn之堆利用(userfaultfd+msg消息队列+pipebuffer)
前置知识
以下分析使用的都是linux-5.9.10的源码。
Userfaultfd机制 进程在访问内存时,并不会直接访问物理内存,而是访问物理内存映射的虚拟内存,然后通过MMU查找虚拟内存对应的物理内存。
MMU通过段表和页表查询虚拟地址对应的物理地址,从而返回数据或者修改数据。
当访问到一块未映射的虚拟内存时,MMU查询没有找到对应的物理内存就会触发缺页异常。
一般系统会自动调用异常处理函数来处理异常,但用户进程可以为自己的虚拟地址设置缺页异常处理函数。
通过调用SYS_userfaultfd
可以创建文件标识符uffd
,再通过ioctl
控制命令UFFDIO_API
请求验证版本号和启用某些特性,如果内核userfaultfd的版本号是请求的版本号,并且支持请求启用的所有特性,那么成功启用userfaultfd,返回内核支持的所有特性和控制命令。
最后通过uffdio_register
控制命令注册虚拟内存范围,跟踪指定虚拟地址范围的页错误异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 struct uffdio_api { __u64 api; __u64 features; __u64 ioctls; }; struct uffdio_register { struct uffdio_range range ; __u64 mode; __u64 ioctls; }; void RegisterUserfault (void * start, int len, void * handler) { int uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); uffdio_register ur = { 0 }; uffdio_api ua = { 0 }; ur.range.start = (ULL)start; ur.range.len = len; ur.mode = UFFDIO_REGISTER_MODE_MISSING; ua.api = UFFD_API; ua.features = 0 ; if (ioctl(uffd, UFFDIO_API, &ua) == -1 ) ErrExit("[-] ioctl-UFFDIO_API" ); ioctl(uffd, UFFDIO_REGISTER, &ur); pthread_t pt; pthread_create(&pt, NULL , (void * (*)(void *))handler, (void *)uffd); }
如果访问注册的虚拟地址范围并产生页错误异常,uffd
文件便会转为可读,并存放事件消息,此时原线程会卡住等待handler
函数回复。
handler
函数则需要通过select
或者poll
监听文件可读,接收事件消息,其中事件有以下几种类型
UFFD_EVENT_PAGEFAULT
:页错误异常。
UFFD_EVENT_FORK
:fork调用。
UFFD_EVENT_REMAP
:mremap调用。
UFFD_EVENT_REMOVE
:madvise(MADV_REMOVE)和madvise(MADV_DONTNEED)调用。
UFFD_EVENT_UNMAP
:munmap调用。
然后使用控制命令UFFDIO_COPY
把数据复制到触发页错误异常的虚拟页,或者使用控制命令UFFDIO_ZERO
把虚拟页映射到零页。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 struct uffdio_copy { __u64 dst; __u64 src; __u64 len; #define UFFDIO_COPY_MODE_DONTWAKE ((__u64)1<<0) __u64 mode; __s64 copy; }; void UserfaultHandler (int uffd) { uffd_msg msg; uffdio_copy uc = { 0 }; unsigned long * data = (unsigned long *)mmap(NULL , 0x1000 , PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1 , 0 ); for (;;) { pollfd pf = { 0 }; pf.fd = uffd; pf.events = POLLIN; poll(&pf, 1 , -1 ); read(uffd, &msg, sizeof (msg)); if (msg.event != UFFD_EVENT_PAGEFAULT) { puts ("Pass one event." ); continue ; } uc.src = (ULL)data; uc.dst = msg.arg.pagefault.address & ~(getpagesize() - 1 ); uc.len = getpagesize(); uc.mode = 0 ; uc.copy = 0 ; ioctl(uffd, UFFDIO_COPY, &uc); break ; } }
在linux-5.11之前,普通用户都可以注册缺页异常处理函数,但在之后限定成仅有特权用户可注册。
因此在5.11之前的内核,我们可以给一块未映射的内存注册userfaulthandler
,然后内核在调用copy_from_user
将该内存复制到内核堆空间时就会卡住等待回复。如果此时另一个线程调用内核函数删除该内核堆,并调用其他函数创建新的堆块,再恢复卡住的线程就会造成uaf。
msg消息队列 内核进程之间或者用户进程之间以及内核和用户之间进行通信时可以使用msg消息队列进行通信。
用户可通过msgget
获取唯一标识符msgid
,通过__key
查找对应的msg队列,IPC_PRIVATE
表示创建新的msgid
,__msgflg
为0666|IPC_CREAT
表示key有对应的msg队列就返回该队列id,否则创建新的队列并返回id。
1 2 int msgget (key_t __key, int __msgflg) ;int msgid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
得到msgid
后可通过msgsnd
发送消息。
1 2 3 4 5 6 7 8 9 long ksys_msgsnd (int msqid, struct msgbuf __user *msgp, size_t msgsz, int msgflg) { long mtype; if (get_user(mtype, &msgp->mtype)) return -EFAULT; return do_msgsnd(msqid, mtype, msgp->mtext, msgsz, msgflg); }
其中msgp
表示需要发送的消息,mtype
表示类型,mtext
为可拓展成员,具体大小自定义。
1 2 3 4 struct msgbuf { __kernel_long_t mtype; char mtext[1 ]; };
do_msgsnd
关键代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 static long do_msgsnd (int msqid, long mtype, void __user *mtext, size_t msgsz, int msgflg) { struct msg_queue *msq ; struct msg_msg *msg ; int err; struct ipc_namespace *ns ; DEFINE_WAKE_Q(wake_q); ...... msg = load_msg(mtext, msgsz); msg->m_type = mtype; msg->m_ts = msgsz; ...... msq = msq_obtain_object_check(ns, msqid); ...... if (msgflg & IPC_NOWAIT) { err = -EAGAIN; goto out_unlock0; } ...... if (!pipelined_send(msq, msg, &wake_q)) { list_add_tail(&msg->m_list, &msq->q_messages); msq->q_cbytes += msgsz; msq->q_qnum++; atomic_add (msgsz, &ns->msg_bytes); atomic_inc (&ns->msg_hdrs); } ......
先了解三个结构体,msg_msg
和 msg_queue
以及msg_msgseg
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 struct msg_queue { struct kern_ipc_perm q_perm ; time64_t q_stime; time64_t q_rtime; time64_t q_ctime; unsigned long q_cbytes; unsigned long q_qnum; unsigned long q_qbytes; struct pid *q_lspid ; struct pid *q_lrpid ; struct list_head q_messages ; struct list_head q_receivers ; struct list_head q_senders ; } __randomize_layout; struct msg_msg { struct list_head m_list ; long m_type; size_t m_ts; struct msg_msgseg *next ; void *security; }; struct msg_msgseg { struct msg_msgseg *next ; };
每个msgid
都对应一个msg_queue
,msg_queue
中存放着msg_msg
组成的双向消息链表。
上述源码中的load_msg
便是将用户传入的消息buf处理,生成msg_msg
。
然后将其链入msgid
对应的msg_queue->q_messages
。
load_msg
源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 struct msg_msg *load_msg (const void __user *src, size_t len) { struct msg_msg *msg ; struct msg_msgseg *seg ; int err = -EFAULT; size_t alen; msg = alloc_msg(len); if (msg == NULL ) return ERR_PTR(-ENOMEM); alen = min(len, DATALEN_MSG); if (copy_from_user(msg + 1 , src, alen)) goto out_err; for (seg = msg->next; seg != NULL ; seg = seg->next) { len -= alen; src = (char __user *)src + alen; alen = min(len, DATALEN_SEG); if (copy_from_user(seg + 1 , src, alen)) goto out_err; } err = security_msg_msg_alloc(msg); if (err) goto out_err; return msg;
可见load_msg
先调用alloc_msg
函数创建msg
。
alloc_msg
源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 static struct msg_msg *alloc_msg (size_t len) { struct msg_msg *msg ; struct msg_msgseg **pseg ; size_t alen; alen = min(len, DATALEN_MSG); msg = kmalloc(sizeof (*msg) + alen, GFP_KERNEL_ACCOUNT); if (msg == NULL ) return NULL ; msg->next = NULL ; msg->security = NULL ; len -= alen; pseg = &msg->next; while (len > 0 ) { struct msg_msgseg *seg ; cond_resched(); alen = min(len, DATALEN_SEG); seg = kmalloc(sizeof (*seg) + alen, GFP_KERNEL_ACCOUNT); if (seg == NULL ) goto out_err; *pseg = seg; seg->next = NULL ; pseg = &seg->next; len -= alen; } return msg;
借用lotus的关系图,可知msg_msg
和msg_msgseg
的关系如下
msg_msg
的0x30偏移开始存放用户data。 如果msgsz
不大于((size_t)PAGE_SIZE-sizeof(struct msg_msg))
时,只会创建msg_msg
来保存整个消息。
当msgsz
大于((size_t)PAGE_SIZE-sizeof(struct msg_msg))
时,便会扩展创建msg_msgseg
单向链表来存储多余的数据。
由上分析可知,我们能利用msgsnd
创建8-PAGE_SIZE的堆块。
然后分析msgrcv
函数
1 2 3 4 5 long ksys_msgrcv (int msqid, struct msgbuf __user *msgp, size_t msgsz, long msgtyp, int msgflg) { return do_msgrcv(msqid, msgp, msgsz, msgtyp, msgflg, do_msg_fill); }
do_msgrcv
部分源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 static long do_msgrcv (int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg, long (*msg_handler)(void __user *, struct msg_msg *, size_t )) { int mode; struct msg_queue *msq ; struct ipc_namespace *ns ; struct msg_msg *msg , *copy = NULL ; ...... if (msgflg & MSG_COPY) { if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT)) return -EINVAL; copy = prepare_copy(buf, min_t (size_t , bufsz, ns->msg_ctlmax)); if (IS_ERR(copy)) return PTR_ERR(copy); } mode = convert_mode(&msgtyp, msgflg); ...... msq = msq_obtain_object_check(ns, msqid); ...... msg = find_msg(msq, &msgtyp, mode); ...... if (msgflg & MSG_COPY) { msg = copy_msg(msg, copy); goto out_unlock0; } list_del(&msg->m_list); msq->q_qnum--; msq->q_rtime = ktime_get_real_seconds(); ipc_update_pid(&msq->q_lrpid, task_tgid(current)); msq->q_cbytes -= msg->m_ts; atomic_sub (msg->m_ts, &ns->msg_bytes); atomic_dec (&ns->msg_hdrs); ss_wakeup(msq, &wake_q, false ); ...... out_unlock0: ipc_unlock_object(&msq->q_perm); wake_up_q(&wake_q); out_unlock1: rcu_read_unlock(); if (IS_ERR(msg)) { free_copy(copy); return PTR_ERR(msg); } bufsz = msg_handler(buf, msg, bufsz); free_msg(msg); return bufsz;
msg通过find_msg
函数获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static struct msg_msg *find_msg (struct msg_queue *msq, long *msgtyp, int mode) { struct msg_msg *msg , *found = NULL ; long count = 0 ; list_for_each_entry(msg, &msq->q_messages, m_list) { if (testmsg(msg, *msgtyp, mode) && !security_msg_queue_msgrcv(&msq->q_perm, msg, current, *msgtyp, mode)) { if (mode == SEARCH_LESSEQUAL && msg->m_type != 1 ) { *msgtyp = msg->m_type - 1 ; found = msg; } else if (mode == SEARCH_NUMBER) { if (*msgtyp == count) return msg; } else return msg; count++; } }
它会遍历msq->q_messages双向链表,通过testmsg
匹配消息并返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static int testmsg (struct msg_msg *msg, long type, int mode) { switch (mode) { case SEARCH_ANY: case SEARCH_NUMBER: return 1 ; case SEARCH_LESSEQUAL: if (msg->m_type <= type) return 1 ; break ; case SEARCH_EQUAL: if (msg->m_type == type) return 1 ; break ; case SEARCH_NOTEQUAL: if (msg->m_type != type) return 1 ; break ; } return 0 ; }
testmsg
根据mode
选择筛选模式,然后根据type
找到指定消息,如果mode
为SEARCH_NUMBER
就会直接从首链取出msg。
mode
由convert_mode
得到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static inline int convert_mode (long *msgtyp, int msgflg) { if (msgflg & MSG_COPY) return SEARCH_NUMBER; if (*msgtyp == 0 ) return SEARCH_ANY; if (*msgtyp < 0 ) { if (*msgtyp == LONG_MIN) *msgtyp = LONG_MAX; else *msgtyp = -*msgtyp; return SEARCH_LESSEQUAL; } if (msgflg & MSG_EXCEPT) return SEARCH_NOTEQUAL; return SEARCH_EQUAL; }
当msgflg
为MSG_COPY
时,mode
为SEARCH_NUMBER
。
此时会从消息链表中取出第一个msg(此时还不会将其从链中删除)。
并且prepare_copy
会创建一个消息副本,并把取出的msg复制到副本中并返回,然后直接跳转到out_unlock0
而非调用list_del
将msg从链中删除。
1 2 3 4 if (msgflg & MSG_COPY) { msg = copy_msg(msg, copy); goto out_unlock0; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 struct msg_msg *copy_msg (struct msg_msg *src, struct msg_msg *dst) { struct msg_msgseg *dst_pseg , *src_pseg ; size_t len = src->m_ts; size_t alen; if (src->m_ts > dst->m_ts) return ERR_PTR(-EINVAL); alen = min(len, DATALEN_MSG); memcpy (dst + 1 , src + 1 , alen); for (dst_pseg = dst->next, src_pseg = src->next; src_pseg != NULL ; dst_pseg = dst_pseg->next, src_pseg = src_pseg->next) { len -= alen; alen = min(len, DATALEN_SEG); memcpy (dst_pseg + 1 , src_pseg + 1 , alen); } dst->m_type = src->m_type; dst->m_ts = src->m_ts; return dst; }
由上可知,当msgflg
满足MSG_COPY
时,会直接将第一个消息复制一份并返回。
了解了msgget
、msgsnd
、msgrcv
后,我们可以将其与userfaultfd
结合辅助实现uaf。
流程如下:
调用RegisterUserfault为一个未映射的内存设置缺页异常处理函数。
add一个内核堆块。
创建一个线程函数UAF,等待信号1然后delete该内核堆块。
edit调用copy_from_user将未映射区作为参数。
此时主线程断住,handler线程接收事件并进行操作。
handler线程发送信号1并等待信号2,UAF线程接到信号删除该堆块,并调用msgsnd创建一样大小的堆块,再发送信号2。
handler接收信号2,继续修改该内核堆块,此时该堆块和msg的堆块属于同一个堆块,所以我们可以修改msg的m_ts变大。
msgrcv接收消息,可实现越界读。
pipe_buffer结构体利用 用户进程可通过pipe
函数创建管道,其中pipedes
数组用与接收read
和write
的管道描述符。
1 int pipe (int __pipedes[2 ]) ;
其中pipe
有如下一条调用链:
pipe->do_pipe2->__do_pipe_flags->create_pipe_files->get_pipe_inode->alloc_pipe_info
其中alloc_pipe_info
函数部分代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct pipe_inode_info *alloc_pipe_info (void ) { struct pipe_inode_info *pipe ; unsigned long pipe_bufs = PIPE_DEF_BUFFERS; struct user_struct *user = get_current_user(); unsigned long user_bufs; unsigned int max_size = READ_ONCE(pipe_max_size); pipe = kzalloc(sizeof (struct pipe_inode_info), GFP_KERNEL_ACCOUNT); ...... pipe->bufs = kcalloc(pipe_bufs, sizeof (struct pipe_buffer), GFP_KERNEL_ACCOUNT); ......
其中sizeof(struct pipe_inode_info)
大小为80,sizeof(struct pipe_buffer)
为0x28,
PIPE_DEF_BUFFERS
为0x10。
所以kcalloc(pipe_bufs, sizeof(struct pipe_buffer),GFP_KERNEL_ACCOUNT);
会创建0x400的堆块(实际使用0x28*0x10 = 0x280),即12个pipe_buffer
形成的环形队列。
pipe_buffer
结构体如下
1 2 3 4 5 6 7 struct pipe_buffer { struct page *page ; unsigned int offset, len; const struct pipe_buf_operations *ops ; unsigned int flags; unsigned long private; };
可以看到pipe_buffer
带有一个ops
。
1 2 3 4 5 6 struct pipe_buf_operations { int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *); void (*release)(struct pipe_inode_info *, struct pipe_buffer *); bool (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *); bool (*get)(struct pipe_inode_info *, struct pipe_buffer *); };
当我们调用write
或者read
往pipe
中写入或者读取内容,会调用file->fop
指向的函数表中的函数,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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, }; f = alloc_file_pseudo(inode, pipe_mnt, "" , O_WRONLY | (flags & (O_NONBLOCK | O_DIRECT)), &pipefifo_fops);
可以看到write
会调用pipe_write
进行处理
pipe_write
部分源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 ; ...... 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);
可以看到调用write
会取出pipe_buffer
队列中的第一个pipe_buffer
然后判断其flags
和offset
是否满足条件,满足则调用pipe_buf_confirm
转而调用buf->ops->confirm
。
1 2 3 4 5 6 7 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); }
所以我们可以劫持pipe_buffer
的ops
实现rip劫持,其中rsi为pipe_buffer的堆地址,可供后续magic_gadget利用。