偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

安全漏洞是如何造成的:緩沖區(qū)溢出

譯文
安全 漏洞
自1988年莫里斯蠕蟲誕生以來,緩沖區(qū)溢出漏洞就威脅著從Linux到Windows的各類系統(tǒng)環(huán)境。

自1988年莫里斯蠕蟲誕生以來,緩沖區(qū)溢出漏洞就威脅著從Linux到Windows的各類系統(tǒng)環(huán)境。

緩沖區(qū)溢出漏洞長久以來一直是計(jì)算機(jī)安全領(lǐng)域的一大特例。事實(shí)上,世界上首個(gè)能夠自我傳播的互聯(lián)網(wǎng)蠕蟲——誕生于1988年的莫里斯蠕蟲——就是通過Unix系統(tǒng)中的守護(hù)進(jìn)程利用緩沖區(qū)溢出實(shí)現(xiàn)傳播的。而在二十七年后的今天,緩沖區(qū)溢出仍然在一系列安全隱患當(dāng)中扮演著關(guān)鍵性角色。聲威顯赫的Windows家族就曾在2000年初遭遇過兩次基于緩沖區(qū)溢出的成規(guī)模安全侵襲。而就在今年5月,某款Linux驅(qū)動(dòng)程序中遺留的潛在緩沖區(qū)溢出漏洞更是讓數(shù)百萬臺(tái)家庭及小型辦公區(qū)路由設(shè)備身陷風(fēng)險(xiǎn)之中。

[[147704]]

但頗為諷刺的是,作為一種肆虐多年的安全隱患,緩沖區(qū)溢出漏洞的核心卻只是由一種實(shí)踐性結(jié)果衍生出的簡單bug。計(jì)算機(jī)程序會(huì)頻繁使用多組讀取自某個(gè)文件、網(wǎng)絡(luò)甚至是源自鍵盤輸入的數(shù)據(jù)。程序?yàn)檫@些數(shù)據(jù)分配一定量的內(nèi)存塊——也就是緩沖區(qū)——作為存儲(chǔ)資源。而所謂緩沖區(qū)漏洞的產(chǎn)生原理就是,寫入或者讀取自特定緩沖區(qū)的數(shù)據(jù)總量超出了該緩沖區(qū)所能容納量的上限。

事實(shí)上,這聽起來像是一種相當(dāng)愚蠢、毫無技術(shù)含量的錯(cuò)誤。畢竟程序本身很清楚緩沖區(qū)的具體大小,因此我們似乎能夠很輕松地確保程序只向緩沖區(qū)發(fā)送不超出上限的數(shù)據(jù)量。這么想確實(shí)沒錯(cuò),但緩沖區(qū)溢出仍在不斷出現(xiàn),并始終成為眾多安全攻擊活動(dòng)的導(dǎo)火線。

為了了解緩沖區(qū)溢出問題的發(fā)生原因——以及為何其影響如此嚴(yán)重——我們需要首先談?wù)劤绦蚴侨绾问褂脙?nèi)存資源以及程序員是如何編寫代碼的。(需要注意的是,我們將以堆棧緩沖區(qū)溢出作為主要著眼對象。雖然這并不是惟一一種溢出問題,但卻擁有著典型性地位以及極高的知名度。)

堆疊起來

緩沖區(qū)溢出只會(huì)給原生代碼造成影響——也就是那些直接利用處理器指令集編寫而成的程序,而不會(huì)影響到利用Java或者Python等中間開發(fā)機(jī)制構(gòu)建的代碼。不同操作系統(tǒng)有著自己的特殊處理方式,但目前各類常用系統(tǒng)平臺(tái)則普遍遵循基本一致的運(yùn)作模式。要了解這些攻擊是如何出現(xiàn)的,進(jìn)而著手阻止此類攻擊活動(dòng),我們首先要了解內(nèi)存資源的使用機(jī)制。

在這方面,最重要的核心概念就是內(nèi)存地址。內(nèi)存當(dāng)中每個(gè)獨(dú)立的字節(jié)都擁有一個(gè)與之對應(yīng)的數(shù)值地址。當(dāng)處理器從主內(nèi)存(也就是RAM)中加載或者向其中寫稿數(shù)據(jù)時(shí),它會(huì)利用內(nèi)存地址來確定讀取或?qū)懭胨赶虻奈恢谩O到y(tǒng)內(nèi)存并不單純用于承載數(shù)據(jù),它同時(shí)也被用于執(zhí)行那些構(gòu)建軟件的可執(zhí)行代碼。這意味著處于運(yùn)行中的程序,其每項(xiàng)功能都會(huì)擁有對應(yīng)的地址。

在計(jì)算機(jī)制發(fā)展的早期階段,處理器與操作系統(tǒng)使用的是物理內(nèi)存地址:每個(gè)內(nèi)存地址都會(huì)直接與RAM中的特定位置相對應(yīng)。盡管目前某些現(xiàn)代操作系統(tǒng)仍然會(huì)有某些組成部分繼續(xù)使用這類物理內(nèi)存地址,但現(xiàn)在所有操作系統(tǒng)都會(huì)在廣義層面采用另一種機(jī)制——也就是虛擬內(nèi)存。

在虛擬內(nèi)存機(jī)制的幫助下,內(nèi)存地址與RAM中物理位置直接對應(yīng)的方式被徹底打破。相反,軟件與處理器會(huì)利用虛擬內(nèi)存地址保證自身運(yùn)轉(zhuǎn)。操作系統(tǒng)與處理器配合起來共同維護(hù)著一套虛擬機(jī)內(nèi)存地址與物理內(nèi)存地址之間的映射機(jī)制。

這種虛擬化方式帶來了一系列非常重要的特性。首先也是最重要的,即“受保護(hù)內(nèi)存”。具體而言,每項(xiàng)獨(dú)立進(jìn)程都擁有屬于自己的地址集合。對于一個(gè)32位進(jìn)程而言,這部分對應(yīng)地址從0開始(作為首個(gè)字節(jié))一直到4294967295(在十六進(jìn)制下表示為0xffff'ffff; 232 - 1)。而對于64位進(jìn)程,其能夠使用的地址則進(jìn)一步增加至18446744073709551615(十六進(jìn)制中的0xffff'ffff'ffff'ffff, 264 - 1)。也就是說,每個(gè)進(jìn)程都擁有自己的地址0,自己的地址1、地址2并以此類推。

(在文章的后續(xù)部分,除非另行強(qiáng)調(diào),否則我將主要針對32位系統(tǒng)進(jìn)行講解。其實(shí)32位與64位系統(tǒng)的工作機(jī)理是完全相同的,因此單獨(dú)著眼于前者不會(huì)造成任何影響,這只是為了盡量讓大家將注意力集中在單一對象身上。)

由于每個(gè)進(jìn)程都擁有自己的一套地址,而這種規(guī)劃就以一種非常簡單的方式防止了不同進(jìn)程之間相互干擾:一個(gè)進(jìn)程所能使用的全部參考內(nèi)存地址都將直接歸屬于該進(jìn)程。在這種情況下,進(jìn)程也能夠更輕松地完成對物理內(nèi)存地址的管理。值得一提的是,雖然物理內(nèi)存地址幾乎遵循同樣的工作原理(即以0為起始字節(jié)),但實(shí)際使用中可能帶來某些問題。舉例來說,物理內(nèi)存地址通常是非連續(xù)的;地址0x1ff8'0000被用于處理器的系統(tǒng)管理模式,而另有一小部分物理內(nèi)存地址會(huì)作為保留而無法被普通軟件所使用。除此之外,由PCIe卡提供的內(nèi)存資源一般也要占用一部分地址空間。而在虛擬地址機(jī)制中,這些限制都將不復(fù)存在。

那么進(jìn)程會(huì)在自己對應(yīng)的地址空間中藏進(jìn)什么小秘密呢?總體來講,大致有四種覺類別,我們會(huì)著重討論其中三種。這惟一一種不值得探討的也就是大多數(shù)操作系統(tǒng)所必不可少的“操作系統(tǒng)內(nèi)核”。出于性能方面的考量,內(nèi)存地址空間通常會(huì)被拆分為兩半,其中下半部分為程序所使用、上半部分由作為系統(tǒng)內(nèi)核的專用地址空間。內(nèi)核所占用的這一半內(nèi)存無法訪問程序那一半的內(nèi)容,但內(nèi)核自身卻可以讀取程序內(nèi)存,這也正是數(shù)據(jù)向內(nèi)核功能傳輸?shù)膶?shí)現(xiàn)原理。

我們首先需要關(guān)注的就是構(gòu)建程序的各類可執(zhí)行代碼與庫。主可執(zhí)行代碼及其全部配套庫都會(huì)被載入到對應(yīng)進(jìn)程的地址空間當(dāng)中,而且所有組成部分都擁有自己的對應(yīng)內(nèi)存地址。

其次就是程序用于存儲(chǔ)自身數(shù)據(jù)的內(nèi)存,這部分內(nèi)存資源通常被稱為heap、也就是內(nèi)存堆。舉例來說,內(nèi)存堆可以用于存儲(chǔ)當(dāng)前正在編輯的文檔、瀏覽的網(wǎng)頁(包括其中的全部JavaScript對象、CSS等等)或者當(dāng)前游戲的地圖資源等等。

第三也是最重要的一項(xiàng)概念即call stack,即調(diào)用堆——也簡稱為棧。內(nèi)存??梢哉f是最復(fù)雜的相關(guān)概念了。進(jìn)程中的每個(gè)分線程都擁有自己的內(nèi)存棧。棧其實(shí)就是一個(gè)內(nèi)存塊,用于追蹤某個(gè)線程當(dāng)前正在運(yùn)行的函數(shù)以及所有前趨函數(shù)——所謂前趨函數(shù),是指那些當(dāng)前函數(shù)需要調(diào)用的其它函數(shù)。舉例來說,如果函數(shù)a調(diào)用函數(shù)b,而函數(shù)b又調(diào)用函數(shù)c,那么棧內(nèi)所包含的信息則依次為a、b和c。

安全漏洞是如何造成的:緩沖區(qū)溢出

在這里我們可以看到棧的基本布局,首先是名為name的64字符緩沖區(qū),接下來依次為幀指針以及返回地址。esp擁有此內(nèi)存棧的上半部分地址,ebp則擁有內(nèi)存棧的下半部分地址。

調(diào)用堆棧屬于通用型“棧”數(shù)據(jù)結(jié)構(gòu)的一個(gè)特殊版本。棧是一種用于存儲(chǔ)對象且大小可變的結(jié)構(gòu)。新對象能夠被加入到(即’push‘)該棧的一端(一般為對應(yīng)內(nèi)存棧的’top‘端,即頂端),也可從棧中進(jìn)行移除(即’pop’)。只有內(nèi)存棧頂端的部分能夠通過push或者pop進(jìn)行修改,因此棧會(huì)強(qiáng)制執(zhí)行一種排序機(jī)制:最近添加進(jìn)入的項(xiàng)目也會(huì)被首先移除。而首個(gè)添加進(jìn)入的項(xiàng)目則會(huì)被最后移除。

調(diào)用堆棧最為重要的任務(wù)就是存儲(chǔ)返回地址。在大多數(shù)情況下,當(dāng)一款程序調(diào)用某項(xiàng)函數(shù)時(shí),該函數(shù)會(huì)按照既定設(shè)計(jì)發(fā)生作用(包括調(diào)用其它函數(shù)),并隨后返回至調(diào)用它的函數(shù)處。為了能夠切實(shí)返回至正確的調(diào)用函數(shù),必須存在一套記錄系統(tǒng)來注明進(jìn)行調(diào)用的源函數(shù):即應(yīng)當(dāng)在函數(shù)調(diào)用指令執(zhí)行之后從指令中恢復(fù)回來。這條指令所對應(yīng)的地址就被稱為返回地址。棧用于維護(hù)這些返回地址,就是說每當(dāng)有函數(shù)被調(diào)用時(shí),返回地址都會(huì)被push到其內(nèi)存棧當(dāng)中。而在函數(shù)返回之后,對應(yīng)返回地址則從內(nèi)存棧中被移除,處理器隨后開始在該地址上執(zhí)行指令。

棧的功能非常重要,甚至可以說是整個(gè)流程的核心所在,而處理器也會(huì)以內(nèi)置方式支持這些處理概念。以x86處理器為例,在x86所定義的各個(gè)寄存器當(dāng)中(所謂寄存器,是指處理器內(nèi)的小型存儲(chǔ)位置,其能夠直接由處理器指令進(jìn)行訪問),最為重要的兩類就是eip(即指令指針)以及esp(即棧指針)。

esp始終容納有棧頂端的對應(yīng)地址。每一次有數(shù)據(jù)被添加到該棧中時(shí),esp中的值都會(huì)降低。而每當(dāng)有數(shù)據(jù)從棧中被移除時(shí),esp的值則相應(yīng)增加。這意味著該棧的值出現(xiàn)“下降”時(shí),則代表有更多數(shù)據(jù)被添加到了該棧當(dāng)中,而esp中的存儲(chǔ)地址則會(huì)不斷向下方移動(dòng)。不過盡管如此,esp所使用的參考內(nèi)存位置仍然被稱為該內(nèi)存棧的“頂端”。

eip 為現(xiàn)有執(zhí)行指令提供內(nèi)存地址,而處理器則負(fù)責(zé)維護(hù)eip本身的正常運(yùn)作。處理器會(huì)從內(nèi)存當(dāng)中根據(jù)eip增量讀取指令流,從而保證始終能夠獲得正確的指令地址。x86擁有一項(xiàng)用于函數(shù)調(diào)用的指令,名為call,另一項(xiàng)用于從函數(shù)處返回的指令則名為ret。

call 會(huì)獲取一個(gè)操作數(shù),也就是欲調(diào)用函數(shù)的地址(當(dāng)然,我們也可以利用其它方式來獲取欲調(diào)用函數(shù)的地址)。當(dāng)執(zhí)行call指令時(shí),棧指針esp會(huì)通過4個(gè)字節(jié)(32位)來表現(xiàn),而緊隨call之后的指令地址——也就是返回地址——則會(huì)被寫入至當(dāng)前esp的參考內(nèi)存位置。換句話說,返回地址會(huì)被添加至內(nèi)存棧中。接下來,eip會(huì)將該地址指定為call的操作數(shù),并以該地址為起始位置進(jìn)行后續(xù)操作。

ret 的作用則完全相反。簡單的ret指令不會(huì)獲取任何操作數(shù)。處理器首先從esp當(dāng)中的內(nèi)存地址處讀取值,而后對esp進(jìn)行4字節(jié)的數(shù)值增量——這意味著其將返回地址從內(nèi)存棧中移除出去。這時(shí)eip接受值設(shè)定,并以此為起始位置進(jìn)行后續(xù)操作。

【視頻】

在實(shí)際操作中了解call與ret。

如果調(diào)用堆棧當(dāng)中只包含一組返回地址序列,那么問題當(dāng)然就很簡單了。但真正的難點(diǎn)在于,其它數(shù)據(jù)也會(huì)被添加到該內(nèi)存棧當(dāng)中。內(nèi)存棧的自身定位就是速度快且效率高的數(shù)據(jù)存儲(chǔ)位置。存儲(chǔ)在內(nèi)存堆上的數(shù)據(jù)相對比較復(fù)雜;程序需要全程追蹤內(nèi)存堆內(nèi)的當(dāng)前可用空間、當(dāng)前所使用數(shù)據(jù)片段各自占用多大空間外加其它一系列需要關(guān)注的指標(biāo)。不過內(nèi)存棧本身則非常簡單;要為某些數(shù)據(jù)騰出空間,只需要降低棧指針即可。而在數(shù)據(jù)不需要繼續(xù)駐留在內(nèi)存中時(shí),則增加棧指針。

這種便捷性讓內(nèi)存棧成為一套邏輯空間,能夠存儲(chǔ)歸屬于函數(shù)的各類變量。每項(xiàng)函數(shù)擁有256字節(jié)的緩沖空間來讀取用戶的輸入內(nèi)容。簡單來講,我們只需要在棧指針中減去256這一數(shù)值就能創(chuàng)建出該緩沖區(qū)。而在函數(shù)執(zhí)行結(jié)束時(shí),向棧指針內(nèi)添加添加256就能丟棄這個(gè)緩沖區(qū)。

安全漏洞是如何造成的:緩沖區(qū)溢出

當(dāng)我們正確使用程序時(shí),鍵盤輸入內(nèi)容會(huì)被存儲(chǔ)至name緩沖區(qū)中,隨后為null(即0)字節(jié)。幀指針與返回地址則保持不變。

但這種處理方式也存在局限。內(nèi)存棧并不適合保存規(guī)模龐大的對象;內(nèi)存的整體可用容量通常在線程創(chuàng)建之時(shí)就被確定下來了,而且通常大小為1 MB。因此,那些大型對象必須被保存在內(nèi)存堆中。棧也不適合保存那些需要長久存在,甚至生命周期比單一函數(shù)調(diào)用更長的對象。由于每個(gè)分配的內(nèi)存棧都會(huì)在函數(shù)執(zhí)行完成后被撤銷,因此任何存在于該棧中的對象將無法在函數(shù)結(jié)束后繼續(xù)駐留。不過存在于內(nèi)存堆中的對象則不受此類限制,它們能夠獨(dú)立于函數(shù)之外實(shí)現(xiàn)長期駐留。

內(nèi)存棧存儲(chǔ)機(jī)制并不只適用于程序員在程序中明確創(chuàng)建的命名變量,同時(shí)亦可用于存儲(chǔ)其它任何程序可能需要的數(shù)值。從傳統(tǒng)上講,這算是x86架構(gòu)的一大問題。X86處理器并不能提供太多寄存器(寄存器的總體數(shù)量只有8個(gè),而且其中一部分,例如eip與esp,還需要留作特定用途),因此函數(shù)幾乎無法在寄存器中長期保留所有數(shù)值。為了在不影響現(xiàn)有數(shù)值以供今后檢索的同時(shí)釋放寄存器空間,編譯器會(huì)將寄存器中的數(shù)值添加到內(nèi)存棧當(dāng)中。在此之后,相關(guān)數(shù)值可以pop方式從棧內(nèi)轉(zhuǎn)移回寄存器。用編譯器的術(shù)語來講,這種節(jié)約寄存器空間并保證數(shù)值可重復(fù)使用的操作被稱為spilling。

最后,內(nèi)存棧通常被用于向函數(shù)傳遞參數(shù)。調(diào)用函數(shù)會(huì)將每個(gè)參數(shù)添加到內(nèi)存棧中,而受調(diào)用函數(shù)之后則能夠?qū)⑦@些參數(shù)移除出去。這并不是惟一一種參數(shù)傳遞方式——舉例來說,也可以在寄存器內(nèi)部進(jìn)行參數(shù)傳遞——但卻是最為靈活的方式。

函數(shù)在內(nèi)存棧上的所有具體內(nèi)容——包括其本地變量、spilling寄存器操作以及任何準(zhǔn)備傳遞給其它函數(shù)的參數(shù)——被整體稱為一個(gè)“棧幀”。由于棧幀中的數(shù)據(jù)會(huì)被廣泛使用,因此需要一種能夠?qū)崿F(xiàn)快速引用的辦法與之配合。

棧指針也能完成這項(xiàng)任務(wù),但它的實(shí)現(xiàn)方式有些尷尬:棧指針總會(huì)指向內(nèi)存棧的頂端,因此它需要在添加與移除的數(shù)據(jù)之間來回移動(dòng)。舉例來說,某個(gè)變量可能以esp + 4地址作為起始位置,而在有另外兩個(gè)數(shù)值被添加到棧中時(shí),就意味著該變量現(xiàn)在的訪問位置變成了esp + 12。而一旦某個(gè)數(shù)值被移除出去,那么該變量的位置又變成了esp + 8。

這倒不是什么無法克服的障礙,編譯器本身能夠很輕松地加以解決。不過這仍然無法真正回避棧指針以“內(nèi)存棧頂端”作為起始位置的問題,特別是在手工編碼的匯編程序當(dāng)中。

為了簡化實(shí)現(xiàn)流程,最常見的辦法就是使用一個(gè)次級指針——其需要始終將數(shù)據(jù)保存在每個(gè)棧幀的底部(起始)位置——我們往往將該值稱為幀指針。在x86架構(gòu)中,甚至還有名為ebp的專門寄存器用于存儲(chǔ)這一值。由于這種機(jī)制不會(huì)對特定函數(shù)造成任何內(nèi)部變更,因此我們可以利用它作為訪問函數(shù)變量的一種固定方式:位于ebp – 4位置的值在整個(gè)函數(shù)中始終保持自己的ebp – 4位置。這種效果不僅有助于程序員理解,同時(shí)也能夠顯著簡化調(diào)試程序的處理流程。

安全漏洞是如何造成的:緩沖區(qū)溢出

以上截圖來自Visual Studio,其中顯示了某簡單x86程序完成上述操作的過程。在x86處理器當(dāng)中,名為esp的寄存器負(fù)責(zé)容納頂端內(nèi)存棧中的地址——在本示例中為0x0018ff00,以藍(lán)色高亮表示(在x86架構(gòu)中,內(nèi)存棧實(shí)際上會(huì)不斷向下推進(jìn)并指向地址0,但其仍然會(huì)以棧頂端為起點(diǎn)進(jìn)行地址調(diào)用)。該函數(shù)只擁有一個(gè)棧變量,即name,以粉色高亮表示。其緩沖區(qū)大小固定為32字節(jié)。由于屬于惟一一個(gè)變量,因此其位置同樣為0x0018ff00,與該內(nèi)存棧的頂端保持一致。

x86還擁有一個(gè)名為ebp的寄存器,以紅色高亮表示,其通常專門用于保存幀指針的位置。幀指針的位置緊隨棧變量之后。幀指針之后則為返回地址,以綠色高亮表示。返回地址所引用的代碼片段地址為0x00401048。在這條指令之后的是call指令,很明顯返回地址會(huì)從調(diào)用函數(shù)剩余的地址位置處執(zhí)行恢復(fù)。

安全漏洞是如何造成的:緩沖區(qū)溢出

遺憾的是,gets()實(shí)在是個(gè)極其愚蠢的函數(shù)。如果我們按住鍵盤上的A鍵,那么該函數(shù)會(huì)不間斷地一直向name緩沖區(qū)內(nèi)寫入“A”。在此過程中,該函數(shù)一直向內(nèi)存中寫入數(shù)據(jù),覆蓋幀指針、返回地址以及其它一切能夠被覆蓋的內(nèi)容。

在以上截圖當(dāng)中,name屬于會(huì)定期被覆蓋的緩沖區(qū)類型。其大小固定為64字符。在這里的示例中,它被填寫進(jìn)一大堆數(shù)字,并最終以null結(jié)尾。從上圖中可以清楚地看到,如果name緩沖區(qū)的寫入內(nèi)容超出了64字節(jié),那么該內(nèi)存棧中的其它數(shù)值也會(huì)受到影響。如果有額外的4字節(jié)內(nèi)容被寫入,那么該幀指針就會(huì)被破壞。而如果寫入的內(nèi)容為額外8個(gè)字節(jié),那么幀指針與返回地址將雙雙被覆蓋。

很明顯,這會(huì)導(dǎo)致程序數(shù)據(jù)遭到破壞,但緩存區(qū)溢出還會(huì)造成其它更加嚴(yán)重的后果:通常會(huì)影響到代碼執(zhí)行。之所以會(huì)出現(xiàn)這種情況,是因?yàn)榫彌_區(qū)溢出不僅會(huì)覆蓋數(shù)據(jù),同時(shí)也可能覆蓋內(nèi)存棧中的返回地址乃至其它更為重要的內(nèi)容。返回地址負(fù)責(zé)控制處理器在完成當(dāng)前函數(shù)之后,接下來執(zhí)行哪些指令。返回地址正常來說應(yīng)該處于調(diào)用函數(shù)之內(nèi)的某個(gè)位置,但如果由于緩沖區(qū)溢出而被覆蓋,那么返回地址的指向位置將變得隨機(jī)而不可控制。如果攻擊者能夠利用這種緩沖區(qū)溢出手段,則能夠選定處理器接下來要執(zhí)行的代碼位置。

在這一過程中,攻擊者可能并沒有什么理想的、便捷的“設(shè)備入侵”方法可供選擇,但這并不會(huì)影響惡意活動(dòng)的發(fā)生。用于覆蓋返回地址的緩沖區(qū)同時(shí)也可以被用于保存一小段可執(zhí)行代碼,也就是所謂shellcode,其隨后將能夠下載一段惡意可執(zhí)行代碼、開啟某個(gè)網(wǎng)絡(luò)連接或者是實(shí)現(xiàn)其它一些攻擊手段。

從傳統(tǒng)角度講,這確實(shí)是種令人有些意外的、小處引發(fā)的大問題:總體而言,每款程序在每次運(yùn)行時(shí)都會(huì)使用同樣的內(nèi)存地址——即使在經(jīng)過重啟之后也不例外。這意味著內(nèi)存棧上的緩沖區(qū)位置將永遠(yuǎn)不會(huì)變化,所以用于覆蓋返回地區(qū)的值也可以不斷重復(fù)加以使用。攻擊者只需要一次性找出對應(yīng)地址,就能夠在任何運(yùn)行著存在漏洞的代碼的計(jì)算機(jī)上再度實(shí)施攻擊。#p#

攻擊者的工具箱

在理想狀態(tài)下——當(dāng)然,這是從攻擊者的角度出發(fā)的——被覆蓋的返回地址可以就是緩沖區(qū)的所在位置。當(dāng)程序從文件或者網(wǎng)絡(luò)處讀取輸入數(shù)據(jù)時(shí),往往就會(huì)符合這一條件。

不過在其它情況下,攻擊者則需要?jiǎng)佑靡稽c(diǎn)小技巧。在負(fù)責(zé)處理我們能夠直接閱讀的文本內(nèi)容的函數(shù)中,0字節(jié)(或者稱為‘null’)通常會(huì)被特殊處理;它表示一條字符串的結(jié)尾,而用于操作這些字符串的函數(shù)——包括復(fù)制、比較以及整合等——將會(huì)在接觸到null字符后直接中止。這意味著如果該shellcode中包含有null字符,那么執(zhí)行程度到這里一定會(huì)停止。

【視頻】

查看整個(gè)緩沖區(qū)溢出過程。在這段視頻中,我們將shellcode添加到了緩沖區(qū)內(nèi),而后通過執(zhí)行以棋牌室返回地址。我們的shellcode運(yùn)行了Windows計(jì)算器程序。

安全漏洞是如何造成的:緩沖區(qū)溢出

為了利用這種溢出手段而非單純向內(nèi)存棧中寫入大量“A”以破壞一切內(nèi)容,攻擊者需要在緩沖區(qū)中添加shellcode:這是一小段可執(zhí)行代碼,其能夠執(zhí)行攻擊者所選定的一系列操作。在此之后,返回地址會(huì)被緩沖區(qū)所引用的地址所覆蓋,進(jìn)而在從某函數(shù)調(diào)用返回后將處理器定向至shellcode執(zhí)行位置。

為了實(shí)際這一目標(biāo),攻擊者可以選擇多種技術(shù)手段。代碼片段可以將包含有null字符的shellcode轉(zhuǎn)換為具備同等作用的形式以避免出現(xiàn)問題。它們甚至能夠處理更為嚴(yán)格的限制,例如一條已被篡改的函數(shù)可能只接收能夠通過標(biāo)準(zhǔn)鍵盤進(jìn)行輸入的結(jié)果。

內(nèi)存棧本身的地址中通常也包含有null字節(jié),這同樣會(huì)引發(fā)問題:這意味著返回地址無法直接被設(shè)定為棧緩沖區(qū)的地址。一般來講這倒不是什么大問題,畢竟那些可用于填寫緩沖區(qū)的函數(shù)(當(dāng)然,也會(huì)造成潛在的溢出隱患)會(huì)自行寫入一個(gè)null字節(jié)。但在某些情況下,它們則可被用于將null字節(jié)添加到正確的位置當(dāng)中,從而篡改內(nèi)存棧中的返回地址。

即使無法進(jìn)行返回地址篡改,這種狀況也可被攻擊者們用于重新定向。程序及其全部相關(guān)庫的存在意味著,內(nèi)存當(dāng)中可以駐留可執(zhí)行代碼。大部分此類可執(zhí)行代碼都能夠擁有屬于自己的“安全”地址,也就是說其中不包含任何null字節(jié)。

攻擊者們要做的就是找到一個(gè)包含一條指令的可用地址,例如x86架構(gòu)中的call esp,其會(huì)將棧指針的值作為函數(shù)地址看待并加以執(zhí)行——這顯然非常適合用來承載shellcode。攻擊者隨后會(huì)利用callesp指令的地址來覆蓋返回地址;如此一來,處理器會(huì)在該地址處進(jìn)行一次額外的跳轉(zhuǎn),但最終仍會(huì)運(yùn)行該shellcode。這項(xiàng)利用其它地址強(qiáng)行實(shí)現(xiàn)代碼執(zhí)行的方法被稱為“trampolining”,也就是蹦床。

安全漏洞是如何造成的:緩沖區(qū)溢出

有時(shí)候我們很難利用緩沖區(qū)地址來覆蓋返回地址。為了解決這個(gè)問題,我們可以利用目標(biāo)程序(或者其對應(yīng)庫)中的特定可執(zhí)行代碼片段地址來覆蓋返回地址。這部分代碼片段能幫助我們對緩沖區(qū)位置進(jìn)行轉(zhuǎn)換。

之所以這種方式能夠奏效,是因?yàn)檎缜懊嫣崽岬?,程序及其配套庫在每時(shí)運(yùn)行時(shí)都會(huì)使用同樣的內(nèi)存地址——即使是多次啟動(dòng)甚至在不同設(shè)備之上都不會(huì)改變這一點(diǎn)。而非常有趣的是,用于提供“蹦床”的庫本身并不需要執(zhí)行call esp指令。該庫只需要提供兩個(gè)字節(jié)(在本次示例中為0xff與0xd4)并保證彼此相鄰即可。它們可以作為其它指令中的組成部分甚至直接以數(shù)字形式存在;x86對于這類內(nèi)容并不挑剔。另外,x86的指令長度可以相當(dāng)之長(最高為15字節(jié)!)并指向任意地址。如果處理器從中間部分讀取某條指令——例如在一條長度為4字節(jié)的指令中從第二個(gè)字節(jié)開始讀取——那么最終的執(zhí)行結(jié)果可能會(huì)完全不同、但卻仍然切實(shí)生效??紤]到這一點(diǎn),攻擊者確實(shí)可以很輕松地找到可資利用的“蹦床”。

不過有時(shí)候,攻擊活動(dòng)無法直接將返回地址篡改為所需位置。不過由于內(nèi)存布局總是非常相似,不同設(shè)備或者不同運(yùn)行進(jìn)程之間的設(shè)定幾乎完全相同。舉例來說,某個(gè)可利用的緩沖區(qū)的具體位置可能會(huì)出現(xiàn)變化,而存在差異的幾個(gè)字節(jié)則取決于系統(tǒng)名稱或者IP地址。另外,軟件的小型更新可能也會(huì)讓內(nèi)存地址出現(xiàn)稍許變動(dòng)。為了解決這一問題,攻擊者只需要找到返回地址的大概正確位置即可,而不必保證其完全符合實(shí)際情況。

面對這類狀況,攻擊者的處理辦法也很簡單,這就是使用所謂“NOP sled”技術(shù)。相較于直接向緩沖區(qū)內(nèi)寫入shellcode,攻擊者可以在真正的shellcode之前編寫數(shù)量龐大的多條“NOP”指令(所謂NOP也就是’no-op‘,是指那些不會(huì)真正執(zhí)行的指令),有時(shí)候可以多達(dá)數(shù)百條。要運(yùn)行該shellcode,攻擊者只需要將返回地址設(shè)定在這些NOP指令當(dāng)中的某個(gè)位置即可。只要該地址被包含在NOP當(dāng)中,處理器就會(huì)快速將其略過并直接執(zhí)行真正的shellcode。

你的錯(cuò)、他的錯(cuò)——都是C的錯(cuò)

導(dǎo)致上述攻擊得以實(shí)現(xiàn)的核心bug——具體來講,就是向緩沖區(qū)內(nèi)寫入超出其容納能力的內(nèi)容——聽起來可以很輕松地加以避免。將這些問題完全歸咎于C編程語言及其各類兼容性分支方案——例如C++以及Objective-C——或許有些夸張,但也不能說毫無道理。C語言本身已經(jīng)相當(dāng)陳舊,但卻應(yīng)用廣泛且作為我們操作系統(tǒng)以及各類軟件的基礎(chǔ)性元素存在。正是由于C語言的流行,才讓這些本來可以輕松避免的bug長期生效并影響到無數(shù)開發(fā)者與用戶。

作為C語言自身阻礙安全開發(fā)實(shí)踐的一項(xiàng)實(shí)例,我們在這里要著重談?wù)刧ets()。作為一項(xiàng)函數(shù),gets()會(huì)獲取一條參數(shù)——也就是一個(gè)緩沖區(qū)——并從標(biāo)準(zhǔn)輸入內(nèi)容中(通常意味著’鍵盤輸入內(nèi)容‘)讀取一行數(shù)據(jù),而后將其添加到緩沖區(qū)當(dāng)中。細(xì)心的朋友可能已經(jīng)注意到,gets()當(dāng)中并不會(huì)對將被添加至緩沖區(qū)內(nèi)的參數(shù)長度作出限制,而且作為C語言設(shè)計(jì)中的一種有趣現(xiàn)象,我們沒辦法利用gets()了解緩沖區(qū)的實(shí)際大小。這是因?yàn)間ets()并不會(huì)對輸入內(nèi)容的大小作出任何要求:它只負(fù)責(zé)從標(biāo)準(zhǔn)輸入內(nèi)容中讀取數(shù)據(jù)——直到電腦前的操作者按下回車——而后嘗試將全部內(nèi)容添加到緩沖區(qū)內(nèi),即使操作者寫入的內(nèi)容遠(yuǎn)遠(yuǎn)超出了緩沖區(qū)容納能力,gets()也完全不予理會(huì)。

很明顯,這項(xiàng)函數(shù)屬于徹頭徹尾的安全隱患。由于我們無法制約通過鍵盤輸入的文本內(nèi)容總量,因此也就不可能避免由gets()引發(fā)的緩沖區(qū)溢出結(jié)果。C語言標(biāo)準(zhǔn)的制定者們確實(shí)意識(shí)到了這個(gè)問題,并在1999年的再版C語言規(guī)范中對gets()加以棄用,最終在2011年的更新中將其完全移除。但它的存在——以及不時(shí)出現(xiàn)的實(shí)際使用——證明了C語言確實(shí)給用戶們挖了一個(gè)非常危險(xiǎn)的潛在陷阱。

而作為誕生于1988年的世界首個(gè)可通過互聯(lián)網(wǎng)傳輸?shù)淖晕覐?fù)制惡意軟件,莫里斯蠕蟲利用的恰恰是這項(xiàng)函數(shù)。BSD 4.3 fingerd程序會(huì)通過端口79對網(wǎng)絡(luò)連接進(jìn)行監(jiān)聽,也就是我們常說的finger端口。事實(shí)上,finger也是一個(gè)非常古老的Unix程序,其作為網(wǎng)絡(luò)協(xié)議存在并負(fù)責(zé)識(shí)別是誰登錄到了遠(yuǎn)程系統(tǒng)當(dāng)中。它的使用方式分為兩種;其一是遠(yuǎn)程系統(tǒng)可以利用它來查詢當(dāng)前已經(jīng)登錄的每位用戶,其二則是用于查詢特定用戶名并告知我們與該用戶相關(guān)的部分信息。

每當(dāng)有連接出現(xiàn)在finger的后臺(tái)進(jìn)程當(dāng)中,它都會(huì)利用gets()從網(wǎng)絡(luò)中讀取數(shù)據(jù)并將其添加到內(nèi)存棧中一個(gè)512字節(jié)的緩沖區(qū)內(nèi)。在通常操作中,fingerd會(huì)隨后生成finger程序,并在可能的情況下向其傳遞相關(guān)用戶名。該finger程序才是真正負(fù)責(zé)監(jiān)聽用戶接入或者提供與特定用戶相關(guān)信息的主體,而fingerd本身僅僅負(fù)責(zé)監(jiān)聽網(wǎng)絡(luò)并在需要時(shí)啟動(dòng)finger。

鑒于惟一的“真實(shí)”參數(shù)基本只會(huì)是用戶名,因此512字節(jié)的緩沖區(qū)設(shè)定已經(jīng)不算小了。應(yīng)該沒人會(huì)設(shè)定一個(gè)長達(dá)512位的用戶名——不過系統(tǒng)本身并不會(huì)對此作出強(qiáng)制要求,因?yàn)樵谶@里負(fù)責(zé)內(nèi)容獲取工作的正是臭名昭著的gets()函數(shù)。當(dāng)我們通過網(wǎng)絡(luò)發(fā)出超過512字節(jié)的用戶名時(shí),fingerd就會(huì)乖乖地造成緩沖區(qū)溢出狀況。而這也正是Robert Morris的具體作法:他向fingerd發(fā)送了537字節(jié)的數(shù)據(jù)內(nèi)容(其中包含537個(gè)字節(jié)外中一個(gè)換行符,這直接導(dǎo)致gets()停止讀取輸入數(shù)據(jù)),順利實(shí)現(xiàn)緩沖區(qū)溢出并覆蓋了返回地址。在此之后,返回地址被輕松設(shè)置為內(nèi)存棧中的緩沖區(qū)地址。

莫里斯蠕蟲的可執(zhí)行負(fù)載非常簡單。它會(huì)發(fā)起400條NOP指令,從而讓內(nèi)存棧布局出現(xiàn)輕微的變化,而后再接上一小段代碼片段。這些代碼會(huì)生成一條shell,即/bin/sh。這是攻擊負(fù)載當(dāng)中很常見的選擇;fingerd程序會(huì)以root權(quán)限運(yùn)行,因此在遭到攻擊并被迫運(yùn)行shell時(shí),該shell也將擁有root權(quán)限。另外,fingerd會(huì)被引導(dǎo)至網(wǎng)絡(luò)當(dāng)中,這意味著其接收的“鍵盤輸入內(nèi)容”可以實(shí)際來源于網(wǎng)絡(luò)傳輸,并將輸出結(jié)果通過網(wǎng)絡(luò)發(fā)送出去。這兩大特性都明顯昭示其為shell所利用的潛在可能性,也就是說這一root shell現(xiàn)在已經(jīng)能夠?yàn)楣粽咚h(yuǎn)程操控。

盡管想要繞開gets()并不困難——事實(shí)上即使是在莫里斯蠕蟲剛剛誕生的時(shí)候,就出現(xiàn)了能夠徹底禁用gets()的fingerd修復(fù)版本——但C語言的其它一些組成部分仍然難以被忽略,甚至幾乎不可能被徹底修復(fù)。C語言對于文本字符的處理方式就是一種常見的問題根源。正如之前所提到,C語言在處理字符串時(shí)會(huì)在讀取至null字節(jié)后中止。在C語言中,一條字符串就是一段字符序列,其末尾以null字節(jié)作為字符串中止標(biāo)記。C語言當(dāng)中有一系列函數(shù)負(fù)責(zé)操作這些字符串。其中最典型的例子要數(shù)strcpy()——負(fù)責(zé)從來源處將一條字符串復(fù)制至目標(biāo)位置——以及strcat()——負(fù)責(zé)從來源處將一條字符串添加至目標(biāo)位置——這對奇葩了。這兩項(xiàng)函數(shù)都沒有對指向目標(biāo)緩沖區(qū)的參數(shù)作出長度限制,因此添加之后會(huì)不會(huì)造成緩沖區(qū)溢出根本就不在這二者的考量范圍之內(nèi)。

即使C語言的字符串處理函數(shù)能夠?qū)χ赶蚓彌_區(qū)的參數(shù)長度作出限制,同樣的錯(cuò)誤及溢出狀況仍然得不到徹底解決。C語言分別為strcat()與strcpy()提供一對姐妹函數(shù),分別名為strncat()與strncpy()。名稱當(dāng)中額外的n代表的正是其所獲取參數(shù)的長度。但正如很多資深C語言程序員們所知,這個(gè)n并不是將要寫入的緩沖區(qū)的具體大小;相反,它其實(shí)是來源處將要進(jìn)行復(fù)制的字符數(shù)量。如果來源提供的數(shù)據(jù)量超出了對應(yīng)字符限制(因?yàn)檫_(dá)到了null字節(jié)的位置),那么strncpy()與strncat()將會(huì)通過向目標(biāo)位置復(fù)制更多null字節(jié)的方式來補(bǔ)足差額。換句話來說,這些函數(shù)仍然完全不關(guān)心目標(biāo)緩沖區(qū)的實(shí)際大小。

與gets()不同,我們其實(shí)有能力以安全方式使用以上函數(shù),只不過有點(diǎn)困難罷了。C++與Objective-C都針對C語言的函數(shù)庫提供更理想的替代方案,這使得我們能夠更輕松且更安全地實(shí)現(xiàn)字符串操作——不過由于向下兼容的考量,某些C語言中的陳舊特性仍然被繼承了下來。

除此之外,二者還包含了C語言的一大根本性缺陷:緩沖區(qū)自身并不了解自己的確切大小,而且C語言也根本不會(huì)驗(yàn)證緩沖區(qū)之上所執(zhí)行的讀取與寫入操作——這就使得緩沖區(qū)溢出成為了可能。正是同樣的機(jī)制導(dǎo)致OpenSSL當(dāng)中曝出了Hearbleed漏洞,但值得強(qiáng)調(diào)的是,它并不算是溢出、而屬于讀取越界。OpenSSL當(dāng)中的C代碼會(huì)嘗試讀取超出緩沖區(qū)容納能力的內(nèi)容,并最終導(dǎo)致敏感信息泄露至外部環(huán)境。#p#

修復(fù)此類漏洞

無需贅言,隨著人類智慧的進(jìn)一步發(fā)展,我們?nèi)缃褚呀?jīng)擁有了更多更出色的語言選項(xiàng)——它們會(huì)對指向緩沖區(qū)的讀取與寫入操作進(jìn)行驗(yàn)證,這就徹底阻斷了溢出問題的發(fā)生。由Mozilla打造的Rust等編譯語言、安全運(yùn)行時(shí)環(huán)境的杰出代表Java以及.NET,外加Python、JavaScript、Lua以及Perl等虛擬化腳本語言都徹底解決了緩沖區(qū)溢出的問題(當(dāng)然,.NET仍然允許開發(fā)人員直接關(guān)閉所有保障措施,在這種選項(xiàng)設(shè)置之下緩沖區(qū)溢出會(huì)再度成為可能)。

緩沖區(qū)溢出目前仍然作為安全領(lǐng)域的一大關(guān)注重點(diǎn)存在,同時(shí)也是C語言持久生命力的有效證明。任何存在這一問題的遺留代碼都有可能引發(fā)重大的安全事故。但目前世界上仍在運(yùn)行的C代碼依舊數(shù)不勝數(shù),其中包括眾多主流操作系統(tǒng)的內(nèi)核以及OpenSSL等高人氣代碼庫。即使開發(fā)人員傾向于使用C#這樣安全性更出色的語言,他們也仍然需要使用大量由C語言編寫而成的第三方庫。

性能水平則是C語言繼續(xù)被廣泛使用的另一大理由,雖然關(guān)于這方面的具體判斷方式仍然比較模糊。確實(shí),經(jīng)過編譯的C與C++代碼能夠帶來更理想的執(zhí)行速度表現(xiàn),而且在某些情況下起到了無可替代的重要作用。然而目前大多數(shù)用戶所使用的處理器在絕大部分情況下都處于資源閑置的狀態(tài);如果我們能夠犧牲百分之十的總體性能來讓自己的瀏覽器獲得更為堅(jiān)實(shí)的安全保障,包括緩沖區(qū)溢出以及其它眾多潛在安全隱患,那么相信大家絕對會(huì)選擇這種方式。只要有廠商愿意開發(fā)出這樣值得依賴的瀏覽器,我們就能夠根據(jù)自己的實(shí)際需要作出權(quán)衡。

盡管如此,C語言和它的整個(gè)大家族卻仍然廣泛存在——當(dāng)然也包括由其帶來的緩沖區(qū)溢出風(fēng)險(xiǎn)。

目前已經(jīng)有不少相關(guān)舉措努力阻止溢出錯(cuò)誤影響到開發(fā)人員以及使用者。在開發(fā)過程中,我們可以選擇多種工具對源代碼進(jìn)行分析,并通過程序運(yùn)行來檢測其中是否存在危險(xiǎn)結(jié)構(gòu)或者溢出錯(cuò)誤,這就避免了此類bug被實(shí)際添加到軟件成品當(dāng)中。AddressSantizer等新型工具以及Valgrind等傳統(tǒng)方案都可以實(shí)現(xiàn)上述功能。

然而,這些工具需要開發(fā)人員的積極采用方能奏效,否則就是一堆毫無意義的0和1——也就是說仍有相當(dāng)多的程序并沒有將其納入開發(fā)流程。另有一些系統(tǒng)層面的保護(hù)手段,能夠在緩沖區(qū)溢出問題真正發(fā)生之后盡可能保證其它軟件免受其侵害。在這方面,操作系統(tǒng)以及編譯器開發(fā)者們已經(jīng)采取了一系列方案,旨在提高攻擊者使用這些溢出漏洞的難度。

某些系統(tǒng)的存在目的正是讓一部分特定攻擊活動(dòng)變得更難實(shí)現(xiàn)。當(dāng)前的多套Linux系統(tǒng)補(bǔ)丁就能夠確保系統(tǒng)庫全部被加載在底端內(nèi)存地址處,從而保證其地址中至少包含一個(gè)null字節(jié)。在這種情況下,攻擊者將很難利用C字符串處理方式在緩沖區(qū)溢出攻擊中使用這些地址。

其它防御機(jī)制也更為普遍。目前很多編譯器都擁有某種類型的內(nèi)存棧保護(hù)機(jī)制,其會(huì)將一個(gè)名為“canary”(意為金絲雀)的運(yùn)行時(shí)檢測值寫入到返回地址存儲(chǔ)位置附近的內(nèi)存棧末尾。在每項(xiàng)函數(shù)執(zhí)行結(jié)束之前,系統(tǒng)都會(huì)檢查該值以確定返回指令是否遭到了修改。如果該canary值發(fā)生了變化(因?yàn)槠湓诰彌_區(qū)溢出中被覆蓋),那么該程序?qū)⒘⒓幢罎⒍抢^續(xù)執(zhí)行。

而最重要的單項(xiàng)保護(hù)手段之一正是名為W^X(意為’單純寫入或執(zhí)行‘)、DEP(意為’數(shù)據(jù)執(zhí)行保護(hù)‘)、NX(意為‘不執(zhí)行’)、XD(意為‘執(zhí)行禁用’)、EVP(意為‘增強(qiáng)病毒保護(hù)’,AMD公司往往比較喜歡使用這一術(shù)語)、XN(即‘從不執(zhí)行’)等一系列措施。它們所采取的概念非常簡單。這些系統(tǒng)會(huì)盡可能讓內(nèi)存擁有可寫入能力(適用于緩沖區(qū))或者可執(zhí)行能力(適用于庫及程序代碼),但不會(huì)使其二者兼?zhèn)?。因此,即使攻擊者能夠使緩沖區(qū)出現(xiàn)溢出并控制其中的返回地址,處理器最終仍然會(huì)拒絕執(zhí)行對應(yīng)的shellcode。

無論具體使用什么樣的名稱,這都是一項(xiàng)重要的技術(shù),這主要是因?yàn)槠淠軌蛟跓o需額外成本的前提下起效——這類方案使用的是處理器自身內(nèi)置的、作為虛擬內(nèi)存硬件支持而存在的保護(hù)機(jī)制。

正如之前所提到,在虛擬內(nèi)存當(dāng)中每個(gè)進(jìn)程都擁有屬于自己的內(nèi)存地址。操作系統(tǒng)與處理器會(huì)共同保持一套映射機(jī)制,從而令虛擬地址指向其它位置;有時(shí)候一個(gè)虛擬地址可能會(huì)對應(yīng)一個(gè)物理內(nèi)存地址,但有時(shí)候其會(huì)對應(yīng)磁盤上某個(gè)文件的一部分,有時(shí)候甚至?xí)驗(yàn)樯形捶峙涠粚?yīng)任何對象。這是映射機(jī)制是高度細(xì)化的,通常以4096字節(jié)為一個(gè)區(qū)塊——也就是我們所說的page單位。

用于存儲(chǔ)這一映射的數(shù)據(jù)結(jié)構(gòu)不僅包含有每個(gè)page的位置(物理內(nèi)存、磁盤以及無位置),同時(shí)(通常)也包含有另外三個(gè)用定義page保護(hù)的字位:即該page是否可以讀取、其是否可以寫入以及其是否可以執(zhí)行。在這樣的保護(hù)之下,進(jìn)程對應(yīng)的內(nèi)存區(qū)域能夠被標(biāo)記為可讀取、可寫入但不可執(zhí)行。相反,程序的可執(zhí)行代碼片段以及庫則會(huì)被標(biāo)記為可讀取、可執(zhí)行但不可寫入。

NX的一大出色之處在于,操作系統(tǒng)通過更新獲得對應(yīng)的支持能力之后,它就能夠以追溯方式應(yīng)用于現(xiàn)有程序。某些程序偶爾也會(huì)在運(yùn)行中遇到問題。Java以及.NET當(dāng)中所使用的即時(shí)編譯器就會(huì)在運(yùn)行時(shí)環(huán)境下在內(nèi)存中生成可執(zhí)行代碼,這些代碼則要求內(nèi)存同時(shí)具備可寫入性與可執(zhí)行性(不過嚴(yán)格來講,這些代碼一般不會(huì)同時(shí)要求這兩種能力)。在NX出現(xiàn)之前,內(nèi)存始終同時(shí)具備可讀取性與可執(zhí)行性,因此這些即時(shí)編譯器完全無需針對其可讀取/可寫入緩沖區(qū)作出任何調(diào)整。但在NX出現(xiàn)之后,即時(shí)編譯器必須要確保將內(nèi)存保護(hù)機(jī)制從讀取-寫入變更為讀取-執(zhí)行。

市場對于NX這類安全方案的需求非常明確,特別是對于微軟陣營來說。早在2000年初,兩大蠕蟲的相繼出現(xiàn)就證明了微軟公司的系統(tǒng)代碼當(dāng)中存在著一些嚴(yán)重的安全問題:Code Red于2001年7月感染了35萬9千套運(yùn)行有微軟IIS Web Server的Windows 2000系統(tǒng),而隨后的SQL Slammer則于2003年1月侵入了超過7萬5千套運(yùn)行有微軟SQL Server數(shù)據(jù)庫的系統(tǒng)。這些都讓軟件巨頭陷入嚴(yán)重的被動(dòng)局面當(dāng)中。

這兩種蠕蟲利用的都是內(nèi)存棧中的緩沖區(qū)溢出漏洞,而且令人吃驚的是雖然距離莫里斯蠕蟲誕生已經(jīng)分別過去了13年和15年,但它們的開發(fā)方式幾乎完全相同。三者都將惡意負(fù)載添加到內(nèi)存棧的緩沖區(qū)內(nèi),并通過覆蓋返回地址的方式加以執(zhí)行。(惟一的區(qū)別在于,這兩位相對年輕的繼任者使用了‘蹦床’技術(shù)。相較于當(dāng)初直接將返回地址設(shè)置為內(nèi)存棧地址的方式,這二者將返回地址設(shè)置成了一條能夠傳遞至內(nèi)存棧并執(zhí)行的指令。)

當(dāng)然,這些蠕蟲方案在其它多個(gè)方面也算有所發(fā)展。Code Red的負(fù)載不僅能夠?qū)崿F(xiàn)自我復(fù)制,同時(shí)也會(huì)侵入網(wǎng)頁并試圖執(zhí)行拒絕服務(wù)攻擊。SQL Slammer則囊括了一切感染其它計(jì)算設(shè)備并在網(wǎng)絡(luò)上進(jìn)行傳播的功能組件,同時(shí)將自身體積控制在數(shù)百字節(jié)水平——這意味著受感染的機(jī)器上不會(huì)留下明顯的痕跡,而且重新啟動(dòng)之后這些痕跡就會(huì)徹底消失。這兩種蠕蟲也都開始以互聯(lián)網(wǎng)作為著眼重點(diǎn),這也使它們超越了老祖宗莫里斯蠕蟲、成功感染了更多計(jì)算機(jī)設(shè)備。

不過問題的關(guān)鍵在于,這樣一種能夠被直接利用的緩沖區(qū)溢出漏洞已經(jīng)算是古董級別的隱患了。正是由于兩種蠕蟲病毒的相繼出現(xiàn),才使人們對使用Windows接入互聯(lián)網(wǎng)并作為服務(wù)器系統(tǒng)產(chǎn)生了質(zhì)疑情緒。面對重重壓力,微軟公司表示將開始認(rèn)真對待安全問題。Windows XP SP2就是第一款真正讓安全意識(shí)融入其中的成品。它對軟件進(jìn)行了一系列調(diào)整,包括提供軟件防火墻、調(diào)整IE以避免工具欄乃至插件的靜默安裝——當(dāng)然,也實(shí)現(xiàn)了對NX的支持。

在硬件層面支持NX在2004年之后成為主流,當(dāng)時(shí)英特爾公司剛剛推出了其奔騰4處理器。而操作系統(tǒng)對于NX的支持也在Windows XP SP2邁出第一步后成為了業(yè)界共識(shí)。Windows 8在這方面表現(xiàn)得更加果斷,干脆不支持未配備NX硬件的陳舊處理器。#p#

后NX時(shí)代

隨著NX支持能力的逐步普及,緩沖區(qū)溢出也在當(dāng)下找到了新的實(shí)現(xiàn)途徑——換言之,攻擊者們發(fā)現(xiàn)了一系列能夠有效繞開NX的技術(shù)手段。

其中最早的一種與前面提到的“蹦床”機(jī)制非常相似,它能夠通過來自其它庫或者可執(zhí)行代碼的指令繞開系統(tǒng)在內(nèi)存棧緩沖區(qū)內(nèi)對shellcode的控制。不同于以往尋找可執(zhí)行代碼片段來直接將shellcode傳遞至內(nèi)存棧當(dāng)中,攻擊者們?nèi)缃褶D(zhuǎn)而開始特色確實(shí)擁有實(shí)際作用的代碼片段。

而其中最理想的選項(xiàng)也許要數(shù)Unix的system()函數(shù)了。這項(xiàng)函數(shù)會(huì)獲取一個(gè)參數(shù):一條字符串的地址代表著一條將被執(zhí)行的命令行,從傳統(tǒng)角度講該參數(shù)會(huì)被傳遞至內(nèi)存棧當(dāng)中。攻擊者可以創(chuàng)建一條命令行字符串,并將其添加至內(nèi)存棧中以實(shí)現(xiàn)溢出效果,而且由于在傳統(tǒng)角度上內(nèi)存中所承載的內(nèi)容不會(huì)發(fā)生位置變動(dòng),因此該字符串的地址將以已知形式存在、并作為內(nèi)存棧中配合攻擊活動(dòng)的組成部分。在這種情況下,被覆蓋的返回地址不會(huì)再被設(shè)置為緩沖區(qū)地址,而是被設(shè)置為system()函數(shù)的地址。當(dāng)造成緩沖區(qū)溢出的函數(shù)執(zhí)行完成后,它不會(huì)返回至調(diào)用函數(shù)處,而是運(yùn)行system()以執(zhí)行攻擊者選定的命令。

這就巧妙地繞過了NX的保護(hù)。作為系統(tǒng)庫的組成部分,system()函數(shù)始終處于執(zhí)行狀態(tài)。這種漏洞利用方式并不需要在內(nèi)存棧中執(zhí)行代碼,而只需要從內(nèi)存棧中讀取已有命令行。這項(xiàng)技術(shù)被稱為“return-to-libc”(即回庫),最初是由俄羅斯計(jì)算機(jī)安全專家Solar Designer于1997年發(fā)明的。(libc也就是Unix庫的名稱,其負(fù)責(zé)實(shí)現(xiàn)多種關(guān)鍵性函數(shù),包括system()。Unix庫通常會(huì)被載入到每個(gè)單獨(dú)的Unix進(jìn)程當(dāng)中,而這也使其成為攻擊活動(dòng)的首選目標(biāo)。)

雖然確切有效,但這項(xiàng)技術(shù)在某種程度上亦可以被扼制。一般來講,函數(shù)并不會(huì)從內(nèi)存線中獲取自己的參數(shù),而傾向于將其傳遞到寄存器當(dāng)中。在命令行字符串中傳遞參數(shù)以實(shí)現(xiàn)執(zhí)行雖然想法不錯(cuò),但卻往往會(huì)因?yàn)槠渲谐霈F(xiàn)的惱人null字節(jié)而導(dǎo)致運(yùn)轉(zhuǎn)停止。另外,這會(huì)讓多個(gè)函數(shù)同時(shí)調(diào)用變得非常困難。雖然并非無法解決——同時(shí)提供多個(gè)返回地址而非一個(gè)——但我們將完全無法變更參數(shù)順序、使用返回值或者實(shí)現(xiàn)其它操作。

安全漏洞是如何造成的:緩沖區(qū)溢出

相較于利用shellcode填寫緩沖區(qū),我們現(xiàn)在選擇利用返回地址與數(shù)據(jù)序列進(jìn)行填充。這些返回地址會(huì)在目標(biāo)程序及其庫之內(nèi)傳遞對現(xiàn)有可執(zhí)行代碼片段的控制權(quán)。每個(gè)代碼片段都會(huì)執(zhí)行一項(xiàng)操作而后返回,將控制權(quán)傳遞給下一個(gè)返回地址。

在過去幾年當(dāng)中,return-to-libc技術(shù)被廣泛用于突破現(xiàn)有安全保護(hù)措施。2001年末,安全業(yè)界就曾記錄下多種通過擴(kuò)展return-to-libc執(zhí)行多函數(shù)調(diào)用的方法,并提供了解決null字節(jié)問題的辦法。這些技術(shù)并未受到嚴(yán)格限制,因此2007年由此衍生出的另一種復(fù)雜度更高的攻擊手段開始出現(xiàn)——這種消除了大部分上述限制的方案正是ROP,即“返回導(dǎo)向編程”技術(shù)。

其基本設(shè)計(jì)思路與“回庫”以及“蹦床”差不多,但卻從普適性方面更進(jìn)了一步。“蹦床”是利用單一代碼片段將可執(zhí)行shellcode添加到緩沖區(qū)當(dāng)中,而ROP則是利用大量被稱為“gadget”的代碼片段。每個(gè)gadget都遵循一種特定模式:它會(huì)執(zhí)行某些操作(包括向寄存器中添加一個(gè)值、向內(nèi)存中寫入或者添加兩個(gè)寄存器等等),而后加上一條返回指令。x86的固有特性讓“蹦床”的設(shè)計(jì)思路在這邊再度起效;進(jìn)程當(dāng)中所加載的系統(tǒng)庫中包含著成百上千個(gè)能夠被解釋為“執(zhí)行一項(xiàng)操作,而后返回”的序列,因此它們也成為了實(shí)現(xiàn)ROP攻擊的潛在基礎(chǔ)。

這些gadget彼此之間通過一條長返回地址序列(也可以是其它任何有用或者必需的數(shù)據(jù))被串連在一起,并作為緩沖區(qū)溢出的組成部分被寫入至內(nèi)存棧當(dāng)中。返回指令則很少甚至完全無需借助處理器中的calling函數(shù)——而是單純利用returning函數(shù)——在gadget之間跳轉(zhuǎn)。值得注意的是,人們發(fā)現(xiàn)可資利用的gadget的數(shù)量與種類如此之多(至少在x86平臺(tái)上是如此),攻擊者幾乎能夠利用它們實(shí)現(xiàn)任何目標(biāo)。這一奇特的x86子集在特定使用方式之下往往會(huì)呈現(xiàn)出完備的圖靈特性(雖然其具體功能范圍取決于特定程序所加載的庫類型,并以此決定哪些gadget能夠切實(shí)起效)。

正如“回庫”技術(shù)一樣,所有可執(zhí)行代碼實(shí)際上都來源于系統(tǒng)庫,因此NX保護(hù)也就無處發(fā)力了。這套方案的出色靈活性意味著,攻擊者能夠甚至能夠?qū)崿F(xiàn)原本依靠“回庫”技術(shù)所難于完成的任務(wù),包括調(diào)用從寄存器內(nèi)獲取參數(shù)的函數(shù)或者將來自某一函數(shù)的返回值作為另一函數(shù)的參數(shù)等等。

ROP負(fù)載可謂變化多端。有時(shí)候它們只以簡單的“創(chuàng)建一個(gè)shell“形式的代碼出現(xiàn),但大多數(shù)情況下攻擊者都會(huì)利用ROP來調(diào)用某項(xiàng)系統(tǒng)函數(shù),從而變更某一內(nèi)存page的NX狀態(tài)、將其由可寫入轉(zhuǎn)變?yōu)榭蓤?zhí)行。通過這種方式,攻擊者將能夠利用便捷的非ROP負(fù)載在執(zhí)行過程中實(shí)現(xiàn)ROP。

隨機(jī)性提升

NX的這一弱點(diǎn)早已為安全專家們所了解,而這同時(shí)也成為最大的薄弱環(huán)節(jié)在各類攻擊活動(dòng)中反復(fù)出現(xiàn):攻擊者們在動(dòng)手之前就已經(jīng)掌握了內(nèi)存棧與系統(tǒng)庫的確切內(nèi)存地址。正因?yàn)楦黝惞艋顒?dòng)皆以此類知識(shí)作為基礎(chǔ),因此解決安全隱患的最佳途徑就是使這些知識(shí)失去效用。有鑒于此,地址空間布局隨機(jī)化(簡稱ASLO)技術(shù)應(yīng)運(yùn)而生:它會(huì)對內(nèi)存棧的位置、內(nèi)存內(nèi)庫以及可執(zhí)行代碼的位置進(jìn)行隨機(jī)化處理。一般來講,這些位置在程序每次運(yùn)行時(shí)、系統(tǒng)啟動(dòng)時(shí)或者二者同時(shí)發(fā)生時(shí)都會(huì)出現(xiàn)變化。

這極大地增加了攻擊活動(dòng)的實(shí)施難度,因?yàn)閹缀踉谝灰怪g,攻擊者根本不知道哪些ROP指令片段會(huì)駐留在內(nèi)存當(dāng)中、甚至弄不清楚要實(shí)現(xiàn)溢出的緩沖區(qū)到底在哪里。

ASLR在多個(gè)方面與NX攜手合作,因?yàn)樗饕?fù)責(zé)封殺“回庫”以及“返回導(dǎo)向編程”這兩大NX未能堵住的缺口。然而遺憾的是,它的介入深度有些過度。除了即時(shí)編譯器以及少數(shù)其它非常用程序之外,NX幾乎能夠被成功添加到任何現(xiàn)有軟件當(dāng)中。但ASLR在這方面則問題多多,程序及庫需要確保自身的正常運(yùn)行不會(huì)受到內(nèi)存地址隨機(jī)變化的影響。

舉例來說,在Windows當(dāng)中,DLL就基本不會(huì)受到內(nèi)存地址隨機(jī)化的影響。DLL在Windows系統(tǒng)上始終支持利用不同內(nèi)存地址加載數(shù)據(jù),但EXE文件就沒這么幸運(yùn)了。在ASLR出現(xiàn)之前,EXE文件會(huì)始終以0x0040000作為起始加載位置,并安全地以此為運(yùn)行前提。但ASLR出現(xiàn)之后,情況就完全不同了。為了確保不出現(xiàn)差錯(cuò),Windows在默認(rèn)條件下要求EXE可執(zhí)行文件對ASLR提供支持,并提供啟用選項(xiàng)。不過出于安全的考慮,即使對應(yīng)程序并未明確表達(dá)支持能力,Windows仍然默認(rèn)在所有可執(zhí)行程序及庫中啟用該選項(xiàng)。而且在大多數(shù)情況下,結(jié)果還是令人滿意的。

比較糟心的情況出現(xiàn)在x86 Linux系統(tǒng)當(dāng)中。在使用ASLR的情況下,Linux平臺(tái)的性能損失可能高達(dá)26%。除此之外,這套方案明確要求可執(zhí)行程序與庫以ASLR支持模式進(jìn)行編譯。這意味著管理員根本無法像在Windows環(huán)境中那樣對ASLR進(jìn)行授權(quán)。(x64也沒能徹底解決Linux的性能損失問題,不過損失程度得到了顯著降低。)

在ASLR啟用之后,它能夠?yàn)橄到y(tǒng)提供良好的緩沖區(qū)溢出狀況保護(hù)。不過ASLR本身還遠(yuǎn)遠(yuǎn)稱不上完美——舉例來說,其能夠提供的隨機(jī)水平就比較有限,而且這種情況在32位系統(tǒng)中表現(xiàn)得尤為嚴(yán)重。盡管內(nèi)存空間所能提供的地址數(shù)量高達(dá)40億個(gè),但并不是所有地址都能夠被用于加載庫或者旋轉(zhuǎn)內(nèi)存棧。

相反,其分配方式會(huì)受到各種約束,而且其中一部分還屬于泛用性目標(biāo)??傮w而言,操作系統(tǒng)傾向于將各個(gè)庫以相鄰方式進(jìn)行加載,以保證各個(gè)進(jìn)程的地址空間首尾相連,這樣就能盡可能多地為應(yīng)用程序運(yùn)行提供充裕的內(nèi)存容量。大家當(dāng)然也不希望讓一個(gè)庫以256 MB為單位遍布在整個(gè)內(nèi)存空間當(dāng)中——256 MB是我們能夠作為整體進(jìn)行分配的最大內(nèi)存單位,這種作法會(huì)限制應(yīng)用程序處理大型數(shù)據(jù)庫集的能力。

可執(zhí)行文件和庫通常在啟動(dòng)后必須進(jìn)行加載,且至少被包含在一個(gè)page當(dāng)中。通常來講,這意味著它們必須以4096整數(shù)倍的形式進(jìn)行加載。平臺(tái)也可以對內(nèi)存棧采用類似的協(xié)議;舉例來說,Linux會(huì)以16字節(jié)的整數(shù)倍形式啟動(dòng)內(nèi)存棧。迫于內(nèi)存的壓力,系統(tǒng)有時(shí)候需要對隨機(jī)性進(jìn)行進(jìn)一步削減,從而保證一切能夠順利運(yùn)行。

這種變化看起來影響不大,但卻意味著攻擊者有時(shí)候可以猜測到某個(gè)地址的可能位置,而且有相當(dāng)高的機(jī)率猜測成功。即使猜對的可能性非常低——例如二百五十六分之一——在某種情況下攻擊者依然足以利用其實(shí)施惡意活動(dòng)。當(dāng)攻擊某臺(tái)會(huì)自動(dòng)重啟崩潰進(jìn)程的Web服務(wù)器時(shí),256次攻擊中有255次出現(xiàn)崩潰完全不是什么大問題。只需要經(jīng)過簡單重啟,攻擊者就能再次嘗試下一個(gè)內(nèi)存地址。

不過在64位系統(tǒng)當(dāng)中,由于地址空間變得更加龐大,單純的猜測就不足以解決問題了。攻擊者面對的很可能是上百萬個(gè)——甚至數(shù)十億個(gè)——潛在內(nèi)存地址,機(jī)率如此之低也就不值得我們?yōu)橹畱n心了。

另外,對于攻擊者來說,猜測與崩潰這段手段不適用于瀏覽器之類的場景;沒有哪個(gè)用戶會(huì)連續(xù)對瀏覽器進(jìn)行256次重啟來“幫助”攻擊者完成試探。也就是說,在這種情況下NX與ASLR的聯(lián)手協(xié)作將讓攻擊者們變得無機(jī)可乘。

但如果有其它幫助手段存在,情況就不一樣了。在瀏覽器當(dāng)中的一種常見實(shí)現(xiàn)途徑在于利用JavaScript或者Flash——二者都包含著有能力生成可執(zhí)行代碼的即時(shí)編譯器——向內(nèi)存中塞進(jìn)大量經(jīng)過精心構(gòu)建的可執(zhí)行代碼。由此生成的大型NOP sled也就是我們目前經(jīng)常提到的“heap spraying”(也就是堆噴射)技術(shù)。另一種實(shí)現(xiàn)方式則是找出某個(gè)有可能泄露庫或者棧內(nèi)存地址的次級漏洞,從而幫助攻擊者獲得構(gòu)建自定義ROP返回地址組所必需的相關(guān)信息。

第三種方法在瀏覽器當(dāng)中比較常見:利用那些不支持ASLR的代碼庫。舉例來說,Adobe PDF插件或者微軟Office瀏覽器插件的某些早期版本就不支持ASLR,而且Windows在默認(rèn)情況下不會(huì)強(qiáng)制在非ASLR代碼中啟用該功能。如果攻擊者能夠強(qiáng)制載入這類庫(舉例來說,通過在隱藏的瀏覽器幀中加載某個(gè)PDF文件),那么他們就能夠直接繞過ASLR,即利用這些非ASLR庫容納自己的ROP負(fù)載。

一場永遠(yuǎn)休止的戰(zhàn)爭

攻擊技術(shù)與保護(hù)技術(shù)之爭就像是貓與老鼠的競逐。像ASLR以及NX這樣強(qiáng)大的保護(hù)系統(tǒng)能夠提高安全漏洞的利用門檻,從而在一定時(shí)期內(nèi)徹底阻止緩沖區(qū)溢出這類簡單漏洞的肆虐。然而聰明的攻擊者們?nèi)匀荒軌蛘业狡渌踩毕?,并將它們組合起來以繼續(xù)發(fā)動(dòng)攻勢。

這場軍備競賽仍在不斷升級。微軟公司的EMET(即‘增強(qiáng)緩解體驗(yàn)工具包’)當(dāng)中包含一系列半實(shí)驗(yàn)性保護(hù)方案,旨在檢測“堆噴射”乃至其它任何以ROP為基礎(chǔ)嘗試?yán)锰囟ǜ呶:瘮?shù)的行為。不過在這場永無休止的數(shù)字化對抗中,這些安全技術(shù)同樣在持續(xù)遭到淘汰。這并不是說它們沒有作用——各類新型保護(hù)技術(shù)的出現(xiàn)確實(shí)提高了漏洞利用的難度與成本——但大家必須正視一個(gè)現(xiàn)實(shí),即警惕之心須長久保持。

英文:How security flaws work: The buffer overflow

責(zé)任編輯:藍(lán)雨淚 來源: 51CTO.com
相關(guān)推薦

2015-09-22 14:49:41

網(wǎng)絡(luò)安全技術(shù)周刊

2020-08-10 08:37:32

漏洞安全數(shù)據(jù)

2018-11-01 08:31:05

2019-02-27 13:58:29

漏洞緩沖區(qū)溢出系統(tǒng)安全

2011-11-15 16:00:42

2022-08-09 08:31:40

C -gets函數(shù)漏洞

2017-01-09 17:03:34

2020-10-27 09:51:18

漏洞

2014-07-30 11:21:46

2018-01-26 14:52:43

2009-09-24 18:16:40

2019-03-06 09:00:38

ASLRLinux命令

2010-09-29 15:59:04

2010-12-27 10:21:21

2017-08-30 20:49:15

2011-02-24 09:21:31

2015-03-06 17:09:10

2010-10-09 14:45:48

2010-09-08 15:43:18

2011-03-23 12:39:44

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號