作者:zjq@IceSword Lab
來源:http://www.iceswordlab.com/2018/04/20/samsung-root/

在安卓陣營中,三星手機可以說是最重視安全的了,各種mitigation技術都是早于官方系統應用到自己手機上,并且加入了KNOX技術,在內核層設置了重重校驗,提高了手機root難度。17年下半年,研究過一段時間三星手機s8的內核安全問題,發現了一些比較有意思的漏洞。本文中,將介紹一個race condition漏洞,利用此漏洞繞過KALSR,PXN,CFI,KNOX2.8等拿到了s8內核root權限。目前這些漏洞都已經被修復。

0x0 MobiCore驅動的提權漏洞

在MobiCore驅動中,ioct的MC_IO_GP_REGISTER_SHARED_MEM接口會從slab中分配一塊cwsm buffer,MC_IO_GP_RELEASE_SHARED_MEM接口用來釋放cwsm buffer和相關資源。但是在釋放過程中,由于沒有加鎖,存在race condition進而導致double free的可能:

看此函數的實現,首先從鏈表中查找獲取該內存塊,并將引用計數加1以持有該cwsm buffer。然后通過連續兩個cwsm_put函數減去引用計數并釋放cwsm buffer。cwsm_put的實現是引用計數減1,然后檢查引用計數是否為0,如果為0,則執行cwsm_release函數釋放cwsm,如下所示:

正常情況下,創建該buffer時引用計數被設為1,cwsm_find查找該buffer時引用計數加1,第一個cwsm_put調用減去cwsm_find持有的引用計數,然后第二個cwsm_put將引用計數減為0,并調用cwsm_release釋放資源。

但在client_gp_release_shared_mem函數中,由于cwsm_find和兩個cwsm_put之間并未加鎖保護,使獲取cwsm和釋放cwsm不是原子操作,當race condition發生時,多個線程在cwsm被釋放前調用cwsm_find獲取該buffer后,接下來的多次cwsm_put調用則可以觸發對cwsm的double free。

我們再看cwsm_release這個函數,還是比較復雜的:

其中,cwsm的結構為:

仔細分析cwsm_release函數,我們會發現,這個函數中當race condition發生時, tee_mmu_delete(cwsm->mmu) 會造成cwsm->mmu 的double free, client_put(client) 會造成cwsm->client的double free,最后kfree(cwsm) 也會造成cwsm的double free。三個大小不一的slab內存塊同時double free,極易引起內核崩潰,除非我們在cwsm第一次被釋放后占住該內存,從而控制內存中內容,改變第二次執行此函數中的流程。而list_del_init(&cwsm->list)這一句:

如果我們可以控制cwsm的內容,也就是list->next 和list->prev指針的值,則可以做成一個任意地址寫。

0x1 利用方案

從client_gp_release_shared_mem函數中可以看到,調用cwsm_find獲得buffer和調用cwsm_put釋放buffer時間間隙極小,如何能提高race condition的成功率,有效控制指針,并能盡可能的降低崩潰率呢?通過對slab中內存分配釋放機制的分析,主要采用了幾下幾個方法:

  1. 如何增加race condition成功率呢?kmalloc在slab中分配內存塊會記錄下本線程所在核,kfree釋放內存時,如果判斷當前線程所在核與分配內存時的所在核一致,則將內存釋放到快速緩存鏈表freelist中,這樣當其他線程分配相同大小的內存塊時能快速取到,這樣可以增加釋放后馬上占位的成功率;如果釋放時判斷當前線程所在核與分配內存時的所在核不一致,則將內存釋放到page->freelist中,當其他線程分配內存時,緩存鏈表中內存耗盡后,才會從此鏈表中取用,因為時間間隙很小,這會降低占位成功率。所以分配slab內存,釋放內存,占位內存的線程最好在同一個核上。假設有0,1,2三個核,線程A在0核上分配了buffer,線程B在0核上釋放buffer,同時為了制造race condition需要線程C在1核上釋放buffer,同時線程D在0核上,可以調用add_key系統調用來占用線程B釋放掉的內存塊,并填上我們需要的內容。當然這實際調試中,因為race condition間隙很小,可能需要幾個甚至幾十幾百個線程同時操作來增加成功率。同時,因為race condition間隙很小,可以在0核上增加大量打醬油線程,使其在race condition間隙中獲得調用機會,以增大時間間隙,提高占位的成功率;

  2. 我們在cwsm double free的第一次釋放后將其占住,那么就可以控制其中的內容,填上我們需要的值,因此我們可以將cwsm->list.next設為一個內核地址,利用list_del_init(&cwsm->list)再調用__list_del,可以實現內核地址寫,比如將ptmx->check_flags 設置為我們需要的函數指針;

  3. 當race condition發生時,多個線程調用cwsm_release時,大小不同的slab塊cwsm->mmu,cwsm->client和cwsm都會被重復釋放,在此情況下,內核大概率會崩。因此,當cwsm第一次釋放,我們占住后,需要將cwsm->client和cwsm->mmu填上合適的值,防止內核崩潰。我們先看client_put(client) 函數:

這個函數首先引用計數client->kref減1,如果為0,則調用client_release釋放資源。因此我們可以將client->kref設為大于1的值,防止cwsm->client被二次釋放。

再看tee_mmu_delete(cwsm->mmu),這一句比較麻煩,它將調用mmu_release函數,看內部實現(片段):

可以看到,mmu_release 不僅要釋放mmu,并且要引用mmu中指針。如果我們能控制cwsm->mmu,那么我們必須將cwsm->mmu設為一個合法的slab地址,并且能夠控制這個slab中的內容,否則系統將崩潰。幸運的是,我們找到了一個信息泄露漏洞:

/sys/kernel/debug/ion/event文件將泄露ion中分配的ion_buffer的地址。我們可以利用ion接口分配大量ion_buffer,然后在泄露的地址中查找到連續8k大小(cwsm->mmu的大小)的ion_buffer內存。然后在ion中占住這一塊內存不釋放,將其地址填到cwsm->mmu中,使mmu_release釋放此內存塊,但因為我們在ion中此內存占住不釋放不使用,所以即使被別人重新獲得,也可避免內核崩潰。

0x2 Bypass KALSR

Android 8.0之后安卓手機普遍啟用了內核地址隨機化,而三星手機啟用的要更早一些。此漏洞本身泄露內核地址比較困難,所以還需要一個信息泄露漏洞。debugfs 文件系統一直是比較容易出問題的,我們嘗試著用簡單指令測試了一下:find /sys/kernel/debug | xargs cat,片刻之后,屏幕上打印出了如下信息:

經過分析,這是/sys/kernel/debug/tracing/printk_formats文件所泄露出來的地址,有些函數地址,比如dpm_suspend,此地址加上一個固定的偏移量即可得到內核啟動后的真實函數地址。經過fuzz發現,類似的信息泄露不止一處。

0x3 Bypass PXN && CFI

我們曾在16年mosec會議上介紹過幾種過PXN方法。其中一個方法是,將函數指針kernel_setsockopt覆蓋到ptmx_fops->check_flags,然后通過控制第一個參數跳轉,繞過set_fs(oldfs)語句,當函數執行完,本進程addr_limit被設為0xffffffffffffffff,此時我們可以在用戶態通過一些系統調用直接讀寫內核數據。

然而在s8上使用此方法時確出現了系統崩潰,仔細檢查s8的kernel_sock_ioctl匯編代碼時,發現跳轉指令改變了,跳轉到寄存器的指令改成的直接跳轉到固定地址0xffffffc000c56f6c的指令:

下面看看跳轉到0xffffffc000c56f6c這個地址干了些什么:

如上代碼,實際上是對跳轉地址做了檢查,如果跳轉到的地址的上一條語句是0x00be7bad,則認為是合法地址,執行跳轉,如果不是則認為是非法地址,執行一條非法語句導致內核崩潰。為什么必須要上一條語句是0x00be7bad呢?原來s8在編譯時每一個函數結尾都加上了一句0x00be7bad作為標記,如果上一條語句是0x00be7bad,則表明這個地址是函數的起始地址,否則不是。也就是說,在每一個跳轉到寄存器地址之前都要檢查地址是否為函數的起始地址,否則非法。

雖然此路不通,但是另外一個辦法還是可以的。我們找到了一個比較好用的bug,在s2mm005_flash函數中有一個代碼片段:

文件CCIC_DEFAULT_UMS_FW定義為:”/sdcard/Firmware/usbpd/s2mm005.bin”,由于此文件并不存在,當調用到此代碼時,filp_open將返回錯誤,跳到done返回。可以看到錯誤處理中并沒有恢復addr_limit。也就是當調用此函數失敗時,本進程將得到讀寫內核的權限。

當然上面這個辦法有賴于這個簡單的bug,在錯誤處理中漏掉了set_fs(old_fs)的操作。如果沒有這種bug怎么辦呢?還是有辦法的,我們在內核中找到了這樣的函數:

將此函數地址,利用漏洞覆蓋掉ptms_fops-> check_flags指針,當我們調用check_flags時,可以控制第一個入參,那么合理設置參數內容,可以達到讀寫內核的目的。

0x4 KNOX2.8 && SELinux

三星手機為了提高手機安全性,加入了KNOX,使內核利用難度大大加強。這里簡單介紹一下KNOX2.8在內核中主要實現的特性:

1.與root相關的關鍵數據,比如cred,頁表項等需要在特定內存中分配,此內存中通用cpu端被設為只讀,當需要修改時,則發送指令通過TrustZone進行修改;

2.在調用rkp_call讓TrustZone執行命令時,TrustZone同樣將對數據完整性進行校驗,比如commit_creds函數在創建cred后,調用rkp_call時,TrustZone會檢查本進程credential是否在只讀內存區,檢查本進程id是否大于1000,如果大于1000則不能將新創建的credential修改為小于1000的值,這也使得通過調用rkp_override_creds來修改credential用戶id的辦法不再有效;

3.在SELinux原有權限管理基礎上,增加了額外的完整性校驗,這幾乎影響所有系統調用接口。以open系統調用為例,當打開CONFIG_RKP_KDP配置項時,增加了security_integrity_current的校驗:

可以看到,在security_integrity_current這個函數里,將校驗:進程描述符中cred和security是否在只讀內存區分配,bp_cred與cred是否一致(防止被修改),bp_task是否就是本進程,mm->pgd和cred->bp_pgd是否一致,current->nsproxy->mnt_ns->root和current->nsproxy->mnt_ns->root->mnt->bp_mount是否一致。如果其中某一項關鍵數據被修改而導致檢驗不通過,則導致系統產生panic,并打印出錯誤信息;

4.在load_elf_binary -> flush_old_exec函數中增加校驗,如果進程為id小于1000,為內核進程,并且load的二進制文件及不再”/”目錄又不在”/system”目錄下則內核panic。

這使得利用用戶態調用__orderly_poweroff函數在內核中創建內核線程的方法將被阻止;KNOX還在內核其他地方加入了大量的檢驗。

KNOX的加入,使得以前常用的一些修改credential 用戶id去root辦法都比較難辦了。隨著KNOX版本的迭代,勢必會對內核的保護越來越強化。但是就筆者當時研究的KNOX2.8而言,依然還有一些弱點可供利用,進而拿到root權限,讀寫高權限文件,起內核shell等。

前面提到,KNOX限制root的一個措施就是在大部分系統調用中,都會進行數據完整性校驗,如果我們將進程credential修改非只讀區,則會校驗失敗。這些校驗函數都是掛接在全局變量security_hook_heads下面,比如open系統調用會調用security_hook_heads下掛的file_open鉤子函數,最后調用到selinux_file_open進行權限和數據完整性校驗。但是security_hook_heads這個全局變量卻是可讀寫的,我們可以利用漏洞讀寫內核,將此變量下面掛的鉤子函數有選擇的設置為NULL,不僅可以繞過該校驗,還可以繞過SELinux的檢查。比如,我們可以把本進程credential設置為替換為一塊可讀寫內存,將id修改為root用戶,同時將和讀寫相關的校驗函數設為NULL。這樣可以用root用戶穩定的讀寫系統中高權限文件。進行其他操作時,也可以通過禁用相關校驗函數繞過校驗,當然這種方法有些簡單粗暴,需要小心使用,因為這些校驗函數有些和系統耦合緊密,如果不小心很容易引起系統crash,操作完成后應該盡快恢復。在KNOX之前版本中,有研究員曾經通過調用__orderly_poweroff函數,可以利用內核起一個root進程,繞過了commit_creds中的校驗,但是KNOX2.8中在load_elf_binary中增加了對用戶id和binary路徑的校驗。然而我們發現,雖然load_elf_binary增加了此校驗,但是load_script中卻沒有加上這個校驗,這就意味著,雖然我們不能在內核中加載自己的binary,但是可以起一個root腳本進程,在腳本中進行我們需要的操作。

總結:

本文介紹了如何利用一個s8中race condition驅動漏洞,一步步繞過KALSR,PXN,CFI,KNOX2.8等mitigation機制,拿到root權限,讀寫高權限文件,并在內核中起一個shell進程。三星在內核加固方面下了很大功夫,KNOX的引入顯著提高了root的難度,隨著后面版本的不斷迭代,對內核的加固會越來越強,值得持續的跟蹤研究。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/587/