缓冲区溢出学习
一、寄存器ESP、EBP、EIP
二、函數調用模式
堆棧由邏輯堆棧幀組成。當調用函數時邏輯堆棧幀被壓入棧中,當函數返回時邏輯堆棧幀被從棧中彈出。堆棧幀包括函數的參數,函數地局部變量,以及恢復前一個堆棧幀所需要的數據,其中包括在函數調用時指令指針(IP)的值。
當一個例程被調用時所必須做的第一件事是保存前一個 FP(這樣當例程退出時就可以恢復)。然后它把SP復制到FP,創建新的FP,把SP向前移動為局部變量保留空間。這稱為例程的序幕(prolog)工作。當例程退出時,堆棧必須被清除干凈,這稱為例程的收尾(epilog)工作。Intel的ENTER和LEAVE指令,Motorola的LINK和 UNLINK指令,都可以用于有效地序幕和收尾工作
三、函數示例原理
void function(int a, int b, int c) {char buffer1[5];char buffer2[10]; } void main() {function(1,2,3); }使用gcc的-S選項編譯, 可以產生匯編代碼輸出?
即$ gcc -S -o example1.s example1.c?
對function()的調用被匯編成如下代碼:
以從后往前的順序將function的三個參數壓入棧中, 然后調用function()
pushl %ebp movl %esp,%ebp subl $20,%esp將幀指針EBP壓入棧中. 然后把當前的SP復制到EBP, 使其成為新的幀指針. 我們把這個被保存的FP叫做SFP. 接下來將SP的值減小, 為局部變量保留空間.
我們必須牢記:內存只能以字為單位尋址. 在這里一個字是4個字節, 32位. 因此5字節的緩沖區會占用8個字節(2個字)的內存空間, 而10個字節的緩沖區會占用12個字節(3個字)的內存空間. 這就是為什么SP要減掉20的原因。
?
從上圖來看,假如我們輸入的buffer1超長了,直接覆蓋掉后面的sfp和ret,就可以修改該函數的返回地址了。
四、具體實例
函數foo是正常的函數,在main函數中被調用,執行了一段非常不安全的strcpy工作。利用不安全的strcpy,我們可 以傳入一個超過緩沖區buf長度的字符串,執行拷貝后,緩沖區溢出,把ret返回地址修改成函數bar的地址,達到調用函數bar的目的。
#include <stdio.h> #include <string.h> void foo(const char* input) {char buf[10];printf("My stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n");strcpy(buf, input);printf("buf = %s\n", buf);printf("Now the stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n"); } void bar(void) {printf("Augh! I've been hacked!\n"); } int main(int argc, char* argv[]) {printf("Address of foo = %p\n", foo);printf("Address of bar = %p\n", bar);if (argc != 2){printf("Please supply a string as an argument!\n");return -1;}foo(argv[1]);printf("Exit!\n");return 0; }若仍然采用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) nBreakpoint 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 $0xfffffff0,%esp 0x08048434 <main+7>: pushl -0x4(%ecx) 0x08048437 <main+10>: push %ebp //(中間省略……) 0x08048494 <main+103>: call 0x80483d4 <foo> 0x08048499 <main+108>: movl $0x8048653,(%esp) //就是這里了!哈 0x080484a0 <main+115>: call 0x8048340 <puts@plt>因此,我們只要輸入一個超長的字符串,覆蓋掉0x08048499,變成bar的函數地址0x8048419,就達到了調用bar函數的目的。
六、Python腳本調試
為了將0x8048419這樣的東西輸入到應用程序,我們需要借助于Perl或Python腳本,如下面的Python腳本
import os arg = 'ABCDEFGHIJKLMN' + '"x19"x84"x04"x08' cmd = './test ' + arg os.system(cmd)上面的08 04 84 19要兩個兩個反著寫,大端序和小端序的問題,執行一下:
$python hack.py Address of foo = 0x80483d4 Address of bar = 0x8048419 //bar的函數地址 My stack looks like: 0xb7fc24e0 0x8048616 0xbf832484 0xbf832484 0xb7fc1ff4 0xbf832498 0x8048499 //strcpy前函數返回地址0x8048499buf = ABCDEFGHIJKLMN Now the stack looks like: 0xbf83246e 0x8048616 0x42412484 0x46454443 0x4a494847 0x4e4d4c4b 0x8048419 //瞧,返回地址被修改為了我們想要的bar的函數地址0x8048419 Augh! I've been hacked! //哈哈!bar函數果然被執行了!七、堆溢出
堆是內存的一個區域,它被應用程序利用并在運行時被動態分配。堆內存與堆棧內存的不同在于它在函數之間更持久穩固。這意味著分配給一個函數的內存會持續保持分配直到完全被釋放為止。這說明一個堆溢出可能發生了但卻沒被注意到,直到該內存段在后面被使用。?
?
示例程序:
執行結果檢驗:
[root@localhost]# ./heap1 hackshacksuselessdata input at 0x8049728: hackshacksuselessdata output at 0x8049740: normal outputnormal output [root@localhost]# ./heap1 hacks1hacks2hacks3hacks4hacks5hacks6hacks7hackshackshackshackshackshackshacks input at 0x8049728: hacks1hacks2hacks3hacks4hacks5hacks6hacks7hackshackshackshackshackshackshacks output at 0x8049740: hackshackshackshacks5hacks6hacks7hackshacks5hackshacks6hackshacks7 [root@localhost]# ./heap1 "hackshacks1hackshacks2hackshacks3hackshacks4what have I done?" input at 0x8049728: hackshacks1hackshacks2hackshacks3hackshacks4what have I done? output at 0x8049740: what have I done? //我們看到,output變成了what have I done?what have I done? [root@localhost]#八、格式化字符串錯誤
這類錯誤是指使用printf,sprintf,fprint等函數時,沒有使用格式化字符串。?
如把printf("%s", input)寫成printf(input)?
當input輸入一些非法制造的字符時,內存將有可能被改寫,執行一些非法指令
九、Unicode和ANSI緩沖區大小不匹配
我們經常碰到需要在Unicode和ANSI之間互相轉換,絕大多數Unicode函數按照寬字符格式(雙字節)大小,而不是按照字節大小來計算緩沖區大小,因此,轉換的時候不注意的話就可能會造成溢出。比如最常受到攻擊的函數是MultiByteToWideChar
示例程序:
BOOL GetName(char *szName) {WCHAR wszUserName[256];// Convert ANSI name to Unicode.MultiByteToWideChar(CP_ACP, 0,szName,-1,wszUserName,sizeof(wszUserName)); //問題出在這個參數上,sizeof(wszUserName)將會等于2*256=512個字節 }wszUserName是寬字符的,因此,sizeof(wszUserName)將會是256*2個字節,因此存在潛在的緩沖區溢出問題。
MultiByteToWideChar(CP_ACP, 0,szName,-1,wszUserName,sizeof(wszUserName) / sizeof(wszUserName[0]));這樣才是正確的寫法
十、發現問題
在Visual Studio中可以采用代碼分析功能來得到缺陷代碼的位置?
源代碼分析工具:?
ApplicationDefense、SPLINT、ITS4和Flawfinder等?
二進制分析工具:?
各種fuzzing工具包和靜態分析程序,例如Bugscan
十一、預防問題
在Visual Studio 2013以后的版本中,強制要求用安全的函數來代替不安全的函數,比如scanf要用s_scanf來取代(這個設計可以通過預編譯命令來忽略,我猜測也可以在解決方案屬性中選擇“允許不安全的代碼”來解決)?
而編譯器的/GS選項能夠阻止堆棧的破壞,保證堆棧的完整性,但是不能完全防止緩沖區溢出問題
總結
- 上一篇: 字符串经典题之正则匹配字符串
- 下一篇: glibc free 死锁