利用
目標
直到現在我們已經看出這是一個典型的UAF漏洞並且一個位於用戶空間迷途的文件描述符指向內核中的PING 套接字可以被攻擊者獲得。接下來我們要填充套接字對象,重新使用這個對象。之後我們可以執行內核中任意代碼,最終完成Android設備的提權。
實際上,我們使用套接字對象的close函數。當close(sockfd)調用時,內核最終會進入如下代碼
int inet_release(struct socket *sock)
{
struct sock *sk = sock->sk;
if (sk)
{
long timeout;
sock_rps_reset_flow(sk);
ip_mc_drop_socket(sk);
timeout = 0;
if (sock_flag(sk, SOCK_LINGER) &&!(current->flags & PF_EXITING))
timeout = sk->sk_lingertime;
sock->sk = NULL;
sk->sk_prot->close(sk, timeout);
}
return 0;
}
內核調用inet_release來釋放內核中套接字對象相關的sockfd。並在函數底部調用sk->sk_port->close(sk, timeout)。
實際上sk_port是sk類型結構的一個成員,它指向一個確定的函數指針。而具體是什麼函數取決於協議類型,包括TCP、UDP、PING等。
如果被釋放的PING套接字對象sk重新填充我們可以完全控制的內容,那麼sk_port就完全處於我們的控制之下。它可以被指定一個用戶空間的虛擬地址。這個地址的指針sk->sk_port->close處於我們的控制下,如果PAN沒有被應用到內核,我們最終可以控制內核環境的PC register。實際情況是,市場上大多流行的Android設備沒有采用PAN,所以我們不需要考慮這一點。
通俗的講,在我們的方案裏有兩個重要因素需要被考量。一個是被攻擊利用的套接字對象穩定可靠的填充數據;另一個是重新填充的內容要完全被我們控制。
重新填充
在這個root發掘中最困難的事情是將我們需要的合適數據重寫到被釋放的套接字對象。並且爲了用戶root過程中的用戶體驗要確保整個進程穩定可靠。可靠精確的填充是我們工作的重頭戲。
Linux內核的內存管理機制一般採用SLAB/SLUB Allocator,用來高效的管理內核對象。
不同的SLABs爲內核不同的對象而創建,毫無疑問一個PING cache被創建作爲我們攻擊的PING套接字對象。這樣的分離設計廣泛存在於用戶程序中,比如 Isolated Heap 在IE和Partition在Chrome等。當在內核中面對這些設計,攻擊者利用A類型對象佔據一塊B類型對象的內存空間就顯得不是那麼容易。
另一個會帶來不穩定性的因素在於Linux內核的多線程支持。上百個task同時運行在單個系統上是一件很普遍的事情。這些執行的task也會引起內核中對象的allocation和de-allocation。這些都會最終影響到內核的堆佈局。而一個可預測的堆佈局對於UAF的重要不言而喻。
市場上的多數Android設備採用SLUB分配器,這對於我們來講是一個好消息。如果攻擊的目標對象比如PING套接字大小在512到1024之間,那麼填充就會變得很簡單。因爲SLUB Allocator傾向於將相似大小的對象放入到SLAB cache。這意味着,如果攻擊的目標對象大小爲512,那它有可能被放入同樣大小的SLAB中。因此,這種環境下的對象將被完全控制。事實上,512大小的對象可以在用戶程序中被穿件,一種方法就是sendmmsg。sendmmsg的執行期間, 內核將使用kmalloc在內核中分配一個緩衝區,暫時存儲傳輸數據包。這個數據包大小可以被我們自己指定,在這種情況下被設置爲512。並且緩衝區的內容可以完全被我們控制因爲這只是我們想通過sendmmsg傳遞的數據。
請注意,這是一個極好的在內核中填充UAF對象的方案。然而它有一個重要的限制:對象的大小要是SLAB的common-use。換句話說必須要是可以使用kmalloc分配的大小。例如在某些Android設備,PING sock對象的大小爲576,這是512年和1024之間,上面的解決方案是不再有效。
理論上使用kmalloc-size對象填充內核中的任意對象是可行的。它主要利用的是當一整個SLAB空閒,則這塊空間可能被內存回收在將來再利用。給予這個,我們可以首先創建大量PING sock對象來佔據沒有存起其他內容的SLABs,然後嘗試釋放所有來觸發UAF漏洞。幾個被完全釋放的SLABs就生成了。再然後通過sendmmsg分配一定數量的大小爲512的緩衝區。極有可能這些緩衝區佔據了之前存儲的PING sock並填充了它。
然而, 這種方法很難控制,有巨大的不確定性。我們無法精確的知道哪個緩衝區佔據了之前PING sock在SLAB的存儲空間,使得整個root開發不穩定可靠。
此外,PING sock的大小在不同設備中是不同的。如果我們需要一個普遍的解決方案,我們不應該依賴於這些設備上PING sock的大小。
到這兒,爲了利用漏洞而填充被釋放的PING sock對象的精巧技術將出現。注意,我們不關心關於PING sock在設備上的大小問題。這一次,我們不利用其它內核對象來完成填充工作,而是用physmap。
Physmap第一次被提及到在《ret2dir: Rethinking kernel isolation》。它一大塊內存的內核空間,直接將用戶空間內存映射到內核空間,用於提升系統的性能。
意思是我們可以反覆調用mmap在用戶空間填充大量數據,它們中大部分將直接出現在內核空間。我們的目的是利用用戶空間的數據覆蓋被釋放的對象。有幾個問題需要關注。
如圖9所示,我們可以看到在內核空間中,physmap和SLAB一般處於不同的位置。Physmap一般處於較高的地址,SLAB處於較低的地址。爲了讓它們在內核空間的中間部分碰撞,我們首先要創建大量對象,將內核分配器分配的起始地址擡高,增加physmap和SLAB內存重用的可能性。這一步稱作“lifting”。
在我們所利用的目標CVE-2015-3636,我們只需要用PING sock對象提升,因爲在內核中易於分配(calling socket)和釋放(calling close)。
如圖10,提升之後,創建一定數量的PING sock,但這次它們是易於攻擊的。因爲先前的提升,它們在內核空間中處於高地址。
在之後的de-allocation,我們通常釋放這些爲了提升的PING sock對象。而對於攻擊的目標對象,我們釋放它們觸發漏洞,即對一個PING sock兩次connect。
然後我們調用mmap並且在用戶空間填充我們想要的數據到映射空間。這些數據8 dwords一組。每8 dwords重複。
一個大問題是我們什麼時候停止填充,當我們的目標對象已經在physmap中被數據覆蓋。爲了解決這個問題,在我們的8 dwords中,除了一些key value用來控制flow和在最後避免內核衝突,在特定的入口填充預先設計的魔數magic value。
每次我們填充完一定量的數據,我們調用ioctl(sockfd, SIOCGSTAMPNS, (struct timespec*))在這些目標對象。
如圖11,讀出sk和sk->sk_stomp。通過具體的參數調用ioctl,我們可以成功獲取對象中固定偏移的一個dword值。我們比較魔數和這個值,就可以知道是否覆蓋。這一步確定root的可靠性。
當我們已經精確地覆蓋了目標對象,調用close來懸空對象的文件描述符。內核將最終調用sk->sk_port->close,此時sk_port將處在我們的控制之下,並且它指向完全由我們控制的位置,因此這個函數指針close就被獲得。最終我們控制了內核環境中PC register的值。
注意,我們最終的填充方案不依賴與特定的配置或者Android設備的內核細節。
64位設備
我們的root方法同樣適用於Android64位設備,基於以下兩個原因:
A. LIST POISON2的值在Android64位設備中仍然是0x200200。在PC Linux上爲0xdead000000000000,有64位長度而且超出了64位系統虛擬地址的範圍。如果這個值不能被映射,當我們第二次connect時就無法避免碰撞。
B. 已證明64位系統中physmap也可以覆蓋SLAB。
ROOT
在控制了內核環境中的PC register之後,我們設計執行代碼,獲取root權限,這是我們最終的目標。最基本的方法重寫當前task的addr_limit值位0,從而任意的在內核空間進行讀寫。之後重寫內核中權限結構提權。
對於那些沒有PXN的設備,事情變得十分簡單。我們只需要設置close函數指針爲一個用戶空間的虛擬地址,啓動一塊修改addr_limit值位0的shellcode。
對於那些有PXN的設備,ret2usr攻擊沒有什麼作用。我們採用ROP(Retrun-oriented Programmming)來達到我們的目的。爲了設計一個可靠的方法,我們使用內核JOP(Jump-Oriented Programming)來重寫當前task的addr_limit值位0。
1) Referred registers during JOP
在JOP時,許多寄存器會更改它們的原始數據。事實上,如果我們不關心它們的原始數據,有極大的可能會丟失。在我們的方案裏,我們調用close函數懸空套接字文件描述符,進入用戶段的JOP鏈。在JOP鏈期間,我們只修改r0到r5的值。改變其他寄存器的值都會在之後導致不可預知的內核崩潰。一些關鍵寄存器的值需要保存一致。
2) Keeping the value of SP
我們使用JOP取代ROP的最主要理由是在ROP通常需要我們調整棧。這些行爲會導致在整個過程中SP寄存器的值未知。SP寄存器的值在整個過程中十分關鍵,在任何時間修改它都是不明智的。
3) Avoiding data corruption on the stack
我們要避免像這樣的gadgets
寄存器x29通常保存SP的值在Linux64位系統中。這些gadgets更改了有關棧的數據,這會影響到未來的內核的執行流。這些行爲會帶來不確定性,應該避免。
4) Exploring core gadgets
我們的JOP鏈主要有兩個task。第一個用於泄露SP值,我們可以從中獲取當前task的task_struct的地址。第二個task重寫當前task_struct的addr_limit,
核心gadgets我們試圖尋找的像這樣:
泄露。寄存器x0的值應該是一個用戶空間的虛擬地址,之後我們可以讀取到寄存器x1的值,這應該就是SP的值。通過跳轉到x2寄存器指向的正確的返回地址,從JOP中返回。x2的值同樣是一個用戶空間的地址。
重寫。寄存器x1的值位0,x0應該是一個和addr_limit地址相關的地址。之後返回到初始返回地址。
總結上面兩步,泄露和重寫。我們嘗試從不同設備的啓動映像尋找gadgets。
5) Leaking tricks
A.在64位Android設備中,寄存器x29通常儲存了SP的值,因此下面的指令可以獲得棧的地址:
B.對於64位的設備,高32位內核虛擬地址通常保持一致。所以對於攻擊者,泄露低32位地址通常已經足夠:
6) Rewriting tricks
當提到市面上Android設備的ROMs,映像中存在的gadget我們可以利用它們來達到內核中任意寫的目的
主要有兩種選擇:
結束語
在本文中,我們披露了CVE-2015-3636的細節和Keen Team如何利用它來達到對於市面上的大多數Android設備提權(4.3及以上)。我們應用在64位設備上root這是世界上已知的第一例。此外,通過應用JOP的技巧,PXN也可以被完全繞過。
譯註:
關於內存分配技術SLAB\SLUB、Physmap
ROP,就是面向返回語句的編程方法