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
[email protected] [email protected]
漏洞位於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,沒有人有權限創建。