袁連海 李湘文 徐 晶
(成都理工大學工程技術學院 樂山 614000)
應用程序在運行時,操作系統將為其分配一片連續的內存區域來存儲各種各樣的數據,這片內存區域叫做緩沖區。緩沖區溢出是用戶向緩沖區中寫入的數據超過緩沖區所能容納的最大容量,使得寫入的數據超過了定義的內存邊界,從而將數據寫進其他區域,在向已經分配的同定存儲空間中存儲多于申請大小的數據時,會發生緩沖區溢出。緩沖區溢出攻擊是攻擊者故意將大于緩沖區定義長度的數據寫入到緩沖區,覆蓋其他區域的數據,達到破壞性目的的操作。程序中存在緩沖區溢出漏洞是十分嚴重的安全問題。緩沖區溢出已經成為一種十分普遍和危險的安全漏洞,存在于各種操作系統和應用軟件中。入侵者可以利用此漏洞攻擊用戶,從而造成了極大的經濟損失和危害。攻擊者可以通過緩沖區溢出攻擊更改緩沖區的數據、注入惡意代碼、改變程序的控制權、使未授權的用戶獲得管理員權限,以致可以非法執行任意代碼。攻擊者可以利用緩沖區溢出漏洞攻擊用戶,嚴重時可以導致程序不能正常運行、計算機關機和系統重啟等結果。攻擊者還可以利用該漏洞運行非法指令,獲得系統超級用戶權限,進行各種各樣的非法操作。利用緩沖區溢出攻擊,可以導致程序運行失敗、重新啟動、執行惡意代碼等后果。緩沖區溢出中最危險的是堆棧溢出,入侵者可以利用堆棧溢出,在函數返回時改變返回程序的地址,讓其跳轉到任意地址,更為嚴重的是,它可被利用來執行非授權指令,甚至可以取得系統特權,進而進行各種非法操作[1]。
緩沖區溢出攻擊在各種操作系統和應用軟件中廣泛存在,緩沖區溢出漏洞是網絡信息安全中最危險的安全漏洞之一。在目前網絡與操作系統安全領域,有50%以上的安全問題都是由于存在緩沖區溢出漏洞造成。從緩沖區溢出攻擊第一次出現到現在,大量信息安全的研究者致力于如何盡量避免緩沖區溢出的產生,及時發現軟件中緩沖區溢出的漏洞,有效防御緩沖溢出攻擊的研究,產生了不少有用、有效的緩沖區溢出防御的方法和技術[2]。
緩沖區溢出通常在采用C和C++語言編寫的應用程序中存在,原因是這類編譯器著重強調程序的運行效率,而缺乏檢查內存是否超越邊界的機制,這樣應用程序就可能存在十分嚴重的安全問題。為更好地理解緩沖區溢出攻擊是如何實現的,先讓我們來了解C程序編寫的進程在執行時內存分布狀況。程序運行時,程序是分別加載到內存中幾個內存分區的,包括代碼段、數據段、BSS段、堆、和棧。上述內存區分別有對應的功能,編譯器在編譯程序時要將程序載入相應的內存區域。

圖1 操作系統進程內存分配區
代碼段又叫做文本段,在這個內存區域,主要存放可執行的進程的指令,包括用戶程序代碼和編譯器生成的相關輔助代碼,其大小取決于具體程序,代碼段的存放起始位置一般是固定的,這要根據系統是32位或者64位,代碼段通常是不可寫入,只能讀取,這是為了防止程序代碼被意外修改。數據段緊挨著代碼段,數據段用來存放已經初始化,但初始化值不為0的變量,程序中定義的已經初始化的靜態變量、已經初始化的全局變量以及常量都保存在數據段區域。BSS段存放沒有初始化的全局變量和靜態變量,這些變量運行時初始化為0。堆內存區用來存放在程序中動態申請的內存區域,在C語言中,調用malloc()函數返回的內存地址就存放在堆里面,釋放堆里面的內存需要調用函數free()來釋放內存。棧內存區存放函數的局部變量、函數的參數及編譯器自身產生的不可見的信息,如從被調用函數返回到調用函數的地址和一些狀態寄存器的值。圖1顯示在Linux操作系統中典型的C程序的內存分布情況,在其他操作系統如UNIX和Windows操作系統中的進程的內存分配情況基本類似。
為了更進一步進行描述,由C或者C++編譯器編譯的程序內存占用包括以下幾個部分:棧由編譯器自動分配釋放,通常存放函數的參數值、局部變量等,操作方式和數據結構中的棧相似。堆內存區經常由程序員分配和釋放,如果程序員不釋放,程序結束時操作系統進行回收。全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,靜態區包括未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域,程序結束后由操作系統釋放;字符串常量區存放常量,程序結束后由系統釋放;程序代碼段存放二進制代碼。例如,下面一段C程序的各種變量的內存分配區域如下。


進程的內存分配情況在邏輯上基本分為三部分,分別是代碼區域、動態數據區域以及靜態數據區域。進程的每個線程都有私有的棧,所以每個線程雖然代碼一樣,但本地變量的數據都是互不干擾。動態數據區一般就是堆棧。全局變量和靜態變量分配在靜態數據區,本地變量分配在動態數據區,即堆棧中。程序通過堆棧的基地址和偏移量來訪問本地變量。緩沖區溢出主要發生在上述內存不同區域中,具體可分為以下3種:1)基于數據段的緩沖區溢出;2)基于Heap的緩沖區溢出;3)基于Stack的緩沖區溢出。還有一些比較特別的緩沖區溢出,如BSS溢出,通常情況下,針對緩沖區溢出漏洞的利用大多數是棧溢出,堆溢出的利用相當困難[3]。
緩沖區溢出攻擊是利用程序不對輸入的數據進行邊界檢查的缺陷,向一個給定緩沖區中寫入過量的數據,當數據量超過給定緩沖區的大小時,輸入數據溢出并覆蓋緩沖區鄰近的數據。黑客利用這種方法,精心設計寫入的數據,可導致程序去執行惡意代碼、讓系統死機或非法獲得系統訪問權。在C或者C++語言編寫的程序中,系統分配的緩沖區允許位于數據段、堆、棧以及BSS段,而與程序執行過程相關的一些數據變量,包括函數參數指針、函數的返回內存地址等數據也是可以存放在這些內存區域。因此,當控制程序執行過程的一些關鍵的數據很容易受到緩沖區溢出攻擊,這時,其值就有可能發生改變,結果是令正在執行的指令轉向另外的代碼,例如可以去運行非法的程序代碼,造成用戶的損失。
棧是一種先進后出的數據結構,是操作系統在建立某個進程時或者線程(在支持多線程的操作系統中是線程)為這個進程分配的內存區域,該存儲區域具有先進后出的特點,系統在編譯的時候可以指定棧的大小。本質上棧是由寄存器EBP和ESP指向的一片內存空間(前者指向棧底,后者指向棧頂),通常由高地址向低地址增長的內存空間,棧中保存一些臨時的數據,例如一個函數中的臨時變量以及函數的返回地址。
在編程中,例如C/C++中,所有的局部變量都是從棧中分配內存空間,實際上也不是什么分配,只是從棧頂向上用就行,在退出函數的時候,只是修改棧指針就可以把棧中的內容銷毀,所以速度最快。自動開辟空間,用來分配局部變量、類的引用(指向堆空間段),棧使用的是一級緩存,通常在被調用時處于存儲空間中,調用完畢立即釋放。棧是由操作系統分配為程序運行過程中的進程分配的內存區域。當程序中執行函數調用的時候,執行流程是:第一是將函數的參數入棧,接著將指令寄存器中的內容入棧,作為返回地址(RET),再接著將基址寄存器(EBP)入棧,再把當前的棧指針(ESP)的內容拷貝到EBP,做為新的基地址;最后把ESP減去恰當的數值,為本地變量留出一定的存儲空間[3]。
因為函數內的局部變量的內存分配是發生在棧里的,因此,如果某一函數內部聲明緩沖區變量,那么,該變量所占用的內存空間是在該函數被調用時所建立的棧。由于對緩沖區變量的一些操作(例如,字符串復制STRCPY)是從低內存地址向高內存地址,而內存中所保存的函數調用的返回地址(RET)通常就位于該緩沖區的上面(高地址),這樣就可能覆蓋函數的返回地址。當用戶復制的內容大于目標緩沖區大小限制時,會出現改寫函數保存在函數棧中的返回地址,這樣就可能使程序的執行流程按照攻擊者的目的執行。下面通過對一個簡單的緩沖區溢出代碼來敘述緩沖區溢出原理[5]。

程序中包含一個個字節的緩沖區,編譯運行以上代碼,將命令行的第一個參數復制進緩沖區,由于程序沒有進行邊界檢查,所以當argv[1]的字符數目超過5時,就會造成緩沖區溢出。在理論上程序的輸入參數超過5個字節就會出現緩沖區溢出,但因為編譯器不同版本原因,實際上復制的字符數目超過9個字節才能真正覆蓋緩沖區。通過實際運行結果可以看到:當輸入5個“B”的時候,程序會正常退出,8個字符也正常輸出,當字符數目達到9個時,運行程序會出現段錯誤SEGMENT FAUIT。
緩沖區溢出攻擊的類型包括以下幾種[3]:棧溢出攻擊、本地指針溢出攻擊、本地函數指針溢出攻擊和BSS函數指針溢出攻擊。棧溢出攻擊是最常見、最主要的緩沖區溢出攻擊形式,這種攻擊方式通過改寫棧中的返回地址、前幀基址指針、函數指針以及在棧中植入攻擊代碼等來實施攻擊。堆BSS的溢出攻擊相對較難,攻擊的數量也少一些。wOOw00是一種堆溢出攻擊,通過溢出改寫存在堆或BSS中的函數指針來獲得控制。Matt Conover等做了深入的分析和研究[3]。
造成緩沖區溢出的因素通常包括以下幾種情況[6]:第一是編程時采用不帶類型安全檢測的程序設計語言。雖然C和C++語言允許編程人員直接訪問內存和寄存器,可以開發接近硬件運行能力性能、運行速度快的應用程序,但是這兩種語言不進行類型安全檢查以及檢測數組安全邊界,在包含字符串以及數組操作時,很容易造成緩沖區溢出,所以大部分出現緩沖區溢出漏洞的程序都是采用C和C++語言。第二種情況是編譯器把程序緩沖區分配在內存中關鍵數據結構附件或者相鄰的位置。最后是開發者采用了不安全的方式訪問緩沖區。假設應用程序需要訪問數據,而用戶在將數據復制到指定的緩沖區位置卻沒有考慮目標緩沖區大小時,可能會出現緩沖區溢出漏洞。
對于緩沖區溢出檢測和防御,眾多軟硬件生產廠家做了很多措施來預防緩沖區溢出漏洞。例如微軟公司在開發工具中增加編譯選項來檢測程序是否存在棧溢出,在操作系統中增加了結構化異常處理覆蓋保護機制,通過阻止修改結構化異常處理增強系統的安全性,在硬件方面,64位CPU引入了NX(No-eXecute)機制,在內存中區分數據區與代碼區,當攻擊者利用溢出使CPU跳轉到數據區去執行時,就會異常終止[6]。
造成緩沖區溢出的主要原因是編程人員存在不好的編程習慣,因此,防御程序存在緩沖區溢出漏洞的最主要的措施是提高程序員代碼編寫規范、養成良好的編程習慣。在人的因素解決后,可以從技術方面進行緩沖區溢出防御,包括編譯器、操作系統等方面。是傳統上系統級防范緩沖區溢出漏洞攻擊的三種最常用的方法。它們都不需要程序員對自己的代碼做任何修改,也基本不會帶來程序性能的降低。單獨一種機制可以在一定程度上降低緩沖區溢出漏洞所帶來的風險,多種機制結合可使防范效果更加有效。比較經典的緩沖區溢出漏洞防御措施如下[11]。
1)不可執行棧
一些程序為了提高執行效率,有時程序會動態生成和執行代碼。如JAVA采用just-in-time編譯技術動態產生的代碼可以提高性能。在Linux操作系統中在發送信號時,發送進程要往棧中插入代碼并且觸發中斷,從而執行棧中包含的代碼,并往接收進程發送信號,這時操作系統會修改棧的可執行屬性,使其變成可執行。編譯器也會在其棧中存儲可執行的代碼以便復用。所以,可執行的棧雖然具有一定的優點,但也留下了一定的安全隱患。為了減少可執行的棧帶來的安全漏洞,可以讓棧改變為不可執行。將棧改變為不可執行通常有兩種方法:鏈接器和操作系統的保護。鏈接器的保護是在編譯器編譯源代碼時,可以采用execstack選項來控制編譯生成的目標文件為堆棧段不可執行。鏈接器鏈接目標文件時,如果某個目標文件的堆棧段被標記了堆棧段可執行,則生成的庫或可執行文件的堆棧段會標記為可執行;如果所有目標文件都標記了堆棧段不可執行,則鏈接生成的庫或可執行文件的堆棧段就會標記為不可執行。Linux系統提供了棧不可執行的措施,以防止攻擊者將攻擊代碼通過棧溢出存儲到進入棧,進而防止執行攻擊代碼。
2)隨機化進程地址
由于計算機系統具有一定的相似性,這樣就存在安全隱患,攻擊者可以通過一臺計算機上分析程序運行行為,設計攻擊方法來攻擊另一臺計算機,所以,可以采用隨機分配進程地址空間來避免不同計算機系統的相似性,從而預防緩沖區溢出漏洞,常見的方法包括:棧地址隨機化以及局部變量地址隨機化等方法。攻擊者為了在棧中插入可運行的攻擊代碼,不僅需要插入攻擊代碼,還要插入攻擊代碼所在的地址值作為函數的返回地址。如果應用程序在每次執行時分配的棧地址都是相同的,攻擊者就很容易獲得函數的返回地址在棧上的存儲地址和插入棧中在棧上的存儲地址,也就很容易將固定的攻擊代碼改為在棧上的地址。隨機化棧地址的另外作用是攻擊者需要利用棧上某個變量時,由于該變量的地址不是固定的,所以攻擊者獲得需要的變量就比較困難。編譯器在編譯程序時,給每個局部變量預留大小隨機的一段存儲空間,因此,每個局部變量的地址也是隨機的;另外,隨機分配棧地址的作用是有限的,因為攻擊者可以進行多次嘗試,從而得到正確的地址[11]。
3)編譯器引進安全檢查機制
編譯器為了檢測程序執行時是否存在緩沖區溢出,一些編譯器提供了驗證碼檢測機制。在編譯時,編譯器在調用函數后和退出函數前都要插入一些代碼。在調用函數后,在棧頂部和函數返回地址之間放入隨機的驗證碼,退出函數前,插入的代碼檢查該驗證碼是否被修改,如果被修改,則報告異常。通常緩沖區溢出時,會從緩沖區的低地址到高地址依次覆蓋,因此如果攻擊者要覆蓋寫返回地址,則必須覆蓋隨機驗證碼,從而可以通過檢查緩沖區被寫前后的驗證碼是否一致判斷是否發生了溢出。
緩沖區溢出攻擊在程序中普遍存在以及具有較大的破壞性,許多研究者對如何有效地防御緩沖區溢出攻擊進行了長期的研究。通過緩沖區溢出攻擊原理我們知道,攻擊者要利用緩沖區溢出進行攻擊,攻擊者經常要在一開始就通過緩沖區溢出植入攻擊執行代碼,接著會改變程序的正常執行流程,最終讓自己的攻擊代碼得以執行。研究者通常對上述攻擊環節中的幾個步驟進行研究,阻斷任何一個環節從而達到防止緩沖區溢出攻擊的發生。
緩沖區溢出攻擊防御分為兩大類:靜態防御和動態防御。靜態防御方法是通過源代碼找到程序的漏洞并進行修改,雖然要找出所有的源代碼漏洞幾乎是不可能的,但是找到已知攻擊漏洞特征再進行修改,就可以大大降低程序被攻擊的可能性。不足之處是需要程序源代碼。獲取源代碼有時對用戶來說是不可能的,另一個缺點是需要不斷升級已知攻擊數據庫,一旦發現攻擊漏洞后,用戶需要知道怎么去修補漏洞。而動態防御是運行有漏洞的程序時,自動生成一個保護環境,讓利用漏洞進行攻擊的行為不能發生。
動態防御研究方面,由Crispin Cowan等開發了一種編譯器技術,該技術不必修改程序源代碼,可以有效預防針對棧的返回地址的緩沖區溢出漏洞攻擊。在這類攻擊中,攻擊者通常先對棧的局域變量區寫入,再從高地址向低地址重寫基址指針和返回地址。通過在前幀基址指針與返回地址之間加入一個稱為偵探字段的虛構域,在返回地址前,需要檢查偵探字段是否和以前一致,如果不一致則報警并終止程序的執行。Stack Shield屬于GCC編譯器的補丁,它能防止針對返回地址和函數指針的溢出攻擊。采用的策略如下:當調用函數時,返回的地址在壓入棧的同時復制到一個受保護的全局數組里面,當函數返回時,通過比較棧中的返回地址是否和全局數組中的一致,來判斷是否受到攻擊。
在靜態防御研究方面,文獻[13]采用一種通過對編譯以后的可執行源代碼靜態分析的策略,通過對函數名的檢測得出哪些屬于危險函數調用,經過對可執行代碼反匯編,獲得匯編代碼,最后判斷危險函數是否引起緩沖區溢出。文獻[14]通過建模源程序代碼,獲得抽象的語法樹、符號表、控制流圖、函數調用圖,并使用區間運算技術來分析和計算程序變量及表達式的取值范圍,而且在函數分析過程中引入函數摘要概念來替換實際的函數調用。文獻[15]也是通過靜態語法樹檢查惡意攻擊代碼。文獻[16]提出了使用靜態特征匹配來查找被檢測程序源代碼中的攻擊代碼,實現的一種工具能夠有效判斷許多種常見的惡意代碼。文獻[17]通過把基于源代碼分析的緩沖區溢出攻擊問題轉化成為一個和危險函數約束條件相關的不等式組求解的數學問題,設計出了基于不等式方程組來建立緩沖區溢出檢測模型。文獻[18]得出源代碼中經常引起緩沖區溢出的代碼的典型特征,例如緩沖區的坐標變量值、危險函數調用情況、指針增減操作、使用指針的循環語句等。通過以上特征,把緩沖區溢出歸類成5種典型的基本類型,還提出了各自的預防策略。文獻[12]提出一種漏洞挖掘方法,該方法基于遺傳算法,根據緩沖區溢出攻擊的典型特征,結合靜態分析的控制流思想,該方法同模擬退火算法相比,具有更高的完備性和更快的收斂速度。但是它并沒有去除程序中的漏洞,在平常環境下漏洞依然存在。動態防御不需要程序的源代碼。可以防御新的惡意代碼對已受保護的目標的攻擊,但是對未保護的新目標攻擊則無能為力。
在網絡和信息技術日益發達的當今社會,網絡攻防技術不斷發展,為了提高系統軟件和應用軟件的安全性能,研究者需要進一步在軟件安全漏洞的系統分析、漏洞檢測、問題發現以及安全防范等各種關鍵技術進行深入研究。論文對進程的內存分布、緩沖區溢出攻擊的基本原理、攻擊種類以及預防方法進行了詳細介紹,對當前主流的緩沖區溢出攻擊防御方法進行了敘述,歸納總結了緩沖區溢出防御的基本方法。有效地防范軟件系統的緩沖區溢出漏洞攻擊涉及各種復雜的技術和方法,開發者需要綜合運用各種防御方法,從多個層面和多種角度出發,實現緩沖區溢出漏洞攻擊的有效防御。但是到目前為止還沒有一個完整的針對緩沖區溢出攻擊的解決方法,軟件開發者需要清晰地意識到緩沖區溢出攻擊的普遍性和危害性,在編程時增強安全意識,養成良好的編寫安全應用程序的思想,寫出高質量的軟件,切除緩沖區溢出的根源。測試軟件時要全面和充分地進行漏洞測試和分析。