作者: 陵軒,美成,宋楊
近些年來,由于Android系統的興起,作為Android 底層實現的 Linux內核其安全問題也是越來越被人們所關注。為了減小漏洞給用戶帶來的危害和損失,Linux 內核增加了一系列的漏洞緩解技術。其中包括DEP,ALSR,更強的 Selinux,內核代碼段只讀,PXN等等。 Linux 中這些安全特性的增加,使得黑客們對漏洞的利用越來越困難。其中,DEP,ALSR,Selinux 等技術在 PC 時代就已經比較成熟了。內核代碼段只讀也是可以通過修改ptmx_fops 指針表等方案來繞過。那么,PXN是什么?它又該如何繞過呢?
PXN其實就是 Privileged Execute-Never 的縮寫,按字面翻譯就是“特權執行從不”。它的開啟與否主要由頁表屬性的PXN位來控制,如圖所示。
在沒有PXN安全機制的Android機器上,我們一般的提權思路大致可分為如下幾步:
上述說到的提權步驟是在沒有PXN影響下的一般思路。那么在帶有PXN的機器上表現又是如何呢?經過我們的多番測試,在不同的機型上表現各不相同。在有的機器上會卡住,shellcode不會繼續執行;有的機器上會產生 Kernel Panic,機器直接重啟。 所以說,PXN的大致工作原理就是在內核狀態下,系統是無法直接執行用戶態代碼的。因此,我們常用的提權思路也就行不通了。更加坑爹的是在絕大部分的arm64系統機型(如三星 s6,華為 p8),和一些 arm32位機型(如三星 note3,三星 s5 等主流機型)的最新 ROM 都默認開啟了PXN。
在CVE-2015-3636漏洞的一種利用方式中,我們最終可以控制inet_release函數中 sk->sk_prot->close 指針的值。對應匯編代碼如圖所示。
圖2中X2寄存器的值即為sk->sk_prot->close指針的地址,這里的X2對應著32位系統中的R2。BLR X2指令就是跳轉到X2所指向的地址處執行。 在開啟了PXN的機型上,如果我們將sk_prot->close指針的地址指向用戶態的提權代碼,那么系統就直接panic重啟了,錯誤信息如下圖所示。在這個panic信息中可以看到PC寄存器的值為0x558d15a2a8,是一個用戶態地址。
既然在內核狀態下PXN機制能阻止系統運行用戶態的代碼,那么它會不會阻止內核層的代碼執行呢?我們將sk_prot->close指針的地址指向了inet_release函數的返回處。代碼如圖所示。
此時再觸發漏洞,函數能正確返回到用戶態,并且系統也沒有panic,從而驗證了PXN是不會阻止內核態代碼執行的。這和PXN自身的原理也是一致的。
基本思路
在帶有PXN的機器上雖然不能執行用戶態的shellcode,但是依然可以執行內核態的代碼。因此,我們的策略就是使用內核ROP:構建內核gadget,使得棧指針sp泄露,然后通過sp計算得到thread_info結構的地址,之后patch thread_info結構的addr_limit字段,從而達到用戶態任意讀寫內核態的目的。
構建gadget
接下來,我們的主要目的就是驗證上述方案的可行性。首要任務就是尋找合適的內核gadget。 假設我們現在已經可以利用漏洞控制X1寄存器的值,如圖所示。
因此,在尋找ROP需要的gadget時,我們以X1所指向的內存區域為基礎來控制其他寄存器的值。在觸發漏洞前,通過mmap函數來創建一塊用戶態可以控制的內存空間。如下所示
void *map_addr_tmp = mmap((void*) 0x30303000, 0x10000,
PROT_WRITE|PROT_READ|PROT_EXEC,
MAP_SHARED|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
做完了上面的準備工作,接下來,我將上述方案分解成下面三步來完成:
首先,用ROP來完成sp的泄露。因為寄存器X1所指向的地址是我們在用戶態自己分配出來,并且可以隨意控制的內存塊,所以我們在第一段gadget中借由X1來布局各個跳轉的地址,然后跳轉到下一段gadget開始執行。 第二段gadget主要是將sp棧指針的值賦給X0,然后跳轉到下一段gadget。在這個例子里面,用戶態是無法直接通過X0來獲得sp的。所以我們在gadget3里面通過一條STR指令將X0的值保存到用戶態。ROP的大致思路如下圖所示。 找gadget在某種程度上是一個體力活,當然也可以通過一些工具來Make life easier,大家可以自行Google。注意這里描述的gadget只是一個大致的思路,和實際中可能會有一些區別。
我們得到sp的值后,就可以計算出addr_limit字段的地址了。在arm64系統上,棧的最大深度為16K。其次,addr_limit字段位于thread_info結構+8的位置。因此,計算方式如下所示:
unsigned?long?thread_info_addr?=?sp?&?0xFFFFFFFFFFFFC000;
unsigned?long?addr_limit_addr?=?thread_info_addr?+?8;
printf("addr_limit_addr:?%p\n",?addr_limit_addr);?
最后,我們通過ROP來設置addr_limit的值為0xFFFFFFFFFFFFFFFF。同樣,X1寄存器我們可以隨意控制其內容。第一段gadget用來設置各個跳轉地址。第二段gadget完成主要任務,即通過一個STR指令來完成patch addr_limit的目的。最后跳轉到inet_release的函數返回處。ROP的大致思路如圖所示。
完成了addr_limit字段的patch之后,那么用戶態就可以任意讀寫內核態了。接下來的選擇就很多了,一種方法是先確定task_struct的地址,它是在thread_info+0x10的位置。得到了task_struct的地址之后,就可以定位到cred字段,之后patch uid、gid、capability、selinux等即可。 實踐中,通過結合已知的漏洞,上述方案在近期發布的一些64位旗艦機型上均能測試成功,如圖所示。當然,這個思路在32位機型上也同樣適用。
假設我們所使用的是一個任意寫的漏洞,那么繞過PXN的方式其實是類似的。首先通過任意寫的洞控制Kernel的一個函數指針,指向我們的gadget,泄露出sp的值。接下來,可以直接通過任意寫的能力去patch addr_limit(不需要再通過ROP去patch了)。
近些年來隨著對Android、Linux的研究越來越深入,通用的Linux平臺漏洞,例如:CVE-2013-6282、CVE-2014-3153 (towelroot)以及最新的CVE-2015-3636(pingpong)紛紛被公之于眾。 同時,Linux上運用的最新的漏洞緩解機制讓系統漏洞的利用越發困難,但繞過這些緩解機制的技術也在推陳出新。攻與防的博弈永遠不會結束。
參考文章:
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.subset.architecture.reference/index.html
https://bugzilla.redhat.com/show_bug.cgi?id=1218074