栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构
从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里 … 。
实现上有硬件方式和软件方式(有些体系不支持硬件栈)
缓冲区溢出攻击主要是利用栈帧的构成机制,利用疏忽的程序破坏栈帧从而使程序转移到攻击者期望或设定的代码上。
/*******************************************************************************************/
详细分析过程调用的相关操作和汇编指令的作用。
还是以代码来分析,非常简单的c程序,不过对于我们所关注的问题,已经足以说明问题了:
1 int f(int *a, int *b) 2 { 3 int c; 4 c = *a + *b; 5 6 return c; 7 } 8 9 int main(void) 10 { 11 int a, b; 12 13 a = 1, b = 2; 14 f(&a, &b); 15 16 return 0; 17 }
在gcc和vc中都可以查看汇编代码的
gcc中生成汇编代码的命令如下:
gcc -E test.c -o test.i
gcc -S test.i -o test.s
然后可以查看test.s
这个程序生成的汇编代码如下(省略了不相关的部分,专注于两个函数的函数体部分):
_f:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl 8(%ebp), %edx
movl 12(%ebp), %eax
movl (%eax), %eax
addl (%edx), %eax
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
leave
ret
_main:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
andl $-16, %esp
movl $0, %eax
movl %eax, -12(%ebp)
movl -12(%ebp), %eax
call __alloca
call ___main
movl $1, -4(%ebp)
movl $2, -8(%ebp)
leal -8(%ebp), %eax
movl %eax, 4(%esp)
leal -4(%ebp), %eax
movl %eax, (%esp)
call _f
movl $0, %eax
leave
ret
用几幅图来说明问题:
1)函数的建立部分:
任何一个函数在开始调用之前都要建立起函数体的栈帧(stack frame)结构,使得ebp作为帧指针(frame pointer),而esp作为栈指针(stack pointer),需要注意的是esp是移动的,因为esp指针保存的是栈顶元素的地址,压栈或者退栈操作都将影响到esp指针的值,而ebp则是不动的,因此关于过程的许多信息比如局部变量的位置等等都是相对于ebp来给出的,ebp和esp这两个指针是如此的重要以至于如果在过程中被破坏了,那么整个过程的栈帧结构也就破坏了,结果不堪设想。涉及到函数体建立的汇编代码是:
pushl %ebp ;保存上一个函数的ebp
movl %esp, %ebp ;使ebp成为目前这个过程的栈帧结构的帧指针
subl $24, %esp ;为过程的栈帧结构分配空间,这样ebp和esp之间的空间就
;可以容纳本过程的局部变量和调用下一个函数
;时需要调用的参数了,注意这个空间的大小可能还要考虑到对齐等因素。
执行完上面三个指令之后,函数的栈帧结构大致如图:
_____ebp(main) 地址高位
| |
| ........ |
| ........ |ebp和esp相差24个字节
| |____esp(main) 地址低位
2)函数体的主体部分:
具体每个函数主体部分可能有较大的差别,不过一般的,函数体的局部变量的都是放在紧跟着ebp的地址空间里,在我们的例子里在main函数中有这样的指令:
movl $1, -4(%ebp) ;-4(%ebp)就是a的所在
movl $2, -8(%ebp) ;-8(%ebp)就是b的所在
因此,此时的main函数的栈帧结构大致如图:
_____ebp(main) 地址高位
|这个空间属于a|
| a |____a的地址在ebp - 4处
|这个空间属于b|
| b |____b的地址在ebp - 8处
| ........ |
| ........ |ebp和esp相差24个字节
| |____esp(main) 地址低位
需要注意的是图中a和b指针的指向,需要提醒的是读取数据的时候是从低地址开始读取的,因此当我们说"a的地址是ebp-4"的时候意味着ebp-4到ebp这32位的地址空间存放的是a的数据,而不是从ebp-4到ebp-8,也就是说存储数据是从低到高的,而给出地址的时候都是给的最低位的地址。
3)准备调用f函数时的参数准备:
在一个函数的栈帧结构中除了存放有本过程的局部变量之外,在栈帧的最低的位置还要存放着这个函数将要调用的函数要用到的参数,如在我们的例子中main函数要调用函数f要用到&a, &b,那么相关的代码是:
leal -8(%ebp), %eax ;把b的地址送入eax寄存器中
movl %eax, 4(%esp) ;再通过eax寄存器把b的地址存入esp + 4处
leal -4(%ebp), %eax ;把a的地址送入eax寄存器中
movl %eax, (%esp) ;再通过eax寄存器把a的地址存入esp处
这样之后,main函数的栈帧结构大致如图所示:
_____ebp(main) 地址高位
|这个空间属于a |
| a |____a的地址在ebp - 4处
|这个空间属于b |
| b |____b的地址在ebp - 8处
| ........ |
| ........ |ebp和esp相差24个字节
| |
| &b的地址 |____esp(main) + 4
| |
| &a的地址 |____esp(main) 地址低位
这就是一个比较完整的main函数的栈帧结构的示意图了,可以看出在最上面的是函数的局部变量,而在最下面的是为被调用函数准备好的参数,不同的机器,操作系统压栈顺序也是不一样的,在我的机子上调用f(&a, &b)的时候压栈是从右到左的,也就是先压入b再压入a。
再说一说lea这个指令,这个指令并不真正的访问存储器,而是产生一个有效地址,假设在这里ebp为0x12345678,那么执行leal -8(%ebp), %eax之后eax的数据就是0x12345670了,这个指令非常有用,经常用于生成指针(指针其实就是地址嘛),还可以用于访问数组的元素,比如说我们要访问数组E[i],假设E的起始地址存放在edx中,而索引也就是i存放在ecx中,再假设这个数组是int类型的,也就是4个字节大小的,那么访问E[i]并且把它的指放入eax就相当于:
movl (%edx, %ecx, 4), %eax
注意这时eax中的是数据,类型是int类型的,如果要得到E[i]的地址,那么我们可以:
leal (%edx, %ecx, 4), %eax
4)执行call指令后的栈帧结构:
call指令相当于下面的两个操作,首先把调用函数中在执行完函数调用后的下一条指令的地址压入栈,在这里其实就是相当于把指令movl $0,%eax的地址压入栈中,然后修改eip指针使它指向被调用函数的起始处(我们知道eip指针存放的是下一条指令的执行地址),此时的栈帧结构
大致如图:
_____ebp(main) 地址高位
|这个空间属于a |
| a |____a的地址在ebp - 4处
|这个空间属于b |
| b |____b的地址在ebp - 8处
| ........ |
| ........ |ebp和esp相差24个字节
| |
| &b的地址 |____esp(main) + 4
| |
| &a的地址 |____esp(main) 地址低位
|main函数中下 |
|一条指令的地址|____esp(此时esp指针已经不再指向main函数的栈帧结构了)
同时,可以看到的是push操作的相当于下面的两条指令,比如说pushl %ebp,就相当于:
subl $4, %esp
movl %ebp, (%esp)
效果都是相同的,不同的是push指令的机器码要比这两个指令要简单的多。
还需要注意的是在访问内存时的两种不同操作,比如:
movl $4, %esp
相当简单,直接把4送进esp就是了。
而
movl $4, (%esp)
就复杂一点,首先得到esp中的值,然后把这个值作为地址,然后把4送入这个地址。
简而言之,没有加括号的时候,寄存器相当于普通的整形变量,而加了括号以后寄存器就相当于指针了,存放着变量的地址。
5)进入f函数以后的栈帧分布情况:
同样的,在进入f函数的时候也要建立起函数的栈帧结构,同样要调用这样的三条指令:
pushl %ebp ;保存上一个函数的ebp
movl %esp, %ebp ;使ebp成为目前这个过程的栈帧结构的帧指针
subl $4, %esp ;为过程的栈帧结构分配空间,
;这样ebp和esp之间的空间就可以容纳本过程的局部变量和调用下一个函数
;时需要调用的参数了,注意这个空间的大小可能还要考虑到对齐等因素。
这时的栈帧结构大致如图:
_____ebp(main) 地址高位
|这个空间属于a |
| a |____a的地址在ebp - 4处
|这个空间属于b |
| b |____b的地址在ebp - 8处
| ........ |
| ........ |ebp和esp相差24个字节
| |
| &b的地址 |____esp + 4
| |
| &a的地址 |____esp(main)
|main函数中下 |
|一条指令的地址 |____ebp + 4(f)
|main函数中 |
|ebp保存在这里 |____ebp(f) 地址低位
注意,首先执行pushl %ebp指令不仅把main函数的ebp栈帧保存在栈中,而且还使得esp指针减少了4,在执行movl %esp, %ebp完后,ebp和esp指向同一个位置了,因此这是ebp处保存的就是main函数的ebp指针的值,而ebp + 4的地方存放的就是main函数中下一条指令的地址了,我们前面说过过程中的局部变量是紧跟着ebp指针的,一旦发生溢出,存储的数据往地址高位走就会覆盖这两个关键的值,如果程序修改指令的地址让它指向一段有害的代码,或者修改ebp指针使得返回的时候到一个有害的函数处,那么后果是不堪设想的--这就是所谓的"缓冲区溢出"。
6)退出函数时的恢复堆栈准备:
一般的,在函数返回的时候都有如下的两条指令:
leave
ret
逐条来解释,首先leave指令相当于以下的两条代码:
movl %ebp, %esp ;恢复esp指针
popl %ebp ;恢复ebp指针
注意看5)的示意图,在执行完movl %ebp, %esp之后,ebp和esp指向同一个位置,前面说过ebp处保存的时main函数栈帧结构的ebp指针的值,再执行popl %ebp的时候,就可以顺利的把调用函数(这里时main函数)的ebp指针恢复了,由此再次强调在函数运行的过程中切记不可修改ebp和esp指针,否则结果是不可预料的。
同时也可以看出pop指令的相当于下面的两条指令,如popl %ebp相当于
movl (%esp), %ebp
addl $4, %esp
执行完leave指令之后,也就是在执行完popl %ebp后,上面说了esp指针已经加上4了,也就是说esp处存放的是main函数中下一条指令的地址了,执行ret指令就相当于popl %eip,也就是把这个地址送入eip指针中(实际并不能这样做)。
上面就是一个函数从建立到调用其它函数再返回的具体分析了,说的应该够清楚的了
这些知识最典型的应用可参见博文缓冲区溢出攻击试验(bufbomb.c)