一、测试环境:
Android模拟器Nexus_6P_API_25@kernel 3.18
二、漏洞介绍
代码位于fs/dcache.c
由于dentry->d_name.name和filename是同样的值,在注释[1]和注释[2]之间,如果执行了注释[4],在strcpy时会导致heap overflow。
具体poc可以参考心许雪的这篇文章《[原创](Android Root)CVE-2017-7533 漏洞分析和复现》。
对于长文件名,两个线程同时跑,由于竞争,在strcpy(event->name, file_name)也会发生heap overflow。
在实际的调试中,file_name的值大于old_name的值0x10字节,两者地址有重叠。如果old_name被堆喷射重新占位后的大小过大,在strcpy时会发生heap overflow。
三、如何利用
由于android 8.0已经引入了PAN机制,内核态不能访问用户态的数据,所以传统的通过覆盖ptmx结构体指针,从而实现任意地址写,变的不可能。或者通过挟持指针指向kernel_setsockopt,修改addr_limit,从而通过管道实现任意地址写,也变的不可能。因为他们都需要从用户态读取数据。
参考ThomasKing在BlackHat的文章asia-18-WANG-KSMA-Breaking-Android-kernel-isolation-and-Rooting-with-ARM-MMU-features。使用内核镜像攻击,可以绕过PAN机制。原理可以参考Geneblue《KSMA -- Android 通用 Root 技术》,简单的说需要利用地址写漏洞,向内核的L1页表写入一个block,这个block可以映射内核物理地址开始到内核地址+1G的地址范围。
对于我的操作系统,内存布局如下:
虚拟地址 物理地址
0xffffffc000000000 0x40000000
0xffffffc060000000 0xA0000000
我们通过向内核页表中写入block,形成如下映射,由于内核地址空间是在物理地址0x40000000~0x80000000之间,这样我们可以在用户态通过0xffffffc200000000~0xffffffc240000000访问整个内核地址空间,具体细节后面漏洞利用会再讲。
虚拟地址 物理地址
0xffffffc200000000 0x40000000
0xffffffc240000000 0x80000000
现在我们可以任意的修改内核地址空间,为了实现root,参考asia-18-WANG-KSMA-Breaking-Android-kernel-isolation-and-Rooting-with-ARM-MMU-features做法,修改setresuid。
有人可能会有这样的问题,如果有了任意地址写,不就已经可以实现root了么?是的,不过针对CVE-2017-7533来说,写的地址并不任意,只能向一个内核地址写入内容。
那么,刚刚我们才说过,这是一个heap overflow漏洞,又如何变成向一个内核地址写入内容呢?
可以参考Retme the art of exploiting unconventional use-after-free bugs in android kernel,通过heap overflow覆盖iovec的addr为内核地址,从而实现向一个内核地址写入内容。
系统调用readv,跟踪代码会执行到:
此时如果不调用write函数,readv函数将会睡眠等待至write函数发生。
如果此时通过heap overflow覆盖了iov_base为内核态地址,当write函数发生时,会向kernel_addr写入数据,而且此时并没有校验iov_base是否为用户态,前面已经校验过了。
四、漏洞利用
1、堆风水
如何使strcpy(event->name, file_name) 可以覆盖iov_base,向其写入kernel_addr。
首先我们看一下我们要布局的对象的大小。
iov = kmalloc(nr_segs*sizeof(struct iovec), GFP_KERNEL); //nr_seg需要大于8,我这里选择10,iov对象的大小是160个字节,从kmalloc-192中分配。如果要覆盖iov,下面的event,file_name就得选比较大的文件名,所以这个漏洞,不能通过短文件名触发。
event = kmalloc(alloc_len, GFP_KERNEL); //分配sizeof(struct inotify_event_info)+strlen(file_name)+1内存 ,44+144+1=189字节,从kmalloc-192中分配。
kfree_rcu(old_name, u.head) old_name也是144个字节,从kmalloc-192分配,这里是释放。
sendmsg堆喷,172个字节,从kmalloc-192分配。
为什么是172个字节?
sizeof(struct inotify_event_info) = 44个字节,event总的长度是192个字节,如果想覆盖iov,那么file_name被堆喷重新占位后的大小应该是192- 44 + 8(iov_addr的字节数)=156,由于file_name的值大于old_name的值16个字节,所以sendmsg应该堆喷156+16个= 172字节。
堆风水步骤:
1)首先起大量线程分配iov对象(通过readv函数,会阻塞线程,所以需要大量线程)。
2)每隔一个对象释放iov,形成空洞。
3)通过以下竞争,event落在空洞上,并且旁边就是另一个iov对象:
4)堆喷占位old_name,strcpy覆盖旁边iov对象iov_addr
2、向内核页表写入block
前面我们说了,向内核页表写入block的目的是在用户态通过0xffffffc200000000~0xffffffc240000000访问整个内核地址空间。
内核已有的映射:
虚拟地址 物理地址
0xffffffc000000000 0x40000000
0xffffffc060000000 0xA0000000
虚拟地址 物理地址
0xffffffc200000000 0x40000000
[峰会]看雪.第八届安全开发者峰会10月23日上海龙之梦大酒店举办!
最后于 2019-2-13 09:32
被jltxgcy编辑
,原因: