當前位置:編程學習大全網 - 編程語言 - (暢想)如何改進編程模式及cpu體系結構防止緩沖區溢出,不要求標準答案,只要想象得有道理

(暢想)如何改進編程模式及cpu體系結構防止緩沖區溢出,不要求標準答案,只要想象得有道理

緩沖區溢出。本文首先解釋什麽是緩沖區溢出,以及它們為何如此常見和如此危險。然後討論廣泛用於解決緩沖區溢出的新 Linux 和 UNIX 方法 ―― 以及為什麽這些方法還不足夠。隨後將展示 C/C++ 程序中防止緩沖區溢出的各種方法,同時包括靜態調整大小的方法(比如標準的 C 庫和 OpenBSD/strlcpy 解決方案)和動態調整大小的解決方案,以及壹些將為您提供幫助的工具。最後,本文以壹些關於緩沖區溢出缺陷的未來發展形勢的預測來結束全文的討論。

如果希望自己的程序是安全的,您需要知道什麽是緩沖區溢出,如何防止它們,可以采用哪些最新的自動化工具來防止它們(以及為什麽這些工具還不足夠),還有如何在您自己的程序中防止它們。

什麽是緩沖區溢出?

緩沖區以前可能被定義為“包含相同數據類型的實例的壹個連續計算機內存塊”。在 C 和 C++ 中,緩沖區通常是使用數組和諸如 malloc() 和 new 這樣的內存分配例程來實現的。極其常見的緩沖區種類是簡單的字符數組。 溢出 是指數據被添加到分配給該緩沖區的內存塊之外。

如果攻擊者能夠導致緩沖區溢出,那麽它就能控制程序中的其他值。雖然存在許多利用緩沖區溢出的方法,不過最常見的方法還是“stack-smashing”攻擊。Elias Levy (又名為 Aleph One)的壹篇經典文章“Smashing the Stack for Fun and Profit”解釋了 stack-smashing 攻擊,Elias Levy 是 Bugtraq 郵件列表(請參閱 參考資料 以獲得相關鏈接)的前任主持人。

清單 1. 壹個簡單的程序

void function1(int a, int b, int c) {

char buffer1[5];

gets(buffer1); /* DON'T DO THIS */

}

void main() {

function(1,2,3);

}

假設使用 gcc 來編譯清單 1 中的簡單程序,在 X86 上的 Linux 中運行,並且緊跟在對 gets() 的調用之後中止。此時的內存內容看起來像什麽樣子呢?答案是它看起來類似圖 1,其中展示了從左邊的低位地址到右邊的高位地址排序的內存布局。

圖 1. 堆棧視圖

內存的底部 內存的頂部

buffer1 sfp ret a b c

<--- 增長 --- [ ] [ ] [ ] [ ] [ ] [ ] ...

為什麽緩沖區溢出如此常見?

在幾乎所有計算機語言中,不管是新的語言還是舊的語言,使緩沖區溢出的任何嘗試通常都會被該語言本身自動檢測並阻止(比如通過引發壹個異常或根據需要給緩沖區添加更多空間)。但是有兩種語言不是這樣:C 和 C++ 語言。C 和 C++ 語言通常只是讓額外的數據亂寫到其余內存的任何位置,而這種情況可能被利用從而導致恐怖的結果。更糟糕的是,用 C 和 C++ 編寫正確的代碼來始終如壹地處理緩沖區溢出則更為困難;很容易就會意外地導致緩沖區溢出。除了 C 和 C++ 使用得 非常廣泛外,上述這些可能都是不相關的事實;例如,Red Hat Linux 7.1 中 86% 的代碼行都是用 C 或 C ++ 編寫的。因此,大量的代碼對這個問題都是脆弱的,因為實現語言無法保護代碼避免這個問題。

在 C 和 C++ 語言本身中,這個問題是不容易解決的。該問題基於 C 語言的根本設計決定(特別是 C 語言中指針和數組的處理方式)。由於 C++ 是最兼容的 C 語言超集,它也具有相同的問題。存在壹些能防止這個問題的 C/C++ 兼容版本,但是它們存在極其嚴重的性能問題。而且壹旦改變 C 語言來防止這個問題,它就不再是 C 語言了。許多語言(比如 Java 和 C#)在語法上類似 C,但它們實際上是不同的語言,將現有 C 或 C++ 程序改為使用那些語言是壹項艱巨的任務。

然而,其他語言的用戶也不應該沾沾自喜。有些語言存在允許緩沖區溢出發生的“轉義”子句。Ada 壹般會檢測和防止緩沖區溢出(即針對這樣的嘗試引發壹個異常),但是不同的程序可能會禁用這個特性。C# 壹般會檢測和防止緩沖區溢出,但是它允許程序員將某些例程定義為“不安全的”,而這樣的代碼 可能 會導致緩沖區溢出。因此如果您使用那些轉義機制,就需要使用 C/C++ 程序所必須使用的相同種類的保護機制。許多語言都是用 C 語言來實現的(至少部分是用 C 語言來實現的 ),並且用任何語言編寫的所有程序本質上都依賴用 C 或 C++ 編寫的庫。因此,所有程序都會繼承那些問題,所以了解這些問題是很重要的。

導致緩沖區溢出的常見 C 和 C++ 錯誤

從根本上講,在程序將數據讀入或復制到緩沖區中的任何時候,它需要在復制 之前檢查是否有足夠的空間。能夠容易看出來的異常就不可能會發生 ―― 但是程序通常會隨時間而變更,從而使得不可能成為可能。

遺憾的是,C 和 C++ 附帶的大量危險函數(或普遍使用的庫)甚至連這點(指檢查空間)也無法做到。程序對這些函數的任何使用都是壹個警告信號,因為除非慎重地使用它們,否則它們就會成為程序缺陷。您不需要記住這些函數的列表;我的真正目的是說明這個問題是多麽普遍。這些函數包括 strcpy(3)、strcat(3)、sprintf(3) (及其同類 vsprintf(3) )和 gets(3) 。 scanf() 函數集( scanf(3)、fscanf(3)、sscanf(3)、vscanf(3)、vsscanf(3) 和 vfscanf(3) )可能會導致問題,因為使用壹個沒有定義最大長度的格式是很容易的(當讀取不受信任的輸入時,使用格式“%s”總是壹個錯誤)。

其他危險的函數包括 realpath(3)、getopt(3)、getpass(3)、streadd(3)、strecpy(3) 和 strtrns(3) 。 從理論上講, snprintf() 應該是相對安全的 ―― 在現代 GNU/Linux 系統中的確是這樣。但是非常老的 UNIX 和 Linux 系統沒有實現 snprintf() 所應該實現的保護機制。

Microsoft 的庫中還有在相應平臺上導致同類問題的其他函數(這些函數包括 wcscpy()、_tcscpy()、_mbscpy()、wcscat()、_tcscat()、_mbscat() 和 CopyMemory() )。註意,如果使用 Microsoft 的 MultiByteToWideChar() 函數,還存在壹個常見的危險錯誤 ―― 該函數需要壹個最大尺寸作為字符數目,但是程序員經常將該尺寸以字節計(更普遍的需要),結果導致緩沖區溢出缺陷。

另壹個問題是 C 和 C++ 對整數具有非常弱的類型檢查,壹般不會檢測操作這些整數的問題。由於它們要求程序員手工做所有的問題檢測工作,因此以某種可被利用的方式不正確地操作那些整數是很容易的。特別是,當您需要跟蹤緩沖區長度或讀取某個內容的長度時,通常就是這種情況。但是如果使用壹個有符號的值來存儲這個長度值會發生什麽情況呢 ―― 攻擊者會使它“成為負值”,然後把該數據解釋為壹個實際上很大的正值嗎?當數字值在不同的尺寸之間轉換時,攻擊者會利用這個操作嗎?數值溢出可被利用嗎? 有時處理整數的方式會導致程序缺陷。

防止緩沖區溢出的新技術

當然,要讓程序員 不犯常見錯誤是很難的,而讓程序(以及程序員)改為使用另壹種語言通常更為困難。那麽為何不讓底層系統自動保護程序避免這些問題呢?最起碼,避免 stack-smashing 攻擊是壹件好事,因為 stack-smashing 攻擊是特別容易做到的。

壹般來說,更改底層系統以避免常見的安全問題是壹個極好的想法,我們在本文後面也會遇到這個主題。事實證明存在許多可用的防禦措施,而壹些最受歡迎的措施可分組為以下類別:

基於探測方法(canary)的防禦。這包括 StackGuard(由 Immunix 所使用)、ProPolice(由 OpenBSD 所使用)和 Microsoft 的 /GS 選項。

非執行的堆棧防禦。這包括 Solar Designer 的 non-exec 補丁(由 OpenWall 所使用)和 exec shield(由 Red Hat/Fedora 所使用)。

其他方法。這包括 libsafe(由 Mandrake 所使用)和堆棧分割方法。

遺憾的是,迄今所見的所有方法都具有弱點,因此它們不是萬能藥,但是它們會提供壹些幫助。

基於探測方法的防禦

研究人員 Crispen Cowan 創建了壹個稱為 StackGuard 的有趣方法。Stackguard 修改 C 編譯器(gcc),以便將壹個“探測”值插入到返回地址的前面。“探測儀”就像煤礦中的探測儀:它在某個地方出故障時發出警告。在任何函數返回之前,它執行檢查以確保探測值沒有改變。如果攻擊者改寫返回地址(作為 stack-smashing 攻擊的壹部分),探測儀的值或許就會改變,系統內就會相應地中止。這是壹種有用的方法,不過要註意這種方法無法防止緩沖區溢出改寫其他值(攻擊者仍然能夠利用這些值來攻擊系統)。人們也曾擴展這種方法來保護其他值(比如堆上的值)。Stackguard(以及其他防禦措施)由 Immunix 所使用。

IBM 的 stack-smashing 保護程序(ssp,起初名為 ProPolice)是 StackGuard 的方法的壹種變化形式。像 StackGuard 壹樣,ssp 使用壹個修改過的編譯器在函數調用中插入壹個探測儀以檢測堆棧溢出。然而,它給這種基本的思路添加了壹些有趣的變化。 它對存儲局部變量的位置進行重新排序,並復制函數參數中的指針,以便它們也在任何數組之前。這樣增強了ssp 的保護能力;它意味著緩沖區溢出不會修改指針值(否則能夠控制指針的攻擊者就能使用指針來控制程序保存數據的位置)。默認情況下,它不會檢測所有函數,而只是檢測確實需要保護的函數(主要是使用字符數組的函數)。從理論上講,這樣會稍微削弱保護能力,但是這種默認行為改進了性能,同時仍然能夠防止大多數問題。考慮到實用的因素,它們以獨立於體系結構的方式使用 gcc 來實現它們的方法,從而使其更易於運用。從 2003 年 5 月的發布版本開始,廣受贊譽的 OpenBSD(它重點關註安全性)在他們的整個發行套件中使用了 ssp(也稱為 ProPolice)。

Microsoft 基於 StackGuard 的成果,添加了壹個編譯器標記(/GS)來實現其 C 編譯器中的探測儀。

非執行的堆棧防禦

另壹種方法首先使得在堆棧上執行代碼變得不可能。 遺憾的是,x86 處理器(最常見的處理器)的內存保護機制無法容易地支持這點;通常,如果壹個內存頁是可讀的,它就是可執行的。壹個名叫 Solar Designer 的開發人員想出了壹種內核和處理器機制的聰明組合,為 Linux 內核創建了壹個“非執行的堆棧補丁”;有了這個補丁,堆棧上的程序就不再能夠像通常的那樣在 x86 上運行。 事實證明在有些情況下,可執行程序 需要在堆棧上;這包括信號處理和跳板代碼(trampoline)處理。trampoline 是有時由編譯器(比如 GNAT Ada 編譯器)生成的奇妙結構,用以支持像嵌套子例程之類的結構。Solar Designer 還解決了如何在防止攻擊的同時使這些特殊情況不受影響的問題。

壹段時間之後,人們又想出了壹種防止該問題的新思路:將所有可執行代碼轉移到壹個稱為“ASCII 保護(ASCII armor)”區域的內存區。要理解這是如何工作的,就必須知道攻擊者通常不能使用壹般的緩沖區溢出攻擊來插入 ASCII NUL 字符(0)這個事實。 這意味著攻擊者會發現,要使壹個程序返回包含 0 的地址是很困難的。由於這個事實,將所有可執行代碼轉移到包含 0 的地址就會使得攻擊該程序困難多了。

具有這個屬性的最大連續內存範圍是從 0 到 0x01010100 的壹組內存地址,因此它們就被命名為 ASCII 保護區域(還有具有此屬性的其他地址,但它們是分散的)。與非可執行的堆棧相結合,這種方法就相當有價值了:非可執行的堆棧阻止攻擊者發送可執行代碼,而 ASCII 保護內存使得攻擊者難於通過利用現有代碼來繞過非可執行堆棧。這樣將保護程序代碼避免堆棧、緩沖區和函數指針溢出,而且全都不需重新編譯。

然而,ASCII 保護內存並不適用於所有程序;大程序也許無法裝入 ASCII 保護內存區域(因此這種保護是不完美的),而且有時攻擊者 能夠將 0 插入目的地址。 此外,有些實現不支持跳板代碼,因此可能必須對需要這種保護的程序禁用該特性。Red Hat 的 Ingo Molnar 在他的“exec-shield”補丁中實現了這種思想,該補丁由 Fedora 核心(可從 Red Hat 獲得它的免費版本)所使用。最新版本的 OpenWall GNU/Linux (OWL)使用了 Solar Designer 提供的這種方法的實現(請參閱 參考資料 以獲得指向這些版本的鏈接)。

其他方法

還有其他許多方法。壹種方法就是使標準庫對攻擊更具抵抗力。Lucent Technologies 開發了 Libsafe,這是多個標準 C 庫函數的包裝,也就是像 strcpy() 這樣已知的對 stack-smashing 攻擊很脆弱的函數。Libsafe 是在 LGPL 下授予許可證的開放源代碼軟件。那些函數的 libsafe 版本執行相關的檢查,確保數組改寫不會超出堆棧楨。然而,這種方法僅保護那些特定的函數,而不是從總體上防止堆棧溢出缺陷,並且它僅保護堆棧,而不保護堆棧中的局部變量。它們的最初實現使用了 LD_PRELOAD ,而這可能與其他程序產生沖突。Linux 的 Mandrake 發行套件(從 7.1 版開始)包括了 libsafe。

另壹種方法稱為“分割控制和數據堆棧”―― 基本的思路是將堆棧分割為兩個堆棧,壹個用於存儲控制信息(比如“返回”地址),另壹個用於控制其他所有數據。Xu et al. 在 gcc 中實現了這種方法,StackShield 在匯編程序中實現了這種方法。這樣使得操縱返回地址困難多了,但它不會阻止改變調用函數的數據的緩沖區溢出攻擊。

事實上還有其他方法,包括隨機化可執行程序的位置;Crispen 的“PointGuard”將這種探測儀思想引申到了堆中,等等。如何保護當今的計算機現在已成了壹項活躍的研究任務。

壹般保護是不足夠的

如此多不同的方法意味著什麽呢?對用戶來說,好的壹面在於大量創新的方法正在試驗之中;長期看來,這種“競爭”會更容易看出哪種方法最好。而且,這種多樣性還使得攻擊者躲避所有這些方法更加困難。然而,這種多樣性也意味著開發人員需要 避免編寫會幹擾其中任何壹種方法的代碼。這在實踐上是很容易的;只要不編寫對堆棧楨執行低級操作或對堆棧的布局作假設的代碼就行了。即使不存在這些方法,這也是壹個很好的建議。

操作系統供應商需要參與進來就相當明顯了:至少挑選壹種方法,並使用它。緩沖區溢出是第壹號的問題,這些方法中最好的方法通常能夠減輕發行套件中幾乎半數已知缺陷的影響。可以證明,不管是基於探測儀的方法更好,還是基於非可執行堆棧的方法更好,它們都具有各自的優點。可以將它們結合起來使用,但是少數方法不支持這樣使用,因為附加的性能損失使得這樣做不值得。我並沒有其他意思,至少就這些方法本身而言是這樣;libsafe 和分割控制及數據堆棧的方法在它們所提供的保護方面都具有局限性。當然,最糟糕的解決辦法就是根本不對這個第壹號的缺陷提供保護。還沒有實現壹種方法的軟件供應商需要立即計劃這樣做。從 2004 年開始,用戶應該開始避免使用這樣的操作系統,即它們至少沒有對緩沖區溢出提供某種自動保護機制。

然而,沒有哪種方法允許開發人員忽略緩沖區溢出。所有這些方法都能夠被攻擊者破壞。 攻擊者也許能夠通過改變函數中其他數據的值來利用緩沖區溢出;沒有哪種方法能夠防止這點。如果能夠插入某些難於創建的值(比如 NUL 字符),那麽這其中的許多方法都能被攻擊者繞開;隨著多媒體和壓縮數據變得更加普遍,攻擊者繞開這些方法就更容易了。從根本上講,所有這些方法都能減輕從程序接管攻擊到拒絕服務攻擊的緩沖區溢出攻擊所帶來的破壞。遺憾的是,隨著計算機系統在更多關鍵場合的使用,即使拒絕服務通常也是不可接受的。因而,盡管發行套件應該至少包括壹種適當的防禦方法,並且開發人員應該使用(而不是反對)那些方法,但是開發人員仍然需要最初就編寫無缺陷的軟件。

C/C++ 解決方案

針對緩沖區溢出的壹種簡單解決辦法就是轉為使用能夠防止緩沖區溢出的語言。畢竟,除了 C 和 C++ 外,幾乎每種高級語言都具有有效防止緩沖區溢出的內置機制。但是許多開發人員因為種種原因還是選擇使用 C 和 C++。那麽您能做什麽呢?

事實證明存在許多防止緩沖區溢出的不同技術,但它們都可劃分為以下兩種方法:靜態分配的緩沖區和動態分配的緩沖區。首先,我們將講述這兩種方法分別是什麽。然後,我們將討論靜態方法的兩個例子(標準 C strncpy/strncat 和 OpenBSD 的 strlcpy/strlcat ),接著討論動態方法的兩個例子(SafeStr 和 C++ 的 std::string )。

重要選擇:靜態和動態分配的緩沖區

緩沖區具有有限的空間。因此實際上存在處理緩沖區空間不足的兩種可能方式。

“靜態分配的緩沖區”方法:也就是當緩沖區用完時,您抱怨並拒絕為緩沖區增加任何空間。

“動態分配的緩沖區”方法:也就是當緩沖區用完時,動態地將緩沖區大小調整到更大的尺寸,直至用完所有內存。

靜態方法具有壹些缺點。事實上,靜態方法有時可能會帶來不同的缺陷。靜態方法基本上就是丟棄“過多的”數據。如果程序無論如何還是使用了結果數據,那麽攻擊者會嘗試填滿緩沖區,以便在數據被截斷時使用他希望的任何內容來填充緩沖區。如果使用靜態方法,應該確保攻擊者能夠做的最糟糕的事情不會使得預先的假設無效,而且檢查最終結果也是壹個好主意。

動態方法具有許多優點:它們能夠向上適用於更大的問題(而不是帶來任意的限制),而且它們沒有導致安全問題的字符數組截斷問題。但它們也具有自身的問題:在接受任意大小的數據時,可能會遇到內存不足的情況 ―― 而這在輸入時也許不會發生。任何內存分配都可能會失敗,而編寫真正很好地處理該問題的 C 或 C++ 程序是很困難的。甚至在內存真正用完之前,也可能導致計算機變得太忙而不可用。簡而言之,動態方法通常使得攻擊者發起拒絕服務攻擊變得更加容易。因此仍然需要限制輸入。此外,必須小心設計程序來處理任意位置的內存耗盡問題,而這不是壹件容易的事情。

標準 C 庫方法

最簡單的方法之壹是簡單地使用那些設計用於防止緩沖區溢出的標準 C 庫函數(即使在使用 C ++,這也是可行的),特別是 strncpy(3) 和 strncat(3) 。這些標準 C 庫函數壹般支持靜態分配方法,也就是在數據無法裝入緩沖區時丟棄它。這種方法的最大優點在於,您可以肯定這些函數在任何機器上都可用,並且任何 C/C++ 開發人員都會了解它們。許許多多的程序都是以這種方式編寫的,並且確實可行。

遺憾的是,要正確地做到這點卻是令人吃驚的困難。下面是其中的壹些問題:

strncpy(3) 和 strncat(3) 都要求您給出 剩余的空間,而不是給出緩沖區的總大小。這之所以會成為問題是因為,雖然緩沖區的大小壹經分配就不會變化,但是緩沖區中剩余的空間量會在每次添加或刪除數據時發生變化。這意味著程序員必須始終跟蹤或重新計算剩余的空間。這種跟蹤或重新計算很容易出錯,而任何錯誤都可能給緩沖區攻擊打開方便之門。

在發生了溢出(和數據丟失)時,兩個函數都不會給出簡單的報告,因此如果要檢測緩沖區溢出,程序員就必須做更多的工作。

如果源字符串至少和目標壹樣長,那麽函數 strncpy(3) 還不會使用 NUL 來結束字符串;這可能會在以後導致嚴重破壞。因而,在運行 strncpy(3) 之後,您通常需要重新結束目標字符串。

函數 strncpy(3) 還可以用來僅把源字符串的 壹部分復制到目標中。 在執行這個操作時,要復制的字符的數目通常是基於源字符串的相關信息來計算的。 這樣的危險之處在於,如果忘了考慮可用的緩沖區空間,那麽 即使在使用 strncpy(3) 時也可能會留下緩沖區攻擊隱患。這個函數也不會復制 NUL 字符,這可能也是壹個問題。

可以通過壹種防止緩沖區溢出的方式使用 sprintf() ,但是意外地留下緩沖區溢出攻擊隱患是非常容易的。 sprintf() 函數使用壹個控制字符串來指定輸出格式,該控制字符串通常包括“ %s ”(字符串輸出)。如果指定字符串輸出的精確指定符(比如 %.10s ),那麽您就能夠通過指定輸出的最大長度來防止緩沖區溢出。甚至可以使用“ * ”作為精確指定符(比如“ %.*s ”),這樣您就可以傳入壹個最大長度值,而不是在控制字符串中嵌入最大長度值。這樣的問題在於,很容易就會不正確地使用 sprintf() 。壹個“字段寬度”(比如“ %10s ”)僅指定了最小長度 ―― 而不是最大長度。“字段寬度”指定符會留下緩沖區溢出隱患,而字段寬度和精確寬度指定符看起來幾乎完全相同 ―― 唯壹的區別在於安全的版本具有壹個點號。另壹個問題在於,精確字段僅指定壹個參數的最大長度,但是緩沖區需要針對組合起來的數據的最大尺寸調整大小。

scanf() 系列函數具有壹個最大寬度值,至少 IEEE Standard 1003-2001 清楚地規定這些函數壹定不能讀取超過最大寬度的數據。遺憾的是,並非所有規範都清楚地規定了這壹點,我們不清楚是否所有實現都正確地實現了這些限制(這在如今的 GNU/Linux 系統上就 不能正確地工作)。如果您依賴它,那麽在安裝或初始化期間運行小測試來確保它能正確工作,這樣做將是明智的。

strncpy(3) 還存在壹個惱人的性能問題。從理論上講, strncpy(3) 是 strcpy(3) 的安全替代者,但是 strncpy(3) 還會在源字符串結束時使用 NUL 來填充整個目標空間。 這是很奇怪的,因為實際上並不存在這樣做的很好理由,但是它從壹開始就是這樣,並且有些程序還依賴這個特性。這意味著從 strcpy(3) 切換到 strncpy(3) 會降低性能 ―― 這在如今的計算機上通常不是壹個嚴重的問題,但它仍然是有害的。

那麽可以使用標準 C 庫的例程來防止緩沖區溢出嗎?是的,不過並不容易。如果計劃沿著這條路線走,您需要理解上述的所有要點。或者,您可以使用下面幾節將要講述的壹種替代方法。

OpenBSD 的 strlcpy/strlcat

OpenBSD 開發人員開發了壹種不同的靜態方法,這種方法基於他們開發的新函數 strlcpy(3) 和 strlcat(3) 。這些函數執行字符串復制和拼接,不過更不容易出錯。這些函數的原型如下:

size_t strlcpy (char *dst, const char *src, size_t size);

size_t strlcat (char *dst, const char *src, size_t size);

strlcpy() 函數把以 NUL 結尾的字符串從“ src ”復制到“ dst ”(最多 size-1 個字符)。 strlcat() 函數把以 NUL 結尾的字符串 src 附加到 dst 的結尾(但是目標中的字符數目將不超過 size-1)。

初看起來,它們的原型和標準 C 庫函數並沒有多大區別。但是事實上,它們之間存在壹些顯著區別。這些函數都接受目標的總大小(而不是剩余空間)作為參數。這意味著您不必連續地重新計算空間大小,而這是壹項易於出錯的任務。此外,只要目標的大小至少為 1,兩個函數都保證目標將以 NUL 結尾(您不能將任何內容放入零長度的緩沖區)。如果沒有發生緩沖區溢出,返回值始終是組合字符串的長度;這使得檢測緩沖區溢出真正變得容易了。

遺憾的是, strlcpy(3) 和 strlcat(3) 並不是在類 UNIX 系統的標準庫中普遍可用。OpenBSD 和 Solaris 將它們內置在 <string.h> 中,但是 GNU/Linux 系統卻不是這樣。這並不是壹件那麽困難的事情;因為當底層系統沒有提供它們時,您甚至可以將壹些小函數直接包括在自己的程序源代碼中。

SafeStr

Messier 和 Viega 開發了“SafeStr”庫,這是壹種用於 C 的動態方法,它自動根據需要調整字符串的大小。使用 malloc() 實現所使用的相同技巧,Safestr 字符串很容易轉換為常規的 C“ char * ”字符串:safestr 在傳遞指針“之前”的地址處存儲重要信息。這種技術的優點在於,在現有程序中使用 SafeStr 將會很容易。SafeStr 還支持“只讀”和“受信任”的字符串,這也可能是有用的。這種方法的壹個問題在於它需要 XXL(這是壹個給 C 添加異常處理和資源管理支持的庫),因此您實際上要僅為了處理字符串而引入壹個重要的庫。Safestr 是在開放源代碼的 BSD 風格的許可證下發布的。

C++ std::string

針對 C++ 用戶的另壹種解決方案是標準的 std::string 類,這是壹種動態的方法(緩沖區根據需要而增長)。它幾乎是不需要傷腦筋的,因為 C++ 語言直接支持該類,因此不需要做特殊的工作就可使用它,並且其他庫也可能會使用它。就其本身而言, std::string 通常會防止緩沖區溢出,但是如果通過它提取壹個普通 C 字符串(比如使用 data() 或 c_str() ),那麽上面討論的所有問題都會重新出現。還要記住 data() 並不總是返回以 NUL 結尾的字符串。

由於種種歷史原因,許多 C++ 庫和預先存在的程序都創建了它們自己的字符串類。這可能使得 std::string 更難於使用,並且在使用那些庫或修改那些程序時效率很低,因為不同的字符串類型將不得不連續地來回轉換。並非其他所有那些字符串類都會防止緩沖區溢出,並且如果它們對 C 不受保護的 char* 類型執行自動轉換,那麽緩沖區溢出缺陷很容易引入那些類中。

  • 上一篇:誰能有條理的預測下本屆世界杯的冠軍啊
  • 下一篇:編程時所有符號都要用英文寫嗎?
  • copyright 2024編程學習大全網