技術(shù)員聯(lián)盟提供win764位系統(tǒng)下載,win10,win7,xp,裝機純凈版,64位旗艦版,綠色軟件,免費軟件下載基地!

當(dāng)前位置:主頁 > 教程 > 服務(wù)器類 >

心臟出血漏洞 Heartbleed 固定大小緩沖區(qū)分析

來源:技術(shù)員聯(lián)盟┆發(fā)布時間:2018-04-20 00:22┆點擊:

Heartbleed 是來自O(shè)penSSL的緊急安全警告:OpenSSL出現(xiàn)“Heartbleed”安全漏洞。這一漏洞讓任何人都能讀取系統(tǒng)的運行內(nèi)存,文名稱叫做“心臟出血”、““擊穿心臟””等。

 

 

為什么固定大小緩沖區(qū)這么流行

心臟出血漏洞是最新發(fā)現(xiàn)的安全問題,由長字符串導(dǎo)致緩沖區(qū)越界。最常見的緩沖區(qū)越界發(fā)生在如下兩種條件同時滿足中:

    程序中一個組件A向另外一個組件B傳遞了一個指針,也可能同時傳遞長度信息
    組件B忽略了,或者沒有正確使用這個長度信息。此信息規(guī)定了指針所指向的內(nèi)存區(qū)域能夠存儲多少數(shù)據(jù)。

上述條件都滿足的程序結(jié)構(gòu)之所以能引起緩沖區(qū)越界,一個重要原因是,調(diào)用者A分配了一塊內(nèi)存,但是只有當(dāng)數(shù)據(jù)被真正讀取的時候,才能知道程序到底需要分配多大的內(nèi)存空間,因為不會被讀取的數(shù)據(jù),我們完全可以不保存它。 換句話說,一個函數(shù)負責(zé)分配空間,然后調(diào)用另外一個函數(shù)向該空間填充數(shù)據(jù)的結(jié)構(gòu),都會有點不安全。

即使這點危險能夠通過正確的檢查內(nèi)存邊界的方式成功避免,但是邊界檢查也會引入其自身存在的負面效果。 比如,我的一位前同事, 他創(chuàng)建了一個文本文件, 此文本文件壓縮了數(shù)萬個字符構(gòu)成的單行字符串。 然后他又將這個文件作為輸入,傳遞給了許多其他部件,比如編譯器,文本處理程序等等。 幾乎所有的這些程序都會出現(xiàn)這樣那樣的異常行為,例如,直接崩潰,或者悄無聲息的忽略掉輸入字符串的最后一截。

應(yīng)對該問題的簡單解決方案是:如果程序中任何部分涉及讀入長度不確定的輸入,那就應(yīng)該負責(zé)分配足夠大的內(nèi)存來保存這些輸入。當(dāng)然,在C++語言中使用STL標準庫就能輕松實現(xiàn)。但是在C語言中,卻沒有簡單有效的實現(xiàn)代碼,可以從輸入讀入一個單行字符串,返回包含該輸入的內(nèi)存指針,無視輸入的長度。 任何在C語言中實現(xiàn)此功能的嘗試,都或多或少的存在一些副作用。

我也曾靜下心來在當(dāng)時工作的部門,嘗試在C語言庫中增加一個針對上述問題的解決方案。 如果有人想要將使用了我寫的函數(shù)的代碼分享到別的地方,我想讓他們也能將我寫的函數(shù)作為其中一部分發(fā)布出去。 我所增加的函數(shù)的名稱是readline,且為方便使用而設(shè)計:只需要傳入一個文件指針(例如 stdin)作為輸入,此函數(shù)就能讀入一整行的輸入,返回一個指向以NULL結(jié)尾的此字符串的第一個字符,無需考慮輸入的長度。 如果讀到了文件結(jié)束符(EOF),就返回一個NULL指針。

顯然,任何分配內(nèi)存,并返回指向該內(nèi)存指針的函數(shù)都存在一個問題:該內(nèi)存何時被釋放? 我考慮過讓readline函數(shù)的調(diào)用者負責(zé)釋放,但是覺得很多調(diào)用函數(shù)可能會忘記釋放內(nèi)存。那么此時的緩沖區(qū)越界問題又變成了內(nèi)存泄漏問題。

最后,我決定采取在別的地方看到的策略:readline將會返回一個指向內(nèi)存空間的指針,并且保證其中的內(nèi)容在下一次調(diào)用readline函數(shù)之前都會保持不變。這種策略不僅可以減少用戶的擔(dān)憂,而且也能讓實現(xiàn)更簡單:程序?qū)⒋鎯σ粋€靜態(tài)指針(static pointer) 指向(動態(tài)分配的)緩沖區(qū)。緩沖區(qū)的大小將隨讀入的行的長度需要增減。 這種機制能讓readline函數(shù)在最常用的場景中簡單好用,并且安全。

 代碼如下      

char *line;
while ((line = readline(stdin)) != NULL) {
      /* Process a line */
}
 



當(dāng)然,這種機制也有他自身存在的問題。比如,在同一個表達式中,兩次調(diào)用readline函數(shù)將導(dǎo)致未定義行為(undefined behavior)。因為當(dāng)程序員計劃在第二次調(diào)用readline()函數(shù)之后,試圖保存兩次調(diào)用readline所讀入的全部數(shù)據(jù)時,第一次調(diào)用所創(chuàng)建的內(nèi)存空間,將在第二次調(diào)用時被釋放掉。 此外,該代碼會在讀入輸入的最后一行后,因為不再被調(diào)用,會一直占用內(nèi)存空間。實際上,它所浪費的內(nèi)存空間是整個輸入中最長的那一行的長度。在實現(xiàn)該函數(shù)時,雖然我在緩沖區(qū)小于輸入行長度時,都會重新分配更大的緩沖區(qū),但是卻沒有允許緩沖區(qū)變小。因為我覺得反復(fù)分配釋放內(nèi)存的所導(dǎo)致的性能下降,相比于在少數(shù)清醒下浪費一點點內(nèi)存空間來說不值得。

很顯然,我高估了人們所能忍受的內(nèi)存分配延遲開銷:當(dāng)我?guī)讉€月后回頭看這些代碼時,發(fā)現(xiàn)有人已經(jīng)將我所寫的readline版本完全修改為固定的4096-字符緩沖區(qū)。據(jù)我所了解,他的動機是完全避免運行時存儲分配的開銷。換句話說,為了避免只有在少數(shù)情況下才存在的多次內(nèi)存分配器調(diào)用,他悄悄的讓所有使用readline函數(shù)的程序,在行的長度大于4096個字符時,出現(xiàn)了很大的安全隱患。

之所以花了大量的篇幅講這樣一個故事,是因為它透露出我覺得非常重要的幾點:

    緩沖區(qū)越界通常發(fā)生在程序中某個部分A分配內(nèi)存,而實際需要的存儲空間大小只有另一個部分B知道。
    在程序中的同一個函數(shù)內(nèi)部分配內(nèi)存,并將其填充。這種方式解決了緩沖區(qū)分配的問題,而付出的代價是必須要讓程序的另一個函數(shù)負責(zé)內(nèi)存的釋放。內(nèi)存的分配和釋放在程序的兩個不同的函數(shù)中。
    這種分配和釋放在兩個不同的函數(shù)將會導(dǎo)致程序的可用性問題,除非在編程語言上有系統(tǒng)的支持,否則很難繞開。
    即使用戶為了安全和通用性,需要接收這個現(xiàn)實,但是他們可能也無法接受動態(tài)分配內(nèi)存引入的開銷。

我想,程序員不愿為了安全而引入運行時開銷,是很多安全性問題之所以普遍存在的原因。 我們將在下周詳細聊聊這種現(xiàn)象。