在程序运行中,栈主要用来保存局部变量,函数参数,函数调用的返回地址以及栈底。以x86为例,与栈关系比较大的几个寄存器主要是:
ebp:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部
esp:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶
eip:指令寄存器(extended instruction pointer),其内存放着一个指针,该指针永远指向下一条等待执行的指令地址
在函数的一层层调用过程中每个函数都会有自己的一段栈,一般把它叫做函数的栈帧。每个函数的栈帧都保存了自己的局部变量,自己栈帧底的值,返回地址以及上一次函数的参数。这样文字描述比较抽象,下面用一个例子和图表来说明。假设程序中函数的调用关系是:funcA调用funcB,funcB调用funcC,funcA->funcB->funcC,用伪代码来表示就是:
funcC()
{
.....
}
funcB()
{
...
funcC();
...
}
funcA()
{
...
funcB();
...
}
那么这个调用关系的函数栈帧就可以用下面这个图来大致描述:
从这个图上可以看到,首先栈是从高地址往低地址延展,所以funcA在上面高地址处,funcC在低地址处。每个函数栈帧的结构都差不多,这里主要就是要说明一些图片里面的:返回地址,保存的ebp。返回地址指的是被调用函数的返回地址,保存的ebp是指自己的栈帧底部的值。已funcB为例,返回地址就是funcC返回到funcB的地址,保存的ebp就是funcB的栈帧底的值。
程序在正常运行中,就是通过这样的方式保存了每个函数的现场,当返回的时候就可以恢复如初。如果程序运行过程中出现错误导致crash,我们也可以使用gdb工具通过分析coredump文件,查看函数调用关系来确定问题出现的范围。但是如果函数的栈帧被破坏掉了,就是返回地址核保存的ebp值出错,这样gdb是没办法获取函数的调用关系,也就无法确定问题的大致范围,会造成问题难以处理。不过如果好好的理解上面这个函数栈帧图后,还是可以通过自己手动去分析。下面具体看下分析的过程。
下面这个是测试用的源码:
char names[] = "book cat dog building vegetable curry";
void func(void)
{
char buf[5];
strcpy(buf,names);
}
int main(void)
{
func();
return 1;
}
这个函数是使用了strcpy的一个漏洞,通过names里面的值把函数的栈帧全部给填充了,造成栈帧破坏。
下面先编译这个程序:
gcc -g -fno-stack-protector -o stack_test stack_test.c #这里关闭了栈保护,来模拟堆栈完全破坏的情况
运行程序后,会coredump,产生core文件,使用gdb加载coredump文件和可执行文件。可以看到,栈被破坏都无法显示调用关系了,所以也就无法确定问题出现的情况。
看下寄存器的信息
可以看到,ebp和esp的值都不是太对,按道理ebp应该和esp不会差太多,eip表示的是代码执行地址
按照x86 ELF文件的布局,正常应该在0x08040000附近
根据函数调用栈的关系,可以知道调用函数的时候会将ebp和返回地址压栈,函数返回的时候,会先将ebp弹出,然后弹出返回地址给eip,eip就是pc指针,cpu就会跳转到eip保存的地址里面去。这里正是因为eip的值不正确,导致程序异常退出了。我们知道,栈的方向是从高地址向低地址方向生长,而数据是从低地址向高地址,所以栈被破坏应该是从某个地址往高地址方向,也就是外层函数的栈被破坏了,但是正在执行的这个函数,有部分栈并没有被破坏,是有可能寻找到一些信息的。
将esp前面的一段内存输出,看看情况可以看到这段内存里面,按照地址从高到低,保存了eip和ebp的值,同时可以看到红框的两个地址,很明显是代码的执行地址(0x0804xxxx),可以看下这两个地址的代码是什么
先看看0x0804a040
可以看到,没有反汇编出函数名
下面看看0x08048422地址
可以看到,反汇编出函数名为:func,然后就可以定位到问题,应该就在func函数里面。