CVE-2019-2215
背景
该漏洞由Google公司的Project Zero小组于2017年末发现,2018年初修复了该漏洞,但2019年9被Google公司的分析小组(TAG)发现较新的android上依然存在该漏洞并确认已应用在实际攻击中。并分配CVE编号CVE-2019-2215。
漏洞成因
前置知识
select函数主要用来监视文件描述符的变化情况,多用于实现非阻塞socket。
poll函数和select实现的功能差不多,但poll效率更高,作用是把当前的文件描述符挂到等待队列。在2.5.44版本后,poll被epoll取代。
epoll机制是Linux内核为了处理大批量文件描述符而作了改进的poll,它显著提高了程序在大量并发连接中只有少量活跃的情况下cpu的利用率。
epoll相关的系统调用如下:
|
|
epoll_ctl函数的控制码如下:
|
|
漏洞描述
首先我们来看下该漏洞的描述,Android内核的binder驱动中的释放后重引用漏洞。成功利用该漏洞可实现本地权限升级。漏洞成因是我们通过epool_ctl选项EPOLL_CTL_ADD监听binder描述符时,会触发底层函数binder_poll分配一个binder_thread结构体并通过poll_wait函数把binder_thread结构体中的wait元素的地址添加到epoll机制的等待队列中。当别的线程使用BINDER_THREAD_EXIT选项退出binder句柄时,底层函数会去释放掉前面创建的binder_thread结构体的内存,但并不会从相应的等待队列中删除对binder_thread->wait的引用,导致程序退出时epoll清理代码使用binder_thread->wait元素时由于binder_thread结构体已经被释放而导致释放后重引用。
Poc
下面是作者给出的poc
|
|
漏洞原理
大概了解了该漏洞的原理之后,我们来具体分析一下该漏洞,原理挺简单,搞懂UAF我们需要明白3个问题,内存在哪申请,内存在哪释放以及内存在哪重引用。搞清楚这三个问题那么我们也就算搞明白了这个漏洞了。
内存的申请
先来看内存的申请,触发链如下:
epoll_ctl:EPOLL_CTL_ADD -> ep_insert -> ep_item_poll -> binder_poll -> binder_get_thread
|
|
当我们在用户态使用epoll_ctl函数并且使用EPOLL_CTL_ADD选项时,则可以触发到内核底层binder_poll函数。该函数首先通过binder_get_thread函数去申请一个binder_thread结构体,获取成功之后通过poll_wait函数把binder_thread结构体的wait成员添加到epoll等待队列中(注意这里的添加操作),到此内存分配的流程我们分析完了。
涉及到的binder_thread结构体
|
|
然后来看下在何时对binder_thread结构体内存进行的释放,通过binder句柄的ioctl接口传入BINDER_THREAD_EXIT选项即可触发内核对binder_thread的释放:
binder_ioctl:BINDER_THREAD_EXIT -> binder_thread_release -> binder_thread_dec_tmpref -> binder_free_thread -> kfree
|
|
最后再来看一下binder_thread结构体内存在哪做了重引用了:
.release -> ep_eventpoll_release -> ep_free -> ep_unregister_pollwait -> ep_remove_wait_queue -> remove_wait_queue
|
|
在对epoll句柄做release操作时会调用到ep_eventpoll_release函数,底层会执行到ep_remove_wait_queue函数,函数内通过pwq->whead获取到前面的binder_thread结构体的wait成员,然后执行链表删除操作。因为前面已经释放了binder_thread结构体,所以这里的wait引用就是释放后重引用。
后来通过对内核代码的分析知道我们也可以通过epoll_ctl函数的EPOLL_CTL_DEL选项去主动触发ep_eventpoll_release的调用触发释放后重引用,调用链如下。
epoll_ctl:EPOLL_CTL_DEL -> ep_remove -> ep_unregister_pollwait -> ep_remove_wait_queue -> remove_wait_queue
从等待队列中删除一个节点的流程如下:
|
|
|
|
总结一下触发流程:binder_thread申请->binder_thread释放->binder_thread->wait重引用
补丁用意
linux补丁官网上在2017年12月份就修复了,但是怎么android又报了出来呢?
Syzboot日志
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/drivers/android/binder.c?h=linux-4.14.y&id=7a3cee43e935b9d526ad07f20bf005ba7e74d05b
如果这个线程使用了poll,确保我们从任何epoll的数据结构中移除带有POLLFREE的等待队列,waitqueue_active是安全的,因为我们要保持内锁在这里使用。
漏洞利用
漏洞利用思路
下面参考着文末贴出的Exp分析一下该漏洞的利用思路,利用分两步:
1.通过漏洞获取当前进程的task_struct地址。
2.利用前面获取的task_struct计算出进程limit的值,通过漏洞修改进程limit的值,实现内核的任意读写。
泄漏task_struct地址
内核地址读,思路:首先利用BINDER_THREAD_EXIT选项触发binder_thread结构体的释放,接着创建一对pipe读写句柄,利用聚合写writev函数对pipefd[1]进行写操作,写入一组我们准备好的iovec数组到内核态去占位前面释放的binder_thread结构体。因为其中有两个iovec结构体刚好能与释放的binder_thread->wait元素重合,所以使用epoll_ctl的EPOLL_CTL_DEL选项触发重引用的链表删除操作时,刚好可以把重合的第二个iovec结构体的iovec.iov_base的值改为binder_thread->wait.task_list的地址。此时用户态通过read去读取数据时,实际读取的就是iovec.base指向的内存,这样就可以从binder_thread->wait.task_list开始读取。读取的长度由iovec->iov_len控制,该长度由我们可控,这就实现了内核读的效果。
|
|
来看一下具体实现:
|
|
以上是调试数据,可以看到当执行到remove_wait_queue的时候,参数q指向的task_list地址为0xffffffd7b7a3dea0,正好就是binder_poll中thread->wait的首地址,并且根据q->lock的内容为0x100010000可以确定我们用户态数据的占位是成功的,参数wait指向的task_list地址为0xffffffd86c38ed20,wait->task_list.next与wait->task_list.next都指向q->task_list,因为这task_list链表只有两个节点,所以通过链表删除操作之后则把q->task_list.next和q->task_list.prev都改为了0xffffffd7b7a3dea8(q->task_list的地址),而task_list.next,task_list.prev分别对应上与binder_thread->wait重叠的第一个iovec的iov_len元素以及第二个iovec的iov_base,最后当用户态read的时候就会从第二个iovec的iov_base(0xffffffd7b7a3dea8)开始读iovec.iov_len的长度到用户态内存,这样我们就成功泄漏了内核数据,泄漏的数据偏移0xe8(pxiel2上面)刚好就是当前进程的task_struct结构体的地址。
修改limit值
内核地址任意写,思路:通过上一步的内核读有了当前进程的task结构体那么下一步怎么提权呢?此时还需要构造一个内核写,通过内核写去修改进程limit的值,这样就能实现内核任意地址读写的效果。
首先还是需要先触发binder_thread结构体的释放。不过这次是通过socketpair函数创建一对匿名已连接套接字,通过recvmsg函数对socks[0]做接收操作,传入的参数2是一个msghdr结构体,msghdr.msg_iov指向的是我们准备好的iovec数组,让这组iovec数组去内核中占位前面释放的binder_thread结构体。iovec数组中有两个iovec刚好能与binder_thread->wait元素重合,然后使用epoll_ctl的EPOLL_CTL_DEL选项触发重引用的链表删除操作,把重合的第二个iovec结构体的iovec.iov_base的值改为binder_thread->wait.task_list的地址,再利用用户态调用write函数把需要覆盖的数据写入到socks[1]。当socks[1]中有数据的时候,recvmsg则把读取到的数据写入到iovec数组的每个iovec结构体的iov_base指向的内存,因为前面已经利用漏洞把重合的第二个iovec结构体的iovec.iov_base修改成了binder_thread->wait.task_list的地址,所以这里会直接把伪造数据写入到binder_thread->wait.task_list的地址,从而实现对指定内核地址写的操作。
|
|
具体实现:
|
|
以上为调试信息,通过日志可以看到,内核一共调用了两次skb_copy_datagram_iter函数,该函数是把通过套接字接收到的数据写到msghdr.msg_iov指向的iovec结构体数组的iov_base元素指向的内存中,属于recvmsg的底层函数。
第一次是把’X’(0x58)写到地址0xffffffcc03dfeca0中的指针指向的内存中。
第二次则比较精巧,共写0x30个字节的数据,但分两次写。
第二次写剩下的8个字节,因为0xffffffcc03dfecc0也属于一个iov_base,并且0xfffffffffffffffe还没开始写,所以最后的0xfffffffffffffffe就被写到前面覆盖的内核地址指向的内存中,因为这里使用的内核地址指向的是进程limit,所以实现了对进程limit的修改。
这里的写操作还是挺巧妙的,不得不佩服作者。
|
|
内核任意读写
最后我们成功修改当前进程的limit之后就可以通过pipe组合对内核内存任意读写了,有了任意读写则可以修改关键结构体达到提权的效果。
|
|
Poc测试
|
|
总结思考
最后总结一下本次的UAF漏洞模型,动态申请的结构体变量的引用被添加到链表中,但在释放该结构体内存时并没有从链表中删除该变量的引用,导致利用该节点时出现释放后重引用的问题。一般这种链表拆卸漏洞最有可能出现内核地址任意读写漏洞。
申请对象内存->添加到链表->释放内存->通过链表访问对象
时间线
|
|
相关资料
Author: Let_go
Link: http://github.com/2019/10/08/CVE-2019-2215/
Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.