堆栈概念

      2005-6-14 19:5
大学的时候上汇编课没怎么好好学,内存里面到底是什么个样子也没搞清楚:( 汉、 最近看了《黑客技术》(注:的确是很好的文章,里面讲的东西很全面,取这个名字有点委屈内容了,源码也很有说明价值),确切说是今天早上在车上忽然对内存景象有了点类似开天的那种感觉的了解,现在发上来,希望高手继续指正。见笑了:)

我结合一下内存缓冲溢出吧,可能里面的语言有些地方类似于《黑客技术》里的,但决不是简单的COPY:

我们在画内存的时候习惯于从上往下,即我们一般印象中的内存走向大概是这样的:
低址

高址
这是一个前提,下面我们讨论程序运行期间大概的内存布局:

/――――――――    内存低端

| 程序段(CS) |

|―――――――――|

| 数据段(DS) | ↓

|―――――――――|

| 堆栈段(SS) |

\―――――――――/ 内存高端
(可能还会有附加段(ES),并且可能数据段会被安排在堆栈段之下,这个不属于本文讨论范围,下面的讨论均基于这种布局)

其中:
程序段里放着程序的机器码和只读数据,这个段通常是只读,对它的写操作是非法的。(注:这个好理解)

数据段放的是程序中的静态数据。(注:比如程序中声名的常量)

那么你在程序中申请的局部变量应该就放在堆栈段了吧,这里面还可以放什么呢?也就是说还有哪些动态的数据?那就是程序函数调用时所牵扯到的参数的传递和返回值,还有返回地址(程序中紧跟在被调用函数后面的语句的地址)等。这里指明一点,上面的内存分配规则是指的在高级语言中,别误导了。

所以缓冲区溢出就只能发生在堆栈段,因为只有这个段才是程序中动态变化的,也只有这个段才是可在程序中直接操作的(注:不知道这样说是否合适)

那么下面我们把镜头对准本文的主角:堆栈段。

要顺便说明的是堆栈的当前栈顶地址是保存在堆栈指针的寄存器(SP)里的,当前栈底地址是保存在(BP)里面的,这两个都是堆栈段内的偏移地址,堆栈的段地址是保存在(SS)里,这个对阅读汇编代码的时候有用处。

我们先来看一下当程序中发生函数调用时,计算机的行为:首先把参数压入堆栈;然后保存指令寄存器(IP)中的内容,做为返回地址(RET);第三个放入堆栈的是基址寄存器(BP);然后把当前的栈指针(SP)拷贝到BP,做为新的基地址;最后为本地变量留出一定空间,把SP减去适当的数值。这种堆栈内的布局对我们理解为什么会发生溢出很重要。下面我们来看看具体程序中函数调用及执行后堆栈的变化:

////////////////////////////////这段例子来自《黑客技术》,稍加修改
example1.c:

------------------------------------------------------------------------------

void function(int a, int b, int c) {

char buffer1[5];

char buffer2[10];

}

void main() {

function(1,2,3);

}

------------------------------------------------------------------------------

为了理解程序是怎样调用函数function()的,使用-S选项,在Linux下,用gcc进行编译,产生汇编代码输出:

$ gcc -S -o example1.s example1.c

看看输出文件中调用函数的那部分:

pushl $3

pushl

- + P A

- {logtitle}

      {Class} {publishtime}
{logsummary}
标签集:TAGS:{tags}
我要留言To Comment 阅读全文Read All | 回复Comments({commentcount}) 点击Count({viewcount})

{phototitle}

{phototitle}

  • 点击:Hits:{viewcount}
  • 回复:Comments:{commentcount}
  • 发表:PostTime:{posttime}

{logsummary}

{logtitle}

      {Class} {publishtime}
{logcontent}
标签集:TAGS:{tags}
回复Comments({commentcount}) 点击Count({viewcount})

回复Comments

{commentauthor}
{commentauthor}
{commenttime}
{commentnum}
{commentcontent}
作者:
{commentrecontent}


pushl

call function

这就将3个参数压到堆栈里了,并调用function()。指令call会将指令指针IP压入堆栈。在返回时,RET要用到这个保存的IP。

在函数中,第一要做的事是进行一些必要的处理。每个函数都必须有这些过程:

pushl %ebp

movl %esp,%ebp

subl $20,%esp

这几条指令将EBP,基址指针放入堆栈。然后将当前SP拷贝到EBP。然后,为本地变量分配空间,并将它们的大小从SP里减掉。由于内存分配是以字为单位的,因此,这里的buffer1用了8字节(2个字,一个字4字节)。Buffer2用了12字节(3个字)。所以这里将ESP减了20。这样,现在,堆栈看起来应该是这样的。

buffer2 内存低端(栈顶)
buffer1
sbp
ret ↓
a
b
c 内存高端(栈底)


(注:这个例子只是让我们了解了一下布局,但我们已经可以看到如果我们给buffer2一个大于12字节的值,那么很自然的就会有多余的部分往下流,可以流到buffer1的地盘,也可以流到ret的地盘,这就看程序中是怎么安排的了。这是很危险的。这是一个隐患。下面我们看看缓冲区到底是如何被溢出的以及而后又被利用的。)

存在象strcpy这样的问题的标准函数还有strcat(),sprintf(),vsprintf(),gets(),scanf(),以及在循环内的getc(),fgetc(),getchar()等。


example2.c

----------------------------------------------------------------------

void function(char *str) {

char buffer[16];

strcpy(buffer,str); //隐患,
}

void main() {

char large_string[256];

int i;

//large_string的最后一个元素是结尾符,
for( i = 0; i < 255;i++) large_string[i]='A';

function(large_string);

}

----------------------------------------------------------------------

  这个程序是一个经典的缓冲区溢出编码错误。函数将一个字符串不经过边界检查,拷贝到另一内存区域。当调用函数function()时,堆栈如下:

buffer 内存低端(栈顶)
sbp
ret ↓
*str 内存高端(栈底)


很明显,程序执行的结果是"Segmentation fault (core

dumped)"或类似的出错信息。因为从buffer开始的256个字节都将被*str的内容'A'覆盖,包括sbp,

ret,甚至*str。'A'的十六进值为0x41,所以函数的返回地址变成了0x41414141, 这超出了程序的地址空间,所以出现段错误。

可见,缓冲区溢出允许我们改变一个函数的返回地址。通过这种方式,可以改变程序的执行顺序。

////////////////////////////////这段例子来自《黑客技术》,稍加修改


我们总结一下:要了解为什么会发生缓冲区溢出思维方向要经历三次转向,
1,内存是向下生长的,并且堆栈段在程序段之下, 这个方向可以用这个表示: ↓
2,堆栈段是向上生长的,所以每次进新的数据的时候SP要自减 这个方向可以用这个表示: ↑
3,变量的值是从上往下写的。(注:这里的意思是,比如变量
char str[]的值是字符串'ABC',那么A在B上,B在C上,
至于A又是什么方向存储的我们就不讨论了,) 这个方向可以用这个表示: ↓
标签集:TAGS:
回复Comments() 点击Count()

回复Comments

{commentauthor}
{commentauthor}
{commenttime}
{commentnum}
{commentcontent}
作者:
{commentrecontent}