實驗環境:
編譯器:vs2015
系統:win10 64位
實驗原理
如上圖所示,棧地址增長方向是向低地址方向增長的,每次調用函數時,先把參數壓入棧底,然後會把被調用函數的返回地址(此地址爲call指令下一條指令)壓到棧底。另外還需要保存main函數的棧底地址在棧裏面,被調用函數的棧頂指針esp被保存爲該函數的棧底,接下來的低地址位分配局部變量。
如果c/c++沒有檢測局部變量內容越界問題,那麼,局部變量長度過長時,分配的地址空間直接覆蓋了old ebp,同時覆蓋了ret addr。而當ret addr指向的是有意義的地址,就會直接執行相關的代碼,這就是緩衝區攻擊。
實驗步驟:
初建項目
在vs2015中新建一個項目,命名爲SecurityLab,創建Lab.cpp文件,並把以下代碼拷貝到項目中:
#include <stdio.h>
#include <string.h>
void overflow(const char* input)
{
char buf[8];
printf("Virtual address of 'buf' = Ox%p\n", buf);
strcpy(buf, input);
}
void fun()
{
printf("Function 'fun' has been called without an explicitly invocation.\n");
printf("Buffer Overflow attack succeeded!\n");
// your other codes, e.g. deleting files
// what happens when return?
}
int main()
{
printf("Virtual address of 'overflow' = Ox%p\n", overflow);
printf("Virtual address of 'fun' = Ox%p\n", fun);
// char input[] = "AAAbbbbbbaaa\x44\x12\xC4\x00";//bad input
//注意這裏是7個A,但其實默認添加了’\0’在最後面
char input[] = "AAAAAAA";//good input, ASCII code of 'A' is 41
overflow(input);
return 0;
}
設置斷點
在overflow函數入口設置斷點,同時在overflow函數結束處設置斷點,啓動調試,發現出現以下錯誤:
分析:這個是一個警告來的,如今上升到錯誤標準,導致無法進行編譯。
解決方案:
調試→Security屬性→C/C++→SDL檢查
當啓動安全檢查時,如果啓動sdl檢查,就會把額外的安全警告作爲錯誤。
查看彙編指令
在main函數overflow入口的斷點處,查看反彙編指令(Ctrl+Alt+D)如下:
按F10,轉到push語句,可以發現eax指向的是input存儲的地址0x00ccfec0,如下爲input指向地址空間存儲的內容:(內存窗口搜索得到)
可見此時將參數壓入棧中,而add指令對應的地址是作爲call結束之後需要跳到的地址,0x00F319A6 這個地址就是overflow函數調用後需要返回的地址。
初探overflow
進入overflow函數內部,查看彙編指令:
這裏ebp指向的值爲0x00ccfde4作爲該函數入口,然後push壓站操作都是爲保護現場。通過查看內存棧空間,可以發現該值再偏移4個字節的地址所存的值正是call overflow下一條指令的地址。寄存器存儲的值如下:
而這句話:
這裏開始分配空間,
內存細探
當執行到char buf[8]時,通過內存查找buf位置,位置爲0x00ccfdd4,得到內存地址如下:
這裏地址是向低位增長的,留意前面一段:
後面四位a6 19 f3 00正好是overflow的返回地址,實質爲0x00f319a6,可見此時已經將返回地址壓入裏面了,再前面四位恰好是edi的地址,繼續執行,直到執行完strcpy,發現棧空間變化如下:
RTC檢查
查看彙編指令,我們會發現有這個函數:
這個函數與基本運行時檢查有關
當被設置如下表示時:
編譯器會對棧空間情況進行檢查,如果buf越界,就會報以下錯誤:
可見,編譯器是可以檢查數組越界問題的,這也是一種安全策略。
Security檢查
這裏pop把之前push的內容彈出來,恢復現場,並進行安全檢查,這個與下面設置有關:
其可以檢測出堆棧緩衝區溢出,而且能夠與sdl檢查結合,提醒用戶使用安全的類,規避緩衝區溢出風險。
當檢測出堆棧緩衝區溢出時,會報以下錯誤:
返回main函數
如上圖指令空間所示,爲即將彈出的地址,對比buf的地址0x00ccfdd4與彈出對應的地址相差16字節,而這裏的地址偏移量+4byte正好是overflow出口地址。
在這個彙編指令裏,esp爲棧頂指針,始終指向棧頂,而ebp爲臨時指向棧頂的指針,是用來操作相關棧頂操作的,顯然,把棧頂彈出後,esp就會偏移4個字節。
當執行完pop指令,esp指向棧頂地址,此時偏移4字節,正好指向overflow出口地址。
從這裏可以看出,我們在編譯器無安全檢查的機制下,完全可以把這個返回地址覆蓋掉,使得返回的地址指向其他函數入口地址,從而實現緩衝區攻擊。
緩衝區溢出攻擊準備
那麼接下來,我們把安全檢查禁用,基本運行檢查設置爲默認值,來通過調試實現緩衝區攻擊:
因爲vs在每次build過程中都會重新分配堆棧空間,所以每次fun函數地址都會更替,那麼我們可以在調試過程中,根據fun的地址直接修改input的值,來完成自定義的其他函數的調用。
另外一方面,在設置安全檢查,以及基本運行檢查,因爲涉及到保護現場狀態,故棧堆中會各增加4個字節,即去掉兩個保護時,buf初始化地址離返回地址會相差12個字節。
緩衝區溢出攻擊
在input定義的時候設置斷點,在沒有賦值前直接修改buf的值,因爲已經經過編譯,所以此時修改重新生成代碼fun的入口地址是一樣的。
這裏修改input的值爲
則會發現,返回的時候會直接調用到fun函數,觀察內存地址如下:
後四位在fun函數調用時的壓棧變成41414141。
此時,窗口打印出fun函數裏的內容:
執行完,查看彙編指令
此時esp指向0051F854地址,此地址存儲着
0x0051f890 地址爲返回會調用地址,因爲該地址不是指向某函數入口或者可執行代碼地址,而是
所以會有訪問衝突,出現以下信息:
至此,緩衝區攻擊完成。
編譯器保護機制
就vs2015編譯器而言,其對於緩衝區溢出的保護是多重的,主要分爲以下幾種:
基本運行時檢查
a /RTCc
向較小的數據類型賦值導致數據丟失,如int a= 4; char c= a;
b./RTCs
堆棧運行時錯誤檢查,其可以檢測到局部變量的溢出和不足,也可以進行堆棧指針認證,確保操作指針不是損壞的。
c./RTCu
保證使用的變量被初始化。
d./RTC1 == /RTCs+/RTCu
參考:
運行時錯誤檢查
安全檢查
/GS 針對檢查緩衝區溢出
對於可能出現緩衝區溢出問題的函數,編譯器將在堆棧上返回地址之前分配空間,在進入函數時,用安全Cookie加載分配的空間。在推出函數時,以及在64bit操作系統上展開幀的過程中,將調用helper函數,以確保Cookie值保持不變,不同的值則表示可能已覆蓋堆棧。如果檢測到不同的值,則終止進程。
受保護項:
函數調用的返回地址,用於函數異常處理程序的地址,易受攻擊的函數參數
SDL安全週期檢查
啓用/GS後,將會執行緩衝區溢出檢測的嚴格模式,等同於#pragma strict_gs_checking(push,on)進行編譯,其會把可能造成緩衝區溢出的warning升級爲error提醒,導致程序無法編譯通過。
思考與總結
實驗的關鍵:
利用局部變量分配棧空間,以及棧空間向低地址增長的特點,來實現局部變量數據溢出而覆蓋高地址位的棧空間,通過恰到好處得覆蓋esp棧頂地址對應的值,從而跳轉到相應代碼的指令執行位置,進而執行相關惡意代碼,實現緩衝區溢出攻擊。
實驗的侷限性:
- 編譯器會有相關的安全機制防止緩衝區溢出。
- 代碼的執行位置是不可預測的。
- 指令地址過大也無法用輸入字符填充。因爲手工輸入的字符轉換爲16進制,只能表示7f以內的,以上的無法用字符填充。
參考:
簡明x86彙編語言教程_司徒彥南