Own your Android! Yet Another Universal Root(二)

利用

目标

直到现在我们已经看出这是一个典型的UAF漏洞并且一个位于用户空间迷途的文件描述符指向内核中的PING 套接字可以被攻击者获得。接下来我们要填充套接字对象,重新使用这个对象。之后我们可以执行内核中任意代码,最终完成Android设备的提权。

实际上,我们使用套接字对象的close函数。当close(sockfd)调用时,内核最终会进入如下代码

int inet_release(struct socket *sock)
{
	struct sock *sk = sock->sk;

	if (sk) 
	{
		long timeout;

		sock_rps_reset_flow(sk);
	
		ip_mc_drop_socket(sk);

		timeout = 0;
		if (sock_flag(sk, SOCK_LINGER) &&!(current->flags & PF_EXITING))
			timeout = sk->sk_lingertime;
		sock->sk = NULL;
		sk->sk_prot->close(sk, timeout);
	}
	return 0;
}

内核调用inet_release来释放内核中套接字对象相关的sockfd。并在函数底部调用sk->sk_port->close(sk, timeout)

实际上sk_portsk类型结构的一个成员,它指向一个确定的函数指针。而具体是什么函数取决于协议类型,包括TCPUDPPING等。

如果被释放的PING套接字对象sk重新填充我们可以完全控制的内容,那么sk_port就完全处于我们的控制之下。它可以被指定一个用户空间的虚拟地址。这个地址的指针sk->sk_port->close处于我们的控制下,如果PAN没有被应用到内核,我们最终可以控制内核环境的PC register。实际情况是,市场上大多流行的Android设备没有采用PAN,所以我们不需要考虑这一点。

通俗的讲,在我们的方案里有两个重要因素需要被考量。一个是被攻击利用的套接字对象稳定可靠的填充数据;另一个是重新填充的内容要完全被我们控制。

重新填充

在这个root发掘中最困难的事情是将我们需要的合适数据重写到被释放的套接字对象。并且为了用户root过程中的用户体验要确保整个进程稳定可靠。可靠精确的填充是我们工作的重头戏。

Linux内核的内存管理机制一般采用SLAB/SLUB Allocator,用来高效的管理内核对象。

不同的SLABs为内核不同的对象而创建,毫无疑问一个PING cache被创建作为我们攻击的PING套接字对象。这样的分离设计广泛存在于用户程序中,比如 Isolated Heap IEPartitionChrome等。当在内核中面对这些设计,攻击者利用A类型对象占据一块B类型对象的内存空间就显得不是那么容易。

另一个会带来不稳定性的因素在于Linux内核的多线程支持。上百个task同时运行在单个系统上是一件很普遍的事情。这些执行的task也会引起内核中对象的allocationde-allocation。这些都会最终影响到内核的堆布局。而一个可预测的堆布局对于UAF的重要不言而喻。

市场上的多数Android设备采用SLUB分配器,这对于我们来讲是一个好消息。如果攻击的目标对象比如PING套接字大小在5121024之间,那么填充就会变得很简单。因为SLUB Allocator倾向于将相似大小的对象放入到SLAB cache。这意味着,如果攻击的目标对象大小为512,那它有可能被放入同样大小的SLAB中。因此,这种环境下的对象将被完全控制。事实上,512大小的对象可以在用户程序中被穿件,一种方法就是sendmmsg。sendmmsg的执行期间  内核将使用kmalloc内核中分配一个缓冲区暂时存储传输数据包。这个数据包大小可以被我们自己指定,在这种情况下被设置为512。并且缓冲区的内容可以完全被我们控制因为这只是我们想通过sendmmsg传递的数据。



请注意,这是一个极好的在内核中填充UAF对象的方案。然而它有一个重要的限制:对象的大小要是SLABcommon-use。换句话说必须要是可以使用kmalloc分配的大小。例如在某些Android设备,PING sock对象的大小为576,这是512年和1024之间上面的解决方案是不再有效。

理论上使用kmalloc-size对象填充内核中的任意对象是可行的。它主要利用的是当一整个SLAB空闲,则这块空间可能被内存回收在将来再利用。给予这个,我们可以首先创建大量PING sock对象来占据没有存起其他内容的SLABs,然后尝试释放所有来触发UAF漏洞。几个被完全释放的SLABs就生成了。再然后通过sendmmsg分配一定数量的大小为512的缓冲区。极有可能这些缓冲区占据了之前存储的PING sock并填充了它。


然而这种方法很难控制,有巨大的不确定性。我们无法精确的知道哪个缓冲区占据了之前PING sockSLAB的存储空间,使得整个root开发不稳定可靠。

此外,PING sock的大小在不同设备中是不同的。如果我们需要一个普遍的解决方案,我们不应该依赖于这些设备上PING sock的大小。

到这儿,为了利用漏洞而填充被释放的PING sock对象的精巧技术将出现。注意,我们不关心关于PING sock在设备上的大小问题。这一次,我们不利用其它内核对象来完成填充工作,而是用physmap

Physmap第一次被提及到在《ret2dir: Rethinking kernel isolation》。它一大块内存的内核空间直接用户空间内存映射到内核空间,用于提升系统的性能

意思是我们可以反复调用mmap在用户空间填充大量数据,它们中大部分将直接出现在内核空间。我们的目的是利用用户空间的数据覆盖被释放的对象。有几个问题需要关注。

       

如图9所示,我们可以看到在内核空间中,physmapSLAB一般处于不同的位置。Physmap一般处于较高的地址,SLAB处于较低的地址。为了让它们在内核空间的中间部分碰撞,我们首先要创建大量对象,将内核分配器分配的起始地址擡高,增加physmapSLAB内存重用的可能性。这一步称作“lifting”。

在我们所利用的目标CVE-2015-3636,我们只需要用PING sock对象提升,因为在内核中易于分配(calling socket)和释放(calling close)。

如图10,提升之后,创建一定数量的PING sock,但这次它们是易于攻击的。因为先前的提升,它们在内核空间中处于高地址。

在之后的de-allocation,我们通常释放这些为了提升的PING sock对象。而对于攻击的目标对象,我们释放它们触发漏洞,即对一个PING sock两次connect

然后我们调用mmap并且在用户空间填充我们想要的数据到映射空间。这些数据8 dwords一组。每8 dwords重复。

一个大问题是我们什么时候停止填充,当我们的目标对象已经在physmap中被数据覆盖。为了解决这个问题,在我们的8 dwords中,除了一些key value用来控制flow和在最后避免内核冲突,在特定的入口填充预先设计的魔数magic value

每次我们填充完一定量的数据,我们调用ioctl(sockfd, SIOCGSTAMPNS, (struct timespec*))在这些目标对象。


如图11,读出sksk->sk_stomp。通过具体的参数调用ioctl,我们可以成功获取对象中固定偏移的一个dword值。我们比较魔数和这个值,就可以知道是否覆盖。这一步确定root的可靠性。

当我们已经精确地覆盖了目标对象,调用close来悬空对象的文件描述符。内核将最终调用sk->sk_port->close,此时sk_port将处在我们的控制之下,并且它指向完全由我们控制的位置,因此这个函数指针close就被获得。最终我们控制了内核环境中PC register的值。

注意,我们最终的填充方案不依赖与特定的配置或者Android设备的内核细节。

64位设备

我们的root方法同样适用于Android64位设备,基于以下两个原因:

A. LIST POISON2的值在Android64位设备中仍然是0x200200。在PC Linux上为0xdead000000000000,有64位长度而且超出了64位系统虚拟地址的范围。如果这个值不能被映射,当我们第二次connect时就无法避免碰撞。

B. 已证明64位系统中physmap也可以覆盖SLAB

ROOT

在控制了内核环境中的PC register之后,我们设计执行代码,获取root权限,这是我们最终的目标。最基本的方法重写当前taskaddr_limit值位0,从而任意的在内核空间进行读写。之后重写内核中权限结构提权。

对于那些没有PXN的设备,事情变得十分简单。我们只需要设置close函数指针为一个用户空间的虚拟地址,启动一块修改addr_limit值位0shellcode

对于那些有PXN的设备,ret2usr攻击没有什么作用。我们采用ROPRetrun-oriented Programmming)来达到我们的目的。为了设计一个可靠的方法,我们使用内核JOPJump-Oriented Programming)来重写当前taskaddr_limit值位0

1) Referred registers during JOP

JOP时,许多寄存器会更改它们的原始数据。事实上,如果我们不关心它们的原始数据,有极大的可能会丢失。在我们的方案里,我们调用close函数悬空套接字文件描述符,进入用户段的JOP链。在JOP链期间,我们只修改r0r5的值。改变其他寄存器的值都会在之后导致不可预知的内核崩溃。一些关键寄存器的值需要保存一致。

2) Keeping the value of SP

我们使用JOP取代ROP的最主要理由是在ROP通常需要我们调整栈。这些行为会导致在整个过程中SP寄存器的值未知。SP寄存器的值在整个过程中十分关键,在任何时间修改它都是不明智的。

3) Avoiding data corruption on the stack

我们要避免像这样的gadgets


寄存器x29通常保存SP的值在Linux64位系统中。这些gadgets更改了有关栈的数据,这会影响到未来的内核的执行流。这些行为会带来不确定性,应该避免。

4) Exploring core gadgets

我们的JOP链主要有两个task。第一个用于泄露SP值,我们可以从中获取当前tasktask_struct的地址。第二个task重写当前task_structaddr_limit

核心gadgets我们试图寻找的像这样:


泄露。寄存器x0的值应该是一个用户空间的虚拟地址,之后我们可以读取到寄存器x1的值,这应该就是SP的值。通过跳转到x2寄存器指向的正确的返回地址,从JOP中返回。x2的值同样是一个用户空间的地址。

重写。寄存器x1的值位0x0应该是一个和addr_limit地址相关的地址。之后返回到初始返回地址。

总结上面两步,泄露和重写。我们尝试从不同设备的启动映像寻找gadgets

5) Leaking tricks

A.64Android设备中,寄存器x29通常储存了SP的值,因此下面的指令可以获得栈的地址:


B.对于64位的设备,高32位内核虚拟地址通常保持一致。所以对于攻击者,泄露低32位地址通常已经足够:


6) Rewriting tricks

当提到市面上Android设备的ROMs,映像中存在的gadget我们可以利用它们来达到内核中任意写的目的

主要有两种选择:



结束语

在本文中,我们披露了CVE-2015-3636的细节和Keen Team如何利用它来达到对于市面上的大多数Android设备提权(4.3及以上)。我们应用在64位设备上root这是世界上已知的第一例。此外,通过应用JOP的技巧,PXN也可以被完全绕过。


译注:

关于内存分配技术SLAB\SLUBPhysmap

ROP,就是面向返回语句的编程方法


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章