当前位置:文档之家› 缓冲区溢出攻击原理与防范

缓冲区溢出攻击原理与防范

缓冲区溢出攻击的原理与防范陈硕2004-7-12读者基础:熟悉C语言及其内存模型,了解x86汇编语言。

缓冲区溢出(buffer overflow)是安全的头号公敌,据报道,有50%以上的安全漏洞和缓冲区溢出有关。

C/C++语言对数组下标访问越界不做检查,是引起缓冲区溢出问题的根本原因。

本文以Linux on IA32(32-bit Intel Architecture,即常说的x86)为平台,介绍缓冲区溢出的原理与防范措施。

按照被攻击的缓冲区所处的位置,缓冲区溢出(buffer overflow)大致可分为两类:堆溢出1(heap overflow)和栈溢出2(stack overflow)。

栈溢出较为简单,我先以一些实例介绍栈溢出,然后谈一谈堆溢出的一般原理。

栈溢出原理我们知道,栈(stack)是一种基本的数据结构,具有后入先出(LIFO, Last-In-First-Out)的性质。

在x86平台上,调用函数时实际参数(arguments)、返回地址(return address)、局部变量(local variables)都位于栈上,栈是自高向低增长(先入栈的地址较高),栈指针(stack pointer)寄存器ESP始终指向栈顶元素。

以图表1中的简单程序为例,我们先将它编译为可执行文件,然后在gdb中反汇编并跟踪其运行:$ gcc stack.c –o stack -ggdb -mperferred-stack-boundary=2在IA32上,gcc默认按8个字节对齐,为了突出主题,我们令它按4字节对齐,最末一个参数的用处在此。

图表1在每条语句之后列出对应的汇编指令,注意这是AT&T格式汇编,mov %esp, %ebp 是将寄存器ESP的值赋给寄存器EBP(这与常用的Intel汇编格式正好相反)。

// stack.c#01 int add(int a, int b)#02 {// push %ebp// mov %esp,%ebp#03 int sum;// sub $0x4,%esp#04 sum = a + b;// mov 0xc(%ebp),%eax// add 0x8(%ebp),%eax// mov %eax,0xfffffffc(%ebp)#05 return sum;// mov 0xfffffffc(%ebp),%eax1本文把静态存储区溢出也算作一种堆溢出。

2 Stack 通常翻译为“堆栈”,为避免与文中出现的“堆/heap”混淆,这里简称为“栈”。

// leave// ret#06 }#07#08 int main()#09 {// push %ebp// mov %esp,%ebp#10 int ret = 0xDEEDBEEF;// sub $0x4,%esp// movl $0xdeedbeef,0xfffffffc(%ebp)#11 ret = add(0x19, 0x82);// push $0x82// push $0x19// call 80482f4 <add>// add $0x8,%esp// mov %eax,0xfffffffc(%ebp)#12 return ret;// mov 0xfffffffc(%ebp),%eax// leave// ret#13 }图表 1 典型的函数调用当程序执行完第10行时,堆栈如图表2所示。

图中每格表示一个double word(4字节)。

图表 2 堆栈状况1EBP是栈帧指针(frame pointer),在整个函数的运行过程中,它始终指向间于返回地址和局部变量之间的一个double word,此处保存着调用端函数(caller)的EBP值(第9行对应的两条指令正是起这个作用)。

EBP所指的位置之下是局部变量,例如EBP-4是变量ret 的地址,-4的补码表示正好是0xFFFFFFFC,第11行上方的movl指令将0xDEEDBEEF 存入变量ret。

当函数返回时,须将EBP恢复原值。

leave指令相当于:mov %ebp, %esp // 先令esp指向saved ebppop %ebp // 弹出栈顶内容至ebp,此时esp正好指向返回地址,ebp也恢复原值ret指令的作用是将栈顶元素(ESP所指之处)弹出至指令指针EIP,完成函数返回动作。

执行第11条语句时,先将add()的两个参数按从右到左的顺序压入堆栈,call指令会先把返回地址(也就是call指令的下一条指令的地址,此处为一条add指令3)压入堆栈,3C语言为了实现变长参数调用(就像printf()),通常规定由调用端负责清理堆栈,这条add指令正是起平衡堆栈的作用。

然后修改指令指针EIP,使程序流程(flow)到达被调用函数处(第2行)。

当程序运行到第4行时,堆栈的情况如图表3所示。

图表 3 堆栈情况2图中灰色部分是main()的栈帧(stack frame,又称活动记录:activation record),其下是add()的栈帧,从中可以看出,保存函数返回地址(return addr)的位置比第一个局部变量高8字节。

由此我们想到,函数可以修改自己的返回地址。

下面我们做一个试验。

// retaddr.c#01 #include <stdio.h>#02#03 void malice()#04 {#05 printf("Hey, you've been attacked.\n");#06 }#07#08 void foo()#09 {#10 int* ret;#11 ret = (int*)&ret + 2; // get the addr of return addr#12 (*ret) = (int)malice; // set my return addr to malice()#13 }#14#15 int main()#16 {#17 foo();#18 return 0;#19 }图表 4 改变函数返回地址图表4列出了一个函数改变自己返回地址的程序,foo()函数将自己的返回地址改为malice()函数。

编译运行这个程序,结果如下:$ gcc retaddr.c -o retaddr -ggdb -mpreferred-stack-boundary=2$ ./retaddrHey, you've been attacked.Segmentation fault (core dumped)core dump 4发生在malice()返回时,我们来分析一下究竟发生了什么。

首先,在进入main()函数后,在执行第17行之前,堆栈情况如图表5-(a)所示,这是main()的栈帧;随后,进入函数foo(),在执行第11行之前,堆栈布局如图表5-(b)所示,灰色部分是调用端main()的栈帧;执行第11行之后,ret 指向函数的返回地址(图表5-(c));第12行修改*ret ,将返回地址设为malice()的入口。

foo()函数结束后,本应返回到main(),执行第18行的语句return 0;然而由于返回地址被修改,foo()函数返回后进入函数malice(),在执行第5行之前,堆栈的情况如图表5-(d)。

这时堆栈已被破坏,malice()函数的返回地址处存放的是main()函数保存的EBP 值(图中的 saved EBP* ),malice()函数返回后,会跳转到 saved EBP* 所指的地址,oops!接下来发生的事情想必大家都知道了☺(a) (b)(c) (d)图表 5 堆栈情况3继续我们的试验:如何让这个程序正常退出?我想到的办法是,利用main()函数的局部变量伪造一个貌似合法的堆栈,让malice()返回后,程序得以安全退出。

办法很简单,在malice()的返回地址处放上exit()的入口地址☺,当然,我们还要顺便伪造传给exit()的参数。

改进后的main()见图表 6。

4如果没有出现core dumped 字样,请先执行 ulimit –c unlimited 。

#02 #include <stdlib.h>#15 int main() #16 {#17 volatile int exit_val = 100; #18 volatile int dumy = 0;#19 volatile void* ret_addr = &exit; #20 foo(); #21 }图表 6 改进后的“修改函数返回地址”示例使用volatile 关键字是为了防止编辑器将这些看似没用的局部变量优化掉。

进入函数malice()后,堆栈情况如图表 7-(a)所示。

与图表 5-(d)比较可知,malice()会把ret_addr作为自己的返回地址,我们已在此处填上了exit()的入口地址。

当malice()返回后,程序进入exit()函数,这时堆栈如图表 7-(b)所示(注意,exit()没有保存ESP )。

exit()函数会把100认为是传递给自己的参数,还会认为返回地址是0,但是exit()永不返回,所以不会造成core dump ,程序正常结束,返回给操作系统的代码是100。

(a) (b)图表 7 堆栈情况4有了以上对函数调用栈的了解,接下来,我们可以谈谈栈上的缓冲区溢出了。

利用缓冲区溢出,我们能 1) 自由修改EIP ,控制程序流程;2) 植入shellcode ,获得root shell 。

所谓shellcode ,是指能调出shell 的程序,功能如同shellcode1.c (图表 8)。

#01 #include <unistd.h>#02#03 int main() #04 {#05 char* name[2]; #06#07 setuid(0); // required if bash is used #08 name[0] = "/bin/sh"; #09 name[1] = NULL;#10 execve(name[0], name, NULL); #11 return 0; #12 }图表 8 shellcode1.c如果以root权限执行这段程序,我们就能获得一个root shell,Wow! 先试一把:$ gcc -o shellcode1 shellcode1.c$ whoamischen$ ./shellcode1sh-2.05b$ whoamischen咦?怎么没有变身root?噢,忘了将shellcode1的owner设为root,还要设置suid位:$ sudo chown root shellcode1$ sudo chmod +s shellcode1$ whoamischen$ ./shellcode1sh-2.05b# whoamirootsh-2.05b# id// 不放心,再确认一下☺uid=0(root) gid=500(schen) groups=500(schen)当然,我们不能直接使用图表8中的程序,需要把它转换为机器码,再注入缓冲区。

相关主题