如果一個程序有錯誤,并且只在某些特殊的情況下面出現,它并不是什么大問題。通常,你能夠避開這些特殊的情況,使得程序中的錯誤故障不會發生危害。你甚至可以按照你的意愿,在你的程序中加入這些小小的“臭蟲”。
但是,有時有些程序處于安全界限的邊緣位置。他們從另外的程序作為輸入,但不是按照程序本來的存取方法。
我們常見的一些例子:你從的郵件閱讀器讀取任何人給你發的郵件,然后顯示在你的顯示器上,而它們本不應當這樣做的。任何被接在因特網上的計算機的TCP/IP棧都從因特網上的獲得任何人的輸入信息,并且能夠直接存取你的計算機,而你的網上鄰居們確不能這樣。
任何具有這種功能的程序都必須小心對待。如果在其中有任何的小錯誤,它就能在允許任何人-未被授權的人做任何的事情。具有這種特性的小“臭蟲”被叫做“漏洞”或者更正式地被叫做“弱點”。
這里有一些漏洞的共同特點。
心理學上問題
當你寫軟件的正常部分的時候,如果用戶的操作是正確的,那你的目的是完成這件事。當你寫軟件的安全敏感部分的時候,你一定要使得任何沒有被信任的用戶都不可能完成操作。這意味著你的程序的很大部分必須在很多情況下功能正常。
編制加密和實時程序的程序員精于此道。而最其程序員則由于他們的通常的工作習慣使得他們的于使他們的軟件從未考慮安全的因素,換而言之,他們的軟件是不安全的。
變換角色漏洞
很多漏洞是從不同的運行著的程序中發現的。有時是一個極小的錯誤或者及其普通的錯誤也會造成安全漏洞。
例如,假設你有本來打算讓你在打印你的文檔之前想通過PostScript解釋器預覽它。這個解釋器不是安全敏感的程序;如果你不用它,它一點也不會成為你的麻煩。但是一旦你用它來處理從別人那里得到的文件,而那個人你并不知道也不值得信任。這樣你就可能招致很多麻煩。他人可以向你發送能刪除你所有文件或者復制你所有文件到他人可以得到的地方的文檔。
這是大部分Unix TCP/IP棧的脆弱性的根源-它是在網絡上的每個人都值得信任的基礎上開發的,而被應用在這個并不如當初所想象安全的環境中。
這也是Sendmail所發生的問題的根源。直到它通過審查,它一直是很多是漏洞的根源。
再更進一步講,當函數在合理的范圍內使用時是安全的,如果不這樣的話,他們將造成無法想象的災難。
一個最好的例子就是gets()。如果你在你控制輸入使用gets()函數,而你正好輸入比你預定輸入大得多的緩沖區,這樣,你就達到了你的目的。對付這個得最好的補丁就是不要做類似這樣的事或者設定比原先大的多的緩沖區,然后重新編譯。
但是,當數據是來自非信任的數據源的時候,gets()能使緩沖器溢出,從而使程序能做任何事情。崩潰是最普通的結果,但是,你通常能地精巧地安排使得數據能象代碼一樣執行。
這就是它所帶給我們的...
緩沖區溢出漏洞
當你往數組寫入一個字符串,并且越過了數組邊界的時候,會發生緩沖器溢出。
幾個能引起安全問題的緩沖器溢出情況:
1.讀操作直接輸入到緩沖區;
2.從一個大的緩沖區復制到一個小的緩沖區;
3.對輸入的緩沖區做其他的操作。
如果輸入是可信的,則不成為安全漏洞,但也是潛在的安全隱患。這個問題在大部分的Unix環境中很突出;如果數組是一些函數的局部變量,那么它的返回地址很有可能就在這些局部變量的堆棧中。這樣就使得實現這種漏洞變得十分容易,在過去的幾年中,有無數漏洞是由此造成的。
有時甚至在其他的地方的緩沖區都會產生安全漏洞——尤其是他們在函數指針附近。
需要尋找的東西:
沒有任何邊界檢查的危險的函數:strcpy,strlen,strcat,sprintf,gets;
帶邊界檢查的危險的函數:
strncpy snprintf--這些忽視字符串結尾標記的函數往往會把其他(可能是敏感的)數據復制到緩沖區中,這樣就有可能破壞程序;strncat不存在這問題,但對于snprintf不敢肯定,而strncpy是絕對存在的;錯誤地使用strncat,這樣會把一個空的字節放在數組的后面;
安全-敏感程序的崩潰--任何崩潰都來自指針錯誤,而最多的這類錯誤來源于緩沖區溢出。
嘗試給安全敏感程序輸入大的的輸入——在環境變量中(如果環境變量沒有被信任),在命令行參數中(如果命令行參數沒有被信任),在未被信任的網絡連接上讀取了在未被信任的文件。如果他們把這些輸入解釋成為很多段,并試圖利用這些龐大的數據段。這時,你就要當心系統或者程序的崩潰了。如果你遇到崩潰了,看看是否在你輸入的地方發生。
不正確邊界檢查。如果有好幾百行代碼都做邊界檢查,而不是被集中在兩三處地方,那么其中出錯的可能性會很大。
一個全面安全解決方案是用帶邊界檢查的編譯系統重新編譯所有的安全敏感程序。
就我所知,Richard W. M. Jones 和Paul Kelly為gcc寫的帶邊界檢查的版本是業界的第一個工程??梢栽趆ttp:www.doc.ic.ac.uk/~phjk/BoundsChecking.html找到。
Greg McGary()做了一些其他的工作。
。
Richard Jones和Herman ten Brugge做了其他的工作。
。
Greg在中比較了不同的實現方法?;靵y的代表當你讓一個常規程序去打開一個文件,程序會請求操作系統打開此文件。由于這個程序是以你的權限運行的,如果你沒有權限打開這個文件,系統就會拒絕你的請求。
但是,如果你讓安全敏感程序打開——CGI腳本,一個setuid程序,一個setgid程序,任一網絡服務程序——它不能依靠系統內置的的自動保護。因為它能做一些你不能做的事情。就以web網絡服務器來說,它能做,而你不能做的事是非常少的。但是它至少能讀取一些文件的私人信息。
很多這樣程序會在他們收到的數據上做某種檢查。但常常有以下的幾種缺陷:
* 由于它們的檢查具有時間依賴性,所以存在競爭條件。如果一個程序首先對一個文件用stat()來核查你是否具有寫的權限,然后(假設你是這樣做了)打開它,這樣你就很有可能同時能改變這個文件,盡管你本來是沒有這個權限。(一種可行的解決方案是在打開文件之前就對文件stat()或lstat(),以非破壞的打開方式打開文件,用fstat()來帶開文件句柄,然后比較你是否是對同一個文件stat()。)
* 它們會通過分析文件名來檢查,但是它們的檢查和操作系統的又不一樣。這個在很多Microsoft操作系統的web服務器上有很大的問題;這些操作系統對文件和它們的關聯不會做很嚴格的分析。Web服務器通過查看文件名來決定你的下一步的動作;通常,你可以運行一些特定的文件(基于文件名的分析),但是你不能讀取它們。如果默認的操作允許你讀取一個文件,然后改變文件名使得服務器認為它是另一種類型的文件,但是操作系統還是認為它是本來的文件,這樣你就可以成功地讀取那個文件了。
* 它們會用及其復雜的方法檢查,但是由于設計者的無知,這種方法帶有漏洞。
* 它們只進行相當普通的檢查。
* 它們檢查相當簡單。比如,很多老式的Unix Web服務器能夠讓你下載用戶public_html目錄除非操作系統禁止這樣做。如果你做了符號連接或硬連接的話到其他人的目錄的話,就有可能在Web服務器允許的情況下,下載他人的目錄。
在任何情況下,如果程序由于你的權限限制而不能完成時,可以使用setfsuid(), setreuid()來幫助。
另一個問題是標準庫頻繁地查看用來打開文件的環境變量,而且在進行這些操作的時候沒有改變他們的權限(事實上,他們也不能。)。這樣,我們被迫去分析文件名以便知道它是否合理。
一些操作系統會用錯誤的權限core dump,如果你能使setuid的程序崩潰,你就能象文件主人一樣改寫文件。
Fail-openness
最安全敏感的系統不能在一定條件下做正確的事情。他們能用兩種不同的方法失?。?
* 當他們允許存取的時候,動作被拒絕;這被叫做fail-open。
* 當他們不允許存取的時候,動作被拒絕;這被叫做fail-closed。
CGI腳本通常要執行另外一個程序來處理傳遞給他們的命令行用戶數據。為了避免這些數據被shell當成指令來解釋,CGI腳本會除掉特殊字符如‘<’‘|’‘“以及其他等等。你能以fail-open方法通過有被除掉的“壞特性”的一覽表來過濾這些字符。如果漏了其中的一個,這個就成為一個安全漏洞了。你還可以通過fail-closed的方法制作一個“好特性的”一覽表來是合法的字符通過。這樣,即便是忘了一個,也只不過是一個小麻煩,而不會構成安全上的漏洞。例子(Perl在中)在
與Fail-open的系統相比,Fail-closed的系統得不是很方便,尤其是失敗比較頻繁的情況下,但比較安全。
基本上所有我看到的為了防護MAC或MICROSOFT操作系統的臺式計算機的程序都是fail-open類型的。-如果你是這個程序停止工作,你就能完全控制計算機。相對應,如果你使Unix‘login’程序停止工作,任何人都不能控制計算機了。
吞噬資源
許多程序在編制的時候都假定系統資源是足夠使用的(看上面心理學上的問題)。很多程序從不考慮資源不夠的情況,因此這些程序經常出錯。
經常性需要檢查的情況是:
* 在存儲器不夠或內存分配出錯的情況下,調用malloc或者new通常會返回一個空的指針。
* 如果未信任的用戶能用盡系統的資源,(這個也是拒絕服務的一種,但也是很多程序的通?。?
* 如果可供打開的文件句柄已經用盡--調用open()會返回-1。
* 如果程序不能fork或者子進程在初始化的過程中由于吞噬資源而僵死。
信賴未經確信的信道
如果你在以太網上傳送明文的密碼,而同一個網段上有不可靠的人存在,或者如果你建立了一個全球可寫的文件,然后試圖從那個文件中讀取數據,或者你用O_TRUNC而不是O_EXCL在/tmp目錄下創建文件等等,總之你正在依賴不可靠的傳輸媒介上完成你的工作。在這種情況下,如果一個攻擊者能破壞這種不可靠的信道,他們就可以在你毫不知覺的情況下更改信道/媒介中的數據,(最壞的情況是他們在/tmp目錄下對被信任的文件做符號連接,這樣就可以破壞受權限保護文件的內容,而不是建立一個臨時文件,gcc也有類似的漏洞,這個漏洞將使在你編譯的程序中插入任何代碼。),即便他們不能破壞任何數據,他們也可以非法讀取受權限保護的數據。
錯誤的默認設定
如果有些默認的設定存在不明顯但是不安全的情況,很容易被人們所忽視。例如,如果你解rpm包然后建立一些全球可寫的配置文件,你不太會注意到它們,除非你非常仔細地尋找安全漏洞。這就說明人們在解rpm包的時候,會在系統上留下安全漏洞。
大接口
如果安全接口非常小,則整個系統安全性能會比接口大的好。它大非常為了安全比條件有可能。這個很容易理解,比如,我的房子只有一扇門,人們可以通過這扇門進入我的屋子,而在我睡覺的時候,我能記得區把它鎖住。如果我房子的不同的部分中有五扇門,全部都能通往外部,我很有可能忘了鎖它們之中的一個。
因此,網絡服務器看來于比setuid程序更安全。Setuid程序從所有的不可靠的來源接受信息-環境變量,文件描述符,虛擬存儲器映像,命令行參數和其他文件輸入。網絡服務器程序只從網絡socket獲得輸入(也有可能從文件)。
qmail是小的安全接口的例子。僅僅qmail很小一部分(有十多行的程序,比我以前在linux-security-audit郵件發送清單上說多)部分是以“root”的權限運行的。余下的或是以特殊的qmail用戶或者以郵件接受者的權限在運行。
在qmail內部,緩沖器-溢出的檢查被集中在兩個小函數中,其他全部的函數調用這兩個函數來修改檢查字符串。這是另一個安全接口小的例子——一些檢查部分錯誤的機會會更小。
你運行著更多的網絡守護程序,那么將擴大網絡和你的機器之間的安全接口。
如果你有Internet防火墻,你的網絡和因特網之間的安全接口被減少到1臺機器。
另外,看不可靠的HTML主頁和不可靠的JavaScript腳本也有安全接口的差異;JavaScript腳本解釋器的安全接口比HTML里的要大而復雜。
經常被突破的程序
在過去經常被突破的程序在他們的后續版本中也可能有漏洞存在,因此需要在可能的情況下替換他們。在BSD系統中用mail.local替換/bin/mail就是因為這個原因。
如果你想檢查它的代碼,當然這個是不錯的主意,但是更好的辦法是改寫代碼或者不使用他們。
薄弱的安全部件
任何安全的系統都可以被分為安全部件。例如,我的Linux系統把為數眾多的部件分為“用戶”、“內核”、“網絡”,而“網絡”還可以被分為一些諸如“網絡連接”等子部件。在這些根據系統安裝和認證過程而在各個部件之間建立了很好的信任關系。(比如我的用戶,kragen在我把我的口令發送出去后,會信賴我的網絡連接。)
必須加強所有安全部件之間測信任關系。如果你運行圖書館終端,或許你希望終端僅僅存取圖書館的數據庫(并且只能讀)。你根本就不想為他們提供Unix的shell。
而Mirabilis ICQ對因特網的用戶都給予信任,顯然這個是不安全的。
在另一方面,tcp_wrappers信任從反向DNS查詢得到的結果,然后交由shell處理把它交給外殼。在使用Netscape Communicator以squid做代理的時候,會把歷史一覽表中用戶FTP口令放入URL。JavaScript程序和其他的web服務器能看到這個URL。
被忽略的情況
不信任邏輯。由于驗證很困難,所以if-else和swith-case語句是危險的。如果你能發現任何人永遠執行不到的代碼分支,它極有可能含有錯誤。如果你能發現邏輯上的數據流的合并——例如,如果有兩條語句,每個做兩個事情中的一個,然后第一個的輸出被送到第二個的輸入--如果這些代碼沒有經過測試,那有可能含有漏洞。
查看條件語句中的else和查看switch語句中的default以確保他們是fail-closed的。
gcc-pg-a會使程序產生一個bb.out文件用來幫助你確定是否代碼中的所有分支都被執行到了。我認為這些都是最近的IP拒絕服務攻擊問題的根源。
低級錯誤
許多人們信賴只有少數人檢查的代碼。如果軟件的代碼僅僅只有少數人可以閱讀,那個其中必定會有很多錯誤。如果這些代碼涉及到安全的部分,就會造成安全漏洞。令3Com公司最近汗顏的事件就是一個極好的例子。他們全部的CoreBuilder和SuperStack II hub被發現有秘密的通用口令,而這個口令是用來處理用戶的緊急事件而設置的。
作者:dspman