我和你,或许分开才是最好的选择,那些曾经的过客,就当做最美丽的风景线,在以后的以后,我会好好坚强。
缓冲溢出弱点诞生于70年代。Morris Worm(80年代)可以认为是它们的第一次公开应用。从90年代开始,相关的文档,如著名的Aleph1的”Smashing the Stack for Fun and Profit”和代码已经在互联网上公开。
这篇文章是关于某种需要非常重视的主题的系列文章的开始,并且包括了很多的细节;它的目的是解释和阐明非常基本的漏洞类型,即所谓的本地溢出,而且论述了如何编写利用这种漏洞的代码。
为了理解接下来的内容,需要一些C和汇编的知识。虚拟内存,一些操作系统的基本知识例如象一个进程在内存里是如何布局的。你必须知道什么是setuid二进制文件,而且当然你需要至少能够使用UNIX系统。如果你有gdb/cc调试编译的经验,那就非常好了。文档特定在Linux/ix86环境下。细节的不同之处取决于操作系统或者你使用的架构。在接下来的文档里面,将介绍相应的更高级的溢出和shellcode技术。
最新版本的文档可以在这里找到:
http://www.enderunix.org/documents/eng/bof-eng.txt
什么是溢出?
如果你知道C,你肯定知道什么是字符型数组。假设你用C写代码,你应该已经知道数组的基本特性,象:数组拥有同样类型的对象,例如:int,char,float。就像所有的数据结构一样,它们能够被分成是”静态”或者是”动态”。静态变量被填入程序的数据段,然而,动态变量在内存中的可执行程序的堆栈区域分配和重新分配。”基于堆栈”的溢出就在这里发生了,我们在数据结构中填更多的数据,也就是说多于一个数组能够存储的数据,我们忽略许多重要的数据超越这个数组的界限。简单的,拷贝20字节到一个只能存储12字节的数组里。
一个Linux ELF格式二进制的存储结构相当的复杂。特别是在ELF (详细内容,在google中搜索”Executable and Linkable Format”)和共享库引入后,它已经变得更加复杂。然而,基本上,每一个进程运行时有3段:
1.文本段,是一个只读部分包括所有的程序指令。对于等同于下面C代码的指令集合将被包括在这段。
for (i = 0; i
2.数据段是初始化了的和未初始化的数据(也被认为是BBS段)所在的块。
if you code;
int i;
the variable is an uninitialized variable, and it'll be stored in
the "uninitialized variables" part of the Data Segment. (BSS)
and, if you code;
int j = 5;
the variable is an initialized variable, and the the space for
the j variable will be allocated in the "initialized variables"
part of the Data Segment.
3.一个被称为”堆栈”的段,在这里动态变量(或者在C里面叫自动变量)被分配和重新分配;并且为函数返回临时存储地址。例如,在下面代码片段中,i变量在堆栈中产生,仅仅在函数返回后,它就消亡了。
int myfunc(void)
{
int i;
for (i = 0; i 如果我们用符号表示堆栈:
0xBFFFFFFF ---------------------
| |
| . |
| . |
| . |
| . |
| etc |
| env/argv pointer. |
| argc |
|-------------------|
| |
| stack |
| |
| | |
| | |
| V |
/ /
\ \
| |
| ^ |
| | |
| | |
| |
| heap |
|-------------------|
| bss |
|-------------------|
| initialized data |
|-------------------|
| text |
|-------------------|
| shared libraries |
| etc. |
0x8000000 |-------------------|
_* STACK *_
堆栈在基本术语里面是一个数据结构,你们都可以从你们的数据结构课程记起来。它有同样的基本操作。它是一个LIFO(后进,先出)的数据结构。它的处理过程通过一些特殊的指令象PUSH和POP由CPU直接控制。你PUSH一些数据到堆栈里面,又POP一些其它的数据。不论谁最后到,它将是最先出来的那个。因此,用专业术语说,第一个将被从堆栈中推出来的是最后一个被推进去的。
在CPU中注册的SP(堆栈指针)包括将要从堆栈中推出来的数据地址。不论SP指向最后的数据还是堆栈中最后数据的后面数据是CPU-specific的;然而,我们的目标ix86结构,SP指向堆栈中最后数据的地址。在ix86保护模式(32位/双字)下,PUSH和POP指令在4字节单元中完成。在这里要说的另外一个重要的细节是堆栈向下增长,也就是,如果SP是0xFF,执行PUSH EAX指令后,SP将变成0xFC并且EAX的值将被放到0xFC地址里。 PUSH指令将从ESP(回顾一下上面的图)中减去4个字节,并且将推入一个双字到堆栈,放置双字到ESP寄存器所指的地址中。另一方面,POP指令,读取ESP寄存器中的地址,POP掉堆栈地址所指的值,并且加4到ESP(加4到ESP寄存器中的地址)。假设ESP初始化为0x1000,让我们观察下面的汇编代码:
PUSH dword1 ;value at dword1: 1, ESP's value: 0xFFC (0x1000 - 4)
PUSH dword2 ;value at dword2: 2, ESP's value: 0xFF8 (0xFFC - 4)
PUSH dword3 ;value at dword3: 3, ESP's value: 0xFF4 (0xFF8 - 4)
POP EAX ;EAX' value 3, ESP's value: 0xFF8 (0xFF4 4)
POP EBX ;EBX's value 2, ESP's value: 0xFFC (0xFF8 4)
POP ECX ;ECX's value 1, ESP's value: 0x1000 (0xFFC 4)
当堆栈被用做动态变量的临时存储的时候,它被用来存储一些调用存储临时变量函数的地址和在函数间传递参数。而且,当然,这也是邪恶的来场。
EIP寄存器,CALL和RET指令
在每一个机器周期中,CPU查询指令指针寄存器存储中存储的内容(在ix86 32位保护模式中是EIP-扩展指令指针)来知道下一步要执行什么。下一个将要执行的指令地址存储在EIP寄存器中。通常,地址是连续的,意味着下个将要执行的下个指令在内存中比当前指令靠前几个字节。CPU根据当前指令是几个字节长度计算出”靠前的几个字节”,然后把这几个字节值加到当前地址。举个例子,假设当前指令地址是0x8048438。这是写在EIP中的值。因此,CPU执行在内存地址:0x8048438中找到的指令。比方说,是一个PUSH指令:
push 雙
CPU知道一个PUSH指令是1字节长,因此下一个指令将在0x8048439,可能是
mov %esp,雙
在执行PUSH时,CPU将把MOV地址放到EIP中。
本文缓冲区溢出解密一 到此结束。人生的游戏不在于拿了一副好牌,而在于怎样去打好坏牌,世上没有常胜将军,勇于超越自我者才能得到最后的奖杯。小编再次感谢大家对我们的支持!