日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

安全编程: 防止缓冲区溢出

發(fā)布時間:2024/4/11 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 安全编程: 防止缓冲区溢出 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

防止如今最常見的程序缺陷

本文討論 Linux/UNIX 系統(tǒng)中最常見的缺陷:緩沖區(qū)溢出。本文首先解釋什么是緩沖區(qū)溢出,以及它們?yōu)楹稳绱顺R姾腿绱宋kU。然后討論廣泛用于解決緩沖區(qū)溢出的新 Linux 和 UNIX 方法 ―― 以及為什么這些方法還不足夠。隨后將展示 C/C++ 程序中防止緩沖區(qū)溢出的各種方法,同時包括靜態(tài)調整大小的方法(比如標準的 C 庫和 OpenBSD/strlcpy 解決方案)和動態(tài)調整大小的解決方案,以及一些將為您提供幫助的工具。最后,本文以一些關于緩沖區(qū)溢出缺陷的未來發(fā)展形勢的預測來結束全文的討論。

David A. Wheeler(dwheelerNOSPAM@dwheeler.com), 專職研究員

2004 年 3 月 01 日

  • 內容

在 IBM Bluemix 云平臺上開發(fā)并部署您的下一個應用。

1988 年 11 月,許多組織不得不因為“Morris 蠕蟲”而切斷 Internet 連接,“Morris 蠕蟲”是 23 歲的程序員 Robert Tappan Morris 編寫的用于攻擊 VAX 和 Sun 機器的程序。 據有關方面估計,這個程序大約使得整個 Internet 的 10% 崩潰。 2001 年 7 月,另一個名為“Code Red”的蠕蟲病毒最終導致了全球運行微軟的 IIS Web Server 的 300,000 多臺計算機受到攻擊。2003 年 1 月,“Slammer”(也稱為“Sapphire”)蠕蟲利用 Microsoft SQL Server 2000 中的一個缺陷,使得南韓和日本的部分 Internet 崩潰,中斷了芬蘭的電話服務,并且使得美國航空訂票系統(tǒng)、信用卡網絡和自動出納機運行緩慢。所有這些攻擊 ―― 以及其他許多攻擊,都利用了一個稱做為?緩沖區(qū)溢出?的程序缺陷。

1999 年 Bugtraq(一個討論安全缺陷的郵件列表)進行的一次非正式調查發(fā)現,三分之二的參與者認為第一號的缺陷就是緩沖區(qū)溢出(要了解相關背景,請參閱本文后面?參考資料部分列出的“Buffer Overflows: Attacks and Defenses for the Vulnerability of the Decade”一文)。從 1997 年到 2002 年 3 月,CERT/CC 發(fā)出的半數安全警報都基于緩沖區(qū)缺陷。

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

什么是緩沖區(qū)溢出?

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

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

為了理解 stack-smashing 攻擊(或其他任何緩沖區(qū)攻擊)是如何進行的,您需要了解一些關于計算機在機器語言級實際如何工作的知識。在類 UNIX 系統(tǒng)上,每個進程都可以劃分為三個主要區(qū)域:文本、數據和堆棧。?文本區(qū)域包括代碼和只讀數據,通常不能對它執(zhí)行寫入操作。?數據區(qū)域同時包括靜態(tài)分配的內存(比如全局和靜態(tài)數據)和動態(tài)分配的內存(通常稱為?堆)。?堆棧區(qū)域用于允許函數/方法調用;它用于記錄函數完成之后的返回位置,存儲函數中使用的本地變量,向函數傳遞參數,以及從函數返回值。每當調用一個函數,就會使用一個新的?堆棧幀來支持該調用。了解這些之后,讓我們來考察一個簡單的程序。

清單 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 ?
<--- 增長 --- [ ] [ ] [ ] [ ] [ ] [ ] ...
堆棧的頂部 ? ? ? ? ? ? 堆棧的底部

許多計算機處理器,包括所有 x86 處理器,都支持從高位地址向低位地址“倒”增長堆棧。因此,每當一個函數調用另一個函數,更多的數據將被添加到左邊(低位地址),直至系統(tǒng)的堆棧空間耗盡。在這個例子中,當?main()?調用?function1()?時,它將 c 的值壓入堆棧,然后壓入 b 的值,最后壓入 a 的值。之后它壓入?return (ret)?值,這個值在?function1()?完成時告訴?function1()?返回到?main()?中的何處。它還把所謂的“已保存的幀指針(saved frame pointer,sfp)”記錄到堆棧上;這并不是必須保存的內容,此處我們不需要理解它。在任何情況下,function1()?在啟動以后,它會為?buffer1()?預留空間,這在圖 1 中顯示為具有一個低地址位置。

現在假設攻擊者發(fā)送了超過?buffer1()?所能處理的數據。接下來會發(fā)生什么情況呢?當然,C 和 C++ 程序員不會自動檢查這個問題,因此除非程序員明確地阻止它,否則下一個值將進入內存中的“下一個”位置。那意味著攻擊者能夠改寫?sfp?(即已保存的幀指針),然后改寫?ret(返回地址)。之后,當?function1()?完成時,它將“返回”―― 不過不是返回到?main()?,而是返回到攻擊者想要運行的任何代碼。

通常攻擊者會使用它想要運行的惡意代碼來使緩沖區(qū)溢出,然后攻擊者會更改返回值以指向它們已發(fā)送的惡意代碼。這意味著攻擊者本質上能夠在一個操作中完成整個攻擊!Aleph On 的文章(請參閱?參考資料)詳細介紹了這樣的攻擊代碼是如何創(chuàng)建的。例如,將一個 ASCII 0 字符壓入緩沖區(qū)通常是很困難的,而該文介紹了攻擊者一般如何能夠解決這個問題。

除了 smashing-stack 和更改返回地址外,還存在利用緩沖區(qū)溢出缺陷的其他途徑。與改寫返回地址不同,攻擊者可以 smashing-stack(使堆棧上的緩沖區(qū)溢出),然后改寫局部變量以利用緩沖區(qū)溢出缺陷。緩沖區(qū)根本就不必在堆棧上 ―― 它可以是堆中動態(tài)分配的內存(也稱為“malloc”或“new”區(qū)域),或者在某些靜態(tài)分配的內存中(比如“global”或“static”內存)。基本上,如果攻擊者能夠溢出緩沖區(qū)的邊界,麻煩或許就會找上你了。 然而,最危險的緩沖區(qū)溢出攻擊就是 stack-smashing 攻擊,因為如果程序對攻擊者很脆弱,攻擊者獲得整個機器的控制權就特別容易。

為什么緩沖區(qū)溢出如此常見?

在幾乎所有計算機語言中,不管是新的語言還是舊的語言,使緩沖區(qū)溢出的任何嘗試通常都會被該語言本身自動檢測并阻止(比如通過引發(fā)一個異常或根據需要給緩沖區(qū)添加更多空間)。但是有兩種語言不是這樣:C 和 C++ 語言。C 和 C++ 語言通常只是讓額外的數據亂寫到其余內存的任何位置,而這種情況可能被利用從而導致恐怖的結果。更糟糕的是,用 C 和 C++ 編寫正確的代碼來始終如一地處理緩沖區(qū)溢出則更為困難;很容易就會意外地導致緩沖區(qū)溢出。除了 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++ 程序改為使用那些語言是一項艱巨的任務。

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

導致緩沖區(qū)溢出的常見 C 和 C++ 錯誤

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

遺憾的是,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 系統(tǒng)中的確是這樣。但是非常老的 UNIX 和 Linux 系統(tǒng)沒有實現?snprintf()?所應該實現的保護機制。

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

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

防止緩沖區(qū)溢出的新技術

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

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

  • 基于探測方法(canary)的防御。這包括 StackGuard(由 Immunix 所使用)、ProPolice(由 OpenBSD 所使用)和 Microsoft 的 /GS 選項。
  • 非執(zhí)行的堆棧防御。這包括 Solar Designer 的 non-exec 補丁(由 OpenWall 所使用)和 exec shield(由 Red Hat/Fedora 所使用)。
  • 其他方法。這包括 libsafe(由 Mandrake 所使用)和堆棧分割方法。

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

基于探測方法的防御

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

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

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

非執(zhí)行的堆棧防御

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

Linux 中實現這個目的的最初補丁在 1998 年被 Linus Torvalds 拒絕,這是因為一個有趣的原因。即使不能將代碼放到堆棧上,攻擊者也可以利用緩沖區(qū)溢出來使程序“返回”某個現有的子例程(比如 C 庫中的某個子例程),從而進行攻擊。簡而言之,僅只是擁有非可執(zhí)行的堆棧是不足夠的。

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

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

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

其他方法

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

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

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

一般保護是不足夠的

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

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

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

C/C++ 解決方案

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

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

重要選擇:靜態(tài)和動態(tài)分配的緩沖區(qū)

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

  • “靜態(tài)分配的緩沖區(qū)”方法:也就是當緩沖區(qū)用完時,您抱怨并拒絕為緩沖區(qū)增加任何空間。
  • “動態(tài)分配的緩沖區(qū)”方法:也就是當緩沖區(qū)用完時,動態(tài)地將緩沖區(qū)大小調整到更大的尺寸,直至用完所有內存。

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

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

標準 C 庫方法

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

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

  • strncpy(3)?和?strncat(3)?都要求您給出?剩余的空間,而不是給出緩沖區(qū)的總大小。這之所以會成為問題是因為,雖然緩沖區(qū)的大小一經分配就不會變化,但是緩沖區(qū)中剩余的空間量會在每次添加或刪除數據時發(fā)生變化。這意味著程序員必須始終跟蹤或重新計算剩余的空間。這種跟蹤或重新計算很容易出錯,而任何錯誤都可能給緩沖區(qū)攻擊打開方便之門。
  • 在發(fā)生了溢出(和數據丟失)時,兩個函數都不會給出簡單的報告,因此如果要檢測緩沖區(qū)溢出,程序員就必須做更多的工作。
  • 如果源字符串至少和目標一樣長,那么函數?strncpy(3)?還不會使用 NUL 來結束字符串;這可能會在以后導致嚴重破壞。因而,在運行strncpy(3)?之后,您通常需要重新結束目標字符串。
  • 函數?strncpy(3)?還可以用來僅把源字符串的?一部分復制到目標中。 在執(zhí)行這個操作時,要復制的字符的數目通常是基于源字符串的相關信息來計算的。 這樣的危險之處在于,如果忘了考慮可用的緩沖區(qū)空間,那么?即使在使用strncpy(3)?時也可能會留下緩沖區(qū)攻擊隱患。這個函數也不會復制 NUL 字符,這可能也是一個問題。
  • 可以通過一種防止緩沖區(qū)溢出的方式使用?sprintf()?,但是意外地留下緩沖區(qū)溢出攻擊隱患是非常容易的。?sprintf()?函數使用一個控制字符串來指定輸出格式,該控制字符串通常包括“?%s?”(字符串輸出)。如果指定字符串輸出的精確指定符(比如?%.10s?),那么您就能夠通過指定輸出的最大長度來防止緩沖區(qū)溢出。甚至可以使用“?*?”作為精確指定符(比如“?%.*s?”),這樣您就可以傳入一個最大長度值,而不是在控制字符串中嵌入最大長度值。這樣的問題在于,很容易就會不正確地使用?sprintf()?。一個“字段寬度”(比如“?%10s?”)僅指定了最小長度 ―― 而不是最大長度。“字段寬度”指定符會留下緩沖區(qū)溢出隱患,而字段寬度和精確寬度指定符看起來幾乎完全相同 ―― 唯一的區(qū)別在于安全的版本具有一個點號。另一個問題在于,精確字段僅指定一個參數的最大長度,但是緩沖區(qū)需要針對組合起來的數據的最大尺寸調整大小。
  • scanf()?系列函數具有一個最大寬度值,至少 IEEE Standard 1003-2001 清楚地規(guī)定這些函數一定不能讀取超過最大寬度的數據。遺憾的是,并非所有規(guī)范都清楚地規(guī)定了這一點,我們不清楚是否所有實現都正確地實現了這些限制(這在如今的 GNU/Linux 系統(tǒng)上就?不能正確地工作)。如果您依賴它,那么在安裝或初始化期間運行小測試來確保它能正確工作,這樣做將是明智的。

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

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

OpenBSD 的 strlcpy/strlcat

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

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 庫函數并沒有多大區(qū)別。但是事實上,它們之間存在一些顯著區(qū)別。這些函數都接受目標的總大小(而不是剩余空間)作為參數。這意味著您不必連續(xù)地重新計算空間大小,而這是一項易于出錯的任務。此外,只要目標的大小至少為 1,兩個函數都保證目標將以 NUL 結尾(您不能將任何內容放入零長度的緩沖區(qū))。如果沒有發(fā)生緩沖區(qū)溢出,返回值始終是組合字符串的長度;這使得檢測緩沖區(qū)溢出真正變得容易了。

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

SafeStr

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

C++ std::string

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

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

工具

有許多工具可以在緩沖區(qū)溢出缺陷導致問題之前幫助檢測它們。 例如,像我的 Flawfinder 和 Viega 的 RATS 這樣的工具能夠搜索源代碼,識別出可能被不正確地使用的函數(基于它們的參數來歸類)。這些工具的一個缺點在于,它們不是完美的 ―― 它們會遺漏一些緩沖區(qū)溢出缺陷,并且它們會識別出一些實際上不是問題的“問題”。但是使用它們仍然是值得的,因為與手工查找相比,它們將幫助您在短得多的時間內識別出代碼中的潛在問題。

結束語

借助知識、謹慎和工具,C 和 C++ 中的緩沖區(qū)溢出缺陷是可以防止的。不過做起來并沒有那么容易,特別是在 C 中。如果使用 C 和 C++ 來編寫安全的程序,您需要真正理解緩沖區(qū)溢出和如何防止它們。

一種替代方法是使用另一種編程語言,因為如今的幾乎其他所有語言都能防止緩沖區(qū)溢出。但是使用另一種語言并不會消除所有問題。許多語言依賴 C 庫,并且許多語言還具有關閉該保護特性的機制(為速度而犧牲安全性)。但是即便如此,不管您使用哪種語言,開發(fā)人員都可能會犯其他許多錯誤,從而帶來引入缺陷。

不管您做什么,開發(fā)沒有錯誤的程序都是極其困難的,即使最仔細的復查通常也會遺漏其中一些錯誤。 開發(fā)安全程序的最重要方法之一是?最小化特權。那意味著程序的各個部分應該具有它們需要的唯一特權,一點也不能多。這樣,即使程序具有缺陷(誰能無過?),也可能會避免將該缺陷轉化為安全事故。但是在實踐中如何做到這點呢?下一篇文章將研究如何實際地最小化 Linux/UNIX 系統(tǒng)中的特權,以便您能防止自己不可避免的錯誤所帶來安全隱患。

總結

以上是生活随笔為你收集整理的安全编程: 防止缓冲区溢出的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。