司徒凌云, 王林章, 李宣東, 劉 楊
1(南京大學 計算機科學與技術(shù)系,江蘇 南京 210023)
2(計算機軟件新技術(shù)國家重點實驗室(南京大學),江蘇 南京 210023)
3(School of Computer Science and Engineering, Nanyang Technological University, Singapore 210023, Singapore)
軟件緩沖區(qū)溢出漏洞在CWE/SANS排名的25個最危險軟件漏洞中位列第三[1],是當今危害最為廣泛和嚴重的安全漏洞之一.緩沖區(qū)溢出是指輸入數(shù)據(jù)超過緩沖區(qū)可容納的最大數(shù)據(jù)量,進而超出部分溢出到臨近存儲區(qū)域的軟件漏洞.其產(chǎn)生的根本原因是使用非安全類型編程語言如 C/C++,強調(diào)效率優(yōu)先,而對內(nèi)存操作不做邊界檢查.緩沖區(qū)溢出可被惡意利用進而控制主機,獲得系統(tǒng)權(quán)限,執(zhí)行任意代碼,對安全攸關(guān)系統(tǒng)造成重大危害.典型的例子最早可追溯到1988年爆發(fā)的MORRIS蠕蟲[2],其利用BSD操作系統(tǒng)后臺程序的緩沖區(qū)溢出漏洞進行攻擊,數(shù)天之內(nèi)控制了近6 000臺網(wǎng)絡主機,幾乎導致互聯(lián)網(wǎng)完全癱瘓,造成了近一千萬美元的經(jīng)濟損失;再例如2001年7月爆發(fā)的Code Red蠕蟲[3]攻擊了近36萬臺服務器,造成超過26億美元的損失.速度最快的屬隨后于2003年1月爆發(fā)的Slammer蠕蟲[4],其基于微軟SQL Server的緩沖區(qū)溢出漏洞進行攻擊,在10分鐘之內(nèi)感染了7.5萬臺主機,最終感染主機60多萬臺,造成經(jīng)濟損失達50億美元之巨.
為了抵御緩沖區(qū)溢出漏洞的威脅,一方面,編程人員的安全編程技能不斷提升;另一方面,各種緩沖區(qū)溢出漏洞的檢測、防護技術(shù)也相繼提出,典型的可分為靜態(tài)方法和動態(tài)方法.靜態(tài)方法[5-8]不需執(zhí)行程序,基于源碼分析,能夠有效發(fā)現(xiàn)常見的緩沖區(qū)溢出漏洞.其優(yōu)勢在于速度快,可處理規(guī)模大,并且在一定假設前提下可以證明程序徹底擺脫某種特定類型的緩沖區(qū)溢出漏洞;其不足在于誤報率和漏報率較高;相對而言,動態(tài)方法[5]可獲得較高的精度,代價是巨大的額外開銷和性能損失;進一步地,部分動、靜態(tài)結(jié)合的技術(shù)也相繼提出[9,10].
上述方法在一定程度上提高了緩沖區(qū)溢出攻擊的門檻,緩解了緩沖區(qū)溢出漏洞造成的危害.但是面對當今信息社會軟件規(guī)模不斷擴大,軟件數(shù)量不斷增多(包括眾多現(xiàn)有的 C/C++編寫的系統(tǒng)以及以往的 C/C++遺留代碼)的現(xiàn)實,緩沖區(qū)溢出漏洞的數(shù)目不減反增.圖1所示為1989年~2017年,CVE公布的歷年緩沖區(qū)溢出漏洞數(shù)目,由此可知,緩沖區(qū)溢出漏洞依舊是威脅軟件安全的重大安全漏洞之一.
值得一提的是,緩沖區(qū)溢出不僅僅局限于非安全類型編程語言 C/C++,安全類型的編程語言代碼 Java,Perl,其底層基礎(chǔ)同樣面臨的緩沖區(qū)溢出攻擊的威脅[9].
書寫安全的代碼可能是唯一能夠使軟件徹底擺脫各種安全漏洞的終極辦法,軟件工程領(lǐng)域的研究一直致力于幫助編程人員書寫安全的代碼.遺憾的是,依靠目前的編程范式以及各種緩沖區(qū)溢出漏洞的防護措施,要徹底地消除緩沖區(qū)溢出漏洞幾乎是不可能的.因此,緩沖區(qū)溢出漏洞檢測技術(shù)與工具顯得至關(guān)重要,它們是檢測與修復、預防與保護、度量與評估等多方面工作的基礎(chǔ)與核心.
迄今為止,學術(shù)界和工業(yè)界提出了各種緩沖區(qū)溢出漏洞的檢測技術(shù)與工具.然而面對眾多的檢測技術(shù)與工具,使用者如何有效地進行選擇,進而應用到緩沖區(qū)溢出漏洞的檢測與修復、預防與保護、度量與評估等多個方面,是一個具體而實際的問題.
有必要對現(xiàn)有的軟件緩沖區(qū)溢出檢測技術(shù)與工具進行梳理,然而目前對于緩沖區(qū)溢出漏洞檢測技術(shù)與工具的梳理大多基于研究者的研究視角,而非使用者的應用視角.基于研究視角,其重點關(guān)注緩沖區(qū)溢出檢測方法的技術(shù)細節(jié),以靜態(tài)方法、動態(tài)方法和混合方法進行分類闡述.這樣的梳理對于研究者而言清晰明了,有利于研究者深入了解各種檢測技術(shù)細節(jié)進而進行技術(shù)改進.但是對于使用者而言不夠直觀實用,因為使用者不關(guān)心技術(shù)與工具的進一步改進,其關(guān)心的更多的是如何在有限成本的制約下,有效地選擇或者組合出能夠最大限度滿足自身需求的檢測工具進行應用,并達到盡可能好的效果.
該命題的回答可以歸結(jié)為用戶需求的細分與相應檢測技術(shù)工具的匹配,這需要同時深入了解用戶需求和緩沖區(qū)溢出檢測技術(shù)與工具.然而,一方面,用戶需求并不單一,是紛繁各異的,不同的行業(yè)、不同的場景下,用戶需求更是千差萬別,同時,用戶需求并不獨立,多個需求之間可能相互制約、需要進行多需求的平衡;另一方面,緩沖區(qū)溢出檢測工具也是門類繁多,各有特色,要完成用戶需求的細分與相應檢測工具的匹配,在繁雜各異的用戶需求與多種多樣的緩沖區(qū)溢出檢測技術(shù)與工具之間建立一張全面、條理清晰、而又便于用戶理解、使用的映射圖譜是非常困難的,同時也是非常有價值的.
本文站在使用者的立場,從技術(shù)與工具實際應用的視角出發(fā),在概述緩沖區(qū)溢出漏洞類型與特征的基礎(chǔ)上,從軟件生命周期階段的檢測與修復、緩沖區(qū)溢出攻擊階段的預防與保護、基于認識與理解途徑的度量評估這3個應用視角,對緩沖區(qū)溢出缺陷檢測技術(shù)與工具進行梳理,一定程度上在用戶需求與檢測技術(shù)與工具之間建立了一張映射圖譜,為用戶實際中有效選擇緩沖區(qū)溢出檢測技術(shù)與工具提供了指導,也為進一步的研究工作奠定了基礎(chǔ).
1 緩沖區(qū)溢出類型與特征
緩沖區(qū)溢出[11,12]是一種軟件漏洞,對于強調(diào)效率優(yōu)先的非安全類型編程語言 C/C++而言,當試圖將超過緩沖區(qū)所能容納的數(shù)據(jù)輸入到緩沖區(qū)時,因其不做邊界檢查,就會發(fā)生溢出.其中,緩沖區(qū)是指計算機中存儲數(shù)據(jù)的一段連續(xù)內(nèi)存區(qū)域,包括數(shù)據(jù)段、堆段、棧段.
當發(fā)生緩沖區(qū)溢出時,溢出的數(shù)據(jù)流入到緩沖區(qū)臨近的內(nèi)存區(qū)域,進而會覆蓋、修改臨近內(nèi)存區(qū)域中的值.緩沖區(qū)溢出攻擊就是利用這一特點進行的.攻擊者精心構(gòu)造輸入內(nèi)容,造成緩沖區(qū)溢出,進而使溢出部分修改附近內(nèi)存中諸如返回地址、函數(shù)指針、棧幀基址、指針變量等關(guān)鍵類型的值,使其指向攻擊者希望程序后續(xù)執(zhí)行的位置,從而改變程序控制流,最終執(zhí)行攻擊代碼(攻擊代碼可能位于構(gòu)造的輸入內(nèi)容之中,也可能是系統(tǒng)庫中的函數(shù)),實現(xiàn)其攻擊目的.
基于上述的理解,給出緩沖區(qū)溢出漏洞的定義,并對典型緩沖區(qū)溢出攻擊進行說明.
定義 1(緩沖區(qū)溢出漏洞).緩沖區(qū)溢出漏洞是一種軟件缺陷,指輸入數(shù)據(jù)的長度超過了緩沖區(qū)能夠容納的長度,超出的部分數(shù)據(jù)溢出到臨近的內(nèi)存區(qū)域的一種異常.
典型的緩沖區(qū)溢出攻擊是攻擊者基于緩沖區(qū)溢出漏洞,通過構(gòu)造輸入,造成緩沖區(qū)溢出,使溢出數(shù)據(jù)修改了臨近內(nèi)存區(qū)域的關(guān)鍵值(如 Return Address,Heap Metadata等),進而劫持控制流,執(zhí)行注入的或者重用已有代碼(如系統(tǒng)庫函數(shù)等)構(gòu)成的攻擊代碼的一種軟件安全攻擊.
1.1 緩沖區(qū)溢出漏洞類型
緩沖區(qū)溢出漏洞按照不同的標準有不同的分類:按照緩沖區(qū)所在內(nèi)存區(qū)域的位置可分為棧溢出、堆溢出和數(shù)據(jù)段溢出;按照導致溢出的內(nèi)存操作函數(shù)分為字符串操作(如strcpy函數(shù)等)導致的溢出和格式化輸出(如sprintf函數(shù)等)導致的溢出等;按照溢出數(shù)據(jù)修改的關(guān)鍵值類型分為修改返回地址的溢出、修改函數(shù)指針的溢出、修改指針變量的溢出等.
下面簡要介紹幾種典型緩沖區(qū)溢出漏洞類型,主要包括棧溢出、Return-into-Libc溢出、off-by-one溢出、堆溢出、數(shù)據(jù)段溢出、格式化字符串溢出和整數(shù)溢出.
1.1.1 棧溢出
棧溢出是被利用最廣泛的溢出漏洞.每一次函數(shù)調(diào)用,棧中會存放該函數(shù)對應的棧幀,幀中包含函數(shù)參數(shù)、函數(shù)返回地址、棧幀基址等信息.例如,函數(shù)func的棧幀如圖2所示.
Stack Smashing是基于棧溢出的典型攻擊,1996年,AlephOne在文獻[13,14]中進行了詳細的論述,其基本過程如圖3所示.首先,攻擊者通過精心構(gòu)造包含惡意代碼的輸入內(nèi)容傳入函數(shù)(如Pointer指向的數(shù)組);然后,函數(shù)內(nèi)部內(nèi)存操作函數(shù)(例如strcpy等)將輸入內(nèi)容拷貝到緩沖區(qū),進而造成溢出,溢出部分數(shù)據(jù)會修改臨近緩沖區(qū)的關(guān)鍵值,即函數(shù)的返回地址;最后,當函數(shù)執(zhí)行結(jié)束返回時,程序執(zhí)行跳轉(zhuǎn)到被修改過的返回地址所指向的地址,即緩沖區(qū)中惡意攻擊代碼所在的位置,進而執(zhí)行攻擊代碼.
此外,如果攻擊者不是將攻擊代碼注入到緩沖區(qū)中,而是重用已有代碼,系統(tǒng)庫函數(shù)(如system(·),exec(·)等)作為攻擊代碼,那么這樣的溢出攻擊叫做Return-into-Libc溢出攻擊.ROP(return oriented programming)[15,16]通過RET地址重用Gadgets代碼(即已在內(nèi)存中的指令序列).JOP(jump oriented programming)[17,18]通過Call/Jmp指令重用 Gadgets代碼構(gòu)成攻擊代碼.函數(shù)返回地址是最重要的攻擊目標之一,除此之外,指針變量、函數(shù)指針、棧幀基址等都是重要的攻擊目標,即,可以通過覆蓋修改指針變量(如上例中的 Pointer)的值和函數(shù)指針(func pointer)指向攻擊代碼[19].Off-by-one溢出則指的是輸入內(nèi)容恰好超出緩沖區(qū)一位數(shù)據(jù)的溢出,其通常產(chǎn)生于試圖將一個數(shù)組中的所有元素逐個復制到緩沖區(qū)中的循環(huán)中,如圖4所示.
該程序意在將 input中的數(shù)據(jù)逐個復制到長度為 128的 buffer數(shù)組中,但由于 for循環(huán)中i<128寫成了i<=128,所以該程序會復制129個數(shù)值到buffer中,進而造成off-by-one溢出.
1.1.2 堆溢出
堆是由程序運行時運用malloc(·)和free(·)等函數(shù)動態(tài)分配、釋放的內(nèi)存塊組成,每一個內(nèi)存塊都包含自身內(nèi)存大小和指向下一個內(nèi)存塊的指針等信息.雖然堆中沒有函數(shù)返回地址,但是攻擊者可以通過修改堆中的函數(shù)指針或者指針變量,進而達到修改程序控制流,執(zhí)行攻擊代碼的目的.典型的,如圖5所示[20].
圖 5(a)展示的是一個典型的在堆中動態(tài)分配和釋放的內(nèi)存塊情況,chunk1是一個已分配的內(nèi)存塊,包含其之前存儲的塊的大小和它本身的大小信息,User data部分即提供程序?qū)懭霐?shù)據(jù)的buffer區(qū)域.chunk3是一個臨近 chunk1且已被釋放的內(nèi)存塊,chunk2和 chunk4是位于堆中其他任意位置的已被釋放的內(nèi)存塊.chunk2,chunk3,chunk4在一個雙向鏈表結(jié)構(gòu)中,chunk2是鏈中的第1個內(nèi)存塊,其前向指針指向 chunk3,后向指針指向了鏈中前一個內(nèi)存塊.chunk3的前向指針指向chunk4,后向指針指向了chunk2.chunk4是鏈中最后一個內(nèi)存塊,其前向指針指向了鏈中下一個內(nèi)存塊,后向指針指向chunk3.圖5(b)則展示了一個攻擊,當chunk1中的User data部分溢出,攻擊者將覆蓋重寫chunk3的管理信息,chunk3的前向指針被修改指向棧中函數(shù)f0返回地址的前12個字節(jié)位置,后向指針被修改指向可以跳轉(zhuǎn)到后幾個字節(jié)然后執(zhí)行攻擊代碼的代碼位置(code to jump over dummy).當chunk1后續(xù)被釋放,就會和chunk3合并成了一個大的空閑內(nèi)存塊.由于Chunk3不再是一個獨立的空閑內(nèi)存塊,必須首先從空閑結(jié)點鏈表中移除 chunk3.其過程如下:chunk3→fd→bk=chunk3→bk,chunk3→bk→fd=chunk3→fd.即fd指向位置12個字節(jié)之后的內(nèi)存位置的值(即Returnaddressf0的地址)會被bk指向位置的值(即Codeto jump over dummy地址)重寫,bk指向位置8字節(jié)之后的內(nèi)存位置的值(dummy內(nèi)的地址)會被fd指向位置的值(即 Localvariablef0的地址)重寫.因此,在圖 5(b)中的返回地址會被一個指向跳轉(zhuǎn)代碼的指針重寫,進而越過存儲fd的地址區(qū)域,進而執(zhí)行注入的攻擊代碼(InjectedCode).
1.1.3 數(shù)據(jù)段溢出
數(shù)據(jù)段溢出[21]與堆段溢出類似,數(shù)據(jù)段中存儲的是初始化和未初始化的全局/靜態(tài)變量.如圖6所示.
上述程序中,如果str的長度超過buffer容量就會造成溢出,覆蓋函數(shù)指針fptr,這樣就可以改變程序的執(zhí)行流程,使其跳轉(zhuǎn)并執(zhí)行攻擊代碼.
1.1.4 格式化字符串溢出
格式化字符串溢出[11,22,23]主要由格式化字符函數(shù)如fprintf,sprintf,snprintf,syslog等引起.對于格式化字符串系函數(shù),如果不按照規(guī)定給定正確的輸入、輸出格式以及相應變量,就可能發(fā)生溢出.典型的如函數(shù)sprintf(char*str,constchar*format,...)是將格式化的數(shù)據(jù)寫入str所指的數(shù)組中,并添加‘