(原創)攻擊方式學習之(3) - 緩沖區溢出(Buffer Overflow)
堆棧溢出
堆棧溢出通常是所有的緩沖區溢出中最容易進行利用的。了解堆棧溢出之前,先了解以下幾個概念:
- 緩沖區
- 簡單說來是一塊連續的計算機內存區域,可以保存相同數據類型的多個實例。
- 堆棧
- 堆 棧是一個在計算機科學中經常使用的抽象數據類型。堆棧中的物體具有一個特性:最後一個放入堆棧中的物體總是被最先拿出來,這個特性通常稱為後進先出 (LIFO)隊列。堆棧中定義了一些操作。兩個最重要的是PUSH和POP。PUSH操作在堆棧的頂部加入一個元素。POP操作相反,在堆棧頂部移去一個 元素,並將堆棧的大小減一。
- 寄存器ESP、EBP、EIP
- CPU的ESP寄存器存放當前線程的棧頂指針,
- EBP寄存器中保存當前線程的棧底指針。
- CPU的EIP寄存器存放下一個CPU指令存放的內存地址,當CPU執行完當前的指令後,從EIP寄存器中讀取下一條指令的內存地址,然後繼續執行。
現 代計算機被設計成能夠理解人們頭腦中的高級語言。在使用高級語言構造程序時最重要的技術是過程(procedure)和函數(function)。從這一 點來看,一個過程調用可以象跳轉(jump)命令那樣改變程序的控制流程,但是與跳轉不同的是,當工作完成時,函數把控制權返回給調用之後的語句或指令。 這種高級抽象實現起來要靠堆棧的幫助。堆棧也用於給函數中使用的局部變量動態分配空間,同樣給函數傳遞參數和函數返回值也要用到堆棧。
堆棧由邏輯堆棧幀組成。當調用函數時邏輯堆棧幀被壓入棧中,當函數返回時邏輯堆棧幀被從棧中彈出。堆棧幀包括函數的參數,函數地局部變量,以及恢復前一個堆棧幀所需要的數據,其中包括在函數調用時指令指針(IP)的值。
當一個例程被調用時所必須做的第一件事是保存前一個 FP(這樣當例程退出時就可以恢復)。然後它把SP復制到FP,創建新的FP,把SP向前移動為局部變量保留空間。這稱為例程的序幕(prolog)工 作。當例程退出時,堆棧必須被清除幹凈,這稱為例程的收尾(epilog)工作。Intel的ENTER和LEAVE指令,Motorola的LINK和 UNLINK指令,都可以用於有效地序幕和收尾工作。
下面我們用一個簡單的例子來展示堆棧的模樣: example1.c:
為了理解程序在調用function()時都做了哪些事情, 我們使用gcc的-S選項編譯, 以產生匯編代碼輸出:
$ gcc -S -o example1.s example1.c通過查看匯編語言輸出, 我們看到對function()的調用被翻譯成:
pushl $3 pushl $2 pushl $1 call function
以從後往前的順序將function的三個參數壓入棧中, 然後調用function(). 指令call會把指令指針(IP)也壓入棧中. 我們把這被保存的IP稱為返回地址(RET). 在函數中所做的第一件事情是例程的序幕工作:
將幀指針EBP壓入棧中. 然後把當前的SP復制到EBP, 使其成為新的幀指針. 我們把這個被保存的FP叫做SFP. 接下來將SP的值減小, 為局部變量保留空間. 我 們必須牢記:內存只能以字為單位尋址. 在這裏一個字是4個字節, 32位. 因此5字節的緩沖區會占用8個字節(2個字)的內存空間, 而10個字節的緩沖區會占用12個字節(3個字)的內存空間. 這就是為什麽SP要減掉20的原因. 這樣我們就可以想象function()被調用時堆棧的模樣:
所以,從上圖來看,假如我們輸入的buffer1超長了,直接覆蓋掉後面的sfp和ret,就可以修改該函數的返回地址了。下面來看一個示例吧。
示例
關於如何編寫Shell Code,如何在內存中預先準備好一段危險的執行代碼以及如何精確計算通過緩沖區溢出執行那段危險代碼同時又讓返回地址調回原來返回地址……這中間涉及太 多的底層匯編知識,小弟不才也只是走馬觀花,成不了真正的黑客高手。但從黑客朋友的水平之高看來,提高我們的代碼安全性是非常必要的!
因此,在這個例子中,我們假設所謂的危險代碼已經在 源代碼中,即函數bar。函數foo是正常的函數,在main函數中被調用,執行了一段非常不安全的strcpy工作。利用不安全的strcpy,我們可 以傳入一個超過緩沖區buf長度的字符串,執行拷貝後,緩沖區溢出,把ret返回地址修改成函數bar的地址,達到調用函數bar的目的。
用GCC編譯上面的程序,同時註意關閉Buffer Overflow Protect開關:
gcc -g -fno-stack-protector test.c -o test
為了找出返回地址,我用gdb調試上面編譯出來的程序。
//(前面啟動gdb,設置參數和斷點的步驟省略……) (gdb) r Starting program: /media/Personal/MyProject/C/StackOver/test abc Address of foo = 0x80483d4 //函數foo的地址 Address of bar = 0x8048419 //函數bar的地址Breakpoint 1, main (argc=2, argv=0xbfe5ab24) at test.c:24 24 foo(argv[1]); //在調用foo函數前,我們查看ebp值 (gdb) info registers ebp ebp 0xbfe5aa88 0xbfe5aa88 //ebp值為0xbfe5aa88 (gdb) n
Breakpoint 2, foo (input=0xbfe5c652 "abc") at test.c:4 4 { (gdb) n 6 printf("My stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n"); //執行到foo後,我們再查看ebp值 (gdb) info registers ebp ebp 0xbfe5aa68 0xbfe5aa68 //ebp值變成了0xbfe5aa68 //我們來查看一下地址0xbfe5aa68究竟是啥東東: (gdb) x/ 0xbfe5aa68 0xbfe5aa68: 0xbfe5aa88 //原來地址0xbfe5aa68存放的居然是我們之前的ebp值,其實豁然開朗了,因為這是執行了push %ebp後將之前的ebp保存起來了,和前面說的居然是一樣的! (gdb) n My stack looks like: 0xb7ee04e0 0x8048616 0xbfe5aa74 0xbfe5aa74 0xb7edfff4 0xbfe5aa88 //看,在代碼中輸入堆棧信息中也出現了熟悉的0xbfe5aa88,因此可以斷定該處為保存的上一級的ebp值。對應上上面那個圖中的sfp。 0x8048499 //假如0xbfe5aa88就是sfp的話,那0x8048499應該就是ret(返回地址)了,下面來驗證一下
7 strcpy(buf, input); //查看0x8048499裏面是什麽東東 (gdb) x/i 0x8048499 0x8048499 <main+108>: movl $0x8048653,(%esp) //這句代碼是main函數中的代碼,正是我們執行完foo函數後的下一個地址。不信,看看main的assemble: (gdb) disassemble main Dump of assembler code for function main: 0x0804842d <main+0>: lea 0x4(%esp),%ecx 0x08048431 <main+4>: and