CVE-2015-3636漏洞是一个典型的UAF(use-after-free)漏洞,被广泛用于Android设备的提权。这个漏洞虽然出来已久,但是仍然很有学习价值。它是第一个已知的可应用与Android64位设备的提权漏洞,作为一个内核漏洞,不依赖于Android设备。对于初学者来说是极好的学习Android系统安全和root的例子。
虽然网上也有一些分析,但是大多数只有一个大概的漏洞成因或思路。而本文是漏洞的发现者Keen Team的两位大牛所写的,在思路的完整程度上应该是所有分析文章中最好的。
<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;
}
然而,当第二次调用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));