前言:
这篇文章我到现在还没写完,原因就是我太忙了,但我会尽力写完,想让大家知道什么才是高级的hack技巧,什么才是真正的hacking的乐趣,计算机网络世界的万物可以说是程序,攻击网络或者是计算机其实就是程序的攻击,这才是至高的hacking技巧,大家随我来。。。
Buffer Overflows介绍
Generalities
大多数的缓冲溢出攻击都是通过改变程序运行的流程到入侵者植入的恶意代码,其主要目的是为了取得超级用户的shell。原理是相当简单的:将恶意指令存放在buffer中,这段指令可以得到进程的控制权,从而达到攻击的目的
在这篇文档里我们主要介绍两项buffer overflow技术:stack overflows和heap overflows。
Process memory
Global organization
当一个程序开始运行的时候,一些基本信息(指令,变量。。。)会事先装入内存,一个进程维护着它自己的一段内存空间,我们称为进程空间(上下文),它维护着进程所需要的代码段,堆栈段和数据段。
.
在进程空间的高地址区域存放着进程相关的环境变量以及参数:env串,arg串,env指针(如图1.1).
.
之后的内存空间由两部分组成,stack和heap,它们都在进程运行的时候被分配。
stack(栈)用来存放函数参数,局部变量,以及一些允许在一个函数调用之前找回stack的信息。。。stack遵循LIFO的原则(后进先出)来访问系统,并且向内存低地址方向增长。
动态分配的变量存放在heap区;通常,调用malloc函数用来返回一个指针指向一个heap区地址。malloc是用户层的动态分配内存的函数,它总是在heap区分配一段连续的内存空间。
.
.bss和.data区存放全局变量,和一些静态变量(在编译的时候分配)。.data区包含了静态已初始化的数据,.bss区则包含了未初始化的数据。
.
最后一个内存区域,.text,包含了程序指令代码和一些只读数据。
图 1.1: 进程空间组织图
动口不如动手,我们举几个简单的例子可以让大家更好的理解,让我们看看每种变量不同存储方式:
heap
int main(){
char * tata = malloc(3);
...
}
tata是一个指针,它指向heap区的一段内存空间的起始地址。
.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区.
函数调用
我们现在来分析一下内存(stack)里函数调用过程的细节,并且试着理解有关实现机制。
在unix系统里,一个函数调用的过程可以分为以下三步:
准备堆栈: 保存当前栈帧指针。一个栈帧可以理解成堆栈里的一个逻辑单元,它描述一个函数的基本单元。一些函数需要的内存信息也被保存。
调用: 函数的参数和返回地址被保存进堆栈,目的在于函数返回之后程序需要到哪里去继续执行
返回(或结束): 恢复调用函数之前保存的原来的堆栈。
.
下面一个简单的代码可以帮助大家理解以上介绍过程是如何工作的,并且这可以让更好掌握buffer overflow技术。
让我们看看这段代码:
[e4gle@redhat72 e4gle]$ cat 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
0x8048451 : push $0x2
0x8048453 : push $0x1
0x8048455 : push $0x0
0x8048457 : call 0x8048430
e4glecall()函数的调用包含以上四个指令:三个参数压栈(反序排列),然后调用函数。
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 toto:
0x8048430 : push %ebp
0x8048431 : mov %esp,%ebp
0x8048433 : sub $0x4,%esp
以上代码是我们函数的初始阶段:保存当前环境(当前栈指针%ebp压栈),第二条指令使%ebp指向堆栈的顶端,第三条指令
为函数调用准备足够的堆栈空间(一般为局部变量准备)。
0x8048436 : movl $0x4,0xfffffffc(%ebp)
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)
以上指令是函数返回部分,第一条指令使%ebp和%esp指针恢复到初始化前的值(注意,不是调用函数之前的值,所以栈指针指向的地址仍旧低于e4glecall()的参数地址)。第二条指令安排指令寄存器,该指令寄存器在每次函数返回的时候被访问,用来指定该在哪条指令继续执行。
.
以上的例子说明了在函数调用的时候堆栈的组织情况。在以后的介绍中,我们将比较关注内存分配上。假如一片内存区域被不小心破坏了,这就有可能使攻击者来扰乱堆栈,并且执行一些恶意代码。因为堆栈控制着函数的调用返回,也就是控制着程序的运行流程,通过扰乱堆栈来拿到程序的流程控制权就可以完成一次攻击了。
当一个函数返回时,下一条指令地址会从堆栈拷贝到EIP指针。因为这个地址是保存在堆栈的,所以如果我们能覆盖这个地址成新的地址的话,那么就有可能使程序在我们覆盖的新地址继续执行,我们再在此地址处放置我们的代码(称之为shellcode)或者此地址直接指向glibc库里面的一个函数指针(如system()),那么该程序就被我们控制了。
我们现在来看一下缓冲区,缓冲区也是作为局部变量被分配在堆栈的,而缓冲区一般又是可以提交给用户去定制的,所以一般的堆栈攻击都以缓冲区为载体。
缓冲区,以及它有哪些可利用的安全问题
在c语言里,字符串,或者缓冲区,都可以以一个指针来描述,该指针通常指向一片内存区域的首地址。并且对于缓冲区来说
都以出现NULL字节为缓冲区的结束标记,所以一个缓冲区的中间是不可能出现空字节的(这很重要)。也不能像计算内存空间那样计算buffer的大小,它的大小取决于字符的数量。
.
现在让我们更详细地来看看buffer在内存中的组织。
首先,因为每个分配的缓冲区都是限制大小的,要防止所有的溢出攻击是相当困难的。这是我们经常讨论的,当strcpy函数用的不够谨慎,就可以使用户可以控制缓冲区,他可以拷贝一个大的缓冲区到另一个较小的缓冲区中,那么这时候就发生了缓冲溢出。
这里有一个内存的组织示意图:第一个例子是wxy缓冲区在内存的存储情况,第二个是两个连续的缓冲区wxy和abcde在内存中的存储情况。
图 1.2:buffer在内存中的存储
.
注意一下右边的例子中,这里有两个未利用的字节,是因为内存中存储数据是以一个字(四个字节)对齐的。因而,一个6格字节的buffer就需要两个字(8个字节)的内存空间。这样实际上该buffer只用了6个字节,当然就有两个字