Own your Android! Yet Another Universal Root(一)

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]

摘要
近幾年,發現通用的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,沒有人有權限創建。


(未完)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章