缓冲区溢出(Buffer Overflow)
主题:缓冲区溢出(Buffer Overflow)
缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量,溢出的数据覆盖在合法数据上(可以联想单片机
对栈的操作)。理想的情况是程序检查数据长度并不允许输入超过缓冲区长度的字符,但是绝大多数程序都会假设数据长度总是与所
分配的储存空间相匹配,这就为缓冲区溢出埋下隐患。
通过向程序的缓冲区写入超出其长度的内容,导致缓冲区的溢出,从而破坏程序的堆栈,造成程序崩溃或使程序转而执行其它
指令,以达到攻击的目的。造成缓冲区溢出的原因是程序中没有仔细检查用户输入的参数。
缓冲区溢出是一种非常普遍、非常危险的漏洞,在各种操作系统、应用软件中广泛存在。利用缓冲区溢出攻击,可以导致程序
运行失败、系统宕机、重新启动等后果。更为严重的是,可以利用它执行非授权指令,甚至可以取得系统特权,进而进行各种非法
操作。课程的练习基本没什么实际意义。后面会补充一些缓冲区溢出的例子,以备深入学习该主题。
缓冲区溢出攻击原理
大多数的缓冲溢出攻击都是通过改变程序运行的流程到入侵者植入的恶意代码,其主要目的是为了获取超级用户的shell。原理
相当简单:将恶意指令存放在buffer中,这段指令可以得到进程的控制权,从而达到攻击的目的。
缓冲区溢出的技术:栈溢出(stack overflow)、堆溢出(heap overflow)。
1.堆和栈
当一个程序开始运行时,一些基本信息(指令、变量等)会事先装入内存,一个进程维护着它自己的一段内存空间,称为进程空间
(上下文),它维护着进程所需要的代码段、堆栈段和数据段,如图1。
图1 进程空间组织图
在进程空间的高地址区域存放着进程相关的环境变量以及参数:env串、arg串,env指针之后的内存空间由栈(stack)和堆
(heap)两部分组成,它们都在进程运行时被分配。
栈用来存放函数参数、局部变量,以及一些允许在一个函数调用之前找回栈的信息。栈遵循LIFO(后进先出)的原则来访问
系统,并且向内存的低地址方向增长。
动态分配的变量存放在堆区。通常,调用malloc函数用来返回一个指针指向一个堆区的地址。malloc函数是用户层的动态分配
内存的函数,总是在堆区分配一段连续的内存空间。
.bss和.data区存放全局变量和一些静态变量(在编译的时候分配)。.data区包含了静态已初始化的数据,.bss区则包含了未
初始化的数据。最后一个内存区域,.text包含了程序指令代码和一些只读数据。
例子:
heap
int main(){
char * tata = malloc(3); // tata是一个指针,指向堆区的一段内存空间的起始地址。
...
}
.bss
char global;
int main(){
...
}
int main(){
static int bss_var;
...
}
// global和bss_var将存储在.bss区。
.data
char global = 'a';
int main(){
...
}
int main(){
static char data_var = 'a';
...
} // global和data_var将存储在.data区。
2.函数调用
在Unix系统中,一个函数调用的过程可以分为以下三步:
1)准备堆栈
保存当前栈帧指针。一个栈帧可以理解成堆栈里的一个逻辑单元,它描述了一个函数的基本单元,一些函数需要的内存信息也被保存。
2)调用
函数的参数和返回地址被保存进堆栈,目的在于函数返回之后程序需要到哪里去继续执行。
3)返回(或结束)
恢复调用函数之前保存的原来的堆栈。
下面用一个简单的代码可以帮助我们理解以上介绍过程是如何工作的,并且这可以让我们更好地掌握缓存溢出(buffer overflow)技术
e4glecall.c:
int e4glecall (int a, int b, int c) {
int i=4;
return (a+i);
}
int main (int argc, char **argv) {
e4glecall(0,1,2);
return 0;
}
现在用gdb来反汇编上面编译好的程序,目的是为了更透彻地说明以上步骤。这里涉及两个重要的寄存器:指向当前栈帧的EBP和指向栈顶的ESP。
反汇编后的main函数:
(gdb) disassemble main
Dump of assembler code for function main:
0x8048448 : push %ebp
0x8048449 : mov %esp,%ebp
0x804844b : sub $0x8,%esp
以上是main函数的开始部分。若要详细了解一个函数的细节,可以看后面的e4glecall函数。
0x804844e : sub $0x4,%esp
0x8048457 : call 0x8048430
e4glecall()函数的调用包含了以上4个指令:3个参数压栈(反序排列),然后调用函数:
0x804845c : add $0x10,%esp
以上指令描述e4glecall()函数返回到main()函数:将堆栈指针指向返回地址,所以必须使堆栈指针增加,因为堆栈是向内存的低地址方向增长的。这样,我们返回到
了初始的环境,也就是e4glecall()函数调用之前。
0x804845f : mov $0x0,%eax
0x8048464 : leave
0x8048465 : ret
...
End of assembler dump.
最后两条指令用于main()函数的返回。
反汇编后的e4glecall()函数:
(gdb) disassemble e4glecall
Dump of assembler code for function e4glecall:
0x8048430 : push %ebp // 保存当前环境(当前栈指针 %ebp压栈)
0x8048431 : mov %esp,%ebp // 使%ebp指向堆栈的顶端
0x8048433 : sub $0x4,%esp // 为函数调用准备足够的堆栈空间
以上代码是函数的初始阶段。
0x8048436 : mov1 $0x4,0xfffffffc (%ebp) // 使%ebp和%esp指针恢复到初始化前的值
0x804843d : mov 0xfffffffc(%ebp), %eax // 指令寄存器安排
0x8048440 : add 0x8(%ebp), %eax
0x8048443 : mov %eax, %eax
//下面是函数指令
0x8048445 : leave
0x8048446 : ret
0x8048447 : nop
End of assembler dump
(gdb)
以上代码是函数返回部分。
说明:原书在此处引用一段较长的代码,本文只做引用:《黑客编程手册-黑客编程Follow Me》第二章 黑客工具的工作原理37-41页。
上面的例子说明了在函数调用的时候堆栈的组织情况。假如一片内存区域被不小心破坏了,这就有可能使攻击者来扰乱堆栈,
并且执行一些恶意代码。因为堆栈控制着函数的调用返回,也就是控制着程序的运行流程,通过扰乱堆栈来拿到程序的流程控制权
就可以完成一次攻击了。
当函数返回时,下一条指令地址会从堆栈拷贝到EIP指针。因为这个地址是保存在堆栈的,所以若我们能够覆盖这个地址成为
新的地址的话,那么就有可能使程序在我们覆盖的新地址继续执行,我们再在此地址处放置我们的代码(称之为shellcode)或者
此地址直接指向glibc库里面的一个函数指针,那么该程序就被我们控制了。
缓冲区及其安全问题
在C语言中,字符串或者缓冲区,都可以以一个指针来描述,该指针通常指向一片内存区域的首地址。并且对于缓冲区来说,都
可以出现NULL字节为缓冲区的结束标记,所以一个缓冲区的中间是不可能出现空字节的。也不能像计算内存空间那样计算Buffer的
大小,它的大小取决于字符的数量。
因为每个分配的缓冲区都是限制大小的,要防止所有的溢出攻击是相当困难的。当strcpy函数用得不够谨慎,就可以使用户控制
缓冲区,它可以拷贝一个大的缓冲区到另一个较小的缓冲区中,那么这时候就发生了缓冲溢出。
堆栈溢出
大部分的缓冲溢出攻击都发生在堆栈,因为缓冲区就是在堆栈里定义的,而且攻击者更容易控制堆栈的数据。首先介绍一般的
执行恶意代码的方法(通常称此恶意代码为shellcode,它能给我们一个root shell),然后举例说明。
激活记录的概念:每当一个函数调用发生时,调用者会在堆栈中留下一个激活记录,它包含了函数结束时返回的地址。攻击者
通过溢出这些自动变量,使这个返回地址指向攻击代码。通过改变程序的返回地址,当函数调用结束时,程序就跳转到攻击者设定
的地址,而不是原先的地址。这类缓冲区溢出被称为“stack smashing attack”,是当前(2003年)最常用的缓冲区溢出攻击方式。
EIP寄存器:它保存着程序要执行的下一条指令的地址,若想控制程序的运行流程就要使EIP寄存器保存我们给出的指令地址。
当一个程序在运行的时候,下一个执行指令的地址会被保存入堆栈。所以,若我们成功修改了EIP中的内容,那么我们就可以强制
让EIP指向我们想要的地址去执行。然后,当函数返回时,程序就会改变运行流程到我们的恶意代码。然而,要精确定位返回地址
的位置也并不容易。
在内存区域中,定位shellcode的地址也不是容易的事。需要计算出堆栈指针到缓冲区的距离,但是我们只知道问题程序的缓冲
区的位置(反汇编得到)。所以,我们把shellcode放到缓冲区的中间,然后在缓冲区的起始位置开始填充NOP指令。NOP指令什么
也不做,这可以增加猜测shellcode地址的命中率,这样若我们的猜测地址落在NOP上,那么就继续向下直到找到shellcode为止。
参考:
《黑客编程手册》 2003.4
《WebGoat v2.2技术文档》
《OWASP Testing Guide v3.0》
《堆溢出学习笔记》:http://drops.wooyun.org/tips/1621
《简单的缓冲区溢出》:http://drops.wooyun.org/papers/1421
《缓冲区溢出攻击初学者手册(更新版)》:http://www.freebuf.com/articles/system/40107.html
《64位Linux下的栈溢出》:http://drops.wooyun.org/tips/2288
注:
本文作为一篇学习笔记,参考了黑客防线杂志(http://hacker.com.cn/)2003年4月出版的《黑客编程手册》中的技术讲解和例子。目的是了解、
熟悉buffer overflow这项技术。任何时候需要这些知识,通过翻阅笔记,可以快速的回忆起技术细节。
乌云知识库的文章明确声明未经允许不能转载的,所以只做了链接。