函数调用-栈分析

时间:2021-03-06 03:39:01

From:http://www.cnblogs.com/killerlegend/p/3916937.html

Author:KillerLegend

Date:2014.8.16

这篇文章参考1996年Aleph One的一篇文章: http://www1.maths.leeds.ac.uk/~read/bofs.html,我只想说,原文的年龄几乎和我一样大的啊...额..一不小心暴露了年龄...1996年到现在按四舍五入差不多已有二十年...这篇文章在国外还是相当有名的,是第一篇基于缓冲区栈溢出攻击且系统的描述Exploit的文章(exploit这个词还真不好翻译,在OxfordDictory中的解释是: A software tool designed to take advantage of a flaw in a computer system, typically for malicious purposes such as installing malware,大概意思就是利用计算机系统的漏洞进行攻击的工具).只不过时间流逝,很多都失效了,不过这并不妨碍我们来学习一下系统栈..个人不才,没有完全看懂,但是收获是,对于系统栈有了一些基本的了解,跟随文章写下自己的理解,分享所得,后续会继续从事漏洞,渗透,破解,互联网攻防,系统安全等方面的研究,有这一方面爱好的伙伴,希望我们可以成为好朋友,以便交流,共同进步.OK,Let’s go..

操作环境:

Windows7 Ultimate 64bit + Cygwin v 1.7.28 + GCC v4.8 + G++ v7.6.1

首先来分析如下的代码,假设保存为main.cpp:

void function(int a, int b, int c) 
{
char buffer1[5];
char buffer2[10];
}

int main()
{
function(
1,2,3);
return 0;
}
我们来看一下对应的汇编代码,在Cygwin中输入:

gcc-c-Wa,-ahldn-gmain.cpp|more

函数调用-栈分析

另外也可以输入gcc –S main.cpp,然后输入cat main.s来查看纯汇编代码,或者使用gcc –c main.cpp,然后使用objdump –d main.cpp可以查看编译后的指令代码.

从汇编代码可以看对于function函数的调用代码为:

movl$3,8(%esp)

movl$2,4(%esp)

movl$1,(%esp)

call__Z8functioniii

第一行将字符常量3(AT&T汇编语法的字符常量前加字符$,寄存器地址用圆括号而不是方括号,寄存器前加%)移到esp+8所在的地址,第二行将2放入esp+4所在的地址,第三行将1放入esp所在的地址.最后一行是调用函数,这个命令将会使指令指针EIP指向栈帧的顶部,我们将这个保存的EIP指针称之为返回地址RET.然后看function内部的代码:

pushl%ebp

movl%esp,%ebp

subl$16,%esp

第一句保存原有ebp的值,也就是保存原来的栈帧指针FP(也就是调用函数的栈帧起始地址),第二句将esp的值放入到ebp中,即将当前的栈指针地址放入ebp,使其成为新的FP(Frame Pointer),我们将已经保存的FP称为SFP.然后将esp值减少16,这个是为局部变量分配空间.

OK,继续,我们需要记住,内存的地址只能是16大小的乘积.这里的局部变量一共占据有15个字节,因此分配了16个空间大小.

在脑子中始终记住,函数调用时,栈看起来像下面这样:

函数调用-栈分析

Intel构架中栈的生长方向是由高到低.压入参数,返回地址,栈帧指针,局部变量...

接下来看--------缓冲区溢出

缓冲区溢出是由于进入一个缓存区的数据超过了其所能处理的量而造成的.看例子:

#include <cstring>
void function(char *str)
{
char buffer[16];
strcpy(buffer,str);
}

int main()
{
char large_string[256];
int i;
for( i = 0; i < 255; i++)large_string[i] = 'A';
function(large_string);
return 0;
}

保存为main.cpp.然后g++ main.cpp –o main,编译后运行,出错如下:

函数调用-栈分析

可以看到Exception offeset为0x41414141.在终端窗口运行将会显示segmentation fault错误提示.

这是由于使用strcpy而不是strncpy造成的.因为strcpy没有边界检测,复制的字符串数超过了分配的空间.让我们看一看在调用函数的时候栈是什么样子的:

函数调用-栈分析

在这里发生了什么?为什么我们会得到一个segmentation fault?很简单,strcpy复制*str中的内容(larger_string[])到buffer[],直到遇见null字符为止.然而,我们可以看到buffer[]比*str要小得多.buffer[]是16bytes,而我们打算将256bytes的东西放到buffer[]里面去.这意味这在buffer之后的240个字节都将会被覆盖重写,包括sfp,ret,甚至是*str.由于A的ASCII码值为41,所以ret中包含的地址值就为0x41414141,这超出了进程的地址空间,当函数返回后,尝试读下一条指令时,出现了上述场景.

显然,一个缓冲区溢出允许我们改变函数的返回地址.因此,通过这种途径,我们可以改变应用程序的执行流程.回到第一个例子,然后回忆一下函数调用时栈中的情况:

函数调用-栈分析

原文提到通过修改代码如下让跳过x=1的语句:

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);
}
这个是无法修改的,即使你通过参数-fno-stack-protector[命令:gcc -o main main.c -fno-stack-protector]去掉GDB的反缓冲区溢出攻击机制也不行,这个让我研究了一下午,都没有搞定,上面的ret地址无法修改,即使你确实在程序中给的ret的值增加了,但是你使用printf命令打印值的时候,发现根本没有改变.我不知道这是不是系统内部本身的保护机制.不过这一下午没有白费,下面就让我们来研究一下栈:

为了方便研究,我给上面的两个数组赋予了初值,完整的C++程序如下:

#include <stdio.h>
void function(int a, int b, int c)
{
char buffer1[5]="ABCD";
char buffer2[10]="123456789";
}

int main()
{
int x;

x
= 0;
function(
1,2,3);
x
= 1;
printf(
"%d\n",x);
return 0;
}
使用命令: gcc -g main.cpp -o main编译该文件,然后用gdb载入,按照下图输入命令:

函数调用-栈分析

然后我们开始查看堆栈的值,输入如下命令:

(gdb) x/x $ebp

然后会看到ebp寄存器的值,这个时候ebp的值就指向函数function的栈帧,值如下:

0x28fef8: 0x0028ff28

另外注意我们传入的参数是1,2,3,buffer1和buffer2的值分别是ABCD和123456789,其ASCII值分别是4142434400,最后的零零是系统自动加上的,这个我想不用多说了.另一个是31323334353637383900.我之所以给它们赋予初值,是因为这样可以更好的帮助我们理解栈的构造,然后我们从偏离ebp下28地址处开始向上追溯,截图如下左边图形:

函数调用-栈分析

从ebp-28处开始,GDB会自动的将栈中的参数出栈显示(一定要记住,栈的生长方向是自高地址到底地址的,因此,出栈的时候,将是从低地址向高地址回溯),图中左侧的是存储单元地址,右侧是该存储单元的值,按照图中标志,1处顺着栈生长的方向就是函数function的栈帧(Stack Frame,SF),而1处黄色方块所圈起来的就是buffer2的值,也就是说,局部变量中buffer2是最后存入栈的,其前是buffer1,也就是2处的紫色方框,再往前我们来到3处,我们看到一个地址,这个地址就是我们刚刚提到的寄存器ebp的值,实际上这里保存的是调用函数的ebp地址,为什么这么说呢?我们可以让GDB继续往上走出栈参数,如下:

函数调用-栈分析

如上图所示,3处所在的紫色框就是调用function函数的函数的栈帧(以后我会用SF指代),2处就是栈帧的起始地址,所以被调用函数会一段空间保存调用函数的SF地址,以便被调用函数执行完毕后,返回到主函数继续执行剩余的部分.好了,继续回到上面那个图,看完3,我们继续看4,4又是一个地址,那么这个地址是什么呢?它实际上是函数执行完后的返回地址(也就是下一条指令的地址),这个可以通过汇编来很清楚的明白,如下图,我们只反汇编main函数:

函数调用-栈分析

显然如上图所示,红色方框是main函数中对函数function的调用,因此函数调用完应该返回到蓝色线所标志的地址处继续指令的执行,我们可以很明显的看到函数返回的地址是0x00401429,这和上面分析的4处说包含的值一模一样,说明还要调用函数的返回地址.然后我们继续往上走到5处,显然保存的是参数,再往上走实际上就进入另一个函数的栈帧了.因此,函数调用的大致可以描述如下:

函数调用-栈分析

我们上面说的很多地方设计到了栈帧,关于函数和栈帧比较详细的说明如下:

考虑到执行环境的许多未知数,函数经常设置一个栈帧,此栈帧允许访问函数的参数以及自动函数变量(就是我们平常什么关键字也不加的变量).栈帧的本质目的是让每一个子例程都可以独立地在其栈上执行,并且每一个子例程可以就像在栈顶执行一样.当一个函数被调用的时候,会在当前esp所指的位置创建一个新的栈帧.栈帧就像栈上的一个分区.来自以前函数中的所有项都会在这个分区(栈)中较高的位置并且不应该被修改.当前的每一个函数都有权访问(为这个函数所分配的)栈中的剩余部分(从栈帧到栈页的终止部分).当前的函数总是有权访问”栈顶”(指函数这个子例程所在的栈(分区)的栈顶),并且这样的函数并不需要考虑其他函数或者重新所使用的内存.结合上述描述我想到此你大概已经理解了函数的调用,对于ebp我们应该明确它应该指向函数的栈帧的头部,bp就是base pointer的缩写,从其意思也可以窥其一二.

这个说明来自wikibooks上,原文链接[ http://en.wikibooks.org/wiki/X86_Disassembly/Functions_and_Stack_Frames]

好了,这篇文章涉及的内容其实并不多,就是简单的说明了函数的简单调用以及栈的构造,很浅显,当然主要原因在于水平有限,不过,每天一个脚印,一步步向前走.文章如果有什么地方说的不恰当,请您指明!

另外关于函数栈的扩展阅读:

http://dirac.org/linux/gdb/02a-Memory_Layout_And_The_Stack.php