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

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

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

陈硕

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),%eax

1本文把静态存储区溢出也算作一种堆溢出。

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 $0x8,%esp

// mov %eax,0xfffffffc(%ebp)

#12 return ret;

// mov 0xfffffffc(%ebp),%eax

// leave

// ret

#13 }

图表 1 典型的函数调用

当程序执行完第10行时,堆栈如图表2所示。图中每格表示一个double word(4字节)。

图表 2 堆栈状况1

EBP是栈帧指针(frame pointer),在整个函数的运行过程中,它始终指向间于返回地址和局部变量之间的一个double word,此处保存着调用端函数(caller)的EBP值(第9行对应的两条指令正是起这个作用)。EBP所指的位置之下是局部变量,例如EBP-4是变量ret 的地址,-4的补码表示正好是0xFFFFFFFC,第11行上方的movl指令将0xDEEDBEEF 存入变量ret。当函数返回时,须将EBP恢复原值。leave指令相当于:

mov %ebp, %esp // 先令esp指向saved ebp

pop %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

#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

$ ./retaddr

Hey, you've been attacked.

相关文档
最新文档