緩存區與緩存區溢出
- 緩衝區的定義
連續的一段存儲空間。 - 緩衝區溢出的定義
指寫入緩衝區的數據量超過該緩衝區能容納的最大限度,造成溢出的數據改寫了與該緩衝區相鄰的原始數據的情形。 - 緩衝區溢出的危害
利用緩衝區溢出實現在本地或者遠程系統上實現任意執行代碼的目的,從而進一步達到對被攻擊系統的完全掌控;
利用緩衝區溢出進行DoS(Denial of Service)攻擊;
利用緩衝區溢出破壞關鍵數據,使系統的穩定性和有效性受到不同程度的影響;
4.緩衝區溢出的根本原因:主要是因爲分配內存的時候沒有檢測緩衝區的邊界,當分配的內存超過緩衝區容納的最大的限度時,就會發生溢出,導致溢出的函數有:Strcpy、Strcat、Gets、Sprintf等,這些函數都沒有檢測數值邊界。
緩存區攻擊模式
攻擊者主要是利用溢出來覆蓋某些空閒塊頭部指針,進而進行重定向到惡意程序,進而進行攻擊。惡意數據可以通過命令行參數、環境變量、輸入文件或者網絡數據注入。
進程在內存中的佈局
1.代碼段:放置程序的可執行代碼
2.數據段:放置初始化的全局變量和初始化的局部靜態變量
3.BBS段:放置未初始化的全局變量和未初始化的局部靜態變量
4.堆:可以進行內存的動態分配
5.堆棧:用於存放函數中的局部變量、函數參數、返回地址、調用函數的棧基址。
6.進程的環境變量和參賽
緩衝區溢出的位置
1.堆棧
2.堆
3.數據段
4.BBS段
堆棧溢出
函數調用過程及溢出原理
1.參數入棧
2.保存指令寄存器中的內容,作爲返回地址
3.當前的EBP入棧
4.把ESP指針中的值拷到EBP中
5.爲本地變量留出一定的空間,把ESP減出適當的值
那麼調用函數前入棧的參數有:傳給函數的參數、函數的局部變量等
調用後:恢復EBP、恢復eip、局部變量不變
溢出原理:
1.通過緩衝區溢出修改棧中的返回地址
2.當函數調用返回時,eip的地址爲修改後的地址,並執行shellcode
堆棧的緩衝區溢出
溢出實例如下:
實例1:
void function(char *str)
{
char buffer[4];
strcpy(buffer, str);
}
void main (int argc, char **argv)
{
char large_string[8];
int i;
for(i=0; i<8; i++)
//large_string的長度爲8
large_string[i]=‘A’
//下面函數內部的變量數組的長度爲4,發生溢出
function(large_string);
}
實例2:
char shellcode[]=
"\x41\x41\x41\x41\x41\x41\x41\x41"//填充buffer的8字節;
"\x41\x41\x41\x41"//覆蓋掉EBP
"\x12\x45\xfa\x7f"//覆蓋返回後的EIP,windows中JMP ESP通用地址,ESP即指向下代碼:
"\x33\xC0\x50\xC6\x04\x24\x6C\xC6\x44\x24\x01\x6C\x68"
"\x52\x54\x2E\x44\x68\x4D\x53\x56\x43\x8B\xC4\x50\xB8"
"\x77\x1D\x80\x7C" //LoadLibraryA()
"\xFF\xD0\x33\xC0\x50\xC6\x04\x24\x63\xC6\x44\x24\x01"
"\x6F\xC6\x44\x24\x02\x6D\x68\x61\x6E\x64\x2E\x68\x63"
"\x6F\x6D\x6D\x8B\xC4\x50\xB8"
"\xC7\x93\xBF\x77" //system的地址
"\xFF\xD0";
void main()
{
char buffer[8]={0};
unsigned int i;
strcpy(buffer,shellcode);
}
堆溢出
堆溢出實例代碼:
void main(int argc, char **argv)
{
char *buf1 = (char *) malloc(16);
char *buf2 = (char *) malloc(16);
strcpy(buf1,”AAAAAAAAAAAAAAA”);
strcpy(buf2, argv[1]);
}
當在控制端輸入的字符串長度大於16時,則會發生溢出,正常情況下和溢出情況下的示意圖如下:
堆溢出分析
-
同LINUX一樣,Windows的HEAP區是程序動態分配一塊內存區域,動態分配和釋放對象,用於事先不知道程序所需對象數量和大小或者對象太大不適合堆棧分配的情況。
-
程序員一般調用C函數malloc/free或者C++的new/delete或者WIN32
API函數HeapAlloc/HeapFree來動態分配內存,這些函數最終都將調用ntdll.dll中的RtlAllocateHeap/
RtlFreeHeap來進行實際的內存分配工作,所以只需要分析RtlAllocateHeap/RtlFreeHeap。 -
當分配的堆在存儲數據時超過了所分配的內存空間,就會產生堆溢出。
-
對於一個進程來說可以有多個HEAP區,每一個HEAP的首地址以句柄來表示:hHeap,這也就是RtlAllocateHeap的第一個參數。
每個HEAP區的整體結構如下:
-
heap總體管理結構區存放着一些用於HEAP總體管理的結構。
-
雙指針區存放着一些成對出現的指針,用於定位分配內存以及釋放內存的位置。
-
用戶分配內存區是用戶動態分配內存時實際用到區域,也這是HEAP的主體。
-
當調用RtlAllocateHeap(HANDLE hHeap,DWORD dwFlags,SIZE_T
dwBytes)來分配內存時將進行以下操作:
1.對參數進行檢查,如果dwBytes過大或小於0都按照出錯處理,根據dwFlags來設置一些管理結 構;檢查是否爲DEBUG程序,對於DEBUG的程序與實際運行的程序每個內存塊之間的結構是不同的 。
2.根據要分配的內存的大小(dwBytes)決定不同的內存分配算法,我們只分析小於1024 bytes的情況;
3.從雙指針區找到用戶內存區的末尾位置,如果有足夠的空間分配所需的內存,就在末尾+dwBytes+8的位置放置一對指針來指向雙指針區的指向用戶內存區末尾位置的地方;
4.在後面同時設置雙指針區的指向用戶內存區末尾位置的指針指向進行完分配之後的用戶內存區末尾位置。 -
兩塊連續分配的內存塊之間並不是緊挨着的,而是有8字節的管理結構,最末尾的一塊內存後面還另外多了8字節的指針指向雙指針區。
假設有以下程序:
buf1 = HeapAlloc(hHeap, 0, 16);
buf2 = HeapAlloc(hHeap, 0, 16);
連續分配了兩塊16字節內存,實際在內存中(用戶分配區)的情況是這樣的:
-
在第二次分配內存的時候會利用第一塊內存管理結構後面那兩個指針進行一些操作,其中會有一次寫內存的操作:
mov [ecx], eax
mov [eax+4], ecx
-
假設分配完buf1之後向其中拷貝內容,拷貝的內容大小超過buf1的大小,即大於16字節,就會發生溢出,當覆蓋掉了那兩個4字節的指針,而下一次分配buf2之前又沒有把buf1釋放掉的話,那麼就會把一個4字節的內容寫入一個地址當中,而這個內容和地址都是能夠控制的,這樣我們就可以控制函數的流程轉向的shellcode了。
數據段的緩衝區溢出
下面是一個導致數據段溢出的程序,在該程序中可以看到程序對buf數組進行了不合理的內存分配導致該數組越界。
void Overflow_Data(char* input)
{
static char buf[4]=”CCCC”;
int i;
for (i = 0; i < 12 ; i++)
buf[i] = ‘A’;
}
溢出攻擊實例
棧溢出的攻擊實例
在分析棧溢出的攻擊實例前,先分析一下Win32對廢棄棧的處理,對廢棄棧的處理一共含有三種模式。
Win32對廢棄棧的處理
NSR模式
在下圖中,R指向了Shellcode地址, 但執行“mov esp,ebp”恢復調用者棧信息時,Win32會在被廢棄的棧中填入一些隨機數據。
該模式下的攻擊代碼有:
#include<stdio.h>
int main(int argc,char **argv)
{
char buf[500];
strcpy(buf,argv[1]);
printf("buf 0x%8x\n",&buf);
getchar();
return 0;
}
NRS模式
當函數執行返回指令時,程序跳轉到shellcode所在的地方,棧在1G(~0x00FFFFFF)以下
如果R直接指向Shellcode,則在R中必然含有空字節‘\0’. Shellcode將被截斷。原理圖如下:
攻擊的示例代碼如下:
R.S模式
該溢出模式主要是使用環境變量來發生溢出,進而執行shellcode,Win32平臺無SUID機制,本地溢出沒有意義,同樣會由於R中含空字節會被截斷,原理圖如下:
堆棧緩衝區溢出的危害
- 改寫返回地址
- 改寫調用函數棧的棧幀地址
- 改寫函數指針
- 改寫虛函數指針
- 改寫異常處理指針
- 改寫數據指針
基於堆的緩衝區溢出
- 在Linux中,堆空間按照Doug Lea算法實現動態分配。
- 在C程序中,標準庫函數malloc()/free()用於從堆中動態申請/釋放塊;對於C++程序,相應函數爲new/delete。
緩衝區溢出攻擊的防禦技術
類型安全的編程語言
Java, C#, Visual Basic,Pascal, Ada, Lisp, ML屬於類型安全的編程語言。
相對安全的函數庫
例如在使用C的標準庫函數時,做如下替換
strcpy -> strncpy
strcat -> strncat
gets -> fgets
缺點
使用不當仍然會造成緩衝區溢出問題。
修改的編譯器
1.增強邊界檢查能力的C/C++編譯器:
例如針對GNU C編譯器擴展數組和指針的邊界檢查。Windows Visual C++ .NET的GS 選項也提供動態檢測緩衝區溢出的能力。
2.返回地址的完整性保護
將堆棧上的返回地址備份到另一個內存空間;在函數執行返回指令前,將備份的返回地址重新寫回堆棧。
許多高性能超標量微處理器具有一個返回地址棧,用於指令分支預測。返回地址棧保存了返回地址的備份,可用於返回地址的完整性保護
3.缺點:
性能代價
檢查方法仍不完善
內核補丁
1.將堆棧標誌爲不可執行來阻止緩衝區溢出攻擊;
2.將堆或者數據段標誌爲不可執行。
3.例如Linux的內核補丁Openwall 、 RSX、 kNoX、ExecShield和PaX等實現了不可執行堆棧,並且RSX、 kNoX、ExecShield、PaX還支持不可執行堆。另外,爲了抵制return-to-libc這類的攻擊,PaX增加了一個特性,將函數庫映射到隨機的內存空間
4.缺點:對於一些需要堆棧/堆/數據段爲可執行狀態的應用程序不合適;需要重新編譯原來的程序,如果沒有源代碼,就不能獲得這種保護
靜態分析方法
字典檢查法
1.遍歷源程序查找其中使用到的不安全的庫函數和系統調用。
2.例如靜態分析工具ITS4、RATS (Rough Auditing Tool for Security)等。其中RATS提供對C, C++, Perl, PHP以及Python語言的掃描檢測。
3.缺點:誤報率很高,需要配合大量的人工檢查工作
程序註解法
1.包括緩衝區的大小,指針是否可以爲空,輸入的有效約定等等。
2.例如靜態分析工具LCLINT、SPLINT (Secure Programming Lint)。
3.缺點:依賴註釋的質量
動態檢測方法
輸入檢測方法
1.向運行程序提供不同的輸入,檢查在這些輸入條件下程序是否出現緩衝區溢出問題。不僅能檢測緩衝區溢出問題,還可以檢測其它內存越界問題。
2.採用輸入檢測方法的工具有Purify、Fuzz和FIST(Fault Injection Security Tool)。
3.缺點:系統性能明顯降低。輸入檢測方法的檢測效果取決於輸入能否激發緩衝區溢出等安全問題的出現。
Canary-based 檢測方法
1.將canary(一個檢測值)放在緩衝區和需要保護的數據之間,並且假設如果從緩衝區溢出的數據改寫了被保護數據,檢測值也必定被改寫。
2.例如動態檢測工具StackGuard、StackGhost、ProPolice、PointGuard等。
3.缺點:多少工具通過修改編譯器實現檢測功能,需要重新編譯程序;這種方法無法檢測能過繞過檢測值的緩衝區溢出攻擊
基於硬件的防禦技術
1.Intel/AMD 64位處理器引入稱爲NX(No Execute)或者AVP(Advanced Virus Protection)的新特性,將以前的CPU合爲一個狀態存在的“數據頁只讀”和“數據頁可執行”分成兩個獨立的狀態。
2.ELF64 SystemV ABI通過寄存器傳遞函數參數而不再放置在堆棧上,使得64位處理器不僅可以抵制需要注入攻擊代碼的緩衝區溢出攻擊還可以抵制return-to-libc這類的攻擊。
參考資料:
教材:
李毅超 曹躍,網絡與系統攻擊技術 電子科大出版社 2007
周世傑 陳偉 鍾婷,網絡與系統防禦技術 電子科大出版社 2007
參考書:
闕喜戎 等 編著,信息安全原理及應用,清華大學出版社
Christopher M.King, Curitis E.Dalton, T. Ertem Osmanoglu(常曉波等譯). 安全體系結構的設計、部署與操作,清華大學出版社,2003(Christopher M.King, et al, Security Architecture, design, deployment & Operations )
William Stallings,密碼編碼學與網絡安全-原理與實踐(第三版),電子工業出版社,2004
Stephen Northcutt, 深入剖析網絡邊界安全,機械工業出版社,2003
馮登國,計算機通信網絡安全,2001
Bruce Schneier, Applied Cryptography, Protocols, algorithms, and source code in C (2nd Edition)( 應用密碼學 -協議、算法與C源程序, 吳世忠、祝世雄、張文政等譯)
蔡皖東,網絡與信息安全,西北工業大學出版社,2004