面试官不讲武德,居然让我讲讲蠕虫和金丝雀!
1. 蠕蟲病毒簡介
2. 緩沖區(qū)溢出
3. 緩沖區(qū)溢出舉例
4. 緩沖區(qū)溢出的危害
5. 內(nèi)存在計(jì)算機(jī)中的排布方式
6. 計(jì)算機(jī)中越界訪問的后果
7. 避免緩沖區(qū)溢出的三種方法
7.1 棧隨機(jī)化
7.2 檢測棧是否被破壞
7.3 限制可執(zhí)行代碼區(qū)域
8. 總結(jié)
蠕蟲病毒是一種常見的利用Unix系統(tǒng)中的缺點(diǎn)來進(jìn)行攻擊的病毒。緩沖區(qū)溢出一個常見的后果是:黑客利用函數(shù)調(diào)用過程中程序的返回地址,將存放這塊地址的指針精準(zhǔn)指向計(jì)算機(jī)中存放攻擊代碼的位置,造成程序異常中止。為了防止發(fā)生嚴(yán)重的后果,計(jì)算機(jī)會采用棧隨機(jī)化,利用金絲雀值檢查破壞棧,限制代碼可執(zhí)行區(qū)域等方法來盡量避免被攻擊。雖然,現(xiàn)代計(jì)算機(jī)已經(jīng)可以“智能”查錯了,但是我們還是要養(yǎng)成良好的編程習(xí)慣,盡量避免寫出有漏洞的代碼,以節(jié)省寶貴的時間!
1. 蠕蟲病毒簡介
??蠕蟲是一種可以自我復(fù)制的代碼,并且通過網(wǎng)絡(luò)傳播,通常無需人為干預(yù)就能傳播。蠕蟲病毒入侵并完全控制一臺計(jì)算機(jī)之后,就會把這臺機(jī)器作為宿主,進(jìn)而掃描并感染其他計(jì)算機(jī)。當(dāng)這些新的被蠕蟲入侵的計(jì)算機(jī)被控制之后,蠕蟲會以這些計(jì)算機(jī)為宿主繼續(xù)掃描并感染其他計(jì)算機(jī),這種行為會一直延續(xù)下去。蠕蟲使用這種遞歸的方法進(jìn)行傳播,按照指數(shù)增長的規(guī)律分布自己,進(jìn)而及時控制越來越多的計(jì)算機(jī)。
2. 緩沖區(qū)溢出
緩沖區(qū)溢出是指計(jì)算機(jī)向緩沖區(qū)內(nèi)填充數(shù)據(jù)位數(shù)時超過了緩沖區(qū)本身的容量,溢出的數(shù)據(jù)覆蓋在合法數(shù)據(jù)上。理想的情況是:程序會檢查數(shù)據(jù)長度,而且并不允許輸入超過緩沖區(qū)長度的字符。但是絕大多數(shù)程序都會假設(shè)數(shù)據(jù)長度總是與所分配的儲存空間相匹配,這就為緩沖區(qū)溢出埋下隱患。操作系統(tǒng)所使用的緩沖區(qū),又被稱為“堆棧”,在各個操作進(jìn)程之間,指令會被臨時儲存在“堆棧”當(dāng)中,“堆棧”也會出現(xiàn)緩沖區(qū)溢出。
3. 緩沖區(qū)溢出舉例
void?echo() {char?buf[4];???/*buf故意設(shè)置很小*/gets(buf);puts(buf); } void?call_echo() {echo(); }??反匯編如下:
/*echo*/ 000000000040069c?<echo>:? 40069c:48?83?ec?18?????????sub?$0x18,%rsp??/*0X18 == 24,分配了24字節(jié)內(nèi)存。計(jì)算機(jī)會多分配一些給緩沖區(qū)*/ 4006a0:48?89?e7????????????mov?%rsp,%rdi??? 4006a3:e8?a5?ff?ff?ff??????callq?40064d?<gets> 4006a8::48?89?e7???????????mov?%rsp,%rdi 4006ab:e8?50??fe?ff?ff?????callq?callq?400500?<puts@plt> 4006b0:48?83?c4?18?????????add?$0x18,%rsp? 4006b4:c3??????????????????retq? /*call_echo*/ 4006b5:48?83??ec?08?????????????sub?$0x8,%rsp? 4006b9:b8?00?00?00?00???????????mov?$0x0,%eax 4006be:e8?d9?ff?ff?ff???????????callq?40069c?<echo> 4006c3:48?83?c4?08??????????????add?$0x8,%rsp? 4006c7:c3???????????????????????retq在這個例子中,我們故意把buf設(shè)置的很小。運(yùn)行該程序,我們在命令行中輸入012345678901234567890123,程序立馬就會報錯:Segmentation fault。
要想明白為什么會報錯,我們需要通過分析反匯編來了解其在內(nèi)存是如何分布的。具體如下圖所示:
如下圖所示,此時計(jì)算機(jī)為buf分配了24字節(jié)空間,其中20字節(jié)還未使用。
??此時,準(zhǔn)備調(diào)用echo函數(shù),將其返回地址壓棧。
當(dāng)我們輸入“0123456789012345678 9012"時,緩沖區(qū)已經(jīng)溢出,但是并沒有破壞程序的運(yùn)行狀態(tài)。
當(dāng)我們輸入:“012345678901234567 890123"。緩沖區(qū)溢出,返回地址被破壞,程序返回 0x0400600。
這樣程序就跳轉(zhuǎn)到了計(jì)算機(jī)中其他內(nèi)存的位置,很大可能這塊內(nèi)存已經(jīng)被使用。跳轉(zhuǎn)修改了原來的值,所以程序就會中止運(yùn)行。
黑客可以利用這個漏洞,將程序精準(zhǔn)跳轉(zhuǎn)到其存放木馬的位置(如nop sled技術(shù)),然后就會執(zhí)行木馬程序,對我們的計(jì)算機(jī)造成破壞。
4. 緩沖區(qū)溢出的危害
緩沖區(qū)溢出可以執(zhí)行非授權(quán)指令,甚至可以取得系統(tǒng)特權(quán),進(jìn)而進(jìn)行各種非法操作。第一個緩沖區(qū)溢出攻擊--Morris蠕蟲,發(fā)生在二十年前,它曾造成了全世界6000多臺網(wǎng)絡(luò)服務(wù)器癱瘓。
在當(dāng)前網(wǎng)絡(luò)與分布式系統(tǒng)安全中,被廣泛利用的50%以上都是緩沖區(qū)溢出,其中最著名的例子是1988年利用fingerd漏洞的蠕蟲。而緩沖區(qū)溢出中,最為危險的是堆棧溢出。因?yàn)槿肭终呖梢岳枚褩R绯?#xff0c;在函數(shù)返回時改變返回程序的地址,讓其跳轉(zhuǎn)到任意地址。帶來的危害有兩種,一種是程序崩潰導(dǎo)致拒絕服務(wù),另外一種就是跳轉(zhuǎn)并且執(zhí)行一段惡意代碼,比如得到shell,然后為所欲為。
5. 內(nèi)存在計(jì)算機(jī)中的排布方式
內(nèi)存在計(jì)算機(jī)中的排布方式如下,從上到下依次為共享庫,棧,堆,數(shù)據(jù)段,代碼段。各個段的作用簡介如下:
共享庫:共享庫以.so結(jié)尾.(so==share object)在程序的鏈接時候并不像靜態(tài)庫那樣在拷貝使用函數(shù)的代碼,而只是作些標(biāo)記。然后在程序開始啟動運(yùn)行的時候,動態(tài)地加載所需模塊。所以,應(yīng)用程序在運(yùn)行的時候仍然需要共享庫的支持。共享庫鏈接出來的文件比靜態(tài)庫要小得多。
棧:棧又稱堆棧,是用戶存放程序臨時創(chuàng)建的變量,也就是我們函數(shù){}中定義的變量,但不包括static聲明的變量,static意味著在數(shù)據(jù)段中存放變量。
除此之外,在函數(shù)被調(diào)用時,其參數(shù)也會被壓入發(fā)起調(diào)用的進(jìn)程棧中,并且待到調(diào)用結(jié)束后,函數(shù)的返回值也會被存放回棧中,由于棧的先進(jìn)后出特點(diǎn),所以棧特別方便用來保存、恢復(fù)調(diào)用現(xiàn)場。從這個意義上講,我們可以把堆棧看成一個寄存,交換臨時數(shù)據(jù)的內(nèi)存區(qū)。在X86-64 Linux系統(tǒng)中,棧的大小一般為8M(用ulitmit - a命令可以查看)。
堆:堆是用來存放進(jìn)程中被動態(tài)分配的內(nèi)存段,它的大小并不固定,可動態(tài)擴(kuò)張或縮減。當(dāng)進(jìn)程調(diào)用malloc等函數(shù)分配內(nèi)存時,新分配的內(nèi)存就被動態(tài)分配到堆上,當(dāng)利用free等函數(shù)釋放內(nèi)存時,被釋放的內(nèi)存從堆中被剔除。
堆存放new出來的對象,棧里面所有對象都是在堆里面有指向的。假如棧里指向堆的指針被刪除,堆里的對象也要釋放(C++需要手動釋放)。當(dāng)然現(xiàn)在面向?qū)ο蟪绦蚨加?#39;垃圾回收機(jī)制',會定期的把堆里沒用的對象清除出去。
數(shù)據(jù)段:數(shù)據(jù)段通常用來存放程序中已初始化的全局變量和已初始化為非0的靜態(tài)變量的一塊內(nèi)存區(qū)域,屬于靜態(tài)內(nèi)存分配。直觀理解就是C語言程序中的全局變量(注意:全局變量才算是程序的數(shù)據(jù),局部變量不算程序的數(shù)據(jù),只能算是函數(shù)的數(shù)據(jù))
代碼段:代碼段通常用來存放程序執(zhí)行代碼的一塊區(qū)域。這部分區(qū)域的大小在程序運(yùn)行前就已經(jīng)確定了,通常這塊內(nèi)存區(qū)域?qū)儆?strong>只讀,有些架構(gòu)也允許可寫,在代碼段中也有可能包含以下只讀的常數(shù)變量,例如字符串常量等。
下面舉個例子來看下代碼中各個部分在計(jì)算機(jī)中是如何排布的。
#include?<stdio.h> #include?<stdlib.h>char?big_array[1L<<24];?????/*16?MB*/ char?huge_array[1L<<31];????/*2?GB*/int?global?=?0;int?useless()?{return?0;}int?main() {void?*phuge1,*psmall2,*phuge3,*psmall4;int?local?=?0;phuge1?=?malloc(1L<<28);????/*256?MB*/psmall2?=?malloc(1L<<8);????/*256?B*/phuge3?=?malloc(1L<<32);????/*4?GB*/psmall4?=?malloc(1L<<8);????/*256?B*/ }上述代碼中,程序中的各個變量在內(nèi)存的排布方式如下圖所示。根據(jù)顏色可以一一對應(yīng)起來。由于了local變量存放在棧區(qū),四個指針變量使用了malloc分配了空間, 所以存放在堆上,兩個數(shù)組big_ array,huge_array存放在數(shù)據(jù)段,main,useless函數(shù)的其他部分存放在代碼段中。
6. 計(jì)算機(jī)中越界訪問的后果
下面再看一個例子,看下越界訪問內(nèi)存會有什么結(jié)果。
typedef?struct? {int?a[2];double?d; }struct_t;double?fun(int?i) {volatile?struct_t?s;s.d?=?3.14;s.a[i]?=?1073741824;??/*可能越界*/return?s.d; }int?main() {printf("fun(0):%lf\n",fun(0));printf("fun(1):%lf\n",fun(1));printf("fun(2):%lf\n",fun(2));printf("fun(3):%lf\n",fun(3));printf("fun(6):%lf\n",fun(6));return?0;? }打印結(jié)果如下所示:
fun(0):3.14 fun(1):3.14 fun(2):3.1399998664856 fun(3):2.00000061035156 fun(6):Segmentation?fault在上面的程序中,我們定義了一個結(jié)構(gòu)體,其中 a 數(shù)組中包含兩個整數(shù)值,還有 d 一個雙精度浮點(diǎn)數(shù)。在函數(shù)fun中,fun函數(shù)根據(jù)傳入的參數(shù)i來初始化a數(shù)組。顯然,i的值只能為0和1。在fun函數(shù)中,同時還設(shè)置了d的值為3.14。當(dāng)我們給fun函數(shù)傳入0和1時可以打印出正確的結(jié)果3.14。但是當(dāng)我們傳入2,3,6時,奇怪的現(xiàn)象發(fā)生了。為什么fun(2)和fun(3)的值會接近3.14,而fun(6)會報錯呢?
要搞清楚這個問題,我們要明白結(jié)構(gòu)體在內(nèi)存中是如何存儲的,具體如下圖所示。
結(jié)構(gòu)體在內(nèi)存中的存儲方式GCC默認(rèn)不檢查數(shù)組越界(除非加編譯選項(xiàng))。而越界會修改某些內(nèi)存的值,得出我們意想不到的結(jié)果。即使有些數(shù)據(jù)相隔萬里,也可能受到影響。當(dāng)一個系統(tǒng)這幾天運(yùn)行正常時,過幾天可能就會崩潰。(如果這個系統(tǒng)是運(yùn)行在我們的心臟起搏器,又或者是航天飛行器上,那么這無疑將會造成巨大的損失!)
如上圖所示,對于最下面的兩個元素,每個塊代表 4 字節(jié)。a數(shù)組占用8個字節(jié),d變量占用8字節(jié),d排布在a數(shù)組的上方。所以我們會看到,如果我引用 a[0] 或者 a[1],會按照正常修改該數(shù)組的值。但是當(dāng)我調(diào)用 fun(2) 或者 fun(3)時,實(shí)際上修改的是這個浮點(diǎn)數(shù) d 所對應(yīng)的內(nèi)存位置。這就是為什么我們打印出來的fun(2)和fun(3)的值如此接近3.14的原因。
當(dāng)輸入 6 時,就修改了對應(yīng)的這塊內(nèi)存的值。原來這塊內(nèi)存可能存儲了其他用于維持程序運(yùn)行的內(nèi)容,而且是已經(jīng)分配的內(nèi)存。所以,我們程序就會報出Segmentation fault的錯誤。
7. 避免緩沖區(qū)溢出的三種方法
為了在系統(tǒng)中插入攻擊代碼,攻擊者既要插入代碼,也要插入指向這段代碼的指針。這個指針也是攻擊字符串的一部分。產(chǎn)生這個指針需要知道這個字符串放置的棧地址。在過去,程序的棧地址非常容易預(yù)測。對于所有運(yùn)行同樣程序和操作系統(tǒng)版本的系統(tǒng)來說,在不同的機(jī)器之間,棧的位置是相當(dāng)固定的。因此,如果攻擊者可以確定一個常見的Web服務(wù)器所使用的棧空間,就可以設(shè)計(jì)一個在許多機(jī)器上都能實(shí)施的攻擊。
7.1 棧隨機(jī)化
棧隨機(jī)化的思想使得棧的位置在程序每次運(yùn)行時都有變化。因此,即使許多機(jī)器都運(yùn)行同樣的代碼,它們的棧地址都是不同的。實(shí)現(xiàn)的方式是:程序開始時,在棧上分配一段0 ~ n字節(jié)之間的隨機(jī)大小的空間,例如,使用分配函數(shù)alloca在棧上分配指定字節(jié)數(shù)量的空間。程序不使用這段空間,但是它會導(dǎo)致程序每次執(zhí)行時后續(xù)的棧位置發(fā)生了變化。分配的范圍n必須足夠大,才能獲得足夠多的棧地址變化,但是又要足夠小,不至于浪費(fèi)程序太多的空間。
int?main() {long?local;printf("local?at?%p\n",&local);return?0; }這段代碼只是簡單地打印出main函數(shù)中局部變量的地址。在32位 Linux上運(yùn)行這段代碼10000次,這個地址的變化范圍為0xff7fc59c到0xffffd09c,范圍大小大約是。在64位 Linux機(jī)器上運(yùn)行,這個地址的變化范圍為0x7fff0001b698到0x7ffffffaa4a8,范圍大小大約是 。
其實(shí),一個好的黑客專家,可以使用暴力破壞棧的隨機(jī)化。對于32位的機(jī)器,我們枚舉個地址就能猜出來?xiàng)5牡刂贰τ?4位的機(jī)器,我們需要枚舉次。如此看來,棧的隨機(jī)化降低了病毒或者蠕蟲的傳播速度,但是也不能提供完全的安全保障。
7.2 檢測棧是否被破壞
計(jì)算機(jī)的第二道防線是能夠檢測到何時棧已經(jīng)被破壞。我們在echo函數(shù)示例中看到,當(dāng)訪問緩沖區(qū)越界時,會破壞程序的運(yùn)行狀態(tài)。在C語言中,沒有可靠的方法來防止對數(shù)組的越界寫。但是,我們能夠在發(fā)生了越界寫的時候,在造成任何有害結(jié)果之前,嘗試檢測到它。
GCC在產(chǎn)生的代碼中加人了一種棧保護(hù)者機(jī)制,來檢測緩沖區(qū)越界。其思想是在棧幀中任何局部緩沖區(qū)與棧狀態(tài)之間存儲一個特殊的金絲雀值,如下圖所示:
這個金絲雀值,也稱為哨兵值,是在程序每次運(yùn)行時隨機(jī)產(chǎn)生的,因此,攻擊者很難猜出這個哨兵值。在恢復(fù)寄存器狀態(tài)和從函數(shù)返回之前,程序檢查這個金絲雀值是否被該函數(shù)的某個操作或者該函數(shù)調(diào)用的某個函數(shù)的某個操作改變了。如果是的,那么程序異常中止。
英國礦井飼養(yǎng)金絲雀的歷史大約起始1911年。當(dāng)時,礦井工作條件差,礦工在下井時時常冒著中毒的生命危險。后來,約翰·斯科特·霍爾丹(John Scott Haldane)在經(jīng)過對一氧化碳一番研究之后,開始推薦在煤礦中使用金絲雀檢測一氧化碳和其他有毒氣體。金絲雀的特點(diǎn)是極易受有毒氣體的侵害,因?yàn)樗鼈兤匠ow行高度很高,需要吸入大量空氣吸入足夠氧氣。因此,相比于老鼠或其他容易攜帶的動物,金絲雀會吸入更多的空氣以及空氣中可能含有的有毒物質(zhì)。這樣,一旦金絲雀出了事,礦工就會迅速意識到礦井中的有毒氣體濃度過高,他們已經(jīng)陷入危險之中,從而及時撤離。
GCC會試著確定一個函數(shù)是否容易遭受棧溢出攻擊,并且自動插入這種溢出檢測。實(shí)際上,對于前面的棧溢出展示,我們可以使用命令行選項(xiàng)“-fno- stack- protector”來阻止GCC產(chǎn)生這種代碼。當(dāng)用這個選項(xiàng)來編譯echo函數(shù)時(允許使用棧保護(hù)),得到下面的匯編代碼
//void?echo? subq?$24,%rsp?Allocate?24?bytes?on?stack movq??%fs:40,%rax??Retrieve?canary? movq?%rax,8(%rsp)?Store?on?stack xorl?%eax,?%eax?Zero?out?register????//從內(nèi)存中讀出一個值 movq?%rsp,?%rdi??Compute?buf?as?%rsp? call?gets?Call?gets? movq?‰rsp,%rdi?Compute?buf?as?%rsp call?puts?Call?puts? movq?8(%rsp),%rax?Retrieve?canary? xorq?%fs:40,%rax?Compare?to?stored?value???//函數(shù)將存儲在棧位置處的值與金絲雀值做比較 je?.L9??If?=,?goto?ok? call?__stack_chk_fail?Stack?corrupted?? .L9 addq?$24,%rsp?Deallocate?stack?space? ret這個版本的函數(shù)從內(nèi)存中讀出一個值(第4行),再把它存放在棧中相對于%rsp偏移量為8的地方。指令參數(shù)各fs:40指明金絲雀值是用段尋址從內(nèi)存中讀入的。段尋址機(jī)制可以追溯到80286的尋址,而在現(xiàn)代系統(tǒng)上運(yùn)行的程序中已經(jīng)很少見到了。將金絲雀值存放在一個特殊的段中,標(biāo)記為只讀,這樣攻擊者就不能覆蓋存儲金絲雀值。在恢復(fù)寄存器狀態(tài)和返回前,函數(shù)將存儲在棧位置處的值與金絲雀值做比較(通過第10行的xorq指令)。如果兩個數(shù)相同,xorq指令就會得到0,函數(shù)會按照正常的方式完成。非零的值表明棧上的金絲雀值被修改過,那么代碼就會調(diào)用一個錯誤處理例程。
棧保護(hù)很好地防止了緩沖區(qū)溢出攻擊破壞存儲在程序棧上的狀態(tài)。一般只會帶來很小的性能損失。
7.3 限制可執(zhí)行代碼區(qū)域
最后一招是消除攻擊者向系統(tǒng)中插入可執(zhí)行代碼的能力。一種方法是限制哪些內(nèi)存區(qū)域能夠存放可執(zhí)行代碼。在典型的程序中,只有保存編譯器產(chǎn)生的代碼的那部分內(nèi)存才需要是可執(zhí)行的。其他部分可以被限制為只允許讀和寫。
許多系統(tǒng)都有三種訪問形式:讀(從內(nèi)存讀數(shù)據(jù))、寫(存儲數(shù)據(jù)到內(nèi)存)和執(zhí)行(將內(nèi)存的內(nèi)容看作機(jī)器級代碼)。以前,x86體系結(jié)構(gòu)將讀和執(zhí)行訪問控制合并成一個1位的標(biāo)志,這樣任何被標(biāo)記為可讀的頁也都是可執(zhí)行的。棧必須是既可讀又可寫的,因而棧上的字節(jié)也都是可執(zhí)行的。已經(jīng)實(shí)現(xiàn)的很多機(jī)制,能夠限制一些頁是可讀但是不可執(zhí)行的,然而這些機(jī)制通常會帶來嚴(yán)重的性能損失。
8. 總結(jié)
計(jì)算機(jī)提供了多種方式來彌補(bǔ)我們犯錯可能產(chǎn)生的嚴(yán)重后果,但是最關(guān)鍵的還是我們盡量減少犯錯。
例如,對于gets,strcpy等函數(shù)我們應(yīng)替換為 fgets,strncpy等。在數(shù)組中,我們可以將數(shù)組的索引聲明為size_t類型,從根本上防止它傳遞負(fù)數(shù)。此外,還可以在訪問數(shù)組前來加上num小于ARRAY_MAX 語句來檢查數(shù)組的上界。總之,要養(yǎng)成良好的編程習(xí)慣,這樣可以節(jié)省很多寶貴的時間。同時最后也推薦兩本相關(guān)書籍如下所示。
代碼大全(第二版)
高質(zhì)量程序設(shè)計(jì)指南
本文參考:《深入理解計(jì)算機(jī)系統(tǒng)》 https://baike.baidu.com/item/%E7%BC%93%E5%86%B2%E5%8C%BA%E6%BA%A2%E5%87%BA/678453?fr=aladdin#reference-[1]-36638-wrap https://baike.baidu.com/item/%E8%A0%95%E8%99%AB%E7%97%85%E6%AF%92/4094075?fr=aladdin https://zhuanlan.zhihu.com/p/185792677
往期精彩推薦
9個提高代碼運(yùn)行效率的小技巧你知道幾個?
24張圖7000字詳解計(jì)算機(jī)中的高速緩存
扒一扒ELF文件
推薦閱讀:
專輯|Linux文章匯總
專輯|程序人生
專輯|C語言
我的知識小密圈
關(guān)注公眾號,后臺回復(fù)「1024」獲取學(xué)習(xí)資料網(wǎng)盤鏈接。
歡迎點(diǎn)贊,關(guān)注,轉(zhuǎn)發(fā),在看,您的每一次鼓勵,我都將銘記于心~
總結(jié)
以上是生活随笔為你收集整理的面试官不讲武德,居然让我讲讲蠕虫和金丝雀!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 主成分分析(PCA)算法实现iris数据
- 下一篇: 数据库变为可疑_数据库显示可疑的修复方法