6月1號(hào),我提交了一個(gè)linux內(nèi)核中的任意遞歸漏洞。如果安裝Ubuntu系統(tǒng)時(shí)選擇了home目錄加密的話,該漏洞即可由本地用戶觸發(fā)。如果想了解漏洞利用代碼和短一點(diǎn)的漏洞報(bào)告的話,請(qǐng)?jiān)L問https://bugs.chromium.org/p/project-zero/issues/detail?id=836。
背景知識(shí)
在Linux系統(tǒng)中,用戶態(tài)的棧空間通常大約是8MB。如果有程序發(fā)生了棧溢出的話(比如無限遞歸),棧所在的內(nèi)存保護(hù)頁(yè)一般會(huì)捕捉到。
Linux內(nèi)核棧(可以用來處理系統(tǒng)調(diào)用)和用戶態(tài)的棧很不一樣。內(nèi)核棧相對(duì)來說更短:32位x86架構(gòu)平臺(tái)為4096byte , 64位系統(tǒng)則有16384byte(內(nèi)核棧大小由THREAD_SIZE_ORDER 和 THREAD_SIZE 確定)。它們是由內(nèi)核的伙伴內(nèi)存分配器分配,伙伴內(nèi)存分配器是內(nèi)核常用來分配頁(yè)大?。ㄒ约绊?yè)大小倍數(shù))內(nèi)存的分配器,它不創(chuàng)建內(nèi)存保護(hù)頁(yè)。也就是說,如果內(nèi)核棧溢出的話,它將直接覆蓋正常的數(shù)據(jù)。正因如此,內(nèi)核代碼必須(通常也是)在棧上分配大內(nèi)存的時(shí)候非常小心,并且必須阻止過多的遞歸。
Linux上的大多數(shù)文件系統(tǒng)既不用底層設(shè)備(偽文件系統(tǒng),比如sysfs, procfs, tmpfs等),也不用塊設(shè)備(一般是硬盤上的一塊)作為備用存儲(chǔ)設(shè)備。然而, ecryptfs 和overlayfs是例外。這兩者是堆疊的文件系統(tǒng),這種文件系統(tǒng)會(huì)使用其他文件系統(tǒng)上的文件夾作為備用存儲(chǔ)設(shè)備(overlayfs則使用多個(gè)不同文件系統(tǒng)上的多個(gè)文件作為備用存儲(chǔ)設(shè)備)。被用作備用存儲(chǔ)設(shè)備的文件系統(tǒng)稱為底層文件系統(tǒng),其上的文件稱為底層文件。這種層疊文件系統(tǒng)的特點(diǎn)是它或多或少的會(huì)訪問底層文件系統(tǒng),并對(duì)訪問的數(shù)據(jù)做一些修改。 Overlayfs融合多個(gè)文件系統(tǒng),ecryptfs則進(jìn)行了相應(yīng)的加密。
層疊文件系統(tǒng)實(shí)際上存在潛在風(fēng)險(xiǎn),因?yàn)槠湓L問虛擬文件系統(tǒng)的函數(shù)常會(huì)訪問到底層文件系統(tǒng)的函數(shù),相較直接訪問底層文件系統(tǒng)的句柄,這會(huì)增大??臻g??紤]這樣一個(gè)場(chǎng)景:如果用層疊文件系統(tǒng)作為另外一個(gè)層疊系統(tǒng)的備用存儲(chǔ)設(shè)備,由于每一層文件系統(tǒng)的堆疊都增大了??臻g,內(nèi)核棧就會(huì)在某些情況下溢出。但是,設(shè)置FILESYSTEM_MAX_STACK_DEPTH 限制文件系統(tǒng)的層數(shù),只允許最多兩層層疊文件系統(tǒng)放在非層疊文件系統(tǒng)上,就可以避免這個(gè)問題。
在Procfs偽文件系統(tǒng)上,系統(tǒng)中運(yùn)行的每一個(gè)進(jìn)程都有一個(gè)文件夾,每個(gè)文件夾包含一些描述該進(jìn)程的文件。值得注意的是每個(gè)進(jìn)程的“mem”,“ environ”和“cmdline”文件,因?yàn)樵L問這些文件會(huì)同步訪問目標(biāo)進(jìn)程的虛擬內(nèi)存。這些文件顯示了不同的虛擬內(nèi)存地址范圍:
1.“mem”文件顯示了整個(gè)虛擬內(nèi)存地址范圍(需要PTRACE_MODE_ATTACH 權(quán)限)
2.“environ”文件顯示了mm->env_start 到mm->env_end的內(nèi)存范圍(需要PTRACE_MODE_READ權(quán)限)
3.“cmdline”文件顯示了mm->arg_start 到mm->arg_end的地址范圍(如果mm->arg_end的前一個(gè)字符是null 的話)
如果可以用mmap()函數(shù)映射“mem”文件的話(啥意義也沒有,別想太多),就可以映射成如下圖所示的樣子:
接下來,假設(shè)/proc/$pid/mem的映射有一些錯(cuò)誤,那么在進(jìn)程C里的內(nèi)存讀取錯(cuò)誤,將會(huì)導(dǎo)致從進(jìn)程B中映射的內(nèi)存出錯(cuò),進(jìn)而導(dǎo)致進(jìn)程B里出現(xiàn)其它的內(nèi)存錯(cuò)誤,進(jìn)而導(dǎo)致從A進(jìn)程映射的內(nèi)存出錯(cuò),這就是一個(gè)遞歸內(nèi)存錯(cuò)誤。
可是,現(xiàn)實(shí)中這是不可行的,“mem”,“environ”,“cmdline ”文件只能用VFS函數(shù)讀寫,mmap無法使用:
staticconst struct file_operations proc_pid_cmdline_ops = {
.read = proc_pid_cmdline_read,
.llseek = generic_file_llseek,
};
[...]
staticconst struct file_operations proc_mem_operations = {
.llseek = mem_lseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};
[...]
staticconst struct file_operations proc_environ_operations = {
.open = environ_open,
.read = environ_read,
.llseek = generic_file_llseek,
.release = mem_release,
};
相關(guān)ecryptfs文件系統(tǒng),比較有趣的一個(gè)細(xì)節(jié)在于它支持mmap()。用戶看到的內(nèi)存映射必須是解密的,而底層文件系統(tǒng)的內(nèi)存映射是加密的,因而ecryptfs 文件系統(tǒng)不能將mmap()函數(shù)直接映射到底層文件系統(tǒng)的mmap()函數(shù)上。Ecrypt 文件系統(tǒng)在內(nèi)存映射時(shí)使用了自己的頁(yè)緩存。
ecryptfs文件系統(tǒng)處理頁(yè)錯(cuò)誤的時(shí)候,必須以某種方式讀取底層文件系統(tǒng)上加密的頁(yè)。這可以通過讀取底層文件文件系統(tǒng)的頁(yè)緩存(使用底層文件系統(tǒng)的mmap函數(shù))來實(shí)現(xiàn),但是這樣比較消耗內(nèi)存。于是它直接使用底層文件系統(tǒng)的 VFS讀取函數(shù)(通過kernel_read()),這樣做更加直接有效,但是這個(gè)做法有副作用,就是有可能會(huì)mmap() 到通常不能映射的解密后的文件(因?yàn)橹灰讓游募凶x權(quán)限并且包含合法的加密數(shù)據(jù), ecryptfs文件系統(tǒng)的mmap函數(shù)就能工作)。
漏洞分析
在此,我們就能描繪完整的攻擊方式了。首先創(chuàng)建一個(gè)進(jìn)程A,進(jìn)程號(hào)為$A。然后創(chuàng)建一個(gè)ecrypptfs 掛載/tmp/$A,使/proc/$A作為它的底層文件系統(tǒng)(ecryptfs 應(yīng)該只有一個(gè) key,這樣文件名才不會(huì)被加密)?,F(xiàn)在,如果/proc/$A下相應(yīng)的文件有合法的ecryptfs 文件頭的話,那么 /tmp/$A/mem, /tmp/$A/environ 和 /tmp/$A/cmdline就可以被映射。除非有 root 權(quán)限,否則無法將內(nèi)存映射到進(jìn)程 A的00處,也就是 /proc/$A/mem 的開頭。因此從開始讀取 /proc/$/A 總是會(huì)返回-EIO,而且 /proc/$A/mem 不會(huì)有一個(gè)合法的 ecryptfs 文件頭。如此,environ 和 cmdline 文件才有攻擊的可能性。
在使用CONFIG_CHECKPOINT_RESTORE編譯的內(nèi)核(至少是Ubuntu的 distro 內(nèi)核)中,非特權(quán)用戶可以通過prctl(PR_SET_MM, PR_SET_MM_MAP, &mm_map,sizeof(mm_map), 0)設(shè)置mm_struct 中的 arg_start, arg_end, env_start 和 env_end值。這使得映射 /proc/$A/environ 和 /proc/$A/cmdline到任意虛擬內(nèi)存范圍成為可能。(不支持checkpoint-restore的內(nèi)核中,攻擊過程就稍微有點(diǎn)麻煩,但使用所需的參數(shù)區(qū)域和環(huán)境變量的長(zhǎng)度重新執(zhí)行,然后取代部分??臻g的映射,還是有可能的。)
如果一個(gè)有效加密的ecryptfs文件被加載到進(jìn)程A的內(nèi)存中,并且它的環(huán)境變量也被配置為指向這塊區(qū)域,那么環(huán)境變量區(qū)域里的解密形式的數(shù)據(jù)就可以在 /tmp/$A/environ文件中獲取。這個(gè)文件也可以被映射到進(jìn)程B的內(nèi)存中。為了能夠重復(fù)該進(jìn)程,某些數(shù)據(jù)需要反復(fù)加密,進(jìn)而創(chuàng)建一個(gè)加密的matroska 文件,并將這個(gè)文件加載到進(jìn)程 A的內(nèi)存中。這樣一來,映射互相進(jìn)程解密環(huán)境變量區(qū)域的進(jìn)程鏈就建立起來了:
如果映射到進(jìn)程C和進(jìn)程B的內(nèi)存相應(yīng)范圍內(nèi)沒有數(shù)據(jù),進(jìn)程C 中的內(nèi)存錯(cuò)誤(這個(gè)內(nèi)存錯(cuò)誤可能是用戶空間產(chǎn)生也可能是由于用戶空間訪問內(nèi)核空間,比如通過copy_from_user()函數(shù))將會(huì)導(dǎo)致ecryptfs讀取 /proc/$B/environ ,進(jìn)而導(dǎo)致進(jìn)程B中的內(nèi)存錯(cuò)誤,接下來導(dǎo)致ecryptfs讀取 /proc/$A/environ ,最后導(dǎo)致進(jìn)程A中的進(jìn)程錯(cuò)誤。如此循環(huán)往復(fù),最終溢出內(nèi)核棧,使內(nèi)核崩潰。內(nèi)核棧如下:
[...]
[
[
[
[
[
[
[
[
[
[
[
[
[
[
[
[
[
[
[
[
[
[
[
[...]
關(guān)于這個(gè)漏洞的可利用性:利用該漏洞,需要能夠掛載/proc/$pid為ecryptfs文件系統(tǒng)。安裝完ecryptfs-utils包之后(如果安裝Ubuntu時(shí)選擇了home目錄加密, Ubuntu 會(huì)自動(dòng)安裝),使用 /sbin/mount.ecryptfs_私有的setuid輔助函數(shù)就可以做到這一點(diǎn)。
漏洞利用
接下來的描述是平臺(tái)相關(guān)的,這里指amd64。
以前要利用這一類漏洞還是相當(dāng)簡(jiǎn)單的,可以直接覆蓋棧底的thread_info結(jié)構(gòu)體,用合適的數(shù)值重寫restart_block或者 addr_limit,然后根據(jù)所用方式,選擇執(zhí)行用戶空間映射的代碼,還是用copy_from_user() 和 copy_to_user() 直接讀寫內(nèi)核數(shù)據(jù)。
但是,restart_block已經(jīng)從thread_info結(jié)構(gòu)體中移除,并且由于棧溢出觸發(fā)時(shí)棧中有 kernel_read() 的棧幀,所以addr_limit已經(jīng)是KERNEL_DS,而且函數(shù)退出時(shí)將會(huì)重置成 USER_DS 。另外, Ubuntu 16.04以后的內(nèi)核都打開了CONFIG_SCHED_STACHK_END_CHECK 內(nèi)核配置選項(xiàng)。打開這個(gè)選項(xiàng)以后,每次調(diào)度到這個(gè)線程時(shí), thread_info 結(jié)構(gòu)體上方的金絲雀值都會(huì)被檢查;如果金絲雀值不正確的話,內(nèi)核遞歸就會(huì)出錯(cuò)然后崩潰。
由于thread_info結(jié)構(gòu)體中很難照到有價(jià)值的攻擊目標(biāo)(同時(shí)移除thread_info中的數(shù)據(jù)并非有效的緩解措施),我就選擇了其它方式:溢出棧到棧之前的空間,然后利用棧和其它內(nèi)存空間之間會(huì)重合這一點(diǎn)。這種方式的問題就是一定要保證金絲雀值和 thread_info結(jié)構(gòu)中的其它成員不被覆蓋。棧溢出的內(nèi)存布局如下所示(綠色表示可以覆蓋,紅色表示不能覆蓋,黃色表示覆蓋后可能會(huì)有問題):
幸運(yùn)的是,有些棧幀中存在空洞(如果遞歸的最底部采用cmdline而不是environ),遞歸的過程中就會(huì)有一個(gè)5個(gè)QWORD空洞沒有被訪問到。這些空洞足夠用來存放從SRACK_END_MAIC到flags的所有數(shù)據(jù)。這一點(diǎn)可以通過一個(gè)安全遞歸和一個(gè)內(nèi)核調(diào)試模塊來實(shí)現(xiàn),這個(gè)內(nèi)核調(diào)試模塊將棧中的所有空洞標(biāo)綠便于觀察:
接下來的問題是空洞只會(huì)出現(xiàn)在特定的位置,而漏洞利用就需要空洞在準(zhǔn)確的位置出現(xiàn)。下面有一些技巧可以用來對(duì)齊棧空間:
1.在每個(gè)遞歸層上都可以選擇“environ”文件或者“cmdline”文件,它們的棧幀大小和空洞模式都不一樣。
2.任何調(diào)用copy_from_user()都會(huì)導(dǎo)致內(nèi)存錯(cuò)誤。甚至可以將寫入系統(tǒng)調(diào)用和VFS寫入句柄結(jié)合起來,所以每一個(gè)寫入系統(tǒng)調(diào)用和 VFS寫入句柄都會(huì)影響深度(合并深度可以計(jì)算出來,而不用測(cè)試每個(gè)變量)。
在測(cè)試了各種組合之后,我找到一組environ文件和cmdline文件, 還有write ()系統(tǒng)調(diào)用和進(jìn)程的VFS寫句柄的組合。
隨后,就可以遞歸到之前分配的空間,而不會(huì)覆蓋任何危險(xiǎn)數(shù)據(jù)了。然后暫停內(nèi)核線程的執(zhí)行,此時(shí)棧指針指向之前分配的內(nèi)存空間,這些內(nèi)存空間應(yīng)該用新的棧來覆蓋,然后繼續(xù)內(nèi)核線程的執(zhí)行。
為了暫停遞歸中內(nèi)核線程的執(zhí)行,在建立起映射鏈后,映射鏈最后的annonymous映射可以用FUSE映射取代( userfaultfd 函數(shù)并不適用,它不能捕捉遠(yuǎn)程的內(nèi)存訪問)。
對(duì)于先前分配的內(nèi)存,我的exp使用管道(Pipes)。當(dāng)寫入數(shù)據(jù)到新分配的空管道時(shí),伙伴內(nèi)存分配器會(huì)分配一個(gè)內(nèi)存頁(yè),來存放這些數(shù)據(jù)。我的exp通過管道內(nèi)存頁(yè)分配來填充大量?jī)?nèi)存,所以使用clone()創(chuàng)建新進(jìn)程時(shí)就會(huì)觸發(fā)內(nèi)存錯(cuò)誤。這里使用clone() 而非fork(),因?yàn)檎{(diào)用clone()時(shí)只要控制好參數(shù),系統(tǒng)就會(huì)復(fù)制較少的信息,可以減少內(nèi)存分配的干擾。 Clone( ) 函數(shù)調(diào)用過程中,所有的管道內(nèi)存頁(yè)都被填充滿,除了第一次保存的 RIP值——遞歸進(jìn)程暫停在FUSE中時(shí),它保存在期望的 RSP 值之后。寫入較少的數(shù)據(jù)就能致使第二個(gè)管道寫入目標(biāo)棧數(shù)據(jù),這些數(shù)據(jù)在 RIP控制實(shí)現(xiàn)之前就被使用,可能會(huì)導(dǎo)致內(nèi)核崩潰。隨后,遞歸進(jìn)程在FUSE 中暫停時(shí),第二次向所有管道寫入數(shù)據(jù),會(huì)覆蓋保存的 RIP值和其后的數(shù)據(jù),攻擊者也就能夠完全控制全新的棧了。
此時(shí),最后一道防線就是KASLR了。Ubuntu支持KASLR ,只不過KASLR需要手動(dòng)開啟。這個(gè)b最近該BUG已經(jīng)修復(fù)了,現(xiàn)在distros內(nèi)核應(yīng)該是默認(rèn)就開啟KASLR的。雖說這項(xiàng)安全特性幫不上太大的忙,但畢竟KASLR不需要占用太多資源,開啟這項(xiàng)特性就顯得相當(dāng)理所當(dāng)然了。由于大多數(shù)的設(shè)備并不支持向內(nèi)核命令行傳輸特殊參數(shù),所以這里假設(shè)KASLR雖然編譯進(jìn)了內(nèi)核,但仍處于未激活狀態(tài),攻擊者也知道內(nèi)核代碼和靜態(tài)數(shù)據(jù)的地址。
然后就可以用ROP在內(nèi)核里做各種事情了,漏洞利用具體有兩個(gè)方向可以繼續(xù)。可以使用ROP進(jìn)行 commit_creds 類似操作。不過我用了另一個(gè)方法。在棧溢出過程中,原來addr_limit的KERNEL_DS 值保存了起來。棧一次次返回,最終將會(huì)把 addr_limit 重置為USER_DS。但如果我們直接返回到用戶空間, addr_limit 將保持 KERNEL_DS 。所以我這樣構(gòu)造新棧,或多或少?gòu)?fù)制了棧頂?shù)臄?shù)據(jù):
unsigned longnew_stack[] = {
0xffffffff818252f2,/* return pointer of syscall handler */
/* 16 uselessregisters */
0x1515151515151515,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
(unsignedlong) post_corruption_user_code, /* user RIP */
0x33, /* userCS */
0x246, /*EFLAGS: most importantly, turn interrupts on */
/* user RSP*/
(unsignedlong) (post_corruption_user_stack + sizeof(post_corruption_user_stack)),
0x2b /* userSS */
};
殺掉FUSE服務(wù)進(jìn)程后,遞歸進(jìn)程繼續(xù)運(yùn)行到post_corruption_user_code函數(shù)上。這個(gè)函數(shù)可以使用管道向任意內(nèi)核地址寫數(shù)據(jù),因?yàn)?copy_to_user()中的地址檢查已經(jīng)失效。
voidkernel_write(unsigned long addr, char *buf, size_t len) {
int pipefds[2];
if (pipe(pipefds))
err(1, "pipe");
if (write(pipefds[1], buf, len) != len)
errx(1, "pipe write");
close(pipefds[1]);
if (read(pipefds[0], (char*)addr, len) !=len)
errx(1, "pipe read tokernelspace");
close(pipefds[0]);
}
現(xiàn)在你就可以在用戶態(tài)舒服地執(zhí)行任意讀寫操作了。如果你想要root shell,可以覆蓋coredump函數(shù),它存儲(chǔ)在一個(gè)靜態(tài)變量里,然后觸發(fā)一個(gè) SIGSEGV,就可以以root權(quán)限執(zhí)行coredump函數(shù):
char*core_handler = "|/tmp/crash_to_root";
kernel_write(0xffffffff81e87a60,core_handler, strlen(core_handler)+1);
漏洞修復(fù)
有兩個(gè)獨(dú)立的補(bǔ)丁可用于修復(fù)該BUG:其中,2f36db710093禁止通過ecryptfs打開沒有mmap函數(shù)的文件, e54ad7f1ee26禁止在procfs 上層疊任何東西,因?yàn)榈拇_沒什么道理要在其上層疊任何東西。
不過,我還是寫了一個(gè)完整的root提權(quán)漏洞利用程序。我主要想說明linux棧溢出可能會(huì)以非常隱蔽的方式出現(xiàn),即便開啟了一些現(xiàn)有的漏洞緩解措施,它們?nèi)匀豢衫?。在我寫的漏洞?bào)告中,我有提到給內(nèi)核增加內(nèi)存保護(hù)頁(yè),移除棧底部的 thread_info結(jié)構(gòu)體,這樣緩解這類漏洞的利用,有其他操作系統(tǒng)就是這么干的。Andy Lutomirski已經(jīng)開始著手這方面的工作,并發(fā)布了增加了內(nèi)存保護(hù)頁(yè)的補(bǔ)丁包: https://lkml.org/lkml/2016/6/15/1064。
* 本文譯者:Michael23,文章參考來源:Blogspot,轉(zhuǎn)載請(qǐng)注明來自FreeBuf黑客與極客(FreeBuf.COM)