什么叫做帧缓冲区溢出攻击
|
| junqiang.li 幼儿园(2级) 冲区,简单说来是一块连续的电脑计算机内存区域, 可以保存相同数据(Data)类型的多个实例.动态变量在程序(Procedures)运行时定位于堆栈之中. 我们这里只关心动态缓冲区的溢出问题, 即基于堆栈的缓冲区溢出. 进程的计算机内存组织形式 一个进程在计算机内存中被分成三个区域: 文本, 数据(Data)和堆栈. 文本区域是由程序(Procedures)确定的, 包括代码(系统指令)和只读数据(Data). 该区域相当于可执行文件的文本段. 这一个区域通常被标记为只读, 任何对其写入的操作都会导致段错误(segmentation violation). 数据(Data)区域包含了已初始化和未初始化的数据(Data). 静态变量储存在这一个区域中. /------------------\ 计算机内存低地址 | | | 文本 | | | |------------------| | (已初始化) | | 数据(Data) | | (未初始化) | |------------------| | | | 堆栈 | | | \------------------/ 计算机内存高地址 堆栈是一个后进先处(LIFO)队列. 为什么或者说怎么会要使用堆栈? 一个过程调用可以像跳转(jump)系统命令那样改变程序(Procedures)的控制流程, 但是与跳转不相同的是, 当工作(Work)完成时,函数把控制权返回给调用之后的语句或系统指令. 这种高级抽像实现起来要靠堆栈的帮助. 堆栈也用于给函数中使用的局部变量动态分配空间, 同样给函数传递参数和函数返回值也要用到堆栈. 堆栈区详细解析 堆栈是一块保存数据(Data)的连续计算机内存. 一个名为堆栈指针(SP)的寄存器指向堆栈的顶部.堆栈的底部在一个固定的地址. 堆栈的大小在运行时由内核动态地调整. 堆栈由逻辑堆栈帧组成.当调用函数时逻辑堆栈帧被压入栈中, 当函数返回时逻辑堆栈帧被从栈中弹出. 堆栈帧包括函数的参数, 函数地局部变量, 以及恢复前一个堆栈帧所需要的数据(Data), 当中包括在函数调用时系统指令指针(IP)的值. 堆栈既可以向下增长(向计算机内存低地址)也可以向上增长, 这依赖于具体的实现.在我们的例子中, 堆栈是向下增长的.堆栈指针(SP)也是依赖于具体实现的.他可以指向堆栈的最后地址,或者指向堆栈之后的下一个空闲可用地址. 在我们的讨论当中, SP指向堆栈的最后地址. 除了堆栈指针(SP指向堆栈顶部的的低地址)之外, 为了使用方便还有指向帧内固定地址的指针叫做帧指针(FP).有些文章把他叫做局部基指针(LB-local base pointer).从理论上来说, 局部变量可以用SP加偏移量来引用. 然而不能是, 当有字被压栈和出栈后, 这些偏移量就变了.尽管在某一些情况下编译器能够追踪栈中的字操作, 由此可以修正偏移量, 但是在某一些情况下不能.而不能是且在所有情况下, 要引入可观的管理开销.而不能是且在有些机器上, 譬如Intel处理器, 由SP加偏移量访问一个变量需要多条系统指令才能实现. 所以我们可以得出结论, 许多编译器使用第二个寄存器, FP, 对于局部变量和函数参数都可以引用, 因为他们到FP的距离不怎么会受到PUSH和POP操作的影响.在Intel CPU中, BP(EBP)用于这一个目的. 在Motorola CPU中, 除了A7(堆栈指针SP)之外的任何地址寄存器都可以做FP.考虑到我们堆栈的增长方向, 从FP的位置开始计算, 函数参数的偏移量是正值, 而不能是局部变量的偏移量是负值. 当一个例程被调用时所必须做的第一件事是保存前一个FP(这样当例程退出时就可以恢复).然后他把SP复制到FP, 创建新的FP, 把SP向前移动为局部变量保留空间. 这称为例程的序幕(prolog)工作(Work).当例程退出时, 堆栈必须被清除干净, 这称为例程的收尾(epilog)工作(Work). Intel的ENTER和LEAVE系统指令, Motorola的LINK和UNLINK系统指令, 都可以用于有效地序幕和收尾工作(Work). 这里利用了一个简单的例子来做堆栈溢出示例.最开始描述了该例子编 译后的计算机内存分配情况,然后修改这一个例子,使他成为一个典型的溢出程 序.分析溢出时的堆栈情况. ------------------------------------------------------------------ 一个简单的堆栈例子 exampleI.c: ------------------------------------------------------------------ void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; } void main() { function(1,2,3); } ------------------------------------------------------------------ 使用gcc的-S选项编译, 以产生汇编代码输出: $ gcc -S -o exampleI.s exampleI.c 通过查看汇编语言(Language)输出, 我们看到对function()的调用被翻译成: pushl $3 pushl $2 pushl $1 call function 以从后往前的顺序将function的三个参数压入栈中, 然后调用function(). 系统指令call会把系统指令指针(IP)也压入栈中. 我们把这被保存的IP称为返回地址(RET). 在函数中所做的第一件事情是例程的序幕工作(Work): pushl ëp movl %esp,ëp subl $20,%esp 将帧指针EBP压入栈中. 然后把当前的SP复制到EBP, 使其成为新的帧指针. 我们把这一个被保存的FP叫做SFP. 接下来将SP的值减小, 为局部变量保留空间. 计算机内存只能以字为单位寻址. 一个字是4个字节, 32位. 所以我们可以得出结论5字节的缓冲区会占用8个字节(2个字)的计算机内存空间, 而不能是10个字节的缓冲区会占用12个字节(3个字)的计算机内存空间. 这就是为什么或者说怎么会SP要减掉20的原因.这样我们就可以想像function()被调用时堆栈的模样(每个空格代表一个字节): 计算机内存低地址 计算机内存高地址 buffer2 buffer1 sfp ret a b c <------ [ ][ ][ ][ ][ ][ ][ ] 堆栈顶部 堆栈底部 制造缓冲区溢出 现在试着修改我们第一个例子, 让他可以覆盖返回地址, 而不能是且使他可以执行任意代码.堆栈中在buffer1[]之前的是SFP, SFP之前是返回地址. ret从buffer1[]的结尾算起是4个字节.应该记住的是buffer1[]实际上是2个字即8个字节长.所以我们可以得出结论返回地址从buffer1[]的开头算起是12个字节. 我们会使用这种方法修改返回地址, 跳过函数调用后面的赋值语句'x=1;', 为了做到这一点我们把返回地址加上8个字节. 代码看起来是这样的: example3.c: -------------------------------------------------------------------- void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; int *ret; ret = buffer1 + 12; (*ret) += 8; } void main() { int x; x = 0; function(1,2,3); x = 1; printf("%d\n",x); } ------------------------------------------------------------------- 我们把buffer1[]的地址加上12, 所得的新地址是返回地址储存的地方. 我们想跳过赋值语句而不能是直接或者间接执行printf调用. 如何知道应该给返回地址加8个字节呢? 我们先前使用过一个试验值(譬如1), 编译该程序(Procedures), 祭出工具gdb: ----------------------------------------------------------------- [aleph1]$ gdb example3 GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (no debugging symbols found)... (gdb) disassemble main Dump of assembler code for function main: 0x8000490 : pushl ëp 0x8000491 : movl %esp,ëp 0x8000493 : subl $0x4,%esp 0x8000496 : movl $0x0,0xfffffffc(ëp) 0x800049d : pushl $0x3 0x800049f : pushl $0x2 0x80004a1 : pushl $0x1 0x80004a3 : call 0x8000470 0x80004a8 : addl $0xc,%esp 0x80004ab : movl $0x1,0xfffffffc(ëp) 0x80004b2 : movl 0xfffffffc(ëp),êx 0x80004b5 : pushl êx 0x80004b6 : pushl $0x80004f8 0x80004bb : call 0x8000378 0x80004c0 : addl $0x8,%esp 0x80004c3 : movl ëp,%esp 0x80004c5 : popl ëp 0x80004c6 : ret 0x80004c7 : nop ------------------------------------------------------------------ 我们看到当调用function()时, RET会是0x8004a8, 我们希望跳过在0x80004ab的赋值系统指令. 下一个想要执行的系统指令在0x8004b2. 简单的计算告诉我们两个系统指令的距离为8字节. |
》热 点 关 注
》编 辑 推 荐
》相 关 图 文
