Own your Android! Yet Another Universal Root(一)

司寇飞航
2023-12-01

CVE-2015-3636漏洞是一个典型的UAF(use-after-free)漏洞,被广泛用于Android设备的提权。这个漏洞虽然出来已久,但是仍然很有学习价值。它是第一个已知的可应用与Android64位设备的提权漏洞,作为一个内核漏洞,不依赖于Android设备。对于初学者来说是极好的学习Android系统安全和root的例子。

虽然网上也有一些分析,但是大多数只有一个大概的漏洞成因或思路。而本文是漏洞的发现者Keen Team的两位大牛所写的,在思路的完整程度上应该是所有分析文章中最好的。

Own your Android! Yet Another Universal Root
Wen Xu1 Yubin Fu1
1Keen Team
xuwen.sjtu@gmail.com qoobee1993@gmail.com

摘要
近几年,发现通用的Android提权方案变得越来越困难,因为Linux内核极少的漏洞,并且供应商在硬件上应用了防范措施。
在本文中,我们将提出我们的通用提权方案。相关漏洞CVE-2015-3636,是一个典型的UAF漏洞,从Linux内核中被发现。发现这样一个在Linux内核中存在的UAF漏洞确实很困难,因为来自内核分配器的分离分配。我们将展示如何利用内核中的漏洞达成在市面上大多数4.3及以上版本Android设备提权的目的。
总之,我们提出一个通用的方法来利用内核中的UAF漏洞,这意味着对于所有品牌的设备都有效。所有现有的防范机制如PXN都可以被这种方法绕开。并且最重要的是我们独特的未公开的针对内核UAF漏洞的利用技术表现的稳定准确。
BUG分析
漏洞被Keen Team团队的Wen Xu 和 wushi,通过一款PC Linux漏洞发掘软件Trinity。我们移植它到Android的ARM Linux。漏洞已经被最近的Linux内核修复,并分配CVE编号,CVE-2015-3636。
漏洞位于Linux内核底层部分,被确认可以用于一般root。首先socket(AF INET, SOCK DGRAM, IPPROTO ICMP)创建套接字,通过该套接字文件描述符调用connect函数,内核代码处理用户请求方式如下
<span style="font-size:14px;">int inet_dgram_connect(struct socket *sock, struct sockaddr * uaddr,
		       int addr_len, int flags)
{
	struct sock *sk = sock->sk;

	if (addr_len < sizeof(uaddr->sa_family))
		return -EINVAL;
	if (uaddr->sa_family == AF_UNSPEC)
		return sk->sk_prot->disconnect(sk, flags);

	if (!inet_sk(sk)->inet_num && inet_autobind(sk))
		return -EAGAIN;
	return sk->sk_prot->connect(sk, (struct sockaddr *)uaddr, addr_len);
}
</span>
如果sa_family == AF UNSPEC 内核根据协议类型调用指定的disconnect process。对于一个PING (ICMP)套接字,disconnect如下
<span style="font-size:14px;">int udp_disconnect(struct sock *sk, int flags)
{
	struct inet_sock *inet = inet_sk(sk);


	sk->sk_state = TCP_CLOSE;
	inet->inet_daddr = 0;
	inet->inet_dport = 0;
	sock_rps_reset_rxhash(sk);
	sk->sk_bound_dev_if = 0;
	if (!(sk->sk_userlocks & SOCK_BINDADDR_LOCK))
		inet_reset_saddr(sk);

	if (!(sk->sk_userlocks & SOCK_BINDPORT_LOCK)) {
		sk->sk_prot->unhash(sk);
		inet->inet_sport = 0;
	}
	sk_dst_reset(sk);
	return 0;
}
</span>
我们可以看到会调用sk_prot_unhash(sk) ,对于PING (ICMP)这个ping_unhash()如下

</pre><pre name="code" class="cpp"><span style="font-size:14px;">void ping_unhash(struct sock *sk)  
{  
        struct inet_sock *isk = inet_sk(sk);  
        pr_debug("ping_unhash(isk=%p,isk->num=%u)\n", isk, isk->inet_num);  
        if (sk_hashed(sk)) {  
                write_lock_bh(&ping_table.lock);  
                hlist_nulls_del(&sk->sk_nulls_node);  
                sock_put(sk);  
                isk->inet_num = 0;  
                isk->inet_sport = 0;  
                sock_prot_inuse_add(sock_net(sk), sk->sk_prot, -1);  
                write_unlock_bh(&ping_table.lock);  
        }  
}  
EXPORT_SYMBOL_GPL(ping_unhash);  </span>

从源码中可以看到,如果sk_hashed(sk)为真,就会删除它sk_nulls_node在内核中的哈希链表的存储,如下

<pre name="code" class="cpp">static inline void __hlist_nulls_del(struct hlist_nulls_node *n)  
{  
        struct hlist_nulls_node *next = n->next;  
        struct hlist_nulls_node **pprev = n->pprev;  
        *pprev = next;  
        if (!is_a_nulls(next))  
                next->pprev = pprev;  
}         
  
static inline void hlist_nulls_del(struct hlist_nulls_node *n)  
{  
        __hlist_nulls_del(n);  
        n->pprev = LIST_POISON2;  
}   


 
  我们发现在n (sk->sk_nulls_node) 删除之后n->pprev的值变为LIST_POISON2,这是一个常量值定义的宏。实际上无论在64位还是32位Android中这个值都是0x200200。这个虚拟地址可以被映射到攻击者的用户空间。 
  

然而,当第二次调用connect时会发生一些令人惊讶的事情。在套接字对象被从哈希链表中删除后,它仍然是哈希映射的,因为无论是不是哈希映射,取决于sk->sk_node,而sk->sk_node在第一次连接时并未被改变。
因此内核进入if分支并且再次删除sk_nulls_node。当内核执行*pprev = next将会发生冲突,因为当前pprev的值为0x200200,并且如果这个虚拟地址没有被映射到用户空间,之后一个关键页面错误将会发生。0x200200应该在第二次IMCP套接字连接之前被映射到用户空间防止冲突发生。然而,这并不是这个漏洞的全部。简短的看一下代码

<span style="font-size:14px;">static inline void sock_put(struct sock *sk)  
{                 
        if (atomic_dec_and_test(&sk->sk_refcnt))  
                sk_free(sk);  
}   </span>

hlist_nulls_del调用之后,发现sock_put(sk)十分可疑。

内核每次进入if分支,都要减去一次套接字对象在内核中的应用次数。更重要的是,它会检查应用次数是否为0。如果为0,套接字对象将被释放。这意味着如果尝试再一次connect这个套接字对象,引用次数将会变成0,然后内核会释放它。但是用户程序中的文件描述符仍然关联着内核中的套接字对象,这是一个典型的UAF漏洞。

int sockfd = socket(AF_INET,  
SOCK_DGRAM, IPPROTO_ICMP);  
struct sockaddr addr  
= { .sa_family = AF_INET };  
int ret = connect(sockfd, &addr,  
sizeof(addr));  
struct sockaddr _addr  
= { .sa_family = AF_UNSPEC };  
ret = connect(sockfd, &_addr, sizeof(_addr));  
ret = connect(sockfd, &_addr, sizeof(_addr));  

第一次connect必须以sa_family=AF_INET来构造sk。否则if分支不会到达。
注意这个PoC只对Android设备起作用。被允许用来构造PING套接字的group id被定义在/proc/sys/net/ipv4/ping_group_range。在Android设备,一个普通用户有权创建一个默认的PING socket,而在PC Linux,没有人有权限创建。


(未完)

 类似资料: