[譯文] 空指針的樂趣(1)

[譯文] 空指針的樂趣(1)

作者:Jonathan Corbet
原文發佈日期:July 20, 2009
來源:http://lwn.net/Articles/342330/
譯者:王旭 ( http://wangxu.me , @gnawux ) http://wangxu.me/blog/?p=39
翻譯時間:2009年9月2-3日

現在,大部分讀者都已經知道了 Brad Spengler 發佈的“the local kernel exploit”(本地內核漏洞利用)。這一漏洞會影響 2.6.30 內核(以及 RHEL5的一個測試版 2.6.18 內核),受到了多方關注。本文將詳細分析如何利用這一漏洞,以及讓這個漏洞得以成真的令人震驚的一連串錯誤。

TUN/TAP 驅動提供了一個虛擬的網絡設備,它會建立一個隧道;這一驅動在多種場合都有很有用,包括虛擬化、VPN 等很多地方。使用 TUN 時,程序通常打開 /dev/net/tun,然後使用 ioctl() 調用來建立網絡端點。Herbert Xu 近來注意到,缺少對包的審計可能會導致惡意程序能夠佔用大量的內核內存,並導致系統性能下降。他的解決方案是通過一個補丁給該設備添加一個“僞 socket”,使它可以使用內核的審計機制。問題是解決了,但是,回頭看來,它的代價是已入了一個更嚴重問題。

TUN 設備支持 poll() 系統調用。(在 2.6.30 內核中)實現這個功能的函數的開頭是這樣的:

    static unsigned int tun_chr_poll(struct file *file, poll_table * wait)
    {
	struct tun_file *tfile = file->private_data;
	struct tun_struct *tun = __tun_get(tfile);
	struct sock *sk = tun->sk;
	unsigned int mask = 0;

	if (!tun)
	    return POLLERR;

上面有下劃線的的那行代碼是 Herbert 的補丁中添加的,這正是惹禍的開始。精心編寫的內核代碼都會小心的避免對指針的解引用,以避免 NULL;事實上,這裏只在那個條件語句那裏檢查了 tun 指針。並且,這是件好事;現在看來,如果進行了 configuring ioctl() 調用,tun 確實將是 NULL。這時,按照預期,本來 tun_chr_poll() 應該返回一個錯誤狀態。

但 Herbert 的補丁添加的指針解引用是在檢查之前的,這顯然是一個 bug。在正常操作中,這個 bug 的影響會比較有限,如果 tun 是 NULL 的話,會導致內核 oops。oops 會首先殺掉進行這個系統調用的進程,並將回溯信息加入系統日誌,此外就不應該發生什麼其他的事情了。最壞情況下,這應該也就是個拒絕服務問題。

依照上述推理,這是一個小問題,雖人 NULL (0)可能確實是個合法的指針地址。缺省的,不論在用戶空間還是內核空間,虛擬地址空間的底部(“0頁”和它上面的一些頁面)都是不允許任何訪問的,以用來捕捉空指針錯誤(如上描述)。不過,使用 mmap() 系統調用將真實的內存映射到虛擬地址空間的底部仍然是可能的。這個功能有一些合法的用例,包括運行一些過時的程序。儘管如此,大部分現代的系統都通過使用 mmap_min_addr sysctl 設置來禁止映射到0頁。

安全模塊檢查被認爲可以作爲內核已經進行了的檢查的一個補充,但這次,它並沒有如願工作。

這一設置應該阻止用戶空間的程序區映射零頁,這也就保證了空指針的解引用只會導致一次內核的oops。但是,不知何故,如果安全模塊機制被配置入內核的話,2.6.30 中的 mmap() 的代碼會顯示地拒絕執行 mmap_min_addr 。取而代之的是將這個工作留給特定的安全模塊來進行。安全模塊的檢查工作被認爲是內核中已有的檢查工作的一個補充,但它此時卻並不工作。對於 0 頁,安全模塊會授權訪問,而其他情況下則會拒絕訪問。這個錯誤的最後一步是,Red Hat 的缺省 SELinux policy 允許映射 0 頁。這樣,運行 SELinux 實際是降低了系統的安全性。

但沒有 SELinux 的生活也不是就一馬平川了。在沒有 SELinux 的時候,攻擊行爲會被mmap_min_addr 限制,這似乎足夠讓一切結束了。但這是可以通過使用 personality() 系統調用繞過的。打開 SVR4 個性化會在程序被 exec() 調用的時候將一個只讀頁面映射到 0 地址,但只有進程有 CAP_SYS_RAWIO 能力的時候纔會這樣。所以,需要一個更進一步的欺詐行爲:頂級的攻擊代碼設置 SVR4 更興華,然後使用 exec 運行有特殊插件的 pulseaudio 服務器。pulseaudio 服務器是 setuid root 的,所以它將會在調用時映射到 0 頁面。當調用到插件代碼的時候,pulseaudio 將會放棄它的權限,但是,這時 0 頁已經對攻擊代碼可用了,攻擊代碼可以讓 0 頁可寫,並將其自己的數據放在這裏。

上面這些攻擊的結果就是,用戶空間進程是有可能映射0頁而不讓 tun_chr_poll() 發生內核 oops。不過,你可能會想,攻擊者還不能高興得太早,畢竟接下來 tun 就會檢查空指針。這正是這一系列錯誤中的下一個:GCC編譯器缺省會優化掉 NULL 的徹底檢驗。原因在於,因爲這個指針已經被解引用過了(而且也什麼都沒發生),所以它不可能是 NULL。所以,沒有理由再去檢查它了。於是,儘管這個邏輯本來在大部分情況下都有效,但是在 NULL 是一個合法指針的時候卻是錯誤的。

所以,攻擊者這時就能通過一個空 tun 指針而成功進入 tun_chr_poll() 內部了。接下來需要指出如何利用這種情況控制內核。tun_chr_poll() 中後面的下一步代碼是這樣的:

	if (sock_writeable(sk) ||
	    (!test_and_set_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags) &&
	     sock_writeable(sk)))
		mask |= POLLOUT | POLLWRNORM;

注意,sk 的值來自於 tun 的解引用,所以它位於攻擊者的控制之下。SOCK_ASYNC_NOSPACE 是 0,所以 test_and_set_bit() 調用可以用於設置內存中任何字的最低權重位。這是個小小的內存衝突,但這已經被證實是足夠的了,在 Brad 展示的攻擊代碼中,sk->sk_socket->flags 指針指向了 TUN 驅動的 file_operations 結構;特別的,它是指向了 mmap() 函數。TUN驅動不支持 mmap() 調用,所以這個指針通常應該是 NULL,在 poll() 調用之後,它就是 1 了。

攻擊代碼的最後一步就是調用這個打開的 TUN 設備的文件描述符的 mmap() 調用。由於內部的mmap() 已經不是空了(剛剛被我們設置成了 1),內核將會跳到哪裏。那個地址已經在攻擊代碼所映射的0頁面中了,所以,它在攻擊者的控制之下。於是,攻擊代碼使用下一個跳轉跳到其自己的代碼處即可。這樣,當內核調用(它以爲的)TUN 驅動的 mmap() 函數的時候,結果就是任意代碼都可以在內核模式下運行;這裏,攻擊代碼獲得了完全的控制權。

在一個良好設計的系統中,一個單獨的錯誤很少導致災難性的故障。而這裏就是這樣一個例子。很多東西都出錯才導致了這個攻擊成爲可能:安全模塊能夠不顧系統策略而授權訪問地位內存,SELinux 策略允許這些映射,pulseaudio 可以被攻擊代碼利用從而讓這一映射可以被攻擊代碼使用,空指針在解引用之前未被檢驗,並且檢驗被編譯器優化掉了,代碼以某種方式使用空指針可以獲取系統的控制權。這是一條長長的錯誤鏈,其中的每一環節都是讓這一攻擊成功的必要條件。

這個漏洞如今已經被關閉了,不過幾乎可以肯定還有類似的問題。本系列的下一篇文章將會介紹內核開發者們如何應對這一攻擊行爲。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章