CVE-2019-2215复现及分析
2021-02-01 18:58:00 Author: mp.weixin.qq.com(查看原文) 阅读量:146 收藏

本文为看雪论坛精华文章

看雪论坛作者ID:LowRebSwrd

受影响机型

Pixel 2 with Android 9 and Android 10 preview、Huawei P20、Xiaomi Redmi 5A
Xiaomi Redmi Note 5、Xiaomi A1、A3、Moto Z3、Oreo LG phones (run according to )、Samsung S7, S8, S9、Kernel 3.4.x and 3.18.x on Samsung Devices using Samsung Android and LineageOS、Pixel 1 and 2, but not Pixel 3 and 3a.
It was patched in the Linux kernel >= 4.14 without a CVE, accessible from inside the Chrome sandbox.
根据https://bugs.chromium.org/p/project-zero/issues/detail?id=1942公开的poc拿到了任意内核读写权限。
后续文章https://hernan.de/blog/2019/10/15/tailoring-cve-2019-2215-to-achieve-root/。这个漏洞比较好用,并且利用公开的漏洞能够root最新机器。
基于原始poc代码任意地址写的基础上,在patch kernel绕过了一些缓解机制所做的完整工作,但拿到任意地址的原理、过程并未展开陈述,基于此,笔者开始着手复现并阐述其实现原理以及漏洞利用方法。

PoC


#include <fcntl.h>#include <sys/epoll.h>#include <sys/ioctl.h>#include <unistd.h>
#define BINDER_THREAD_EXIT 0x40046208ul
int main(){ int fd, epfd; struct epoll_event event = { .events = EPOLLIN }; fd = open("/dev/binder", O_RDONLY); epfd = epoll_create(1000); epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); ioctl(fd, BINDER_THREAD_EXIT, NULL);}
官方描述如下:

As described in the upstream commit:“binder_poll() passes the thread->wait waitqueue that can be slept on for work. When a thread that uses epoll explicitly exits using BINDER_THREAD_EXIT, the waitqueue is freed, but it is never removed from the corresponding epoll data structure. When the process subsequently exits, the epoll cleanup code tries to access the waitlist, which results in a use-after-free.”

也就是binder_thread->waitqueue,这个链表中连接了epoll data结构,但当调用了BINDER_THREAD_EXIT对应的方法,就会导致binder_thread被释放,当程序结束的时候,epoll相应的结构就会遍历到此成员,造成uaf。
对应的poc步骤为:
1. open(“/dev/binder”),会创建binder_thread
2. epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);初始化binder_thread->wait_queue_head_t,调用add_wait_queue插入wait_queue_t到binder_thread.wait
3. ioctl(fd, BINDER_THREAD_EXIT, NULL); 释放binder_thread结构体
4. 程序结束的时候,会遍历这个链表中,触发uaf。
5. 此外,如果调用epoll_ctl(epfd, EPOLL_CTL_DEL, fd,event)也会遍历到这个链表中。
a)调用remove_wait_queue,然后删除wait_queue_t,会遍历到binder_thread->wait成员(wait_queue_head_t)这样可以跟4是一样的效果。
因此,poc也可以为:

基础知识

了解epoll
写完poc之后,讲解利用之前,不妨看一下内核的基本流程和一些基本概念:
1. epoll是select和poll的升级版,应用程序中调用 select() 和 poll() 函数,使进程进入睡眠之前,内核先检查设备驱动程序上有无对应事件的状态,此时可通过查看 poll() 函数的返回值。
2. 能够在返回值上使用的宏变量有以下组合:
POLLIN, POLLPRI, POLLOUT, POLLERR, POLLHUP, POLLNVAL, POLLRDNORM,  POLLRDBAND, POLLWRNORM, POLLWRBAND, POLLMSG,  POLLREMOVE 

这些值中使用最多的是下面几个组合:
POLLIN | POLLRDNORM 表示可读

POLLOUT | POLLWRNORM 表示可写 

* POLLERR 表示出错
源码解读
它们是如何初始化,结构体是怎么样的?
1、epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);会调用binder_poll函数
2、/dev/binder绑定了一些系统调用,并且实现了binder_poll,binder_poll中对binder_thread.wait进行了初始化,并调用add_wait_queue(重点)
3、其具体调用链为EPOLL_CTL_ADD->ep_insert()->binder_poll函数,binder_poll函数会获取binder_thread结构,调用poll_wait.
binder设备实现的函数如下图:
/dev/binder, 会有binder_poll这个调用:
binder_poll 调用核心的函数为poll_wait:
poll_wait()会调用epq.pt.qproc所对应的回调函数ep_ptable_queue_proc,执行add_wait_queue操作。

以上其具体含义为设置pwq->wait的成员变量func唤醒回调函数为ep_poll_callback;并将ep_poll_callback放入等待队列whead中,ep_poll_callback函数核心功能是当目标fd的就绪事件到来时,将fd对应的epitem实例添加到就绪队列。当调用epoll_wait()时,内核会将就绪队列中的事件报告给应用。

也就是ep_insert会调用到ep_item_poll->binder_poll->poll_wait。

binder_poll 调用核心的函数为poll_wait

主要结构体的初始化都发生在ep_insert->binder_poll中,poll_wait的第一个参数为binder的fd, 第二个参数为binder_thread的wait成员。来看一下它的成员情况:
struct binder_thread {    wait_queue_head_t wait;;;;;;;;;;;}struct __wait_queue_head {    spinlock_t        lock;    struct list_head    task_list;};struct __wait_queue {    unsigned int        flags;    void            *private;    wait_queue_func_t    func;    struct list_head    task_list;};
当调用一次add_wait_queue增加wait_queue_t
当insert多次就会变为以下:
 

epoll_create函数

以上成员如何初始化的呢?需要了解epoll_create函数,open(“/dev/binder”)进入内核会调用binder_open分配binder_proc结构体,epoll_create会调用ep_alloc,对成员进行初始化。
 
在这个链表中,有两种数据结构:等待队列头(wait_queue_head_t)和等待队列项(wait_queue_t)。等待队列头和等待队列项中都包含一个list_head类型,由于我们只需要对队列进行添加和删除操作,并不会修改其中的对象(等待队列项)一开始它是INIT_LIST_HEAD(&q->task_list); next,prev指针分别指向自己。

当初始化时:
而对队列项的初始化wait_queue_t在:

EPOLL_CTL_DEL

epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);函数相对比较简单,会调用remove_wait_queue

当调用remove_wait_queue:

heapspray的理念

1. readv和writev堆喷。
2. Time of check time of use,简称toctou,堆喷完,中间会有一个等待时机,阻塞住内核,可以绕过check,内核的数据通过漏洞已经被改写,然后再use,可以转化为任意地址读或者写。

既然看完了内核的追溯过程,回到poc本身如果是binder_thread结构体的释放,并且是uaf,就会离不开堆喷。

其内核的源码追溯如下:

readv和writev内部会调用kmalloc分配空间,内部采用分散读(scatter read)和集合写(gather write),内核都会调用到do_loop_readv_writev函数。

可以参考retme的https://speakerdeck.com/retme7/the-art-of-exploiting-unconventional-use-after-free-bugs-in-android-kernel。
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
 ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
也会再开始调用rw_copy_check_uvector,其源码如下:
调用kmalloc分配大小,然后根据iov_base依次进行写入或者读取iov_len长度的内容。
struct iovec{     void __user *iov_base;    /* BSD uses caddr_t (1003.1g requires void *) */     __kernel_size_t iov_len; /* Must be size_t (1003.1g) */};
关键要理解的是随着readv和writev调用kmalloc分配完相应的对象,并对之前free掉的object进行占位时,会等待write和read的调用,中间会有一个时机是触发漏洞的时机,以方便对iov_base的修改。

漏洞利用

从这里开始分析如何从poc转变为kernel的任意地址读写,至于任意读写之后到拿到root部分因为网络资料较多,暂不分析。
所使用的手机环境为pixel 2,linux内核版本tag为4.4.116-gbcd0ecccd040
作者的exp可以分为两次的触发漏洞:
1. 触发漏洞,通过创建pipe,writev(堆喷)和read配合使用,泄露task_struct地址。
2. 触发漏洞, 创建socket,readv(堆喷)和write配合使用,实现patch addr_limit内核变量,打开任意内核地址读写。
注意:这两步都会重新触发漏洞,每一步两个函数的之间的调用是有时间差的,并且都会等待下一个函数的开始调用,比如writev会等待read的调用,否则一直阻塞,所以会fork子进程之前会有sleep的动作,以保证执行的先后顺序,fd(文件描述符)之间可以父子进程共享。

泄露内核进程地址


先看第一步:leak_task_struct,观察step1-6(按时间先后顺序)的运行,放大图片来观看:
这里简单总结下:
1. EPOLL_CTL_ADD会调用add_wait_queue;
2. BINDER_THREAD_EXIT释放binder_thread;
3. 调用writev堆喷大小一样的binder_thread结构体;
4. 调用EPOLL_CTL_DEL即remove_wait_queue对链表进程删除,会造成iov_base的修改;
5. 然后调用read,绕过内核的检查,读取iov_base的内容,即造成内核地址数据的泄露。
关键在于remove_wait_queue中
数据成员指向了自己,read的时候读出了内核地址。
 
我在内核中也打印log验证了这一点
之前的漏洞利用都是通过readv进程堆喷,已经检查过iov_base,没有数据,会一直阻塞,这时候会等待write的到来,然而中间的某个时刻会触发漏洞改变iov_base为kernel address,然后进行write,可以往kernel_address写入内容,实现内核地址写,而这种相反的方式扩大了一些苛刻场景的漏洞利用,达到了绕过kalsr的技巧。

patch addr_limit

注释已经写的很清楚了。
iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page_4g_aligned;iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 1;iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF;iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x8 + 2 * 0x10;iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_base = (void *)0xBEEFDEAD;
iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len = 8;
当epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event);调用完毕
iovec_array[IOVEC_INDX_FOR_WQ].iov_len = &(binder_thread->wait+8)                 iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base =&(binder_thread->wait+8)
这时候write调用开始连续写入iov_base.写入的内容如下:
unsigned long second_write_chunk[] = {    1, /* iov_len */    0xdeadbeef, /* iov_base (already used) */    0x8 + 2 * 0x10, /* iov_len (already used) */    current_ptr + 0x8, /* next iov_base (addr_limit) */    8, /* next iov_len (sizeof(addr_limit)) */    0xfffffffffffffffe /* value to write */  };
注意因为在//step 2 write(socks[1], "X", 1) 已经提前写入长度为1的值,所以对iov_len的修改后期并没有起作用,否则将会拷贝iovec_array[IOVEC_INDX_FOR_WQ].iov_len = &(binder_thread->wait+8) 长度的数据到dummy_page_4g_aligned。
然后step 5中write(socks[1],second_write_chunk,sizeof(second_write_chunk))开始对iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base =&(binder_thread->wait+8)这个地址进行写入,长度为0x8 + 2 * 0x10
这时候binder_thread的内部数据发生变化:
binder_thread.wait.task_list.next = 1 //iov_lenbinder_thread.wait.task_list.prev = 0xdeadbeef //basebinder_thread.x1 = 0x8 + 2 * 0x10  //lenbinder_thread.x2 = current_ptr + 0x8//basebinder_thread.x3 = 8
这时候继续进行执行程序:
iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_base = (void *)0xBEEFDEAD;
iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len = 8;
因为这iovec_array是堆喷的数据,它其实相当于binder_thread.x2的内容,已经由0xBEEFDEAD修改成current_prt+0x8了,这时候second_chunk只剩下最后一个值0xfffffffffffffffe,然后继续write(fd,current_prt+0x8, 0xfffffffffffffffe),达到patch addr_limit,从而实现任意内核写,拿到root:
这里需要注意的是:int recvmsg_result = recvmsg(socks[0], &msg, MSG_WAITALL); MSG_WAITALL的标志起到了等待write调用的完成,也就是一直会等待下去。
通过任意地址写,patch task_struct的一些内核变量,达到uid=0的效果,这里写入sid会被阻塞,可能被seilnux禁止,应当先将selinux_enforcing内核变量设置0绕过selinux,然后拿到真正的root。
在pixel 2机器复现了:Android 9系统尝试获取root

有些地方手机会崩溃

1. 崩溃地方在spin_lock_irqsave(&q->lock,flags),
2. readv和write调用,会将所有的内容写入dummy_page_4g_aligned中,可能内核read和write实现的机制不同,但这部分还未分析。

其他知识点

Printk不可见的原因:
diff --git a/lib/vsprintf.c b/lib/vsprintf.cindex 0a51559..279d5ff 100644--- a/lib/vsprintf.c+++ b/lib/vsprintf.c@@ -1514,7 +1514,7 @@ char *pointer(const char *fmt, char *buf, char *end, void                case 3: /* restrict all non-extensioned %p and %pK */                case 4: /* restrict all non-extensioned %p, %pK, %pa*, %p[rR] */                default:-                       ptr = NULL;+                       //ptr = NULL;                        break;                }                break;
或者echo 0>/prcoc/sys/kernel/kptr_restrict也可以。
dmsg或者cat /proc/kmsg log打印不友好或者断断续续,可以cat /dev/kmsg
日志插入的位置:
分别在add_wait_queue,remove_wait_queue和binder_free_thread函数插入前后的log并以进程名字为过滤,指针有可能会被其他的值覆盖,所以最好不要用%p,否则内核会崩溃在自己写的log上。

总结

此次漏洞由syzcaller产生,主要在于设备实现了binder_poll函数,binder_poll函数内部使用了binder_thread结构成员,但未考虑binder_thread结构如果已经释放的情况下,epoll机制仍然使用其中的成员,导致的uaf,其patch在释放binder_thread结构提前会对epoll上的链表进行清理,其漏洞利用特点来看,是tocttou的升级利用,衍生出了某些条件下可以遇到uaf,或者heap overflow这类漏洞实现信息泄露和绕过kalsr的有效机制。
Patch
增加了
在free binder_thread的时候会对wait_queue_head进行处理,置0。
在ep_poll_callback中:
References:
crash:
https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=414028
issue:
https://bugs.chromium.org/p/project-zero/issues/detail?id=1942
poc: 
https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=414030
https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=414885
patch:
https://elixir.bootlin.com/linux/latest/ident/POLLFREE
https://pacsec.jp/psj17/PSJ2017_DiShen_Pacsec_FINAL.pdf
https://github.com/externalist/exploit_playground/blob/master/CVE-2016-2434/exploit_CVE-2016-2434_commented.c
https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=5&ved=2ahUKEwit3fb2zLHlAhWNF6YKHX_DC1UQFjAEegQIAhAB&url=https%3A%2F%2Fsecurityaffairs.co%2Fwordpress%2F92633%2Fhacking%2Fcve-2019-2215-zero-day-exploit.html&usg=AOvVaw2ItkF7ngwGi8z6SfNtHj3x
epoll的简单描述:
https://www.cppfans.org/1418.html

- End -

看雪ID:LowRebSwrd

https://bbs.pediy.com/user-home-726411.htm

  *本文由看雪论坛 LowRebSwrd 原创,转载请注明来自看雪社区。

# 往期推荐

公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458380855&idx=1&sn=e4bdd62b2f12cc173540e3f6a552a0da&chksm=b180dabd86f753abdd22508953c2c8f523fc0202cd81e1da02e5bdad40a3c2d7c3e824235767#rd
如有侵权请联系:admin#unsafe.sh