kernel heap spray
内核堆喷射
当内核中现有的程序没有办法对UAF后的堆块进行操作的时候,可以利用以下的办法来申请释放之后的堆块,然后控制其内容。参考Linux Kernel universal heap spray这篇文章中给出的两种方法,还是特别有效的。
利用msgsnd
我们可以利用msgsnd的方法来申请释放之后的堆块。但是这种方法申请的堆块的前48字节的内容是不能首我们控制的。msgsnd被定义在头文件#include <sys/msg/h>
中,下面是msgsnd的用法。
1 |
|
当调用msgsnd时,传入的内容被从用户空间拷贝到内核空间。在内核态下会根据传入的size大小来用kmalloc申请空间。具体的过程是msgsnd在内核态下对应的函数是do_msgsnd,在do_msgsnd中会调用load_msg,在load_msg中调用alloc_msg来为msg分配空间。
do_msgsnd
1 | long do_msgsnd(int msqid, long mtype, void __user *mtext, |
load_msg,通过alloc_msg为msg分配内存,然后再将消息内容拷贝进去。
1 | struct msg_msg *load_msg(const void __user *src, size_t len) |
alloc_msg,可以看到消息的结构是由一个消息头,每个消息的长度不超过DATALEN_MSG(4048),过长的消息会被分片进行封装,最后再被连在一起。消息头是一个长度为48字节的结构体,每个消息片段都是靠着消息头中的list_head结构连在了一起。
1 | struct list_head { |
这种方法申请的堆块也能够通过msgrcv能够取出消息队列中的消息。很明显这种方法虽然可以控制大小和写入的内容,但是前48个自己的内容是不可控的。
userfaultfd + setxattr
通用的堆喷射
理想状态下通用的堆喷射应该满足三点条件:
- 对象的大小是可控的(任何大小)
- 对象的内容的可控的
- 在利用过程中,申请之后的对象需要保持在内存中。这对于复杂的uaf和竞争情况下是尤为重要的
msgsnd方法只满足第三个条件,它并不是很通用。
userfaultfd
userfaultfd机制是为了。。。而设计的。当一个page fault在被userfaultfd对象注册的区域内发生的时候,发生错误的线程会被强制刮起并且产生一个能通过userfaultfd的文件描述符进行读的event。处理fault_handling的进程会从userfaultfd的文件描述符读取内容,并且通过在 ioctl_userfaultfd 注册的操作进行相应的处理。
首先利用syscall申请一个userfaultfd为uffd
1 | /* Create and enable userfaultfd object. */ |
注册userfaultfd的api,指定api的版本(api)和启用那些功能(features)
1 | uffdio_api.api = UFFD_API; |
注册(指定)内存地址空间到userfaultfd对象上,其中uffdio_range指定了地址空间的范围,mode指定了监控的是什么样的fault,目前只有一种模式支持,即 UFFDIO_REGISTER_MODE_MISSING,当访问的地址空间并没有映射到实际的物理页时会就被触发(mmap分配时不会分配实际的物理内存)。
1 | struct uffdio_range { |
例子中的用法
1 | /* Register the memory range of the mapping we just created for |
接着开启一个线程来监控并处理page_faults
1 | s = pthread_create(&thr, NULL, fault_handler_thread, (void *)uffd); |
首先介绍一下poll()函数
poll函数
1 |
|
用于等待若干个fd(file descriptor)是否已经准备好被读写。
fds指明需要等待的fd的,用以下的结构来描述。event是一个输入参数,用于表示poll感兴趣的fd的活动。revent是一个输出参数,用于表示fd实际发生的活动。
1 | struct pollfd { |
这个函数返回已经准备好被读数据的fd(revent不为0)的个数。
这时例子中的用法,pollfd.events = POLLIN; 表示poll关心fd是否有数据要去读
1 | struct pollfd pollfd; |
从uffd中读取活动,并且查看event的类型是否是UFFD_EVENT_PAGEFAULT,下面的结构
1 | struct uffd_msg { |
1 | /* Read an event from the userfaultfd. */ |
最后通过ioctl(uffd, UFFDIO_COPY, &uffdio_copy)来处理page fault,其中传入的结构如下所示,其中mode被设置成0表示copy之后就会唤醒阻塞的线程,如果设置成UFFDIO_COPY_MODE_DONTWAKE就不会唤醒,需要UFFDIO_WAKE进行手动唤醒。
1 | struct uffdio_copy { |
1 | /* Copy the page pointed to by 'page' into the faulting |
不处理直接 UFFDIO_WAKE 会怎样
第一次page fault会返回 VM_FAULT_RETRY
, 之后就看具体内核的处理方式了。
有的版本内核 VM_FAULT_RETRY
之后会清除 FAULT_FLAG_ALLOW_RETRY
标志位,回到用户态之后再次触发 page fault,此时 handle_userfault
会返回 VM_FAULT_SIGBUS
导致进程收到 SIGBUS 信号终止。
而不巧的是我选择的目标 v5.9 版本的内核是直接在 do_user_addr_fault
中直接 goto 到之前的位置重新处理,但没有清除这一标志位,这就直接导致了一个死循环。
关于mmap
mmap分配时不会分配实际的物理页
mmap的工作原理,当你发起这个调用的时候,它只是在你的虚拟空间中分配了一段空间,连真实的物理地址都不会分配的,当你访问这段空间,CPU陷入OS内核执行异常处理,然后异常处理会在这个时间分配物理内存,并用文件的内容填充这片内存,然后才返回你进程的上下文,这时你的程序才会感知到这片内存里有数据
一些flag的意义
MAP_PRIVATE内存的内容的改变不会被同步到fd中
MAP_SHARED内存的内容的改变会被同步到fd中
MAP_ANONYMOUS会忽略掉fd(不允许将页映射到文件上,某些时候需要fd必须时-1),申请到的页会被清空
下面是我测试的程序
1 |
|
利用userfaultfd+setxattr进行堆喷射
首先我们来说明一下二者在堆喷射的作用,userfaultfd用来挂起setxattr的内容使得setxattr分配的堆块驻留在内核中不被释放;setxattr用于分配之堆块并且控制堆块中的内容。下面是setxattr的代码
1 | static long |
可以看到程序在[5]处根据我们传入的size用kmalloc申请了内存,然后[6]处会将用户态的数据拷贝到申请的堆块中,此时如果触发page fault,此线程就会被挂起并根据userfaultfd的机制回到用户态进行处理。传入的value的位置可以如下图的形式:
page1是已经分配了物理页,而page2没有分配物理页(这意味着访问page2会触发page fault),用userfaultfd机制来监控page2。
我们将需要拷贝的块放在page1和page2之间,将需要写入堆块的内容放在page1中,为什么要这样做呢?在setxattr中进行copy_from_user(kvalue, value, size)
时,当访问到page1的内容时就会拷贝到内核的块中(这样我们就能控制堆块的内容),当访问的page2的地址的时候就触发page fault从而回到userfaultfd的处理线程,这个线程就会被挂起(此内核堆块就会驻留在内核中不会被释放)。
1 | //触发userfaultfd |