作者:庫特@螞蟻安全實驗室
原文鏈接:https://mp.weixin.qq.com/s/bfdwAhRRso34OOZrG2r65Q
文件系統是操作系統的基礎設施之一,其中存在的任何缺陷都會導致嚴重后果。在研究蘋果macOS文件系統的具體實現時,我們在xattr特性中發現了一系列嚴重漏洞。
文章將以CVE-2020-27904和CVE-2019-8852(由天穹實驗室的庫特同學獨立發現并報告)為例,剖析漏洞成因,展示漏洞利用過程用到的獨特技術,對此類漏洞的危害進行了演示,我們應當重視文件系統漏洞帶來的潛在風險。
01 關于xattr
xattr是extended file attributes的縮寫,即文件擴展屬性,是文件元數據的一種。xattr獨立于文件內容存儲,由文件系統分配專用的存儲空間,可以使用xattr為文件添加額外的屬性,實現各種各樣的功能。例如Finder中的顏色標簽,就是xattr的一種應用場景。

可以通過命令xattr、mdls等查看和操作文件xattr。在代碼層面,我們可以通過getxattr、setxattr等系統調用讀取和修改xattr。
02 在FAT文件系統中兼容xattr
macOS支持很多種文件系統格式,HFS+和APFS是蘋果的私有文件系統, 它們支持xattr,這毫無疑問。此外macOS也支持FAT,這是一種比較“古老”的文件系統,僅僅提供相對簡單的文件管理功能。但是經過我的測試,FAT文件系統中,竟然也可以正常使用xattr特性,是一件很神奇的事情。
macOS內核代碼是開源的,我們可以嘗試從代碼層面分析,FAT文件系統是如何支持xattr特性的。答案在以下源文件中bsd/vfs/vfs_xattr.c,當我們調用setxattr系統調用時,FAT文件系統的相關回調函數會返回ENOTSUP,說明FAT并沒有xattr特性的原生支持。但是接下來會執行以下函數default_setxattr,這個函數為FAT等文件系統提供了xattr的一整套兼容方案。

通過閱讀代碼,我們發現,在類似于FAT這種沒有原生支持xattr的文件系統中,蘋果引入了Apple Double文件,來模擬xattr。在setxattr之后,在相同目錄下,會多出一個前綴為"._"的隱藏文件,這就是FAT中存儲xattr的位置了。但這同時也意味著,macOS需要在內核中對xattr文件進行解析,這是一個很危險的操作,如果解析不當很容易導致問題發生。
03 xattr漏洞之一(CVE-2020-27904)
首先簡單介紹一下FAT中存儲xattr的apple double文件結構,其實就是幾種類型的數據依次排序。文件頭部是apple double file header和ext attr header兩個數據結構,之后是attr entry,存儲xattr名字和屬性值在文件中的偏移,然后是xattr data。
漏洞代碼位于bsd/vfs/vfs_xattr.c這個文件中。當進行代碼審計時,注意到這個函數check_and_swap_attrhdr,它的作用是,對讀入內存的apple double文件進行校驗,確認文件結構是否合法。圖示的for循環,用來檢查存儲的xattr鍵值對是否位于有效的數據區。

但是讓我們看一下紅色標記的這一行代碼,做了兩件事情,offset和length相加,檢查是否存在整數溢出,還檢查了attr data的結尾,是否超出header指定的數據區結尾。
但是,這里存在一個問題,這段代碼沒有對offset本身做檢查,或者說沒有對data的起始地址做檢查,當offset < data_start時,attr data將會跟之前的數據結構重合,例如attr entry,ext attr header,file header!當調用setxattr設置xattr時,相當于一個寫操作,是可以更改所有這些可以重疊的數據結構的。
3.1 漏洞利用
我們必須利用setxattr覆寫file header的能力,做一些有用的事情,比如實現任意地址讀寫。讓我們查看所有涉及到這兩個header的代碼,尋找有一些有用的副作用。
首先,找到了以下一段代碼,當setxattr完成后,會通過write_xattrinfo把更改保存到文件。在寫回文件之前,會通過data_start + data_length重新計算文件大小。然而,這兩個字段,都在非法offset的覆蓋范圍之內,我們可以更改其中的任何一個,來增大文件的大小,比如增加到64mb。這樣,write_xattrinfo會遵照我們的指示,把同樣多的內存寫入到文件之中,但是apple double文件一般只會分配64kb大小的內存,如果我們要求保存64mb內存,保存的長度大于實際內存大小,就發生了越界讀操作。越界讀取的內存會保存到apple double文件之中,我們可以在用戶態讀取這個文件的內容,來探測內核的內存信息。

具體參考這張圖,這是我通過以下的代碼,實際dump到apple double文件的內容,使用十六進制顯示。綠色標記(offset+0x78)的是我偽造的一個非法offset,指向data_start字段,這可以通過篡改用戶空間的"._" apple double文件實現。文件偏移64kb開始處開始,就是我們越界讀取到的內核內存。

那么,有什么用處呢?大家都知道,現代內核中,都開啟了ASLR保護,內核信息泄漏,最直觀的用處就是可以用來探測內核內存布局。我在這個內存位置,提前布局了一個ipc_kmsg,參考kmsg的定義,當只有一個kmsg時,prev和next均指向自身,也就是這個kmsg的首地址,通過這一點,我們可以計算出自己在內存中的位置。所以,現在ASLR就不是一個問題了。
現在,我們有了一個oob-read,但是對于拿到內核權限來說,這還不夠,通常我們需要通過一個內核任意地址寫,來實現這個目標,這是這個漏洞最有挑戰性的一個點了。
已知現在可以控制兩個header中的數據,我們繼續查看代碼,觀察還有哪些有用的副作用。來到以下代碼,在VNOP_WRITE寫文件的前后,分別有一個swap_adhdr操作。這個函數做的事情比較簡單,就是把header中的整數,做了一次swap操作,也就是大小端序轉換。

這里為什么要做一個swap操作呢?apple double文件是大端序存儲的,內存中的數據與文件中的字節順序是不一致的,需要做一次端序轉換才能寫入文件。
這段代碼中有一個for循環,循環的次數來自于文件header,而我們可以任意的修改header中的數據,因此for循環的次數,我們是可以控制的!我們可以把循環次數變成一個很大的數,比如一百萬,for循環會一直持續下去,這樣我們就得到了一個越界。但是,這并不是一個oob-write,僅僅是一個oob-swap。
3.2 從oob-swap到uaf
那么,oob-swap可以做一些什么呢?
具體來看,swap操作會改變一個數字的端序,如果轉換之后依然使用小端序來解釋,那么數字的值會發生變化。你可以讓一個整數變大,也可以讓一個整數變小,這就足夠了。
這里依然要用到ipc_kmsg,首先我們把一個特定的kmsg放置在apple double內存之后,這個kmsg就是我們oob-swap的目標。
然后,我們再看一下kmsg結構,oob-swap可以改變一些什么。kmsg頭部的字段是ikm_size,是一個uint32,因為kmsg是變長的,需要使用這個字段記錄kmsg的長度,釋放時,根據這個字段的值釋放當初分配的內存。如果我們利用oob-swap,讓這個字段變大,比如0x1234 -> 0x4321,那如果我們釋放這個kmsg,實際上會多釋放一部分內存,跟隨在這個kmsg之后的其它內核對象,就被一同釋放掉了,但這個對象的引用還在,我們依然可以使用這個已被釋放掉的對象,也就是說,我們得到了一個UaF。于是,我們可以把oob-swap漏洞,轉化成一種非常有用的漏洞類型了!

但是,oob-swap操作,一次連續翻轉12個字節,并且起始位置不是4字節對齊的,因此,我們無法做到只翻轉ikm_size這個字段。實際上,我們得到的是一個10個字節的越界翻轉(綠色標記)。這意味著,我們得到了比我當初設想中更多的副作用,并且這些副作用對我們的漏洞利用是有害的。具體來講,共有ikm_size、ikm_flags、ikm_next三個字段遭到破壞。
當然,我們還是可以控制ikm_size的大小,可以順利觸發overfree的操作。但是,ikm_next是一個很重要的指針,它的損壞,會導致后續內核panic。
根據panic信息,我們找到了以下的代碼,內核在釋放kmg之前,會做一些檢查,我們必須保證,oob-swap之后ikm_next是有效的。

我們再次觀察一下,oob-swap的結果,ikm_flags的高16位,覆蓋了ikm_next指針的低16位,而大部分情況下,ikm_flags的高位是0,所以可以近似的認為,ikm_next低16位被清零了。那么我們如何避免panic呢?
考慮這么一種情況,如果kmsg分配在64kb對齊的地址處,比如0xAABB0000,由于ikm_next指向kmsg本身,也就是ikm_next指針低16位等于0。此時,即使oob-swap把它的低位清零,ikm_next依然是一個有效的指針,因為它的低位原本就是0。這樣做就可以避免后續的panic,得到一個完美的UaF。
為了實現把ipc_kmsg分配到64kb對齊的地址處(0x10000),需要對內核堆進行精確布局。我連續分配了18個大小為0x11000的kmsg,這樣做的好處是,它們的地址會依次遞增,當然了我們只關心低16位的變化,他們的地址分別為xxx1000 xxx2000 ... xxxf000 xxx0000。其中必然包含一個64kb對齊的kmsg!利用我們已經獲得的oob-read能力,可以清楚的知道是哪一個kmsg是我們需要的,如下圖所示。

接下來需要在64kb地址邊界處,精確地對kmsg進行分割,我們把連續的3個kmsg釋放掉,重新分配3段新的內存,包含一個16 page內存頁和兩個8 page內存頁。其中的16 page內存頁預留給xattrinfo使用,它會對齊到64kb,下一個8 page內存頁同樣也會對齊到64kb,這個位置用來放置目標kmsg,是oob-swap破壞的對象,我們將會利用oob-swap把它偽造成為一個16 page大小的kmsg。下一個8 page內存頁,是ool ports page,是我們overfree的對象。經過這一系列操作,然后把oob-swap破壞掉的kmsg釋放,緊隨其后得ool ports page會一并被釋放掉,我們就得到一個完美的UaF。

后面的事情就比較簡單了,可以通過一些通用的漏洞利用技術創建tfp0,獲取到內核任意地址讀寫能力,完成漏洞利用。
1、通過共享內存,在內核中偽造一個fake task,和一個fake port。
2、可以通過OSData對釋放掉的ool ports page重新占位,控制ool port的值,指向fake port。
3、receive ports,得到task port。
4、利用pft trick(pid for task),實現任意地址讀,確定kernel task和kernel map的值。
5、更新fake task,得到tfp0。
04 xattr漏洞之二(CVE-2019-8852)
這個漏洞在2019年的10.15.2版本中修復,已經有一點老了。
還是參考default_setxattr函數,有這樣一段代碼。xattr文件中存在一個特殊的屬性,com.apple.FinderInfo。當設置這個屬性時,會跳轉到以下代碼,用戶可以為這個屬性設置32字節的數據。

問題是,finderinfo的偏移地址,也是來自于文件,并且沒有對這個值的有效范圍進行檢查,當這個值大于64kb時,就會發生越界,越界寫的數據完全受我們控制。
這個漏洞提供了32字節的任意地址讀寫能力,唯一的限制是,讀寫的地址只能位于xattrinfo頁面之后。這是一個比較完美的漏洞,關于漏洞利用的過程,這里就不做過多介紹了。
05 結語
文件系統是內核的一個有效的攻擊面,歷史上這種類型的漏洞并不鮮見。文章展示的漏洞再次證明,通過文件系統漏洞對內核發起攻擊,是一種非常有效的方法,有很大的危害。因此,設計和實現一個文件系統時,需要非常的小心謹慎,對來自用戶空間的任何數據都要進行嚴格校驗。
CVE-2020-27904是一個非常有意思的漏洞,老實說它的漏洞品相并不好,但是通過我們獨特的漏洞利用技術,我們實際上可以做到漏洞的穩定利用,把它轉變成一個完美的漏洞。希望其中用到的一些思路和技術,可以給相關領域的研究者帶來啟發。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1585/
暫無評論