Brief
CVE-2019-2215
是有一个Android平台教科书级别的提权漏洞,Google Project Zero 针对该漏洞做了很详细的分析。如果手上有一台Pixel 2,刷入指定的4.4.177-g83bee1dc48e8
镜像,配合Jann Horn & Maddie Stone或是其他dalao们的exploit即可轻松复现;
可惜我手上并没有一台Pixel 2,甚至都没有可以刷入Android P的4.4内核手机,因此不得不按照CloudFuzz
团队的教程搭建了一套带有漏洞的模拟器环境,才完成了复现(这套环境可以直接在我的github上下载);
从分类上来讲,CVE-2019-2215
是一个UAF漏洞,与之前复现过的CVE-2015-3636
相似,差异点在于两个漏洞利用过程使用的内存读写原语(primitive)。 CVE-2015-3636
利用物理内存喷射完成了task_stuct的泄漏,利用socket的close函数配合ROP完成了任意地址写;而本次复现目标CVE-2019-2215
利用了Retme7在Code Blue 2017说明的利用方法,通过控制 vectored (scatter/gather) I/O的大小来完成申请到Binder未完全释放的内存;通过writev
完成读操作;通过revmsg
完成写操作;
由于本漏洞利用过程中的防护策略相似,因此在patch addr_limit
之后的流程于PingPong root基本相同:改写task_struct中的uid,关闭SELinux,释放root shell; 但是,这第二次的漏洞复现过程还是提升了一些对于细节上的理解;
漏洞成因分析 & 内核基础知识
PoC 分析
Google Project Zero提供了一个简单的PoC用于验证该漏洞:
1 |
|
整个Poc的流程清晰明了,可以概括为以下几个步骤:
打开
binder
驱动;创建一个
epoll
,并将binder fd
加入到epoll
中;调用
binder
驱动的BINDER_THREAD_EXIT
(释放掉binder thread结构)
该Poc运行在一般的内核上没有明显的效果,但是,在开启KASAN的内核上可以抓取到以下的日志:
1 | [ 153.766696] ================================================================== |
通过KASAN的内核日志,可以看到虽然通过BINDER_THREAD_EXIT
已经释放掉了内核中的binder_thread
结构,但是在epoll中仍然链接了一个关于binder_thread
的指针,因此,通过epoll其他的操作的时候即可触发UAF漏洞;
iovec 介绍
为了能让UAF利用成功,我们需要一个内核内的数据结构来劫持掉释放后的binder_thread
内存空间;这里利用到的结构为struct iovec
, 其定义如下:
1 | struct iovec |
这个结构体被用于Vectored I/O中,其功能就是将数据从单一的buffer写到多个buffer中;或是从多个buffer中读取数据到一个buffer中。这个功能很好的减轻了频繁的系统调用(syscall)导致的性能损失;
让我们偷一张图来展示其运作原理:
这张图生动的说明了一个问题,我们可以通过控制iovec的数量来控制被分配到内核内存空间的大小;例如:sizeof(binder_thread) = 408 and sizeof(iovec) = 16
因此,只需要25个iovec struct形成的数组就可以获取到被释放的binder_thread结构 (由于其都会命中kmalloc-512)
并且Vectored_I/O也提供了writev
以及recvmsg
来形成后续的读/写原语;
可以说,如果没有特殊场景的限制,struct iovec
都将是一个不错的用于劫持UAF的数据结构,更多利用思路可以直接看Retme7的slide “The Art of Exploiting Unconventional Use-after-free Bugs in Android Kernel”.
漏洞提权过程
Unlink 原语
通过Poc我们了解了epoll会持有binder_thread
中的某个指针,网上Google几篇分析blog顺着梳理下源码,即可确定这个指针为binder_thread->wait
;
当使用ioctl
调用EPOLL_CTL_DEL
的时候,顺一下源代码,可以看到会触发一次__list_del
1 | static inline void __list_del(struct list_head * prev, struct list_head * next) |
可以将binder_thread->wait.head
写入到binder_thread->wait.head.prev
以及binder_thread->wait.head.next
中;
看一下这两个指针在binder_thread
中的偏移情况
offset | binder_thread | iovecStack |
---|---|---|
0x00 | … | iovec[0].iov_base = 0x0000000000000000 |
0x08 | … | iovec[0].iov_len = 0x0000000000000000 |
… | … | … |
… | … | … |
0xA0 | wait.lock | iovec[10].iov_base = dummy_page |
0xA8 | wait.head.next | iovec[10].iov_len = PAGE_SIZE |
0xB0 | wait.head.prev | iovec[11].iov_base = 0x41414141 |
0xB8 | … | iovec[11].iov_len = PAGE_SIZE |
… | … | … |
Nice, 后续通过iovec[10]和iovec[11]即可完成读写;
在漏洞利用过程中仍然有一个需要注意的细节,即binder_thread + 0xA0
位置的wait.lock
这是一个自旋锁,因此,需要iovec[10].iov_base
的低位全部为0;解决的办法就是通过mmap申请一块内存来保证其低位全部为0
1 | dummy_page = mmap((void *) 0x100000000ul, PAGE_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); |
泄漏 task_struct
task_struct
结构的重要程度不必多说,而struct binder_thread
中恰好就存有task_struct
; 因此,从binder_thread->wait.head
开始泄漏数据,就可以直接获取到许多有价值的信息;
那么,如何来泄漏binder_thread + 0xa8
呢?让我们再偷一张经典图来展示利用流程
而经典图中有一处细节没有展示清楚,即struct iovec
利用起来的一点不便之处:这个结构的生命周期十分短暂
They are allocated by system calls when they are working with the buffers and immediately freed when they return to user mode.
因此,需要向pipe
文件描述符中进行读写,通过限制pipe
的大小,可以及时的阻塞writev/readv
调用;避免了UAF漏洞还未触发,读写操作就已经完成的尴尬;
1 | /** |
改写 Addr_limit
关于addr_limit
在内核漏洞利用场景中,通过改写addr_limit
达到任意地址读/写是一个重要的步骤;在学习和复现CVE-2015-3636
中专门梳理过关于addr_limit
的相关知识。但在本次复现中,又有了一些新的不同;
在CVE-2015-3636
中,通过一次巧妙的ROP实现了set_fs (KERNEL_DS)
将addr_limit
设置成了0xFFFFFFFFFFFFFFFF
,but now:
Vitaly Nikolenko (@vnik5287) pointed out that in arm64 there is a check in
do_page_fault
function which will crash the process if theaddr_limit
is set to0xFFFFFFFFFFFFFFFF
. I did all of my tests on x86_64 system so did not notice that in the beginning.
让我们来看一下do_page_fault
的代码
1 | static int __kprobes do_page_fault(unsigned long addr, unsigned int esr, |
这里检查了如果regs->orig_addr_limit == KERNEL_DS
, 则程序直接go die; 而由于几乎每一次的内存申请都会伴随着do_page_fault
, 我们很难避免在将addr_limit
设置为0xFFFFFFFFFFFFFFFF
后不再触发该函数;因此,大多数漏洞利用都将addr_limit
设置成了0xFFFFFFFFFFFFFFFE
, 在保证不会崩溃的前提下,最大化内存地址的读写范围;
任意地址写原语
现在我们需要想办法将该UAF漏洞转化成一个内核地址写的原语。我们仍然需要使用前面提到的unlink原语,它将binder_thread->wait.head(+0xA8)
,写入到了binder_thread->wait.head->next(+0xA8)
以及binder_thread->wait.head->prev(+0xB0)
中,用图表直观的展示一下:
offset | binder_thread | iovecStack |
---|---|---|
0x00 | … | iovec[0].iov_base = 0x0000000000000000 |
0x08 | … | iovec[0].iov_len = 0x0000000000000000 |
… | … | … |
… | … | … |
0xA0 | wait.lock | iovec[10].iov_base -> keep the spinlock |
0xA8 | wait.head.next-> clobber with +0xA8 | iovec[10].iov_len |
0xB0 | wait.head.prev-> clobber with +0xA8 | iovec[11].iov_base -> write to iovec[10].iov_len |
0xB8 | … | iovec[11].iov_len |
0xC0 | … | iovec[12].iov_base |
0xC8 | … | iovec[12].iov_len |
在unlink原语之后,写入 iovec[11]
的时候实际上会写入iovec[10].iov_len
,通过控制写入iovec[11]
的内容,可以控制后续所有iovec的写入位置 & 写入长度,这里我们尝试将iovec[12].iov_base
改写成addr_limit
的地址;
首先,设置原始传入的iovec_stack
1 | iovec_stack[IOVEC_OVERLAP_INDEX].iov_base = dummy_page; // keep the spinlock |
下面我们控制写入的数据内容:
1 | static uint64_t final_socket_data[] = { |
通过设置iovec[11].iov_len = 0x8 * 4
, 因此,final_socket_data
的前四个数据会被填充到iovec[10].iov_len
至iovec[12].iov_base
的位置,之后第五个数据会被尝试写入iovec[12].iov_base
指向的地址;
可以说这是一个相当有意思的任意地址写原语了,设置可以通过扩大iovec[11].iov_len
来做到触发一次UAF批量的地址写入想要的内容;
唯一的问题,与泄漏task_struct
相同的问题,需要在”写完”iovec[10]
之后能够产生一个阻塞,等待unlink触发之后,再继续写入内容。因此,需要一个socketpair
的recvmsg
来完成,通过设置MSG_WAITALL
flag来完成。
整体代码与泄漏task_struct
相似
1 | printf("[+] Setting up socket.\n"); |
改写cred struct & disable SELinux Enforcing
完成patch addr_limit
之后,我们就可以通过一组pipe fd
完成稳定的任意地址读写了;
通过泄漏出来的task_cred_kptr
(通过task_struct
推算得到),重写task_cred
,将当前进程的UID/GID改写成root
1 | printf("[+] reading cred pointer from task_struct.\n"); |
通过init_nsproxy_kptr
推算出kernel_base
,在根据/proc/kallsyms
中的符号表,计算出selinux_enforcing_kptr
的位置,将其写成0,做到disable selinux
1 | printf("[+] reading init_nsproxy pointer from task_struct.\n"); |
最后的最后,spawn an shell and enjoy your root !
后记
之于复现过程以外的一些启示,基本上就是Google P0 blog的读后感;
在文章的Hunting the BUG
的部分,介绍了P0团队如何通过有限的在野漏洞利用信息,快速定位到漏洞位置。在小米的工作的相似经历让我对这部分尤为感兴趣,因此我在首次读到这里的时候(复现漏洞之前),尝试通过文中的信息定位到漏洞位置,下面是当时我得出的一些结论:
It is a kernel privilege escalation using a use-after-free vulnerability, reachable from inside the Chrome sandbox.
- 这是一个内核UAF漏洞引发的内核提权,其与另一个Chrome漏洞组合完成完整的远程提权利用链;
It works on Pixel 1 and 2, but not Pixel 3 and 3a.
- 这可能是一个linux内核通用漏洞,在不同机型上patch引入不同导致的;可以关注一下内核的UAF patch是否有一些Pixel 3/3a引入的patch
It was patched in the Linux kernel >= 4.14 without a CVE
- 同上,本身pixel 1/2 与 pixel 3/3a 使用的就不是相同的内核版本;
CONFIG_DEBUG_LIST
breaks the primitive.- 看不懂
CONFIG_ARM64_UAO
hinders exploitation.- 看不懂 + 1
The vulnerability is exploitable in Chrome’s renderer processes under Android’s
isolated_app
SELinux domain.- 通用型很强,可以说任意app都可以利用该漏洞, 没有SELinux的限制也可以说明是通过一些“常用“系统调用完成的
The exploit requires little or no per-device customization.
- 说明是个好漏洞,通用型非常好了。极大可能出自内核的通用模块上,排除了由高通等厂商引入的可能;
对比下Maddie Stone给出的推论:
- “It is a kernel privilege escalation using a use-after-free vulnerability, accessible from inside the Chrome sandbox.”
We know that it’s a use-after-free in the kernel.
- “It works on Pixel 1 and 2, but not Pixel 3 and 3a.”
We can diff the Pixel 2 and Pixel 3 kernels looking for changes.
- “It was patched in the Linux kernel >= 4.14 without a CVE.”
The Pixel 3 is based on the Linux kernel 4.9 and doesn’t include the vulnerability, but the fix is not in the 4.9 Linux kernel, only 4.14.
- “CONFIG_DEBUG_LIST breaks the primitive.”
This was an extremely helpful tip. In the kernel, there are only two actions (three functions) whose behavior changes based on the CONFIG_DEBUG_LIST flag: adding (list_add) and deleting (list_del_entry and list_del) from a doubly linked list. Therefore, we could infer that the freed obj is a linked list and has an add or delete performed on it after the free occurs.
- “CONFIG_ARM64_UAO hinders exploitation.”
Likely means that the exploit is using the memory corruption to overwrite the address limit that is stored near the start of the task_struct. (It would normally be stored at the bottom of the stack on Linux <=4.9, but Android backported the change that moved it into task_struct to protect against stack overflows to older kernels.)
- The exploit requires little or no per-device customization.
We can assume the bug and its exploitation methodology are in the common kernel rather than in code that is often customized, like the framework.
- “A list of affected and unaffected devices and their versions.”
Whenever there was a candidate bug that seemed to fit all the requirements above, I then vetted it against the list of affected and unaffected devices.
可以说差距还是很明显的,特别是通过一些内核编译选项就能得出的有效结论直接能反馈到漏洞利用的一些细节操作;同时,反思对于第二点,可能是前期对于该漏洞的关注(毕竟这漏洞已经出了好多年了)让我的结论有些“开天眼”;总之,要继续提高从有限的漏洞细节中快速提取更多有效信息的敏锐性。