首页
论坛
专栏
课程

[原创]CVE-2017-8890 深度分析

Yearthmain 2018-4-16 18:09 3809

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 参考

【ADLab原创首发】“Phoenix Talon”in Linux Kernel —潜伏长达11年之久的内核漏洞

CVE 2017 8890内幕

CVE-2017-8890漏洞分析与利用(Root Android 7.x)

如果有什么需要交流的,可以联系 Yearthmain@outlook.com



快讯:看雪智能设备漏洞挖掘公开课招生中!

最后于 2018-4-17 10:15 被Yearthmain编辑 ,原因: 修改代码
最新回复 (14)
肥皂yzh 2018-4-16 18:15
2
大佬带带我
ID蝴蝶 2018-4-16 21:01
3
顶起,其实可以细点的。
sakura零 2018-4-16 23:58
4
感谢
Yearthmain 2018-4-17 10:06
5
ID蝴蝶 顶起,其实可以细点的。[em_41]
利用确实一笔带过了==!涉及到太多技术细节不太好讲
心许雪 2018-4-17 10:08
6
感谢分析。
有两个问题想请教一下:
1. 存在一个问题:在 64 位机上存在 PXN 防护,需要通过 jop 的方式来绕过这个防护机制,而 jop 需要能够控制至少一个寄存器,但 rcu_head 中的 next 是作为一个地址传过来的,并不能通过寄存器控制,所以在 64 位机上并不能直接通过这种方式完全利用达到提权的目的。 
64位机器上需要通过jop来绕过PXN,jop需要至少控制一个寄存器。这个可以理解。但是这句话“但 rcu_head 中的 next 是作为一个地址传过来的,并不能通过寄存器控制” 是什么意思?博主似乎没有对__rcu_reclaim函数的说明,可否从源码的角度来补充说明一下?

2.当要释放一个 mc_list 的时候,rcu_head 会被链到一个释放链表中
释放mc_list的时候,在ip_mc_drop_socket中是调用 kfree_rcu 来释放的。 mc_list 中rcu_head的释放可以在 rcu_do_batch函数中的while循环中找到。 
但是这个rcu_head是如何链到一个链表里的呢?

补充:精华的地方比如jop,希望博主可以详细描述一下XD
最后于 2018-4-17 15:09 被心许雪编辑 ,原因:
Yearthmain 2018-4-17 11:45
7
心许雪 感谢分析。有两个问题想请教一下:1. 存在一个问题:在 64 位机上存在 PXN 防护,需要通过&nb ...
这个释放流程是由内核中的  rcu  系统实现的,在  ip_mc_drop_socket  中会调用  kfree_rcu,然后通过  rcu  的流程来调用接下来的一系列函数。kree_rcu  这里调用的直接是地址而不是一个指针所以这个值并不可控。上链的操作的话是在  call_rcu  。最后  jop  的话,主体思路首先覆盖一个常用的内核函数以达到永久劫持  EIP,然后泄露内核栈找到  addlimit  修改这个值让任意地址可以读写,然后就已经绕过了  pxn  了。
Yearthmain 2018-4-17 11:46
8
谢谢提出问题,很关键
sakura零 2018-4-17 14:00
9
Yearthmain 利用确实一笔带过了==!涉及到太多技术细节不太好讲
其实可以单开一贴把能讲的都写写,无需关心写的乱不乱。。可能你写一点我们就少摸很多坑。
Yearthmain 2018-4-17 14:02
10
sakura零 其实可以单开一贴把能讲的都写写,无需关心写的乱不乱。。可能你写一点我们就少摸很多坑。
后面再把利用细节整理整理吧,其实思路挺简单的
sakura零 2018-4-17 14:05
11
Yearthmain 后面再把利用细节整理整理吧,其实思路挺简单的
嗯嗯,感谢
XingLiu 2018-4-23 14:06
12
用ipv6  的  mc_list进行spray  在nexus机器上是可以的  但是其他机器  比如华为  魅族是不可以的  mc_list的大小不一样 slab分配的不一样
最后于 2018-4-23 14:09 被XingLiu编辑 ,原因:
Yearthmain 2018-5-4 17:21
13
XingLiu 用ipv6  的  mc_list进行spray  在nexus机器上是可以的  但是其他机器  比如华为&a ...
确实是这样的,并且  sendmsg  在这些手机上也会因为释放会把  slab  块连在链上导致无法触发漏洞后续流程
龙心之火 2018-5-6 01:20
14
参考第三个的链接好像错了,我猜貌似是这个链接:http://www.freebuf.com/articles/terminal/160041.html
wule 5天前
15
楼主的深度分析,感觉还没有那么深啊。控制func之后,做pxn的绕过,func的执行上下文可能根据内核配置、实际运行情况这些因素而不同,到底怎么达到任意地址读、写,这个还得做不少事吧?
返回