該類錯誤是修改了返回指針,一般是由於
1. 數組越界賦值。(數組沒有邊界檢查)int a[8]; a[8],a[9],a[-1]。。都能正常編譯,連接,運行時可能出錯。
2.使用 strcpy等不安全(不帶長度檢測的函數),char a[1], char *b="aaa"; strcpy(a,b);
局部變量(函數內的變量)存在棧中,應為棧是先下(低地址)生長的,故 函數返回指針 要比局部變量的地址高,像類似的a[8]之類的就有機會訪問到 函數返回指針了。
首先運行第一個程序:
#include “string.h”
void fun1(const char *str)
{
char buffer[5];
strcpy((char*)buffer, (char*)str);
}
int main()
{
fun1(”AAAAAAAAAAAAAAAAAAAAAAAAA”);
}
程序執行結果是“段錯誤”。用GDB調試,在從fun1函數返回時出現“Cannot access memory at address 0×41414145”的錯誤提示,這大致符合期望。但有一點搞不明白的是被改寫的返回地址是’AAAA’,即應該是0×41414141才對,實際情況 怎么會多了4個字節 (0×41414145)?
這個程序是由於返回地址無效導致錯誤,那么我就寫一個有效的地址上去吧。運行第二個程序:
void fun1()
{
int i;
const char buffer[] = “111111111″;
for (i = 0; i < 20; i++)
*((int*)(buffer+i)) = (int)buffer;
}
int main()
{
fun1();
}
程序的返回地址被改寫成“有效”的地址“buffer”,只是內容確實無效的指令“11111111”,結果程序的運行結果是:
*** stack smashing detected ***: ./a.out terminated
GCC的緩沖區溢出保護
通過查閱資料知道,GCC有一種針對緩沖區溢出的保護機制,可通過選項“-fno-stack-protector”來將其關閉。實驗中出現的錯誤信息,就正好是檢測到緩沖區溢出而導致的錯誤信息。見出錯程序的匯編代碼:
<fun1>:
push %ebp
mov %esp,%ebp
sub $0×28,%esp
mov %gs:0×14,%eax
mov %eax,-0×4(%ebp) ; 把%gs:0×14保存到-0×4(%ebp) 中
xor %eax,%eax
movl $0×31313131,-0xe(%ebp)
movl $0×31313131,-0xa(%ebp)
movw $0×31,-0×6(%ebp)
movl $0×0,-0×14(%ebp)
jmp 8048423 <fun1+0×3f>
lea -0xe(%ebp),%edx
mov -0×14(%ebp),%eax
add %eax,%edx
lea -0xe(%ebp),%eax
mov %eax,(%edx)
addl $0×1,-0×14(%ebp)
cmpl $0×13,-0×14(%ebp)
jle 8048412 <fun1+0×2e>
mov -0×4(%ebp),%eax
xor %gs:0×14,%eax ; 檢查%gs:0×14與-0×4(%ebp)的值是否相同
je 804843a <fun1+0×56> ; 如果相同則退出函數
call 804831c <__stack_chk_fail@plt> ; 檢測到緩沖區溢出,跳轉到__stack_chk_fail@plt函數
leave
ret
<main>:
lea 0×4(%esp),%ecx
and $0xfffffff0,%esp
pushl -0×4(%ecx)
push %ebp
mov %esp,%ebp
push %ecx
sub $0×4,%esp ; 在棧上分配4字節空間。在進入函數之后,其地址相當於-0×4(%ebp)
add $0×4,%esp
pop %ecx
pop %ebp
lea -0×4(%ecx),%esp
從匯編代碼看出,func2比func1只是多出了一條指令來為臨時變量申請存儲空間,這與其C代碼相吻合;func3比func2卻多出了一大片 代碼,而其C代碼的不同卻只是臨時數組的類型的不同而已。通過分析可知,這些多出的代碼就是用來檢查緩沖區溢出的。在調用函數之前,在棧上分配4字節的存 儲空間。在進入函數之后,一個數(%gs:0×14)保存到這4字節空間中。在退出函數之前做一次檢查,如果剛才保存的數被修改,那可以肯定是發生了緩沖 區溢出。
GS是附加段寄存器,我也沒搞明白是干什么用的,更加不清楚%gs:0×14的值什么時候被確定。這種檢測方法的思路很簡單,就是在棧上放置一個隨 機數,然后檢查這個隨機數有沒有被改寫。由於隨機數放置在返回地址之前,聰明的黑客應該可以通過圖3的方式植入惡意代碼。但對黑客來說,這樣有兩個不便: (1)惡意代碼需植入到當前函數的棧幀上,空間很小,很難植入攻擊性強的代碼;(2)由於主要是利用字符串拷貝來植入惡意惡意代碼,而字符串拷貝是遇到 ’/0′才結束的,這樣就要求惡意代碼起始地址的低8位必須為全0,這樣才可以剛好改寫返回地址,而不會進入“雷區”(隨機數)。在狹小的空間內,完成這 樣的操作實在難於登天;如果可用空間很大的話,這兩個問題其實也不難克服。
圖3. 緩沖區溢出攻擊
關於CPU和操作系統
我一直有個疑問:程序代碼加載在代碼段上,程序數據保存在數據段與堆棧段上,本來是河水不犯井水。緩沖區溢出只發生在數據段與堆棧段,只要規定這兩 個段的內容可讀寫但不可執行,不就不需要擔心緩沖區溢出攻擊了嗎?但事實上,許多程序為了提高效率,會在堆棧段上動態地生成可執行代碼,比如Linux本 身就這樣做,所以“數據段和堆棧段不可執行”這樣的說法是錯的。
Intel和AMD已經在一些CPU上加入了“防病毒”功能,這種CPU可區分哪些地址空間可以執行代碼、而哪些地址空間不可以執行。
小結
通過實驗,知道GCC有一定的保護機制,可防止緩沖區溢出攻擊的發生。但這種方法有一定的局限性,所以仍有攻擊的可能。我匯編很懶,有很多問題沒有 完全搞明白。由於我很懶也沒有毅力(我很痛恨自己的這種作風),不明白的地方留待“以后”研究。至於用Firefox瀏覽網頁究竟會不會中毒的問題,應該 也還是會吧?