方家弘:大家下午好!我是来自腾讯安全科恩实验室的方家弘,我和我的同事申迪给大家带来关于“安卓内核 UAF 漏洞利用探秘”的分享。
今天的分享主要分成四个部分。首先由我介绍在之前的一段时间出现的比较有代表性的安卓内核漏洞。其次要分析像 UAF 这样一个特定类型的漏洞在利用方面有什么样的特点,它涉及到系统组件的基础知识,方便大家理解后续的内容。之后的时间交给申迪,他会具体分析在 perf 子系统中发现的 UAF 漏洞的具体利用过程。最后,分享一点我们的总结和思考。
首先介绍安卓内核漏洞的简要历史。
为什么要对安卓内核进行攻击呢?
将安卓系统看做整体,最早系统中以不同的应用,使用不同的 UID ,对应用之间进行隔离。后来发现这并不能阻挡攻击,又产生了加密文件系统等等安全措施。
而绝大多数措施的实现都依赖于安卓的内核。攻击内核本身是非常有助益的工作。在 Windows 系统当中,攻击内核可以直截了当的达到提权的目的。在安卓系统中,也可以绕过系统中设置的种种限制。对于坏人可以实现他不可告人的目的。
按照时间轴,根据我个人的理解列出了比较有代表性的内核漏洞。当然,不包括我们今天要讲的漏洞。
2013 年 11 月份,CVE-2013-6282 是我们的友商写出的最早的漏洞,它是逻辑问题。在我们去年攻击特斯拉的时候,惊讶地发现这个漏洞居然还存在。当时特斯拉比较自信的认为车辆不可能被攻破,所以他们在内核方面没有做更多的防御。在漏洞被报告,修复之后,特斯拉在内核方面做了非常激进的更新和修复。加粗的漏洞都属于通用的安卓 root 漏洞。
2014 年,美国的两个天才少年发现了 towelroot,这是引起比较大反响的通用 root。这个漏洞类型在当时较新颖,这个漏洞的利用方式也非常创新,引起了不小的波澜。
2015 年 5 月份,我们发现了 CVE-2015-3636。
2016年8月份,我们利用了 CVE-2015-1805。
之后就是非常出名的 Dirty COW,它也是属于逻辑的问题。
2017年4月份,由 Google 研究员发现的 wifi Firmware 的漏洞,这是非常有创新特色的漏洞利用。
之前提到的是几个有代表性的漏洞,6282 属于业务逻辑的问题;1805 是属于越界访问;3153、3636 都属于 UAF 漏洞。今天我们主要针对 UAF 漏洞进行研究和探讨。
顾名思义,UAF 漏洞先要 after Free,而要成功利用这个漏洞,有几个步骤是不能少掉的。一是如何按照顺序去触发 free 和 use。这看似比较简单,以 2015-3636 为例,use 的时机和 free 的时机非常可控,我们可以做任意想做的事情,比较好控制被 free 掉的空间。
3636 这个漏洞的品相非常好,但在很多情况下的控制环节上,可能会面临竞态的问题。在这种情况下,如何稳定的控制好 free 和 use 的顺序,并且稳定的触发 use,就是比较困难的问题。对于不同的漏洞来讲,需要不同的技巧去处理。我们已经能够把时序上的问题清楚,之后就是如何改变内核的控制力,如何控制代码执行。控制出来被 free 的空间方法,不同的漏洞,有不同的方法。
这里牵涉到最重要的就是 Linux 内核的内存管理。我们以图示的形式展示了 UAF 漏洞利用的方式。堆上有 ABCD 四个对象。这是一个比较简单的呈现方式,要做到能够稳定有效的 free 掉目标对象,并且把我们想要的东西填进去,需要对内核存储管理有比较深入的了解,也就牵涉到 Linux 内核的堆管理器。
在安卓系统当中,我们对常见的堆管理器叫 SLUB,它替换了之前常见的 SLAB。从管理结构上来讲,SLUB 是简化版的,U 就是 unqueued 的意思。queued 是队列,做堆管理器,总要有队列,申请的时候从这里拿。它取消了单独存在队列的结构,这样就使得完全空闲的 SLUB 被完全释放掉。它存在 per cpu 的 slab,它的释放和申请的过程会非常快。为了实现兼容性,Linux 的管理是抽象到 kmem 层。之前的内核代码、内核驱动,不需要做任何的修改。如果大家自己编译过 Linux 内核,只需要选择什么样的堆管理器就可以直接使用,其它代码都不用更改。
首先看一下 SLUB 的堆块长什么样子。Linux 内核当中物理内存的页面管理是通过 buddy 进行的。要符合物理页面管理的原则。在 SLUB 当中,都是二的 N 次方组成页组成的。它实际上是巧妙的利用了物理页面描述服的联合,实现了管理。我分配好给这个堆块的这些页,第一个页面的描述符上就会记录堆块中可使用的第一个对象。这就是空闲对象列表的头部。在空闲对象的头部,又会有一个指针,指到下一个空闲对象。
这个东西没有额外的元数据,所有的元数据只存在于原有的结构体当中,存在于你分配给这个堆块的页面当中。这也带来了一些特性,使得它可以帮助我们对特定的漏洞进行利用。
前面提到了 cpu_slub 的概念,分配和释放都是快速的过程。当前分配的对象在于 cpu 绑定 slub 上面,就会进入快速分配的流程。不管怎么样,对于使用堆管理器的用户来讲,肯定会得到空闲的 slub 对象供使用。具体怎么操作,就由 slub 的堆管理器进行。
为什么要设定 cpu_slub,大部分情况下在一个调度周期内会有频繁的对象分配操作。释放也是这样的情况,目前的对象就是隶属于当前的 cpu_slub,这就带来了另外一个非常好的特性。当前 cpu 上释放的对象,我马上要申请的,肯定申请到刚刚释放的对象,这对于填充是非常好的特性。这个特性在其它的漏洞利用当中也会使用到。同时,释放也存在 slow path,这是不可避免的情况。
这里对 SLUB 的特性进行了归纳。按照对象的大小会做一个合并,这会对漏洞利用带来一些问题,你可能不知道这样的堆块当中放的还有其他什么对象。
接下来看两个漏洞的案例,这两个案例充分利用了 slub 堆管理器的特性。
首先是 CVE-2015-1805。
iovec 是数据内核中传递数据的结构。这个漏洞本身是 overrun,牵涉到我们在内核当中如何申请可控的overrun 数组。在安卓当中,很多 API 是被禁用的。最终我们找到 sendmmsg 的调用,你可以得到内容完全可控的数组。它的坏处是放完以后就被销毁掉了。
这个对象本身的生命周期不够长。看似这不是很好的对象,实际上可以回想起之前的一点,在 slub 中一个对象被释放之后,仅仅是在对象的头部写入了指针,这个指针指向下一个可以使用的对象。
2015-1805 的代码路径当中,如果 iov 是 0,根本就不会被处理。如果说喷射的够快,漏洞利用过程够快,被释放掉的 iov 本身还是空的对象,或者又被另一个 iov 填上,根本不会对漏洞造成任何影响。我们只需要控制填进去的第一个 iov 的长度是 0,它就会被忽略掉。即便被释放,这个结果还是有效的 iov。
第二个漏洞,应该是 2016-6187。通过这个方法,把一个品相非常差的漏洞,越界写一个字符节的 0,变成可能导致代码执行的情况。最后也是通过 freelist 指针的特性。
接下来的时间交给申迪,由他分析一下在 perf 子系统中发现的漏洞利用。
申迪:下面给大家介绍我在去年发现的两个 perf 系统的 UAF。在安卓手机上有很多用户有一键 root 的需求。一些 UAF 漏洞的品相并不是特别好,但我至少要写到达到 90% 的成功率。我主要介绍两个在去年年初发现,年底报给 Google 的漏洞。
第一个是 CVE-2016-6787,它是通过 Race触发的漏洞,内核立即崩溃。上周我在 BlackHat 讲了绕过三星 KNOX 的防护机制。
第二个漏洞是 CVE-2017-0403,这是今年才修复的漏洞。这个漏洞有它自己的难点。
在讲漏洞之前,先讲一下 perf 是什么东西,它是系统调用,任何安卓都可以调这个系统调用,攻击内核。这个系统调用有很多问题,我也发现不止两个问题,这边两个是比较好利用的,单独拿出来讲一下。这个系统调用的参数比较复杂,其中一个是你怎么定义你想生成的统计事件,你想定义代码执行的指定周期,或者是真正调用进入内核的时候又调用了哪些调用,它包含了千奇百怪的 perf 事件,可能会进入到分支。
这个系统调用,不管中间发生了什么,最后总会生成 perf_event。每个都不是孤立的,有可能是一组形式出现的,每个 groun 都有一个 leader,每个进程里有多个组,又有大的容器包括这个组。每个进程里有两个 perf_event_context,把作为的 group leader 串联起来,这 list 把组内的串联起来,这三个内核对象有一个比较复杂的连接的关系,其实就是因为这些关系搞得很复杂,系统调用里出了一些 UAF 的问题。
Googl 在 2016 年下半年觉得这个系统调用太危险,直接封掉了。去年年初,这个调用还可以被其它的调用调到,我利用这两个漏洞完成了手机上的 root。
第一个漏洞的问题主要是由于系统调用里的 move_group。
如果 group leader 是软件的,你要插入一个硬件的,这个时候会挪动大量列表相连的关系。首先会把 group leader 从指针上提取出来,把每个 event 粘出来。这两个操作完成之后,又看到红框里的代码,减了 1,这个减 1 没有考虑到并发操作,把一个组摘除了两遍。正常情况下,只减 1,并发操作就要减 2,被多减了一个,引用减到 0,导致触发释放。
主线程先创建一个 group leader,线程 1 触发 move_group操作。右边出现了崩溃,实际是因为 perf_event context 被提前释放。你的进程在第二次进入调度的时候,进入调度器会调入一行,周期执行完了会换出,换出之后再被进程调度器换入。
这个时候就会触发操作。关键是线程调度是很频繁的,基本不是肉眼可观测的。漏洞一旦触发,内核立即就崩溃了。实际上它不是立即崩溃,实际上有一个微笑的操作,线程已经被换出,换入的时候又崩溃了。假如是一个立即的崩溃,对我来说就是毫无意义。这个时候就要集中解决一个问题,怎么让内核不立即崩溃,给我争取到足够的时间去喷堆。
在讲这个问题之前,要先讲一下 Linux 内核的调度知识,包括很多线程,每个线程不可能同时执行。真正执行的是一个,其它都是等待执行。它执行一段时间之后,会有几种情况。有一种情况就是以后再也不需要执行,它就会被杀掉。如果该想继续执行,配合的时间片已经被用尽了,这个时候就被线程调度器切换出去。这种时候就会进入到 interruptible 或者是 uninterruptible。
futex 这个调用可以帮助我们完成真正的睡眠。一旦线程进入睡眠的状态,是不会被任何人主动唤醒。这对我们来说是最理想的状态。
有了这个函数的帮助,整个调用逻辑就变成这样的,从左到右,从触发漏洞,到喷堆、代码执行控制完整的流程图。还是主线程创建 group leader,直接调 queue_me。线程1和线程2都同时触发 perf_event 的工作。调了 queue_me 之后,三个线程全部睡眠掉了。这个时候我需要再建另外一个新进程,帮助我完成喷堆。喷堆并不是很难,1024 的对象,你直接喷堆,直接喷物理页面,也是可以的。可能需要十几秒的时间,把堆喷满。这个时候我可以唤醒进程,或者直接杀掉进程,这都无所谓。线程调度器会调 sched_in。disable 可以指向你想控制的任何一个地址。你可以执行内核任意代码,把权限提到 root。
另外一个 2017-0403 漏洞。最后一条是被释放的对象,每个方块都可以代表 0X10 的字节,它没有任何指针,你怎么控制 PC 做代码执行呢?
free 之后你能锁 use 的部分就是红色的两个框,是 0X10 的单位,可以写两个指针的地址。A 是对象地址本身,第二个是 A+0X20 的位置。你可以用内核的其它对象去喷对,在 0X20 这个位置正好有一个 buffer,把它的地址覆盖成 A 地址本身,你真正写的是地址 A 本身,通过覆盖地址 A 本身的方法把 UAF写成堆溢出,再覆盖其它对象,这个时候就比较好写。如果不能控制 PC 指针,UAF 并不好写。
总结
演讲后,科恩实验室在接受雷锋网(公众号:雷锋网)采访时提到,Google 是非常重视安全的公司,在他们每月发的安全公告里面有很多安全漏洞,但绝大部分安全漏洞的利用场景非常困难,甚至是无法利用的,而其中真正威胁用户手机的漏洞是比较少的。
智能手机刚刚兴起时,众人对安全问题考虑不多。而随着攻击途径和攻击方向的增加,安全问题会被越来越重视。越来越多的人用新的方法去挖掘漏洞,其数量会变得越来越少。一旦发现新的攻击方法、新的漏洞模式,可能就会有一波漏洞攻击方法被爆发出来。随着慢慢被重视,攻击路径被堵住。
因此,面对越来越健壮的 Android 内核,攻击面急剧缩紧,只剩下一部分系统调用可供攻击。在这其中,品相好的漏洞相对于品相不好的漏洞并不多。
但科恩实验室的方家弘告诉雷锋网,一般来说只要是漏洞都需要被修补,事实上品相好的漏洞通常会更快修补,因为被利用的危险更大。这就是为什么优秀的安全研究团队,比如 project zero,常常输出“高质量”的漏洞,也就是能够输出漏洞和利用思路甚至是完整的利用代码。漏洞的品相好坏有时候也很难判断,除非写出具体的利用代码。
当然,这个其实不是安卓的特例。漏洞和很多东西一样,比如玉石原石类,都有一个“品相”。漏洞的品相取决于很多因素,比如漏洞所在的攻击面是否容易访问到、漏洞的触发是否容易和稳定,等等。所以有无法利用的漏洞是很正常的。
申迪也总结了面对无法利用的漏洞做法,首先要确定核心问题到底是什么,是不是真的不能利用,还是有其它办法。也可以借助内核中的其它特性,或者参考过往的案例,毕竟有些思想是共通的,可以帮助你有些想法。
即使最后真的发现无解,也可以丢开一段时间。他回忆起自己写 2014-0403 那个漏洞,也是丢开了 1、2个月,突然想到一个方法写出来的。
最后,不要盲目报告一些无法利用漏洞,没有人觉得你真的厉害。
本文作者:又田 本文转自雷锋网禁止二次转载, 原文链接