KARMA带你看攻防:WrongZone从利用到修复

Wesley13
• 阅读 1026

KARMA带你看攻防:WrongZone从利用到修复

内核是一个操作系统的核心所在,它的安全性直接影响着整个操作系统的安全性。Linux内核作为目前绝大多数 IoT设备的内核,其安全性直接影响着包括Android、Linux等多种平台的设备。一旦Linux内核被攻破,所有依赖于内核的安全机制都岌岌可危(比如加密、进程隔离、支付、指纹验证等)。纵观历史,从Towelroot[1]到PingPongRoot[2],再到DirtyCOW[3],这些Linux内核通用提权漏洞,每一个在当时都能够通杀大批量的Android设备,造成了极其可怕的安全影响。

就在今年4月,由研究员ThomasKing在今年的Zer0Con上公布了Linux内核漏洞CVE-2018-9568,即WrongZone的利用方法[4],他展示了如何利用WrongZone(CVE-2018-9568)打造一个通用Android root方案。在我们的进一步研究后发现,此漏洞能够进一步被做成更加通用的root方案,危害性更大。目前,我们已经发现了两种非常有效的利用方式,可以被证明能够对Android、Linux平台受影响的设备进行非常通用的提权攻击,其危害性不亚于Towelroot、PingPongRoot、DirtyCOW等漏洞。

即便WrongZone危害巨大,且其官方修复补丁[5]已经公开半年多,但是由于移动及IoT生态的碎片化问题,至今依然有约48%[6]的Android手机未修复此漏洞,这些设备随时都可能受到来自黑灰产的攻击。

除此以外,大量的Linux服务器同样也会受到此漏洞的影响。例如,攻击者渗透进企业内网后,可利用此漏洞取得对一台物理机的绝对控制,并以此为跳板,逐步控制整个内网;并且,现在很多服务器都使用docker容器发布应用,利用此漏洞可以做到从docker容器逃逸到宿主机,直接控制宿主机,由此发起对宿主机或其他容器的攻击。

漏洞的攻击利用可以帮助我们了解到漏洞的危害,提升我们自身乃至整个行业的安全意识,但这远远不够。只有攻击,但没有防御,设备安全无法得到保障。只有切实地将受影响设备中的漏洞进行修复,为更多的IoT设备赋予更强的安全能力,才能真正提升整个IoT设备领域的安全能力。为此,百度安全实验室在2016年发布了OASES KARMA漏洞热修复技术。依托KARMA漏洞热修复技术,我们开发了防御热补丁,可保护设备不受此漏洞影响。截至目前,KARMA漏洞热修复技术已经帮助了超过100万台设备在没有OTA****的情况下修复了此漏洞。

在本文中,我们将从漏洞的利用到修复,向大家展示一个更加完整的攻击与防守过程。

此前的WrongZone利用方法中,内核信息泄露条件较为苛刻,构造难度相对较大。该利用方法需要构造相应的jop链来泄露目标内核地址及构造内核读写,这使得最终的利用需要对不同设备做适配工作,影响通用性。我们发现的两种新的利用方法解决了上述利用中存在的难点,很大程度提升了利用的通用性。利用方法一是此前从未公开过的WrongZone利用方法。在方法一中,我们展示了一种新的堆漏洞利用方式,并且在不构造jop链的情况下完成了内核任意地址读的操作,这使得我们最终可以做到自动适配多种设备;利用方法二采用了与公开利用方法相似的思路,我们发现了更加简单高效的信息泄露方法,在不构造jop链的情况下完成了信息泄露;再结合利用方法一中提到的内核任意地址读构造方法,同样可以达到自动适配多种设备的目的。

KARMA带你看攻防:WrongZone从利用到修复

1.漏洞介绍

从Linux内核代码提交日志[5]中可以看到CVE-2018-9568的poc:

KARMA带你看攻防:WrongZone从利用到修复

此漏洞最直接的效果是:攻击者可以把一个来自kmem_cache TCP的tcp_sk object当成kmem_cache TCPv6的tcp6_sk object给释放了,这也是这个漏洞为什么被称为“WrongZone”的原因。由于这两个kmem_cache的object大小不同,通过精心构造,便可以实现内核提权。

以下,我们将被错误释放的tcp_sk object称为wrongzone sk。

KARMA带你看攻防:WrongZone从利用到修复

2.背景知识

在介绍提权内容前,先来介绍几个关键的技术点,以帮助我们更好地理解后续的利用过程。

2.1 slub allocator的工作流程

slub allocator类似于我们经常用到的malloc/free函数,主要用于分配管理内核中的小块内存。一块由2^n个连续物理页组成的一块内存称为slab。

KARMA带你看攻防:WrongZone从利用到修复

上图是一个由四个连续物理页组成的slab。一个slab中包含多个object:

KARMA带你看攻防:WrongZone从利用到修复

一般来讲,object无法填满slab的整个空间,总会留出一小块内存碎片,称为fragmentation。

为了将slab上的空闲object管理起来,slub allocator采用了一种很巧妙的方式:

KARMA带你看攻防:WrongZone从利用到修复

如图所示,因为空闲object的内存不被任何人使用,所以其内部可以存放一个next指针,用来指示下一个空闲object,这样就形成了一个空闲object链表。整个slab的相关信息存放在了slab的首个物理页的struct page结构中,struct page结构中的freelist指向了对应slab的首个空闲object。

根据可用object的数目,slab可以分为三类:

  • 全空:slab上没有在使用的object,所有object均为空闲object;

  • 半满:slab上既有在使用的object,也有空闲object;

  • 全满:slab上没有空闲object

kmem_cache是slub allocator的核心管理结构。其内部的cpu_slab成员是一个percpu类型的kmem_cache_cpu结构,每一个cpu都对应有一个kmem_cache_cpu。

KARMA带你看攻防:WrongZone从利用到修复

kmem_cache_cpu是为加快当前cpu分配对象专门设计的。其内部缓存了多个slab。slab可以分为两类,一类是由kmem_cache_cpu的page直接指向的slab,kmem_cache_cpu的freelist则直接指向这个slab里的空闲object;另一类slab则以链表的形式组织起来,形成partial链表。凡是存放在kmem_cache_cpu中的slab,都是仅给当前cpu分配对象使用,所以,这些slab都处于“冻结”状态。

除了kmem_cache_cpu外,还有一个部分是kmem_cache_node。这个部分同样缓存了一些slab,可以供所有cpu使用,并未被冻结。

KARMA带你看攻防:WrongZone从利用到修复

对象的分配

slub allocator分配一个对象的流程如下:

(1)若当前cpu的kmem_cache_cpu的freelist中有空闲对象,则将freelist头部的空闲对象出链,然后返回此空闲对象,这个执行路径为fast-path;否则,进行以下步骤,也就是slow-path;

(2)若当前cpu的kmem_cache_cpu的page指向的slab的freelist不为空,则将其freelist头部的空闲对象出链,作为返回值。然后将此slab的freelist赋值给kmem_cache_cpu的freelist;否则,执行下一步;

(3)若当前cpu的kmem_cache_cpu的partial中有slab,则将partial链头部的slab出链,kmem_cache_cpu的page将指向此slab,此slab的freelist头部object出链,作为返回值,然后将此slab的freelist赋值给kmem_cache_cpu的freelist;否则,进行下一步;

(4)若kmem_cache的kmem_cache_node中有slab,则将其头部slab出链,kmem_cache_cpu的page将指向此slab,此slab的freelist头部object出链,作为返回值。然后将此slab的freelist赋值给kmem_cache_cpu的freelist,最后从kmem_cache_node中取出足够的slab,放入kmem_cache_cpu的partial链表里进行管理,以加快下次分配对象的速度;否则,进行下一步;

(5)kmem_cache中没有缓存的空闲object,通过伙伴分配系统分配一个新的slab,完成初始化后,kmem_cache_cpu的page将指向此slab,此slab的freelist头部object出链,作为返回值。然后将此slab的freelist赋值给kmem_cache_cpu的freelist。

对象的释放

处于不同位置的object,其释放过程是不一样的。可以分成以下几种情况:

(1)待释放的object处于当前cpu的kmem_cache_cpu的freelist所在的slab上,则直接将object链入kmem_cache_cpu的freelist上,此执行路径为fast-path。(以下情况均为slow-path)

(2)待释放的object的slab处于当前cpu的kmem_cache_cpu的partial链表或者处于其他cpu的kmem_cache_cpu中(包括freelist所在slab和partial链表),此slab处于被冻结状态,将此object入链到此slab的freelist中,同时更新此slab的struct page结构中的freelist。

(3)待释放的object的slab处于kmem_cache_node的partial链表中,此时的slab有两种情况:

1)释放了object之后,slab依然是一个半满的slab,将object链入slab的freelist中,更新此slab的struct page结构中的freelist;

2)释放了object之后,slab就会变为一个全空的slab。此时,除了将object链入slab的freelist中以外,还需要检查当前kmem_cache_node的partial链表中的slab数目是否超过规定值,若超过,则将此slab从kmem_cache_node的partial链表中移除,然后将此slab交由伙伴分配系统进行回收。

(4)待释放的object的slab是一个未冻结的全满slab,这样的全满slab并不被kmem_cache_cpu或者kmem_cache_node管理,释放其上面的object时,object入链到slab的freelist上,同时更新此slab的struct page结构中的freelist,此后这个slab就会成为一个半满的slab,此slab会被链入到当前cpu的kmem_cache_cpu的partial链表中。在链入到kmem_cache_cpu的partial链表前,会根据当前partial链表中包含的空闲对象的数目做不同处理:

1)若partial链表中包含的空闲对象的数目未超过一个合理值,则直接将slab链入到partial链表中;

2)若partial链表中包含的空闲对象的数目超过了一个合理值,则需要将partial链表中的所有的slab解冻,将所有半满的slab转移到kmem_cache_node中进行管理,将所有的全空slab交由伙伴分配系统进行回收。最后,将此slab链入到kmem_cache_cpu的partial链表中。

2.2 kmem_cache TCP、kmem_cache TCPv6的slab大小及object分布

根据我们的统计和调试,绝大多数android设备上的TCP slab和TCPv6 slab大小都是8个page:(后文均以此大小为例进行说明)

KARMA带你看攻防:WrongZone从利用到修复

因为kmem_cache TCP和kmem_cache TCPv6在创建时的slab_flags均为SLAB_DESTROY_BY_RCU,

KARMA带你看攻防:WrongZone从利用到修复

这样就会使TCP slab和TCPv6的slab对应的object大小相应的增加一个指针大小,而freelist的next指针会放在object的最末尾(不考虑对齐等情况),如下图:

KARMA带你看攻防:WrongZone从利用到修复

actual_use部分是在分配tcp_sock对象或者tcp6_sock对象时,对象实际占用的内存,next指针并不包含在内。next指针的修改是由slub allocator的内部逻辑完成,与socket相关操作并无关系。

一个刚刚分配好的TCP slab上的object分布如下,可以看到freelist的next指针在每个object的结尾部分。

KARMA带你看攻防:WrongZone从利用到修复

TCPv6 slab上的object size要比TCP slab的object size要大,其分布如下:

KARMA带你看攻防:WrongZone从利用到修复

2.3 kmem_cache TCP和kmem_cache TCPv6的slab分配

每当kmem_cache TCP和kmem_cache TCPv6 上没有缓存的空闲内存用于分配对象时,就需要分配新的slab,然后从这个新的slab分配对象。每个slab都通过伙伴分配系统分配,对于8 page大小的slab,其分配接口为:

alloc_pages(flags, order);

调用时的参数order为3, flags是GFP_KERNEL结合其他的一些Action modifiers。这样的传入参数最终会从伙伴分配系统中一个order等于3的页块链表里面获取到一个8 page大小的内存块。

对于分配好的TCP slab和TCPv6 slab,slub allocator一般并不会对其进行整个页面的初始化,只会把freelist链表进行一个初始化。同样,在TCP slab和TCPv6 slab在被伙伴分配系统回收时并不会对其内存做特殊的处理。因此,一块slab的fragmentation部分是一段二进制内容,可以通过某些手段进行控制。

KARMA带你看攻防:WrongZone从利用到修复

3.利用方法一


利用方法一是一种新的WrongZone利用方法。本方法中,我们采用了一种新的堆漏洞利用思路,将WrongZone转化为内核越界读,由此展开提权。并且在不构造jop链的情况下完成了内核任意地址读的操作,这使得我们可以做到自动适配多种设备,让最终的exploit有较强的通用性。

3.1 提权思路

可以思考一个场景:

当wrongzone sk在被释放前,它所在的TCP  slab处于全满状态(且不在kmem_cache_cpu中),此时会发生什么?

kmem_cache_free的slow-path中,内核除了会将要释放的object链入到对应的slab的freelist链表中,还会将刚刚释放了一个object的全满的slab链入到kmem_cache_cpu的partial链表中。

在上述场景下,wrongzone sk被释放时,它对应的内存块会正常被链入到所在的TCP slab的freelist中,除此以外,此TCP slab将会被链入TCPv6的kmem_cache_cpu的partial链表中,如下图所示,我们将此包含wrongzone sk的TCP slab 称为Evil TCP slab:

KARMA带你看攻防:WrongZone从利用到修复

如果在wrongzone sk被释放,Evil TCPslab被链入TCPv6 kmem_cache_cpu的partial链表后,立即分配大量的tcp6_sock对象,就会导致必然有tcp6_sock对象是从Evil TCPslab上分配的。理论上只会分配一个,因为释放wrongzone sk后,Evil TCP slab上面的freelist TCPv6 的next指针被置为NULL了。

当然,仅仅从Evil TCP slab上分配到一个tcp6_sock对象,这个操作并没有什么用。但是,如果先进行如下操作之后再来分配大量的tcp6_sock对象,那就会出现意想不到的效果。

(1)如图所示,依次按先后顺序释放A1,A2,A3…...A12(实际数目需要看具体设备,此处12个对象仅为演示说明),期间保留wrongzone sk后面的这个follow sk不释放。

KARMA带你看攻防:WrongZone从利用到修复

因为A1,A2,……A12这些对象都是正常的tcp_sock对象,他们所在的Evil TCP slab处于半满状态(wrongzone sk被释放后,而follow sk不被释放),所以,释放这些对象中的任意一个都仅仅是更新Evil TCP slab的freelist链表。释放完A1,A2,……A12的情景如下图所示:

KARMA带你看攻防:WrongZone从利用到修复

可以看到,在释放完成后,Evil TCP slab的freelist指向了Evil TCP slab最后一个 object,需要注意的是,这个object是tcp_sock对象大小。

(2)此时再来分配大量的tcp6_sock对象。分配期间必然会出现:kmem_cache_cpu上的freelist对应的slab内存空间被耗尽,Evil TCP slab就会从kmem_cache_cpu的partial链表里被移出,成为kmem_cache_cpu的freelist对应的slab,此时再分配一个tcp6_sock对象,就会出现一个“神奇的现象”。读者可以先思考一下这个“神奇的现象”是什么。具体实践时可以看到如下的崩溃:(我们将fragmentation部分内存通过“堆喷”的方式全部喷成了0xdeadbeafdeadbeaf)

KARMA带你看攻防:WrongZone从利用到修复

3.2提权流程

(1)准备evil_mem

准备一块PAGE_SIZE大小的用户态可读写内存evil_mem,其地址为evil_mem_addr,用于后续步骤中分配tcp6_sock对象。需要对evil_mem做如下操作:

1)将evil_mem整个内存初始化为0,此操作目的是为了将后续步骤中的evil_tcp6_sk后面的next指针置为NULL,这样可以让slub allocator继续回到正常的对象分配流程中去;

2)在evil_mem最前面写一个值作为标记,方便后续检查evil_tcp6_sk是否已经在evil_mem上成功分配;

(2)针对page allocator进行“堆喷”

为了能够在内核越界读发生时,将freelist指向我们设定好的用户态地址evil_mem_addr上,得尽可能保证Evil TCP slab的fragmentation全部都填充好evil_mem_addr。

所有的slab最开始都来自于伙伴分配系统。就TCP slab而言,大概率都是来自于从伙伴分配系统中order=3的一个页块链表里面。所以,只要我们找到类似的内核接口,能够不断地通过伙伴分配系统分配同样类型的页块,并且向分配好的页块中全部填充evil_mem_addr,理想状况下,最终能够达到的一个情形是:此order等于3的页块链表里面每一个页块上都充满了evil_mem_addr,如下图:

KARMA带你看攻防:WrongZone从利用到修复

在常见的堆喷函数中,setxattr系统调用可以满足上述要求。

int  setxattr(const char *path, const char *name,

const void *value, size_t size, int flags);

从setxattr的实现中可以看到:

KARMA带你看攻防:WrongZone从利用到修复

先用kmalloc分配了size大小的内存,然后再将用户态的内存value拷贝过去。从kmalloc的内部实现来看,当要分配的内存大小超过2 页时,最终也是调用alloc_pages接口从伙伴分配系统分配内存。

具体“堆喷”时,我们只需要将value这个buf中的内容全部覆盖为evil_mem_addr,size定为8 页,并不断的调用setxattr即可。需要注意的是,并不需要成功调用setxattr,我们只需要完成kmalloc+copy_from_user的操作让用户态的数据成功“堆喷”到分配的8页内存上去就可以了。

可以采取多线程同时堆喷,适当延长“堆喷”时间的方法来提升“堆喷”效果。

(3)绑定cpu

因为我们的利用思路大部分都是针对kmem_cache_cpu而言的,所以将利用进程绑定到某个cpu会提升利用成功率。

(4)创建用户态tcp6_sock及evil socket

1)耗尽kmem_cache TCP中所有的空闲object

此操作的目的是为了之后在分配tcp_sock的时候,必然是从一个刚刚由伙伴分配系统分配的新slab中分配的。因为只有新的slab上面才会有我们事先“堆喷”好的数据。

具体的操作就是创建较多的tcp socket。

2)heap shaping

按顺序执行一下操作,完成heap shaping:

(假设一个全空TCP slab上有t个object)

  • 创建t个tcp socket,即创建t个tcp_sock对象, 我们将这t个tcp_sock对象称为defrag sk,分别编号为defrag_sk_0, defrag_sk_1, ……defrag_sk_t-1;

  • 创建一个wrongzone sk;

  • 创建一个tcp socket,也就是创建一个tcp_sock对象,称为follow sk;

  • 创建t个tcp socket,即创建t个tcp_sock对象,我们将这t个tcp sock对象称为fill sk, 分别编号为fill_sk_0,fill_sk_1, ……fill_sk_t-1;

完成上述步骤后,有很大概率我们可以获取到这样的一个Evil TCP slab:

KARMA带你看攻防:WrongZone从利用到修复

EvilTCP slab此时处于全满状态,wrongzone sk被夹在中间,后面紧跟着follow sk,而defrag sk在Evil TCP slab的上面,fill sk在Evil TCP slab的下面。继续操作:

  • 释放wrongzone sk;

  • 依次释放defrag_sk_0, defrag_sk_1, defrag_sk_2, defrag_sk_3…….defrag_sk_t-1;

  • 依次释放fill_sk_0, fill_sk_1, fill_sk_2, fill_sk_3…… fill_sk_t-1;

完成上述步骤后,我们就可以得到这样一个情形:

KARMA带你看攻防:WrongZone从利用到修复

从上图中可以看到,Evil TCP slab在TCPv6 kmem_cache_cpu的partial链上,并且,其freelist指向最后一个object。

3)循环创建tcpv6 socket,每创建一次,都检查一下evil_mem头部的mark是否被修改。如果发现被修改,就说明我们成功的在evil_mem上创建了一个tcp6_sock对象,记录下此时的tcpv6 socket fd,为evil socket,对应的tcp6_sock对象为evil_tcp6_sk。

为什么会在evil_mem上创建一个tcp6_sock对象?

因为在循环创建tcpv6 socket的过程中,Evil TCPslab必然会被转移到TCPv6 kmem_cache_cpu的freelist所在的slab,并从中分配tcp6_sock对象。从Evil TCPslab上分配第一个tcp6_sock后,freelist更新时,就会出现越界读

KARMA带你看攻防:WrongZone从利用到修复

直接效果就是将freelist指向了一个我们指定的用户态内存,接着再分配一个tcp6_sock对象,自然就分配到了我们控制的用户态内存。

至此,就获取到了一个tcp6_sock完全可以被我们控制的evilsocket。

(5)构造内核任意地址读,打造自动适配exploit

获取到tcp6_sock完全被控制的evilsocket之后,下一步就是利用其进行提权。

一般的提权思路是修改tcp6_sock中的sk_prot成员,将其指向一个已经构造好的proto结构evil_proto:

KARMA带你看攻防:WrongZone从利用到修复

将evil_proto内部所有函数指针都覆盖为kernel_sock_ioctl的地址。通过对evil socket调用socket系统调用,如setsockopt函数,就可以成功执行到kernel_sock_ioctl中。设置好evil_tcp6_sk上某些offset上的值,让kernel_sock_ioctl将当前进程的addrlimit修改为-1,此后就可以利用一些常规手段构造出完整的内核任意地址读写,最后修改当前进程的uid等,完成提权。

上述思路是常见的提权思路。但是因为kernel_sock_ioctl的地址以及提权时涉及到的一些结构的offset在不同的设备上存在差异,使得最终的利用通用性并不高,需要对不同的设备做适配(需要对内核地址、结构偏移等等做硬编码)。为了做到让最终的利用能够自动适配多种设备,我们需要做更多的事情。

一般来说,获取内核任意地址读的方法是先patch addrlimit,然后通过某些系统调用将其构造出来。这个方法更多的是从代码复用的角度来思考的。抛开这个思路,针对CVE-2018-9568,我们可以更多地从数据结构的角度来思考这个问题:

既然已经可以完全控制evil_tcp6_sk,如果我们可以*evil_tcp6_sk*上找到某些指针成员(****或者多层嵌套的指针),并且用户态可以通过socket相关系统调用对此指针成员指向的结构进行二进制层面的读操作,这样就可以通过不断修改此指针的值,完成内核任意地址读了**。此模式的伪代码如下所示:

struct tcp6_sock {

……

struct X *p;

……

}

struct X {

……

long val;

……

}

long syscall_x(……) {

long val;

struct X *pointer  = tcp6_sock->p;

val =  pointer->val;

return val;

}

从伪代码上看,因为我们可以控制tcp6_sock结构上的所有值,所以可以不断修改指针p,只要把p的值改为我们想要读取的内核地址附近,就可以通过不断调用syscall_x读取到对应内核地址附近的值了。

按照上述内核任意地址读的构造方式,我们可以在系统调用getsockopt中找到如下相关代码:

KARMA带你看攻防:WrongZone从利用到修复

此处icsk指向的地址就是evil_tcp6_sk所在地址,icsk_ca_ops指针可以被完全控制,而name会被拷贝到optval中,返回到用户态,整个代码逻辑和上述模式一致。套用上述构造方式,我们就可以完成任意内核地址读的构造。不过,还需要解决另一个问题:如何知道icsk_ca_ops在evil_tcp6_sock上的offset?

icsk_ca_ops本身是可以用setsockopt进行设置的,可以通过对比设置前后evil_tcp6_sk内存块的变化从而得知icsk_ca_ops在evil_tcp6_sock上的offset。设置icsk_ca_ops的代码如下:

if (setsockopt(evil_socket, SOL_TCP,  TCP_CONGESTION, “reno”, strlen(“reno”) + 1) < 0) {

perror(“failed to setsockopt TCP_CONGESTION”);

}

至此,我们在不patch addrlimit的情况下就可以做到任意内核地址读。后续步骤还是按照惯例,从内存中将整个内核读取出来,然后解析符号表就可以得到kernel_sock_ioctl地址,利用中所需要的offset等等都可以从中解析出来,甚至可以动态查找jop链。解决了符号地址、offset等问题之后,再利用常规的提权方式进行提权,这样就完成了自动适配exploit。

(6)后续修复工作

因为evil socket的evil_tcp6_sk是从用户态分配的,直接对其进行释放会引发内核崩溃,所以可以直接把evil socket的sk成员写为NULL,避免其释放。

3.3 提权效果

如下图示:

KARMA带你看攻防:WrongZone从利用到修复

KARMA带你看攻防:WrongZone从利用到修复

4.利用方法二


利用方法二采用了与公开利用方法相似的思路,大体思路是将WrongZone转化为UAF的形式进行利用。在本方法中,我们发现了更加简单高效的信息泄露方法,在不构造jop链的情况下完成了信息泄露;再结合方法一中提到的内核任意地址读构造方法,同样可以达到自动适配多种设备的目的。

4.1 提权思路

首先,我们需要明确一个分配细节:当分配一个tcp_sock对象时,tcp_sock对象后面挨着的next指针并不被修改,slub allocator只是读取此next指针的值,赋给freelist。整个过程只是一个简单的对象出链操作。

大概率来讲,wrongzone sk被释放时,所在slab处于半满状态,如下图所示,wrongzone sk未被释放时的情况:

KARMA带你看攻防:WrongZone从利用到修复

可以看到,wrongzone sk后面紧跟的next指针依然指向了sk_3。

wrongzone sk被释放后的情况如下:

KARMA带你看攻防:WrongZone从利用到修复

可以看到,因为wrongzone sk被当成tcp6_sock释放,导致其后面的next指针并没有被正确地赋值,而是越界写到了sk_3的内存区域。而wrongzone sk后面的next指针依然指向sk_3内存,这样就产生了一个神奇的效果:sk_3所在的内存虽然是出于被占用状态,但却被链入了freelist链中。如果再从此slab上分配很多tcp_sock对象,就会出现sk_3内存又被其他的socket使用。最终效果是:有两个socket共享了同一个tcp_sock。我们把另一个共享了sk_3的socket成为dup_socket。

基于上面的效果,若将此slab上对应的所有socket都close,这样,此slab变为全空状态,但保留dup_socket不close。全空slab在满足某些条件下会被回收,交还给伙伴分配系统。但是,此时dup_socket的sk成员依然指向被回收的slab上,这样就构造出了一个UAF的场景。

后续的利用思路和以前出现过的PingPongRoot思路相似,但是因为内核防护手段的提升,所以,仍有很多的难点需要解决。

4.2 提权流程

假设一个全空的TCP slab上有t个空闲object。

(1)创建大量tcp socket

此处创建大量的socket的主要目的是将kmem_cache TCP上面的所有缓存object都消耗完,尽量让后续步骤中分配tcp_sock时都会从一个刚刚新分配的slab上分配。具体原因见后续步骤;

此部分socket称为run_out_socket.

(2)构造两个dup socket

(为什么要构造两个dup socket?后续部分进行解释)

1)创建一个wrongzone sk;

2)创建3个tcp socket,称为follow_socket;

3)创建t个tcp sockt;

此处创建t个tcp socket的目的是将wrongzone sk所在的slab填满,让其变为全满状态。此部分socket称为fill_socket.

完成此步骤之后,可以看到下图的情况:

KARMA带你看攻防:WrongZone从利用到修复

kmem_cache TCP中有一个处于全满状态的EvilTCP slab。

因为前面(1)中的操作,保证了Evil TCP slab被填满之前是一个刚刚从伙伴分配系统分配出来的新的slab。一个新的slab刚刚被初始化后的情况如下图所示:

KARMA带你看攻防:WrongZone从利用到修复

freelist链表内的object链接顺序和object的地址前后保持一致,这个特性保证了Evil TCP slab的内部细节必然与下图保持一致:

KARMA带你看攻防:WrongZone从利用到修复

4)释放follow_sk_2

释放后Evil TCP slab的细节如下图:

KARMA带你看攻防:WrongZone从利用到修复

5)释放wrongzone sk

释放后Evil TCP slab的细节如下图:

KARMA带你看攻防:WrongZone从利用到修复

可以看到,此时构造了一个有四个“空闲”object的freelist链。

6)分配大量的tcp socket

此处分配大量的tcp_socket的目的有两个:一是保证在分配tcp socket的过程中一定可以将EvilTCP slab中的四个“空闲”object用完;二是为了保证后续必然可以将全空的Evil TCP slab回收。

此部分socket称为make_dup_socket。

分配结束后,可以肯定的是:make_dup_socket里面必然有两个socket,这两个socket分别和follow_socket_0,follow_socket_1共用了tcp_sock结构,这两个socket称为dup_socket。

如何从众多make_dup_socket中找出两个dup_socket?方法比较灵活,可以通过分别给follow_socket_0和follow_socket_1做标记,然后遍历整个make_dup_socket,如果发现相同标记,则就找到了对应的dup_socket。在此我们使用了setsockopt中的SO_REVBUF选项来做标记。

通过做标记的方法,最终就可以找到follow_socket_0对应的dup_socket_0,follow_socket_1对应的dup_socket_1。

(3)回收Evil TCPslab

按步骤进行如下操作:

1)按顺序close所有run_out_socket;

2)依次closedup_socket_0, dup_socket_1;

3)按顺序close所有fill_socket;

完成此步骤后,Evil TCP slab就已经是全空状态了,并且在kmem_cache_cpu的partial链表上。但partial链表上的全空slab并不一定会直接回收,除非满足一定条件。按照背景知识中所讲的内容,全满slab在释放第一个object的时候,slab会被链入到当前kmem_cache_cpu的partial链表中。在链入的过程中如果发现原partial链表中包含的空闲对象数目超过了一个合理值,就必然会对原partial链表中的全空slab进行回收。所以,为了确保Evil TCP slab被回收,还需要释放一些全满slab上的object,也就是下一步要做的事情:

4)按顺序close所有的make_dup_socket(dup_socket除外);

(4)“捕获”Evil TCP slab

通过mmap分配大块内存,可以“捕获”Evil TCP slab,具体原理可参考PINGPONGROOT [2]。最终的效果如下:

KARMA带你看攻防:WrongZone从利用到修复

用户态mmap分配的内存块与Evil TCP slab共享了同一块物理内存。

不过在捕获过程中,有一个问题需要解决:如何知道已经成功“捕获”Evil TCP slab?

前面所讲的两个follow socket的tcp_sock依然在Evil TCP slab上,所以,可以通过socket的某些系统调用对tcp_sock进行操作,留下一些标记。通过标记,我们就可以知道是否成功捕获,并且可以知道tcp_sock对应的用户态地址。mmap一旦成功捕获Evil TCP slab,对应物理页上所有内容都将被置为0。 setsockopt、getsockopt这些系统调用在调用selinux相应的hook函数时,会因为tcp_sock中的sk_security为NULL而失败。

在socket的ioctl实现中找到了可以用来做标记的代码:

KARMA带你看攻防:WrongZone从利用到修复

SIOCGSTAMP和SIOCGSTAMPNS均可以用来对tcp_sock做标记。每次mmap结束后,尝试通过ioctl对follow_socket_0做标记,若发现,mmap的内存中出现了非0值,则说明“捕获”成功。并且可以肯定的是,非0值是follow_sk_0的sk_flags和sk_stamp成员。同时由此计算得到follow_sk_0对应的用户态地址。需要注意的是,这里虽然是想捕获Evil TCP slab,但实际上可能仅仅是捕获到了follow_sk_0和follow_sk_1所在的物理页,并非整个Evil TCP slab。

目前看来,此利用方法和PINGPONG ROOT相似。不过现在面临着更多的挑战。因为现在内核防护手段已经有了很大的提升,即便“捕获”到Evil TCP slab,如果无法越过KASLR[7],PXN[8],PAN[9]等等内核防护手段,同样也无法做到最终的提权。为此我们做了更深入的探究,发现了一整套可以完整越过KASLR, PXN,PAN等内核防护手段的利用方法。

(5)Bypass KASLR/PXN/PAN内核防护,实现提权

经过上述步骤,我们获取到了follow_sk_0对应的用户态地址,并且follow_sk_1也在mmap的内存上。但是此时follow_sk_0的成员中除了sk_flags和sk_stamp以外,全都为0,并没有任何信息泄露。

对于follow_sk_0和follow_sk_1这两个如此残缺的tcp_sock而言,想要通过某些直接的手段,比如调用某些socket系统调用,来泄露内核地址是很困难的,很多执行路径都会崩溃。直接的方法不行,那我们就看看有没有一些间接的手段,比如,是否存在一些链表操作的流程可以被利用?因为链上结点的出入链会影响到前后结点上的值,说不好可以泄露一些内核地址。

查找代码后发现,这样的操作的确是存在的:

将socket bind到指定端口时,会将socket对应的tcp_sock链接到一个“localport bind bucket”中。具体是将tcp_sock的sk_bind_node成员链接到一个hlist中。

KARMA带你看攻防:WrongZone从利用到修复

如上图, hlist中的hlist node包含两个指针,next指针和pprev指针。next指针放在前面,指向下一个hlist_node; pprev指针放在后面,指向前一个hlist_node的next指针的地址,pprev是一个指向指针的指针。

我们可以先简单地考虑下面的这种hlist布局:

KARMA带你看攻防:WrongZone从利用到修复

在follow_sk_0和follow_sk_1之间穿插一个普通的normal_sk,若将中间的normal_sk结点删除,hlist会变成:

KARMA带你看攻防:WrongZone从利用到修复

这个过程非常值得注意。*在删除normal_sk结点时,隐含了两个写操作:normal_sk结点的next指针直接赋值给follow_sk_1结点的next指针,将normal_sk结点的prev指针赋值给follow_sk_0结点的prev指针。整个过程的数据流向是从normal_sk结点流向旁边的follow_sk_1*和follow_sk_0,**并且具体的数据都是这两个结点的地址。这不正是我们想要看到的吗。

按照上面的思路初步推断,如果Evil TCP slab被捕获后,能够形成如下的hlist:

KARMA带你看攻防:WrongZone从利用到修复

follow_sk_1和follow_sk_0结点的next,pprev指针因为mmap的操作均变为NULL。如果此时将normal_sk结点删除,那么follow_sk_1和follow_sk_0结点的地址就自然写到各自的内存上,也就是写在了mmap的内存块上,这样就完成了信息泄露!

整个思路粗略来看是非常可行的,但事实上并不完整。因为在正常操作的情况下,几乎不可能构造出上图中所示的hlist链表。因为为了让Evil TCP slab保持全空状态,我们必须将follow_socket_0、follow_socket_1或者dup_socket_0、dup_socket_1都close掉。close操作必然会使follow_sk_1、follow_sk_0两个结点从hlist中删除。所以,正常操作的情况下,mmap捕获Evil TCP slab成功后,是不可能出现上图中的hlist的。

但是上述思路只是对正常操作而言,如果我们可以构造出一些畸形的hlist呢?注意到,我们其实可以对follow_sk_1和follow_sk_0两个结点重复添加到hlist两次,因为我们有follow_socket_0、follow_socket_1与dup_socket_0、dup_socket_1这两对socket指向这两个结点。神奇的是,在follow_sk_1和follow_sk_0两个结点重复添加两次后形成的畸形hlist上再删除这两个结点一次,这两个结点依然会保留在畸形hlist****上。所以,最终的效果就是,我们既保证了Evil TCP slab为全空状态,又保证了follow_sk_1和follow_sk_0保留在了hlist上。最后,再来通过删除normal_sk做信息泄露就非常方便了。

下面分别介绍几个关键点,最终完成提权:

1)泄露follow_sock_0的内核地址和一个正常socket的tcp_sock地址

这一步就是根据前面所讲的思路设计的。为了完成这一目的,我们还需要在之前的步骤中穿插一些额外的操作:

  1. 创建三个普通tcp socket,称为bind_socket;

  2. 在前面创建完fill_socket之后按顺序执行如下操作:

选择一个可用的本地端口,称为BIND_PORT;

bind bind_socket_0到BIND_PORT;

bind bind_socket_1到BIND_PORT;

bind follow_socket_0到BIND_PORT;

bind bind_socket_2到BIND_PORT;

bind follow_socket_1到BIND_PORT;

完成这些操作后,将会形成如下的hlist:

KARMA带你看攻防:WrongZone从利用到修复

C.在构造好两个dup socket之后按顺序执行如下操作:

binddup_socket_0到BIND_PORT;

binddup_socket_1到BIND_PORT;

因为dup_socket_0,dup_socket_1分别与follow_socket_0,follow_socket_1共享tcp_sock,所以,完成上述步骤后会出现一个畸形的hlist,如下图。

KARMA带你看攻防:WrongZone从利用到修复

接着在后续步骤中,dup_socket_0和dup_socket_1分别被close,hlist变为:

KARMA带你看攻防:WrongZone从利用到修复  D.完成Evil TCPslab“捕获“后按顺序执行如下操作:

close bind_socket_2;

closebind_socket_1;

在未进行上面的两个操作之前, hlist如下:

KARMA带你看攻防:WrongZone从利用到修复

进行上述两个操作之后,hlist变为:

KARMA带你看攻防:WrongZone从利用到修复

此时的hlist非常简单明了,已经出现了内核地址泄露。可以看到,follow_sk_0 sk_bind_node的next指针指向了bind_sk_0的sk_bind_node, pprev指针指向了follow_sk_1sk_bind_node;follow_sk_1 sk_bind_node的next指针指向了follow_sk_0 sk_bind_node。实际操作时的效果如下,打印整个mmap内存块中非0值:

KARMA带你看攻防:WrongZone从利用到修复

然后通过这些泄露出的内核地址可以推算出follow_sock_0的内核地址和一个正常socket,也就是bind_socket_0的tcp_sock地址了。

2)构造内核任意地址读

目前我们获取到了follow_sk_0的内核地址以及对应的用户态地址,可以控制follow_sk_0所在的这一块物理页,这些条件和“利用方法一”中的一样。

同样,使用“利用方法一”中讲到的构造内核任意地址读的方法查找有没有符合模式的相关代码路径。查找相关代码后,我们发现在getsockopt的调用路径上(参考4.4内核代码)有符合模式的代码:

KARMA带你看攻防:WrongZone从利用到修复

可以看到,sk_get_filter函数中的“sk->sk_filter->prog->orig_prog->len”,并且最终返回的是此结果的值,这个表达式正好就是我们想要的模式。所以,我们可以利用此执行路径构造内核任意地址读。构造过程中需要伪造几个对象,这几个对象都可以从我们可以控制的这个物理页上分配。

还需解决另外一个问题:Bypass SELinux。getsockopt中会调用SELinux的hook函数:

KARMA带你看攻防:WrongZone从利用到修复

因为follow_sk_0上的sk_security为NULL,所以直接对follow_socket执行getsockopt会崩溃。但是如果我们伪造一个sk_security_struct对象,并将其地址赋给follow_sk_0的sk_security,同时令sk_security_struct对象的sid为SECINITSID_KERNEL,这样就完美绕过了selinux。

3)Bypass KASLR

用前面内核任意地址读可以获取到bind_sk_0的整个内容,就可以获取到sk_prot,因为bind_sk_0的sk_prot其实就是内核全局变量tcp_prot,这样KASLR就可以Bypass了。

4)提权

提权的所有逻辑与“利用方法一”一样。利用已经获取到的内核任意地址读可以获取到整个内核,然后解析符号表、动态搜索jop等,最终完成提权,不再赘述。

4.3 提权效果

KARMA带你看攻防:WrongZone从利用到修复

KARMA带你看攻防:WrongZone从利用到修复

5.基于KARMA的CVE-2018-9568修复

5.1 官方补丁

官方补丁[5]如下图所示,只需在sk_clone_lock中生成新的sk时,确保新sk的sk_prot_creator与sk_prot一致,就可以从根本上修复CVE-2018-9568。

KARMA带你看攻防:WrongZone从利用到修复

尽管漏洞及补丁公布了很久,但是实际上,依然有很大比例的受影响智能设备没有修复此漏洞。具体原因可以归纳成下面几点:

  • 修复链条长

漏洞信息并未及时从上游同步到终端厂商;漏洞修复完全依赖系统版本升级,无法即时触达终端设备;

  • 碎片化生态

大量的系统版本,导致厂商需要耗费大量精力进行漏洞修复;

  • 协同合作

漏洞修复过程缺乏安全厂商的参与和支持,修复效果不理想;

为了解决智能终端生态中面临的这些问题,能够及时快速的修复系统漏洞,我们自主研发了KARMA自适应系统热修复技术,切实解决了终端生态中面临的这些问题,为广大的智能设备及时修复系统漏洞。

5.2 KARMA自适应系统热修复技术及补丁

KARMA(Kinetic Adaptive Repair for Many AIoT-systems)自适应系统热修复,是百度在业界首创的系统热修复解决方案,使得智能终端设备厂商能够灵活、快速、低成本地修复其设备上的系统漏洞及功能缺陷。该技术能力是百度安全实验室在业界首创技术,拥有14项国内外专利,并曾亮相国际顶级安全会议BlackHat 2016与USENIX Security 2017。

依托此技术,我们为CVE-2018-9568编写了KARMA补丁:

KARMA带你看攻防:WrongZone从利用到修复

通过动态打补丁的方式,在sk_clone_lock函数中打了补丁,在sk_clone_lock函数返回时执行补丁逻辑,确保了sk的sk_prot_creator与sk_prot保持一致。这一点和官方补丁是等价的。因为sk_clone_lock函数本身并不是系统高频操作,所以,打补丁后基本不会对系统性能产生影响。

当然,我们还可以写出更加简单且更加“自适应”的补丁:

KARMA带你看攻防:WrongZone从利用到修复

因为IPV6中的IPV6_ADDRFORM的使用是漏洞触发的必要条件,且目前很多设备实际上并不使用此选项,所以可以在不影响系统正常业务的情况下直接在ipv6_setsockopt函数中打补丁,将IPV6_ADDRFORM选项禁用,这样也能达到漏洞修复的目的。

写好的补丁,只需要通过KARMA自适应系统热修复系统下发到终端设备中,就可以立即修复此漏洞。

KARMA自适应系统热修复具有以下特性:

  • 系统热修复

基于KARMA系统热修复方案,漏洞修复无需系统重启,不修改系统及内核文件,补丁下发即时生效

  • 自适应

补丁只需一次开发,即可适配成百上千机型,无需对于每个机型进行开发,提升修复灵活性;

这些特性使KARMA自适应系统热修复完美解决了智能终端生态中面临的问题,这也让KARMA成为当前智能设备的严峻安全挑战下一个非常有利的武器。

令人欣喜的是,至今KARMA已经帮助了超过100万台智能设备在没有升级系统版本的情况下,修复了CVE-2018-9568漏洞。我们也会持续对更多的智能设备进行安全修复,为更多的智能设备的安全保驾护航。

KARMA带你看攻防:WrongZone从利用到修复

6.写在最后的话


目前为止,依然有庞大数量的智能终端设备遭受着类似CVE-2018-9568这样漏洞的安全威胁。提升智能终端设备的安全性是我们的目标,同时,我们也希望更多的设备厂商能加入到KARMA的队伍中来,为自己的设备赋予漏洞热修复的能力,共同推动整个智能终端行业的安全发展。

KARMA带你看攻防:WrongZone从利用到修复

7.参考


1. https://towelroot.com/

2. https://www.blackhat.com/docs/us-15/materials/us-15-Xu-Ah-Universal-Android-Rooting-Is-Back.pdf

3. https://dirtycow.ninja/

4. https://thomasking2014.com/2019/05/29/Zer0con2019.html

5.https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/net/core/sock.c?id=9d538fa60bad4f7b23193c89e843797a1cf71ef3

6. 统计基于对2019年6月20日国内Android手机安全补丁级别的分布数据得出

7. https://lwn.net/Articles/569635/

8. Privileged eXecute Never,详见arm文档

9. Privileged Access Never,详见arm文档

KARMA带你看攻防:WrongZone从利用到修复

本文分享自微信公众号 - 百度安全实验室(BaiduX_lab)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这