CVE-2017-8890 深度分析
0x00 说在前面的
笔者作为初入安卓内核漏洞利用分析的从业者,自 CVE-2017-8890 这个漏洞被曝之后不久就开始研究其成因、原理、触发以及利用。至今遇到了无数问题,也解决了无数问题,于是记下这篇文章以做小结,一来对这段时间的工作学习做一个归纳总结,二来希望能对同样对安卓内核漏洞感兴趣的朋友有一些细节上的帮助。
0x01 原理分析
漏洞概述
+ 编号:CVE-2017-8890
+ 类型:double free
+ 位置:/net/ipv4/inet_connection_sock.c
+ 描述:
漏洞成因
分析一个漏洞的原理,最直接的方法是从补丁入手。
CVE-2017-8890 的补丁如下所示:
可以看到这个补丁非常简单,只添加了一行代码,作用是将 inet_sk(newsk)->mc_list 置为 NULL。再结合漏洞类型为 double free,很容易得知应该是释放流程中对 mc_list 这个结构体的处理不当,导致了这个漏洞产生。那么这里比较关心的问题如下:
+ 这个对象是什么?
+ 对象在哪里产生?
+ 对象在哪里释放?
+ 为什么会产生漏洞?
对象是什么
通过源码分析和搜索引擎可以知道,mc_list 这个对象代表的是组播列表。其原型如下:
既然这个结构体是组播列表,那么很容易得知,当创建组播,加入组播的时候,很可能就会创建这个结构体。
对象在哪里创建
通过前面的推测,加上内核源码分析,能够得出如下调用链,将会创建这个结构体。
用户态:
setsockopt(MCAST_JOIN_GROUP)
内核态:
SyS_setsockopt() -> sock_common_setsockopt() -> ip_setsockopt() -> do_ip_setsockopt() -> ip_mc_join_group()
对象在哪里释放
知道了对象,创建流程,通过源码分析也能够找到释放的调用链。
用户态:
close(sockfd)
内核态:
sock_release() -> inet_release() -> tcp_close() -> ip_mc_drop_socket()
PS:这里不得不说一下,我很佩服挖到这个漏洞的大佬,但是 【ADLab原创首发】“Phoenix Talon”in Linux Kernel —潜伏长达11年之久的内核漏洞 的两次对象释放写在不同的位置很误导人。
之所以能够看到两次不同的释放,是因为 RCU 系统的存在,ip_mc_drop_socket 这个函数导致释放操作,真正的释放在 __rcu_reclaim。软中断是由于时钟中断导致,都是 RCU 释放流程中的正常操作。两次释放都在同一个点,都是由于用户态 close 导致的。
为什么会产生漏洞
那么问题的关键来了,为什么会产生这个漏洞。
在搜集信息的过程中我们已经知道了这个漏洞是一个 double free,既然是 double free,那必然是一次创建,两次释放。但是从上面对象创建和释放的流程来看,并不能找到两次释放的点。
这个时候,漏洞描述中的一句话给出了分析方向:
It turns out that leave a copy of parent mc_list at accept() time.
在 accept 的时候,子对象会从父对象复制一份 mc_list。
但如果是复制的话,对象应该也会复制一份,那么对子对象和父对象都进行释放,应该是不会产生漏洞的。带着这样的疑问对源码进行了分析之后得到答案:
首先 mc_list 是 sock 对象的一个成员,并且是由 sock 中的指针所指向的对象
accept 的时候会把 sock 对象拷贝一份而不是 mc_list
由以上两点就能够解释漏洞为何产生了 —— 多个对象拥有了指向同一个对象的指针。
那么这个漏洞已经不是 double free 了,甚至可以是 X free,因为只要 accept 多次,那么就会有多个对象指向同一个 mc_list。
漏洞触发
从上面的分析中我们已经得到了几个触发漏洞的关键点:
+ 如何创建对象
+ 如何释放对象
+ 为何产生漏洞
那么 POC 的构建已经是轻而易举了,伪代码如下:
这里需要注意的是 accept 要对应 connect。
0x02 利用分析
因为利用涉及到太多的技术细节,所以这里主要讲思路,具体还是要自己去调试分析才能知道明白,要知道,只有在调试器下,才是没有秘密的。
思路分析
目前,我们拥有的条件如下:
+ 能够触发漏洞
+ 知道这个漏洞是 double free
目的如下:
+ 成功利用漏洞,达到任意地址读写
而漏洞利用就是在拥有的条件之下,不断想方设法提升当前权限,以达到最终目的的过程。
+ 创建漏洞结构体
+ 第一次释放漏洞结构体
+ 通过堆喷覆盖对漏洞结构体进行占位
+ 在用户空间中去使用已经 free 掉的对象,尝试通过某种方式获得在内核中执行代码的能力
那么这里比较关键的问题就是如何进行占位,以及如何在用户态通过这个漏洞获得在内核中执行代码的能力。
堆喷占位
堆喷这个手法,说得直白一点,就是不断地申请内存,由于 linux 的 slab 分配机制会把大小相同的块分配在一起,所以当操作量足够大的时候,随机分配的堆也会逐渐变得可以预测。
从 IDA 中可以得知测试机(Nexus 6P)中的 mc_list 大小为 0x30,那么堆喷的时候只要不断地申请大小为 0x30 的结构体即可。
这里我所用的对象是 ipv6 的 mc_list(一开始使用的方法是 sendmsg,不过这个方法由于内核的释放机制而相当不稳定),首先这个对象大小在测试机上是 0x30,其次因为一些关键的特性,让这个结构体成为非常理想的堆喷对象。
劫持 EIP
前面提到当我们成功进行了占位之后,需要通过某种方式来获得在内核中执行代码的能力,及劫持 EIP。通常我们的做法是用我们的地址去覆盖某个函数指针,然后触发这个指针,这样内核就会把这个地址当成对应的函数去解析,从而达到劫持 EIP 的目的。就可以直接执行 shellcode 了(当然 64 位机存在 pxn 所以需要通过 jop 来绕过)
这里深入分析 mc_list 这个结构体可以看到:
这个结构体的最后一个成员是一个函数指针,那么这个函数会在什么情况下被使用呢?
答案是在 kree_rcu 的释放流程中。
从前面的分析可以看到,最终释放 mc_list 的位置是 __rcu_reclaim,这个函数实现如下所示:
这个函数中首先对 head->func 进行了检测,如果小于 4096,则把这个作为一个便宜,将其释放掉,如果大于 4096,则当成一个函数地址来执行。
那么利用思路就比较清晰了:
+ 第一次释放
+ 覆盖,重点覆盖 head->func
+ 第二次释放,触发 head->func
通过这样的方式的确能够劫持 EIP,不过在实际的利用过程中,存在一个问题:在 64 位机上存在 PXN 防护,需要通过 jop 的方式来绕过这个防护机制,而 jop 需要能够控制至少一个寄存器,但 rcu_head 中的 next 是作为一个地址传过来的,并不能通过寄存器控制,所以在 64 位机上并不能直接通过这种方式完全利用达到提权的目的。
分析到这里的时候,思路一度陷入了僵局,最后从下面这篇文章中得到了解决思路:
CVE-2017-8890漏洞分析与利用(Root Android 7.x)
文中提出的思路如下:
+ mc_list 在内核中以链表形式存在,通过第一个成员指向下一个 mc_list
+ 当要释放一个 mc_list 的时候,rcu_head 会被链到一个释放链表中
+ 我们可以在用户态伪造一个 fake_mc_list,让被堆喷的对象的 next_rcu 指向用户态的 fake_mc_list
+ 通过判断 fake_mc_list.rcu-next 是否为空来确定是否成功的让 fake_mc_list 上链
通过上面的信息可以知道,我们堆喷的对象的前 8 位一定要是可控的,前面提到的 ipv6 的 mc_list 刚好满足这个条件。那么最终的利用思路如下:
+ 用户态创建结构 fake_mc_list
+ mmap(fake_mc_list)
+ 创建漏洞结构体
+ 第一次释放
+ 堆喷
+ 第二次释放,使 fake_mc_list 上链
+ fake_mc_list .rcu->func 为 JOP 地址
0x03 PXN 绕过
pxn 的绕过手法通常是 jop,这其实更算是体力活,所以不详述思路。
我们想要通过 jop 完成某样功能,那么就要先构思一条理想中的 jop,然后仿照这条 jop 在汇编中进行查找,通过正则的方式会比较高效。
举个例子,如果我们想要 patch 一个内核函数地址,假设为 ptmx_ioctl,当前可控寄存器为 x1,那么理想中的 jop 代码应该如下所示:
0x04 参考
CVE 2017 8890内幕
如果有什么需要交流的,可以联系 Yearthmain@outlook.com
本文由看雪论坛 Yearthmain 原创
转载请注明来自看雪社区
往期热门阅读:
点击阅读原文/read,
更多干货等着你~
扫描二维码关注我们,更多干货等你来拿!