1、缓冲区溢出技术,缓冲区溢出攻击与防御,概要( Buffer Overflow ) 缓冲区溢出攻击分析(BOFA) BOFA的检测与防御,概念,原因,造成缓冲区溢出的原因是程序中没有仔细检查用户输入的参数。缓冲区溢出就是将一个超过缓冲区长度的字符串置入缓冲区的结果,程序中没有仔细检查用户输入的参数。例:#include int main()char name8;printf(“Your Name:”);gets(name);printf(“Hello,%s!”,name);return 0; /当输入字符超过8个时,溢出,后果,向一个有限空间的缓冲区中置入过长的字符串可能会带来两种后果 一是过长
2、的字符串覆盖了相邻的存储单元,引起程序运行失败,严重的可导致系统崩溃; 另一种后果是利用这种漏洞可以执行任意指令,甚至可以取得系统特权,由此而引发了许多种攻击方法。,缓冲区溢出攻击与防御,概要 缓冲区溢出攻击分析(BOFA) BOFA的检测与防御,回顾C语言,能改变内存地址内容的语句主要有两类 1)*AB;*指针型语句* 2)AiB;*数组型语句* 其中,A,B均为变量,*A为指针,A 为数组,i为数组 此两种语句分别对应了两种攻击方法。,一:指针型攻击方法,在第1类语句(指针型语句)中,由于A所指向的内存地址取得B的值,因而攻击者能利用该声明来改变函数的返回地址。由此可构成第一种攻击方法,又
3、可称为指针型攻击方法。,取得成功必须具备下列条件,1.一个循环语句,用于将用户输人数据复制到一个缓冲区数组,而且不能检查数据量,同时数组必须紧靠变量A 2.一个循环语句,用于将用户输人数据复制到一个缓冲区数组,而且不能检查数据量,同时数组必须紧靠变量B 3. (1)、(2)必须在*AB执行之前,且(1)、(2)执行后,不能再改变A、B的值。,攻击实施过程,在*AB执行之前,通过(1)、(2)改变A、B的值,当语句*AB执行时,变量A的新值所指向的地址*A将取得变量B的新值(即用户输入的数据)。 利用这种方法,攻击者可以改变任何内存地址的内容,包括返回地址。,二:数组型攻击方法,在第二类语句(数
4、组型语句)中,数组A 中的第i个元素Ai取得B的值。,必须满足的条件,(1)一个循环声明,用于将用户输人数据复制到一个缓冲区数组,而且不能检查数据量,同时数组必须紧靠变量B(2)一个循环声明,用于将用户输人数据复制到数组A,而且不能检查A的上界,每次循环后i的值加1,同时数组A必须紧靠一个返回地址,实施过程一,通过(1)改变B的值,然后通过(2)将B的新值循环输人数组A,直到返回地址也被该新值覆盖。这种攻击中,不仅返回地址被改变,而且介于数组A与返回地址之间的所有内存区域也全被改写。这种方法在缓冲区溢出攻击中使用得最多。,实施过程二,与指针型语句中的攻击方法类似。假设Ak存有函数的返回地址。如
5、果我们用第一种攻击方法使Ak溢出,就可将返回地址的值改写成任何需要的值。,堆栈,从物理上讲,堆栈是就是一段连续分配的内存空间 静态全局变量是位于数据段并且在程序开始运行的时候被加载 动态的局部变量 则分配在堆栈里面 从操作上来讲,堆栈是一个先入后出的队列,其生长方向与内存的生长方向正好相反,我们规定内存的生长方向为向上,则栈的生长方向为向下 压栈的操作pushESP-4 出栈的操作是pop=ESP+4,堆栈结构,堆栈是向下生长 内存向上生长,在一次函数调用中,堆栈中将被依次压入:参数,返回地址,EBP 如果函数有局部变量,接下来,就在堆栈中开辟相应的空间以构造变量 函数执行结束,这些局部变量的
6、内容将 被丢失。但是不被清除 在函数返回的时候,弹出EBP,恢复堆栈到函数调用的地址,弹出返回地址到EIP以继续执行程序,在C语言程序中,参数的压栈顺序是反向的:比如func(a,b,c)。在参数入栈的时候,是先压c,再压b,最后压a 在取参数的时候,由于栈的先入后出,先取栈顶的a,再取b,最后取c,运行时的堆栈分配,看一段小程序,#include int main ( ) char name8; printf(“Please type your name: “); gets(name); printf(“Hello, s!“, name); return 0; ,pushl ebp movl
7、 esp,ebp subl $8,esp 编译并且执行,我们输入ipxodi,就会输出Hello,ipxodi!。,运行时的堆栈分配,内存底部 内存顶部 name EBP ret - ;name 堆栈顶部 堆栈底部,执行完gets(name)之后,内存底部 内存顶部 name EBP ret -ipxodi0 ;name 堆栈顶部 堆栈底部,堆栈溢出,再执行一次,输入ipxodiAAAAAAAAAAAAAAA,执行完 gets(name)之后 内存底部 内存顶部 name EBP ret -ipxodi0 AAAAAAAA ;name 堆栈顶部 堆栈底部,由于我们输入的name字符串太长,na
8、me数组容纳不下,只好向内存顶部继续写A 由于堆栈的生长方向与内存的生长方向相反,这些A覆盖了堆栈的老的元素 EBP,ret都已经被A覆盖了 在main返回的时候,就会把 AAAA 的ASCII码:0x41414141作为返回地址,CPU会试图执行0x41414141处的指令,结果出现错误,这就是一次堆栈溢出,通常,往缓冲区随便填充所造成的溢出,将使返回地址为一个非法的、不存在的地址,从而出现core dump错误,不能达到攻击目的; 最常用的缓冲溢出攻击方法是:攻击者编写一个简短的Shell程序ShellCode。为了在发生缓冲溢出时执行Shell,必须在执行程序的地址空间里加入ShellC
9、ode,并把程序返回地址修改为ShellCode的入口地址。,Shellcode的编写,Shellcode.c,#include void main() char *name2; name0 = “/bin/sh“; name1 = NULL; execve(name0, name, NULL); ,execve( ),execve函数将执行一个程序 程序的名字地址作为第一个参数 一个内容为该程序的argvi(argvn-1=0)的指针数组作为第二个参数 (char*) 0作为第三个参数,execve的汇编代码,$ gcc -o shellcode -static shellcode.c $
10、gdb shellcode (gdb) disassemble _execve Dump of assembler code for function _execve:,如何精简?,经过以上的分析,可以得到如下的精简指令算法: movl $execve的系统调用号,%eax movl “bin/sh0“的地址,%ebx movl name数组的地址,%ecx movl namen-1的地址,%edx int $0x80 ;执行系统调用(execve),问题,当execve执行成功后,程序shellcode就会退出,/bin/sh将作为子进程继续执行 如果execve执行失败,(比如没有/bin
11、/sh这个文件),CPU就会继续执行后续的 指令,结果不知道跑到哪里去了 所以必须再执行一个exit()系统调用, 结束shellcode.c的执行,exit(0)汇编代码,exit(0) 的汇编代码精简,movl $0x1,%eax ;1号系统调用 movl 0,%ebx ;ebx为exit的参数0 int $0x80 ;引发系统调用,execve + exit,movl $execve的系统调用号, %eax movl “bin/sh0”的地址, %ebx movl name数组的地址, %ecx movl namen-1的地址, %edx int $0x80 ;执行系统调用(execve
12、) movl $0x1,%eax ;1号系统调用 movl 0,%ebx ;ebx为exit的参数0 int $0x80 ;执行系统调用(exit),万事具备,还欠什么?,字符串“/bin/sh”name数组 execve + exit,问题 每一次程序都是动态加载,字符串和name数组的地址都不是固定的 在shellcode中如何知道它们的地址呢?,jmp + call,在large_string中填入buffer的地址,把shell代码放到large_string 的前面部分,将large_string拷贝到buffer中,造成溢出,使返回地址变为buffer,而buffer的内容为she
13、ll代码。这样当程序试从 strcpy() 中返回时,就会转而执行shell,如何利用别人的漏洞,利用别人的程序的堆栈溢出获得rootshell 以一个有strcpy堆栈溢出漏洞的程序,利用前面说过的方法来得到shell同样必须完成两件事 把自己的shellcode提供给对方,让对方可以访问shellcode 修改对方的返回地址为shellcode的入口地址,How,必须知道strcpy(buffer,ourshellcode中,buffer的地址 因为当我们把shellcode提供给strcpy之后,buffer的开始地址就是shellcode的开始地址 必须用这个地址来覆盖堆栈,How,对
14、于操作系统而言,一个shell下的每一个程序的堆栈段开始地址都是相同的 可以写一个程序,获得运行时的堆栈起始地址,这样,我们就知道了目标程序堆栈的开始地址,下面这个函数,用eax返回当前程序的堆栈指针。(所有C函数的返回值都放在eax寄存器 里面) - unsigned long get_sp(void) _asm_(“movl %esp,%eax“); -,猜?,我们在知道了堆栈开始地址后,buffer相对于堆栈开始地址的偏移,是程序员自己写出来的程序决定的,我们不知道,只能靠猜测了 不过,一般的程序堆栈大约是几K 左右。所以,这个buffer与上面得到的堆栈地址,相差就在几K之间 显然猜地
15、址这是一件很难的事情010K,前面我们用来覆盖堆栈的溢出字符串为: 现在变为: 其中: N为NOP.NOP指令意思是什么都不作,跳过一个CPU指令周期。在intel机器上,NOP指令的机器码为0x90 S为shellcode A为我们猜测的buffer的地址 这样,A猜大了也可以落在N上,并且最终会执行到 S,缓冲区溢出攻击与防御,概要 缓冲区溢出攻击分析(BOFA) BOFA的检测与防御,缓冲溢出的目的:干扰具有特权运行的程序,以控制整个系统 缓冲溢出攻击方法: )在程序的地址空间里安排攻击代码的方法 植入法:用被攻击程序的缓冲区来存放攻击代码 传递参数法: )控制程序转移到攻击代码的方法
16、激活记录:函数调用时会在堆栈中留下一个激活记录,它包含返回地址攻击者通过溢出这些自动变量,使返回地址指向攻击代码这叫做 “stack smash attack”,攻击成功发生必须同时满足三个条件,将攻击代码注入内存 将函数返回地址指针指向已注入攻击代码的内存地址 执行攻击代码,阻止缓冲区溢出,这种方法的关键是要能禁止恶意代码的注入。最根本的办法是采用安全的C语言库函数的最新版本,或者由程序员编程时手工加入用以检查数组与指针等的上界的代码,对下列经常使用又容易被攻击者利用的函数更需特别重视。 strcpy(),strcat(),sprintf(),vsprintf(),gets().scanf(
17、),以及在循环内的getc(), fgetc(), getchar()等都没有对数组越界加以监视和限制,所以利用字符数组写越界,覆盖堆栈中原先元素的值,就可以修改返回地址。,允许缓冲区溢出但不允许改变控制流,这种方法允许注入外部代码,但禁止未经授权控制流的改变,因而攻击者可以将其攻击代码注入内存并能改变部分地址段内容,但控制流不会被指向攻击代码,从而攻击代码不会被执行。,允许改变控制流但禁止敏感代码的执行,这种方法中,可以注入攻击代码、并能改变返回地址,但攻击代码不能完全执行。 比如可以禁止诸如exec()等系统调用函数的非法使用,在Unix中,当程序使用系统调用函数时,其返回地址将被保存在系
18、统内核堆栈中,而不是普通堆栈中。这样,通过检查系统调用的地址是否来自系统内核堆栈就可知道它是否合法。,缓冲溢出的预防 )编写不产生缓冲溢出的程序进行数组越界检查;用安全的函数代替Strcpy等;注意数据类型转换的安全问题Java受缓冲溢出攻击:由于Java平台JVM来自C程序 )非执行的缓冲区:使被攻击程序数据段地址空间不可执行,从而使攻击者不能执行植入被攻击程序缓冲区的代码 )程序指针完整性检查:采用堆栈保护的编译技术程序指针被引用之前要检查未被修改才可使用 )安装程序补丁:用最新的程序更新有缺陷的程序,在UNIX系统中,黑客要启动一个Shell,以获得和堆栈被溢出的程序相同的权限 如果文件ABC的拥有者为root,而它的文件属性为:r w x r x r x ,则root可读、写、执行文件ABC,其它用户只能以自己的权限读和执行它; 但如果该文件属性为:r w s r x r x ,则 任何一个运行了ABC程序的用户都将获得文件的拥有者root的权限(此时文件ABC叫做Setuid的,即能以拥有者权限运行) 一般,黑客要溢出的程序都是Setuid的,这样当溢出后就能绕过口令升级为root权限,