0x00 前言

近年来,随着内核缓解措施的不断引入,漏洞利用本身受到了极大的限制。CVE-2018-9568是阿里安全的ThomasKing在Zer0Con2019上提出成功利用的漏洞,随后百度安全实验室也披露了同一漏洞的利用方法。笔者所在的安全研究团队C0RE Team于2019年初也完成了漏洞的利用,其方法有别于ThomasKing和百度安全实验室,为了和大家共同学习,写本文来描述C0RE Team的利用思路。

0x01 漏洞描述

CVE-2018-9568于2017年在主线上修复(主线补丁)。代码提交日志上面有给出对应的描述和复现PoC:


图1-1:CVE-2018-9568描述


图1-2:CVE-2018-9568 PoC

按照提交日志上面给出的PoC进行复现,可以得到如下结果:


图1-3:CVE-2018-9568 触发结果

从字面意义上理解,漏洞最直接的结果是将tcp_sock对象作为tcp6_sock来释放,其中tcp_sock源于TCP kmem_cache而tcp6_sock源于TCPv6 kmem_cache。属于一个典型的Type Confusion问题。由于TCP和TCPv6的kmem_cache的对象大小不同,因而在释放时会有非预期的memory corruption发生,进而给攻击者留下了可利用的空间。

0x02 提权方案

从目前已经公开的资料看,阿里安全的安全研究员ThomasKing和百度安全实验室有放出对应的提权利用方案。

ThomasKing作为已知的首位实现提权利用的研究者,给漏洞名为为WrongZone,并于Zer0Con2019首次公开利用手法,利用本漏洞打造一个通用的提权方案,并按照内核缓解措施的演进分别提出对应的绕过技术,为内核缓解措施的绕过提供了一个完美的从入门到精通的案例教程。

百度安全实验室进行了进一步的分析,从自动适配的角度出发,目的是打造更加通用的root方案,提出了两种利用方式,一种是利用了slub的fragmentation将内核分配的TCP对象分配到用户态以达到控制内核对象的目的;另一种是类似ThomasKing的方式,构造两个对象共享同一块memory(类似PingPong Root)。

笔者所在的安全研究团队C0RE Team于2019年初对该漏洞进行了独立研究,并在Pixel2上基于镜像攻击的方法实现了提权。该方法的优点是逻辑相对更加简单,且采用的镜像攻击的手法也避免了Google的KCFI对传统的劫持PC利用方式的缓解。本文将对此种攻击利用手法进行描述。

0x03 背景知识

在进行具体提权利用的阐述前,需要对slub的一些相关背景知识做一下简要的介绍。关于slab在各种网站和书籍上面都有很详细的讲解,本文不做过多的赘述,只是针对提权利用所使用到的几个特性简单描述。

如下的图很清晰的展示了slab的各数据结构的关系:(图片出自这里,大图可以点这里


图3-1:slab数据结构图

其中值得注意的一点是,上图中oo的结构描述的是通用slab的结构(即我们通常所谓的kmalloc-128, kmalloc-256等等),而TCP和TCPv6分别有自己的kmem_cache,由于slab的flag中有SLAB_DESTROY_BY_RCU的标志,所以记录下一个object地址的位置并非与object本身共用且在object开头,而是紧跟object之后由单独的空间存放。


图3-2:TCP/TCPv6对象数据结构图

此外,关于per cpu partial list的部分也需要着重强调一下。

从object分配的角度看,分配的顺序依次是kmem_cache_cpu正在使用的slab,kmem_cache_cpu的per cpu partial list,kmem_cache_node。当kmem_cache_cpu的freelist为空时,会选择per cpu partial list中的slab拿来使用。而kmem_cache_cpu的freelist为空意味着之前一直在使用的slab已经变为满的状态,会断开和per cpu的绑定关系,如图3-1右下角的full slabs所示。

而当full slabs中有object free时,slab系统会依据被free的object所属的kmem_cache来将slab挂给对应kmem_cache的对应kmem_cache_cpu的per cpu partial list上面。

0x04 方案详述

基于前面一小节背景知识的学习,结合前面提到的漏洞结果,从slab的角度看,如果漏洞触发时目标object所属的slab是满slab,那么在将该原本是TCP的object按照TCPv6进行释放会导致slab被挂到TCPv6的per cpu partial list上面。而更进一步,不停的分配TCPv6的object总会有一次是在原本属于TCP的slab上面分配TCPv6的object。

总结一下,我们目前可以利用的信息主要有:

  1. TCP/TCPv6的object在不同时间点下可以占据同一块memory
  2. 从源代码的角度看,TCPv6的object包含了TCP的object对应的结构体

那么我们就可以利用TCPv6的object去修改对应于TCP的object的next pointer,这样我们就可以指定TCP的object所使用的memory的来源。如果我们将来源改为swapper的地址,那么TCP就会从swapper中分配memory,通过用户态对tcp_sock结构体进行设置,我们就可以向swapper中写入虚假的block descriptor,这样就完成了镜像攻击。

在具体描述上述利用步骤之前,我们还需要绕过KASLR的缓解措施。我们假想这样一个场景:

  1. 连续两个TCP的object都以TCPv6的方式进行释放
  2. 使用TCPv6的object来占据释放的memory,这样这两个TCPv6的object就会有重叠的部分:

图4-1:TCPv6的object重叠示意图

回归到源码层面,我们可以看到TCPv6的object,即tcp6_sock是由tcp_sock的结构体紧邻ipv6_pinfo的结构体组成:

struct tcp6_sock {
	struct tcp_sock	  tcp;
	/* ipv6_pinfo has to be the last member of tcp6_sock, see inet6_sk_generic */
	struct ipv6_pinfo inet6;
};

这就意味着重叠的部分分别是前面的tcp6_sock的ipv6_pinfo成员和原TCP的Obj-1的next pointer加上后面的tcp6_sock中的tcp_sock成员


图4-2:源码层面object重叠示意图

由于即使开启KALSR,地址的高24bit依然是0xffffff,所以我们想办法泄露低60bit即可。对相关的结构体成员进行查找,发现tcp_sock中的skc_net源于init_net的全局变量,且对应到ipv6_pinfo的地址的成员都可以通过用户态进行读取:


图4-3:skc_net重叠示意图

struct ipv6_pinfo {
	......
	int			mcast_oif;

	/* pktoption flags */
	union {
		struct {
			__u16	srcrt:1,
				osrcrt:1,
			        rxinfo:1,
			        rxoinfo:1,
				rxhlim:1,
				rxohlim:1,
				hopopts:1,
				ohopopts:1,
	......
};

图4-4:信息泄露

在绕过KASLR之后,我们就可以通过对发生问题的object所在的slub进行各种操作,以达到前面所说的修改TCP object的next pointer的目的,最终在swapper中分配新的object。

结合信息泄露和镜像攻击,整个利用的流程如下:

1 构造一个TCP object的slub,其中连续两个object(Obj-1和Obj-2)会被作为TCPv6释放


图4-5:初始构造slub

2 将第一步构造的slub变为满slub,此时slub被从per cpu partial list中摘除


图4-6:满slub

3 触发漏洞,先将Obj-2按照TCPv6进行释放,此时slub会被挂到TCPv6的per cpu partial list上面


图4-7:释放Obj-2

4 触发漏洞,将Obj-1按照TCPv6进行释放


图4-8:释放Obj-1

5 分配TCPv6的object来占据原本Obj-1和Obj-2,由于步骤3,4是先释放Obj-2后释放Obj-1,此时分配的顺序依然是先分配Obj-1所在的memory(此时已经是TCPv6的object了),然后是Obj-2所在的memory(此时已经是TCPv6的object了)。


图4-9:使用TCPv6重新分配Obj-1和Obj-2

可以看到此时重叠已经出现,Obj-2’v6已经覆盖了Obj-1’v6的部分内存。按照之前绕过KASLR的描述,我们可以通过对Obj-1’v6的部分内容进行读取并拼出运行时init_net的地址,进而计算出KASLR的offset。


图4-10:重叠示意图

6 由于步骤5再一次将slub中空余的两个object进行分配,此时slub再一次变为满slub并从TCPv6的per cpu partial list摘除。此时释放一个TCP object,slub会再一次挂给TCP的per cpu partial list。


图4-11:释放TCP object

7 将Obj-2’v6对应于原本Obj-2的next pointer的位置的内容改为swapper的地址。


图4-12:修改TCP next pointer内容

8 释放Obj-2’v6,并分配TCP object直到占据了原本Obj-2的位置。


图4-13:释放Obj-2'v6

9 此时如果再一次分配TCP object,将会从swapper中分配memory。


图4-14:从swapper中分配memory

10 通过TCP的相关操作来将fake block descriptor写入到swapper中,完成镜像攻击。


图4-15:镜像攻击

11 其他通用提权步骤,如patch kernel, patch selinux等等。

最后附上提权截图:


图4-16:提权示意图

在实作的过程中会遇到一些问题,比如如何确认捕获到next pointer为swapper地址的TCP object呢?笔者使用的方法是根据前面的object(即Obj-1’v6)来判断,由于Obj-1’v6的后半部分(ipv6_pinfo)与和后面的object的前半部分(tcp_sock)有重叠,那么先对Obj-1’v6的重叠区域中的某一个成员做赋值,在每次分配一个object时都查看该成员的值是否有变,如果有变则说明有分配到后面的object。

除此之外,还有一些其他问题,就留待有兴趣实作的读者来发现和克服。

0x05 总结

本文所使用的利用手法,其优点在于逻辑简单清晰,且利用镜像攻击可以避免Google KCFI的束缚,读者可以沿用本思路实现在Pixel3的提权。

目前内核漏洞的提权利用愈加难以实现,笔者在实作本漏洞利用时曾经认为本文使用的方法也许是唯一可行的方案,但是随着ThomasKing和百度安全实验室披露各自的利用细节,不由得让人赞叹方法的多样性。笔者由衷的感觉到,在漏洞利用的研究上,永远不要固步自封,不要给自己的头脑设限。正如乔布斯的经典名言:“Stay hungry. Stay foolish”。

最后,感谢ThomasKing和百度安全实验室对漏洞利用的详细阐述,C0RE Team也撰写本文,共同为其他研究者的提供学习的素材。也许正是这种基于分享的黑客精神,推动了后来的研究者不断前进。



Published

12 July 2019

Tags