緩存溢出
<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
緩存溢出(Buffer overflow),是指在存在緩存溢出安全漏洞的計算機中,攻擊者可以用超出常規長度的字符來填滿一個域,通常是內存區地址。這篇文章就是講解簡單的緩存溢出問題。文章以x86_32 和 linux 系統平臺爲藍本。爲了介紹緩存溢出,數據的存儲地址、主要的彙編指令、重要的寄存器等內容都要講解。
1. 變量存儲
在C語言中,變量屬性有很多中,但是對於緩存溢出問題,我們主要關心的數據的存儲位置,或存儲空間。因此,這裏我們主要關心全局變量,局部變量和靜態變量。在C語言中,通常全局變量和靜態變量被分配於數據段(data section)中,但是局部變量分配在棧(stack)中。另外,大家清楚,棧空間中還存儲CS,IP等一些列與指令地址相關的重要數據,因此,在對局部變量進行數據拷貝的時候,如果拷貝數據塊過大,就可能將IP,CS等寄存器存放的數據空間覆蓋掉,寫入一些非法的數據,當從設IP值時,計算機就跳轉到新的非法數據代碼空間了。這裏有一個簡單的C文件,overflow1.c
int data = 0x66666666;
int func1(void)
{
static int sdata = 0x55555555;
int ret = 0;
return ret;
}
int main(int argc, char *argvs[])
{
func1();
return 0;
}
我們首先對它進行編譯,然後查看編譯後的信息。
# gcc –o overflow1.o –c overflow1.c
# objdump –t overflow1.o
over3.o: file format elf32-i386
SYMBOL TABLE:
<?xml:namespace prefix = st1 ns = "urn:schemas-microsoft-com:office:smarttags" />00000000 l df *ABS* 00000000 over3.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000004 l O .data 00000004 sdata.1280
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .comment 00000000 .comment
00000000 g O .data 00000004 data
00000000 g F .text 00000012 func1
00000012 g F .text 0000001e main
從結果中我們很容易找到data 和 sdata變量的size, section等信息。如果想獲取變量存儲的地址,可以連接後在執行該命令。但是對於局部ret,恐怕你不容易找到它的存放地址,那麼ret的空間在哪裏呢? 不要着急,我們繼續。
# objdump –d overflow1.o
over3.o: file format elf32-i386
Disassembly of section .text:
00000000 <func1>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 10 sub $0x10,%esp
6: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%ebp)
d: 8b 45 fc mov -0x4(%ebp),%eax
10: c9 leave
11: c3 ret
00000012 <main>:
12: 8d 4c 24 04 lea 0x4(%esp),%ecx
16: 83 e4 f0 and $0xfffffff0,%esp
19: ff 71 fc pushl -0x4(%ecx)
1c: 55 push %ebp
1d: 89 e5 mov %esp,%ebp
1f: 51 push %ecx
20: e8 fc ff ff ff call 21 <main+0xf>
25: b8 00 00 00 00 mov $0x0,%eax
2a: 59 pop %ecx
2b: 5d pop %ebp
2c: 8d 61 fc lea -0x4(%ecx),%esp
2f: c3 ret
從彙編代碼裏面恐怕還是不明白ret在哪裏存儲吧?不用擔心,我們回頭看看C源文件,在func1函數裏有一個 int ret = 0 的聲明,聰明的你現在是不是找到對應的彙編語句了,你猜對了,就是下面這句話:
6: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%ebp)
也就是說,ret的空間分配在-0x4%(ebp)指向的空間中。從
1: 89 e5 mov %esp,%ebp
你會發現ebp存放的是esp的內容,也就說-0x4%(ebp)指向的是棧空間地址,而ret就存放在哪裏。
現在明白了變量的存放空間了,下面我們要繼續講解關於棧中其它的信息。
2. 重要指令和寄存器
爲了更好的瞭解緩存溢出,就要了解與其相關的指令和寄存器。我在這部分內容中會講解這些信息。
IP,CS,ebp,esp是我們要講的寄存器。CS:IP指向將要執行的指令的存儲地址。一般的函數跳轉指令和函數調用指令就是通過修改CS:IP的值來達到跳轉目的。當指令段發生改變時,CS寄存器的數值才改變,在同一個指令段中,通常只改變IP的數值就可以了。我們今天介紹的默認只是通過修改IP的值而達到跳轉目的。Esp寄存器存放的當前棧頂地址,而ebp作爲一個備份寄存器,保存着進入新函數後esp的值。
在重要的指令中,主要有push, pop, call, ret, leave. Push 和 pop是一對棧操作指令,push完成入棧操作,將數據寫入棧中,並更新esp的內容,而pop指令與其相反,它將數據從棧中取出,並更新esp的內容。 其中 call指令是函數調用指令,它主要完成指令計數器寄存器(IP) 的入棧操作(爲了簡單期間,這裏不考慮CS入棧問題,有興趣的同學可以去查看引用文獻),類似於指令 “push ip”。而ret指令與call指令相反,是一個函數返回指令,將IP的值從棧中彈出,類似”pop ip”。Leave也可以看成一個符合指令,它類似於 “mov ebp, esp; pop ebp” 兩個指令的效果。
瞭解了上面的基本知識,我們來分析一下彙編代碼。爲了清楚期間,我們將overflow1.o 進行連接,然後查看連接後的重要彙編代碼片段。
# gcc –o overflow1 overflow.o
# objdump –d overflow1
…..
08048324 <func1>:
8048324: 55 push %ebp
8048325: 89 e5 mov %esp,%ebp
8048327: 83 ec 10 sub $0x10,%esp
804832a: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%ebp)
8048331: 8b 45 fc mov -0x4(%ebp),%eax
8048334: c9 leave
8048335: c3 ret
08048336 <main>:
8048336: 8d 4c 24 04 lea 0x4(%esp),%ecx
804833a: 83 e4 f0 and $0xfffffff0,%esp
804833d: ff 71 fc pushl -0x4(%ecx)
8048340: 55 push %ebp
8048341: 89 e5 mov %esp,%ebp
8048343: 51 push %ecx
8048344: e8 db ff ff ff call 8048324 <func1>
8048349: b8 00 00 00 00 mov $0x0,%eax
804834e: 59 pop %ecx
804834f: 5d pop %ebp
8048350: 8d 61 fc lea -0x4(%ecx),%esp
….
從對func1函數開始調用開始,我們跟蹤棧裏面的內容。當call指令執行後,計算機會跳到func1函數處繼續執行,現在就將這些指令合併:
8048344: e8 db ff ff ff call 8048324 <func1>
8048324: 55 push %ebp
8048325: 89 e5 mov %esp,%ebp
8048327: 83 ec 10 sub $0x10,%esp
804832a: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%ebp)
執行後的結果是什麼呢?
第一條指令是將IP壓棧;
第二條指令將ebp壓棧
第三條是將esp的值保存到ebp中
第四條指令更新esp的值,向前16bytes。
第五條指令給ret賦初值0,並且可以確定ret的地址是%ebp - 4
因此,我們得到當前棧的值
IP 0x88-0x8B ;;High address
Ebp-> oldEBP 0x84-0x87 ;; ebp = 0x84
ret 0x80-0x83
Nil 0x7c-0x7f
Nil 0x78-0x7b
Esp-> Nil 0x74-0x77 ;; esp = 0x74
…. …. ;;low address
並且ebp會作爲備份寄存器保留老的esp寄存器的值,當函數返回時,還原esp和ebp,以及IP。緩存溢出就是在還原之前首先將棧中IP的值修改成其餘的數值,從而是CPU跳轉到一個錯誤地址或無效地址。
如果依照上面棧的地址,那麼ret變量的地址應該是0x80, 而老的IP數據的存儲地址應該是0x88,如果在向ret進行數據拷貝時,數據過長,將會覆蓋oldebp和IP的地址,從而導致程序在返回時,將錯誤的IP值彈出到指令計數器中,CPU將會跳轉到該錯誤地址進行代碼執行。下面提供了兩個案例程序,給大家參考。
Examples
Overflow2中是一個典型的內存溢出,作者通過向一個局部變量數組中寫入過長數據,使程序無條件跳轉的my_func() 一個非法函數中。
#include <stdio.h>
#include <string.h>
char strs[32] = {0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, /
0xc4, 0x83, 0x04, 0x08, 0xc4, 0x83, 0x04, 0x08, /
0xc4, 0x83, 0x04, 0x08, 0xc4, 0x83, 0x04, 0x08} ;
/*my_func的地址是0x080483c4*/
int my_func(void)
{
printf("in My Func!/n");
return 87;
}
int print(void)
{
int tmp = 0x33;
int ret = 0x22;
char str[4];
char *data;
strncpy(str, strs, 24);
return ret;
}
int main(int argc, char *argvs[])
{
int ret = print();
printf("ret = %x/n", ret);
return 0;
}
Overflow3.c 是一個不通過函數調用,強制跳轉到my_func()函數,併成功返回到主函數。
#include <stdio.h>
#include <string.h>
int my_func(void)
{
printf("in My Func!/n");
return 87;
}
int print(void)
{
int ret = 0x22;
int str[4];
asm ( /
"mov 0(%%ebp), %%ebx; /
mov %%eax, 0(%%ebp); /
push %%eax; /
sub $4, %%ebp; /
mov %%ebx, 0(%%ebp)" /
: /
: "a"(my_func));
return ret;
}
int main(int argc, char *argvs[])
{
int ret = print();
printf("ret = %x/n", ret);
return 0;
}