蠕蟲病毒是一種常見的利用Unix系統中的缺點來進行攻擊的病毒。緩衝區溢出一個常見的後果是:黑客利用函數調用過程當中程序的返回地址,將存放這塊地址的指針精準指向計算機中存放攻擊代碼的位置,形成程序異常停止。爲了防止發生嚴重的後果,計算機會採用棧隨機化,利用金絲雀值檢查破壞棧,限制代碼可執行區域等方法來儘可能避免被攻擊。雖然,現代計算機已經能夠「智能」查錯了,可是咱們仍是要養成良好的編程習慣,儘可能避免寫出有漏洞的代碼,以節省寶貴的時間!shell
蠕蟲是一種能夠自我複製的代碼,而且經過網絡傳播,一般無需人爲干預就能傳播。蠕蟲病毒入侵併徹底控制一臺計算機以後,就會把這臺機器做爲宿主,進而掃描並感染其餘計算機。當這些新的被蠕蟲入侵的計算機被控制以後,蠕蟲會以這些計算機爲宿主繼續掃描並感染其餘計算機,這種行爲會一直延續下去。蠕蟲使用這種遞歸的方法進行傳播,按照指數增加的規律分佈本身,進而及時控制愈來愈多的計算機。 (來源百度百科)編程
緩衝區溢出是指當計算機向緩衝區內填充數據位數時超過了緩衝區自己的容量,溢出的數據覆蓋在合法數據上。理想的狀況是:程序會檢查數據長度,並且並不容許輸入超過緩衝區長度的字符。可是絕大多數程序都會假設數據長度老是與所分配的儲存空間相匹配,這就爲緩衝區溢出埋下隱患。操做系統所使用的緩衝區,又被稱爲「堆棧」,在各個操做進程之間,指令會被臨時儲存在「堆棧」當中,「堆棧」也會出現緩衝區溢出。 (來源百度百科)數組
void echo() { char buf[4]; /*故意設置很小*/ gets(buf); puts(buf); } void call_echo(){ echo(); }
反彙編以下:安全
/*echo*/ 000000000040069c <echo>: 40069c:48 83 ec 18 sub $0x18,%rsp /*0X18 == 24,分配了24字節內存。計算機會多分配一些給緩衝區*/ 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設置的很小。運行該程序,咱們在命令行中輸入012345678901234567890123,程序立馬就會報錯:Segmentation fault。服務器
要想明白爲何會報錯,咱們須要經過分析反彙編來了解其在內存是如何分佈的。具體以下圖所示:網絡
以下圖所示,此時計算機爲buf分配了24字節空間,其中20字節還未使用。數據結構
此時,準備調用echo函數,將其返回地址壓棧。架構
當咱們輸入「01234567890123456789012"時,緩衝區已經溢出,可是並無破壞程序的運行狀態。less
當咱們輸入:「012345678901234567890123"。緩衝區溢出,返回地址被破壞,程序返回 0x0400600。分佈式
這樣程序就跳轉到了計算機中其餘內存的位置,很大可能這塊內存已經被使用。跳轉修改了原來的值,因此程序就會停止運行。
黑客能夠利用這個漏洞,將程序精準跳轉到其存放木馬的位置,而後就會執行木馬程序,對咱們的計算機形成破壞。
能夠利用它執行非受權指令,甚至能夠取得系統特權,進而進行各類非法操做。緩衝區溢出攻擊有多種英文名稱:buffer overflow,buffer overrun,smash the stack,trash the stack,scribble the stack, mangle the stack, memory leak,overrun screw;它們指的都是同一種攻擊手段。第一個緩衝區溢出攻擊--Morris蠕蟲,發生在二十年前,它曾形成了全世界6000多臺網絡服務器癱瘓。
在當前網絡與分佈式系統安全中,被普遍利用的50%以上都是緩衝區溢出,其中最著名的例子是1988年利用fingerd漏洞的蠕蟲。而緩衝區溢出中,最爲危險的是堆棧溢出,由於入侵者能夠利用堆棧溢出,在函數返回時改變返回程序的地址,讓其跳轉到任意地址,帶來的危害一種是程序崩潰致使拒絕服務,另一種就是跳轉而且執行一段惡意代碼,好比獲得shell,而後隨心所欲。 (來源百度百科)
內存在計算機中的排布方式以下,從上到下依次爲共享庫,棧,堆,數據段,代碼段。各個段的做用簡介以下(更詳細的內容總結見嵌入式軟件開發知識點總結.pdf):
共享庫:共享庫以.so結尾.(so==share object)在程序的連接時候並不像靜態庫那樣在拷貝使用函數的代碼,而只是做些標記。而後在程序開始啓動運行的時候,動態地加載所需模塊。因此,應用程序在運行的時候仍然須要共享庫的支持。共享庫連接出來的文件比靜態庫要小得多。
棧:棧又稱堆棧,是用戶存放程序臨時建立的變量,也就是咱們函數{}中定義的變量,但不包括static聲明的變量,static意味着在數據段中存放變量。除此以外,在函數被調用時,其參數也會被壓入發起調用的進程棧中,而且待到調用結束後,函數的返回值也會被存放回棧中,因爲棧的先進後出特色,因此棧特別方便用來保存、恢復調用現場。從這個意義上講,咱們能夠把堆棧當作一個寄存,交換臨時數據的內存區。在X86-64 Linux系統中,棧的大小通常爲8M(用ulitmit - a命令能夠查看)。
堆:堆是用來存放進程中被動態分配的內存段,它的大小並不固定,可動態擴張或縮減。當進程調用malloc等函數分配內存時,新分配的內存就被動態分配到堆上,當利用free等函數釋放內存時,被釋放的內存從堆中被剔除。
堆存放new出來的對象、棧裏面全部對象都是在堆裏面有指向的、假如棧裏指向堆的指針被刪除、堆裏的對象也要釋放(C++須要手動釋放)、固然咱們如今好面向對象程序都有'垃圾回收機制'、會按期的把堆裏沒用的對象清除出去。
數據段:數據段一般用來存放程序中已初始化的全局變量和已初始化爲非0的靜態變量的一塊內存區域,屬於靜態內存分配。直觀理解就是C語言程序中的全局變量(注意:全局變量纔算是程序的數據,局部變量不算程序的數據,只能算是函數的數據)
代碼段:代碼段一般用來存放程序執行代碼的一塊區域。這部分區域的大小在程序運行前就已經肯定了,一般這塊內存區域屬於只讀,有些架構也容許可寫,在代碼段中也有可能包含如下只讀的常數變量,例如字符串常量等。程序段爲程序代碼在內存中映射一個程序能夠在內存中有多個副本。
下面舉個例子來看下代碼中各個部分再計算機中是如何排布的。
#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*/ /*some print statements....*/ }
上述代碼中,程序中的各個變量在內存的排布方式以下圖所示。根據顏色能夠一一對應起來。因爲了local變量存放在棧區,四個指針變量使用了malloc分配了空間,因此存放在堆上,兩個數組big_array,huge_array存放在數據段,main,useless函數的其餘部分存放在代碼段中。
下面再看一個例子,看下越界訪問內存會有什麼結果。
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; }
打印結果以下所示
fun(0):3.14 fun(1):3.14 fun(2):3.1399998664856 fun(3):2.00000061035156 fun(6):Segmentation fault
在上面的程序中,咱們定義了一個結構體,其中 a 數組中包含兩個整數值,還有 d 一個雙精度浮點數。在函數fun中,fun函數根據傳入的參數i來初始化a數組。顯然,i的值只能爲0和1。在fun函數中,同時還設置了d的值爲3.14。當咱們給fun函數傳入0和1時能夠打印出正確的結果3.14。可是當咱們傳入2,3,6時,奇怪的現象發生了。爲何fun(2)和fun(3)的值會接近3.14,而fun(6)會報錯呢?
要搞清楚這個問題,咱們要明白結構體在內存中是如何存儲的,具體以下圖所示。
GCC默認不檢查數組越界,除非加編譯選項。這也是C的bug之一,越界會修改某些內存的值,得出咱們意想不到的結果。即便有些數據相隔萬里,也可能受到影響。當一個系統這幾天運行正常時,過幾天可能就會崩潰。(若是這個系統是運行在咱們的心臟起搏器,又或者是航天飛行器上,那麼這無疑將會形成巨大的損失!)
如上圖所示,對於最下面的兩個元素,每一個塊表明 4 字節。a數組佔用8個字節,d變量佔用8字節,d排布在a數組的上方。因此咱們會看到,若是我引用 a[0] 或者 a[1],會按照正常修改該數組的值。可是當我調用 fun(2) 或者 fun(3)時,實際上修改的是這個浮點數 d 的字節。這就是爲何咱們打印出來的fun(2)和fun(3)的值如此接近3.14。當輸入 6 時,就修改了對應的這塊內存的值。原來這塊內存可能存儲的其餘用於維持程序運行的內容,並且是已經分配的內存。所示,咱們程序就會報出Segmentation fault的錯誤。當咱們理解了數據結構的機器級表示以及它們是如何運行的,處理這些漏洞也就很輕鬆了。
爲了在系統中插入攻擊代碼,攻擊者既要插入代碼,也要插入指向這段代碼的指針。這個指針也是攻擊字符串的一部分。產生這個指針須要知道這個字符串放置的棧地址。在過去,程序的棧地址很是容易預測。對於全部運行一樣程序和操做系統版本的系統來講,在不一樣的機器之間,棧的位置是至關固定的。所以,若是攻擊者能夠肯定一個常見的Web服務器所使用的棧空間,就能夠設計一個在許多機器上都能實施的攻擊。
棧隨機化的思想使得棧的位置在程序每次運行時都有變化。所以,即便許多機器都運行一樣的代碼,它們的棧地址都是不一樣的。實現的方式是:程序開始時,在棧上分配一段0 ~ n字節之間的隨機大小的空間,例如,使用分配函數alloca在棧上分配指定字節數量的空間。程序不使用這段空間,可是它會致使程序每次執行時後續的棧位置發生了變化。分配的範圍n必須足夠大,才能得到足夠多的棧地址變化,可是又要足夠小,不至於浪費程序太多的空間。
int main(){ long local; printf("local at %p\n",&local); return 0; }
這段代碼只是簡單地打印出main函數中局部變量的地址。在32位 Linux上運行這段代碼10000次,這個地址的變化範圍爲0xff7fc59c到0xffffd09c,範圍大小大約是\({2^{23}}\)。在更新一點兒的機器上運行64位 Linux,這個地址的變化範圍爲0x7fff0001b698到0x7ffffffaa4a8,範圍大小大約是 \({2^{32}}\)。
其實,一個好的黑客專家,可使用蠻力破壞棧的隨機化。對於32位的機器,咱們枚舉\({2^{15}} = 32768\)個地址就能猜出來棧的地址。對於64位的機器,咱們須要枚舉\({2^{24}} = 16777216\)次。如此看來,棧的隨機化下降了病毒或者蠕蟲的傳播速度,可是也不能提供徹底的安全保障。
計算機的第二道防線是可以檢測到什麼時候棧已經被破壞。咱們在echo函數示例中看到,當訪問緩衝區越界時,會破壞程序的運行狀態。在C語言中,沒有可靠的方法來防止對數組的越界寫。可是,咱們可以在發生了越界寫的時候,在形成任何有害結果以前,嘗試檢測到它。
GCC在產生的代碼中加人了一種棧保護者機制,來檢測緩衝區越界。其思想是在棧幀中任何局部緩衝區與棧狀態之間存儲一個特殊的金絲雀( canary)值,以下圖所示:
這個金絲雀值,也稱爲哨兵值,是在程序每次運行時隨機產生的,所以,攻擊者沒有簡單的辦法可以知道它是什麼。在恢復寄存器狀態和從函數返回以前,程序檢查這個金絲雀值是否被該函數的某個操做或者該函數調用的某個函數的某個操做改變了。若是是的,那麼程序異常停止。
英國礦井飼養金絲雀的歷史大約起始1911年。當時,礦井工做條件差,礦工在下井時時常冒着中毒的生命危險。後來,約翰·斯科特·霍爾丹(John Scott Haldane)在通過對一氧化碳一番研究以後,開始推薦在煤礦中使用金絲雀檢測一氧化碳和其餘有毒氣體。金絲雀的特色是極易受有毒氣體的侵害,由於它們日常飛行高度很高,須要吸入大量空氣吸入足夠氧氣。所以,相比於老鼠或其餘容易攜帶的動物,金絲雀會吸入更多的空氣以及空氣中可能含有的有毒物質。這樣,一旦金絲雀出了事,礦工就會迅速意識到礦井中的有毒氣體濃度太高,他們已經陷入危險之中,從而及時撤離。
GCC會試着肯定一個函數是否容易遭受棧溢出攻擊,而且自動插入這種溢出檢測。實際上,對於前面的棧溢出展現,咱們不得不用命令行選項「-fno- stack- protector」來阻止GCC產生這種代碼。當不用這個選項來編譯echo函數時,也就是容許使用棧保護,獲得下面的彙編代碼
/*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 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 je .L9 If =, goto ok call __stack_chk_fail Stack corrupted .L9 addq $24,%rsp Deallocate stack space ret
這個版本的函數從內存中讀出一個值(第4行),再把它存放在棧中相對於%rsp偏移量爲8的地方。指令參數各fs:40指明金絲雀值是用段尋址( segmented addressing)從內存中讀入的,段尋址機制能夠追溯到80286的尋址,而在現代系統上運行的程序中已經不多見到了。將金絲雀值存放在一個特殊的段中,標誌爲「只讀」,這樣攻擊者就不能覆蓋存儲金絲雀值。在恢復寄存器狀態和返回前,函數將存儲在棧位置處的值與金絲雀值作比較(經過第12行的xorq指令)。若是兩個數相同,xorq指令就會獲得0,函數會按照正常的方式完成。非零的值代表棧上的金絲雀值被修改過,那麼代碼就會調用一個錯誤處理例程。
棧保護很好地防止了緩衝區溢出攻擊破壞存儲在程序棧上的狀態。一般只會帶來很小的性能損失。
最後一招是消除攻擊者向系統中插入可執行代碼的能力。一種方法是限制哪些內存區域可以存放可執行代碼。在典型的程序中,只有保存編譯器產生的代碼的那部份內存才須要是可執行的。其餘部分能夠被限制爲只容許讀和寫。許多系統容許控制三種訪問形式:讀(從內存讀數據)、寫(存儲數據到內存)和執行(將內存的內容看做機器級代碼)。之前,x86體系結構將讀和執行訪問控制合併成一個1位的標誌,這樣任何被標記爲可讀的頁也都是可執行的。棧必須是既可讀又可寫的,於是棧上的字節也都是可執行的。已經實現的不少機制,可以限制一些頁是可讀可是不可執行的,然而這些機制一般會帶來嚴重的性能損失。
計算機提供了多種方式來彌補咱們犯錯可能產生的嚴重後果,可是最關鍵的仍是咱們儘可能減小犯錯。例如,對於gets,strcpy等函數咱們應替換爲 fgets,strncpy等。在數組中,咱們能夠將數組的索引聲明爲size_t 類型,從根本上防止它傳遞負數。此外,還能夠在訪問數組前來加上num<ARRAY_MAX語句來檢查數組的上界。總之,要養成良好的編程習慣,這樣能夠節省不少寶貴的時間。同時最後也推薦兩本相關書籍,代碼大全(第二版) 高質量程序設計指南 。
養成習慣,先贊後看!若是以爲寫的不錯,歡迎關注,點贊,轉發,謝謝!
有任何問題,都可經過公告中的二維碼聯繫我