分类: C/C++
2010-06-25 00:40:25
溢出是個很有意思的話題,緩衝區溢出又是當中最常見的一種情況。如果把緩衝區放在堆棧上,造成溢出,那麽有可能情況就變得很妙,比如程序就執行了你的ShellCode,gogo!
先來講將基本原理。
堆棧大家都很熟悉了,這裡就不說了,我們來復習一下程序的函數/過程調用。函數/過程是通過堆棧完成的,之所以這麽說是因爲程序通過堆棧向被調用的函數傳遞參數,當然沒有參數就不傳,同時使用堆棧來保存現場,待被調用之函數/過程執行完後再從中堆棧恢復現場,這個過程大家應該很熟悉,計算機體系結構還是什麽微型計算機技術、操作系統課程裏面都講過。
我們來仔細看看這個過程。
先看如下代碼:
void func_1(void)
...{
int i = func_2(50);
printf("Done!\n");
return;
}
函數func_1調用了函數func_2,向其傳了50這樣一個參數,並將返回結果保存在變量i中。
這裡i是局部變量,局部變量是放在堆棧裏的。func_1調用func_2時,依次進行如下工作:
1、壓參數入棧,一般是從右往左壓,也就是如果函數有多個參數比如func(var1, var2, var3),那麽就是var3先入棧,然後是var2,最後是var1;
2、保存返回地址(PC寄存器的值)。也就是執行完了函數調用要接著做剛才的事情,這裡就是printf("Done\n");這句話的指令地址;
3、保存自個兒堆棧的基址(EBP寄存器的值)。執行完調用後要恢復自個兒的堆棧就用這個。保存EBP寄存器值的方法就是壓棧;
4、跳到被調用函數的地址去執行。這裡就是跳到函數func_2処執行。
函數返回時執行相反的過程:
1、把返回值放到EAX中。如果沒有就不放,如果放不下就把它的地址放進去;
2、恢復現場。彈棧;
3、跳到“以前要執行的下一條指令”処執行。也是通過彈棧來實現的。
看圖。
圖一,函數調用之堆棧示意圖
圖上說得很清楚。
現在我們來考慮溢出的問題。所謂溢出,就是越界覆寫,這當然是很嚴重的事件——不是你的地盤的東西被你寫了。這會導致兩個問題:
1、破壞了別人的東西;
2、自己寫的東西有可能找不回來。
其中猶以第一個問題爲重,我們還是來看看剛才那張圖,想想如果局部變量1的東西把局部變量2覆蓋了,而程序不知道,所以它把局部變量2拿去用了這樣將導致不可預料的後果。
看看下面這段代碼,會有問題嗎?
#include
#include
#include
int main(void)
...{
//退出時打印的訊息
char info[] = "Exiting...";
//操作緩衝區
char buffer[8] = ...{0};
//printf("login_passwd=0x%p info=0x%p insert=0x%p ", login_passwd, info, insert);
printf("Processing... ");
strcpy(insert, "Administrator:123456");
printf("%s ", info);
return TRUE;
}
這段代碼的本意是把管理員的用戶名和密碼複製到緩衝區buffer中,然後退出。可是很不幸,在堆棧上溢出了。出問題的地方很明顯,strcpy複製一個較大的緩衝區到一個較小的緩衝區,於是乎就把這個較小的緩衝區“後面”的一段内存覆寫了,這段内存正好是數組info的,所以就把管理員的密碼打印出來了。
看看圖二,堆棧是這樣的。
圖二,堆棧層次
先聲明的東西先分配堆棧,於是info在buffer的上面(x86的棧是逆向生長的)。可是你會奇怪,strcpy越界覆寫應該是把buffer後面的内存破壞了呀,説是說在後面,其實是它前面的變量遭了殃,因爲堆棧是倒著長的嘛。
好了,看完這個例子,我們再回去看看圖一。你可能會想:緩衝區溢出有什麽用?我想除非別有用心,不然那純粹是個壞東西。不過我覺得別有用心並非是什麽壞事,比如圖一那個棧...把返回地址給改了就不錯...嘿嘿:儅一個函數調用另外一個函數,執行完成返回時卻“意外”地跳到另外一個地方執行去了,比如什麽Shutdown函數之類的。仔細想想,這的確並非不可能,比如讓圖一的那個“局部變量1”溢出並越過“保存的EBP”寫到函數的“返回地址”上去,那麽儅這個函數調用完後“返回地址”中的内容就會彈入EIP寄存器。爲了演示這種可能性,我們來一段代碼。不過代碼中我並不打算讓局部變量溢出從而破壞返回地址——因爲這樣把老的EBP也破壞了。這裡我直接通過指針的計算,“尋址”到返回地址,然後覆寫它。
#include
#include
#include
void bad(void)
...{
printf("haha ");
return;
}
int prblm(int input)
...{
printf("input=%d ", input);
int *p = &input;
printf("p=%p ", p);
//調整指針讓其指向函數返回地址
p += 7;
//覆寫
*p = (int)bad;
printf("exiting func... ");
return TRUE;
}
int main(void)
...{
printf("start... ");
prblm(0x100);
printf(" end! ");
return TRUE;
}
再看圖三,一定要看圖。
圖三,堆棧上之覆寫示意圖
這裡傳入的參數是input,代碼中用p指向它,然後--向下移動一個單位(4個字節),這裡p用的是整形指針,免得以後轉換。移動後的p就指向了保存函數返回的地址的地址,改了它!把它的内容替換成了函數bad的地址。這當然是我們蓄意的代碼,並非什麽溢出,建議運行一把看看會是什麽樣子。。。
bad函數被“意外”地執行了!程序中並沒有執行bad函數的代碼,但是bad卻執行了,這就是因爲函數prblm返回的地址(main函數中調用函數prblm完後下一條代碼的地址)被覆蓋成bad函數的地址了,所以儅函數prblm執行完後程序就跳到bad函數処去執行了。
這就是大夥兒最常用的堆棧溢出。堆棧溢出的原理很簡單,但是要有效地實施卻未必是件容易的事。比如你想對某個程序實施堆棧溢出執行你想要的代碼:
第一、這個程序裏要有溢出點,就是程序設計缺陷的地方,比如一段輸入的數據沒有檢查長度,這就有可能讓你輸入過長的數據而產生覆蓋;
第二、緩衝區是局部自動變量,只有局部自動變量才在堆棧中;
第三、程序中有你想要執行的代碼,如果程序中有你想要執行的代碼溢出纔有意義,否則只是弄壞別人的程序而已,比如對方程序中正好有個AddUser(UserName, Password)這樣的函數,你可以直接跳到這裡執行它。如果對方程序裏沒有逆向執行的代碼,問題就變得更複雜了:你得構造一段你要的代碼,然後想辦法植入到對方程序當中。這樣,首先目標程序要有接收存儲輸入的機制,這樣你才能把代碼放上去,比如它給你提供的一個Input框,Input框中的内容會被放在某塊内存(變量)當中,而且這個地方要合適:你的代碼裏是否包含0?它將被認爲是字符串的結尾。輸入框是否夠大?你的代碼有200字節,而它只能讓你輸入100個字節的數據?
第四、能夠定位到你想執行的代碼,不管是程序中内置的代碼還是你植入的代碼,要想執行它們必須能夠得到它們的地址,能否做到?
這樣看來想要在堆棧溢出上面“做點文章”可謂條件苛刻,不過盡管如此我們還是能夠屢屢得手——縂有那麽些同志寫代碼的時候不注意,給我們留了機會。下一篇我們講要看看“機會”是怎麽產生的以及“機會的把握”。
文章出处: