原文地址:http://www.tenouk.com/Bufferoverflowc/Bufferoverflow2a.html
栈帧布局
栈帧在函数调用时进行构建,以进行内存的隐式分配。内存可以显示的通过malloc(), calloc(), realloc(), new, free和delte在堆上进行申请和释放。不同的操作系统的栈帧布局可能不同,一个典型的栈帧布局如下所示:- 函数参数
- 函数返回地址
- 帧指针
- 错误处理帧
- 局部变量
- 栈缓冲区
- 被调函数保存的寄存器
栈帧的布局如下图所示:
从布局图中很容易可以看出如果发生栈缓冲区溢出,将会复写比缓冲区地址高的那些变量的值,包括局部变量, 异常处理帧, 栈帧指针, 返回地址和函数参数。下面我们仔细分析一下。
在windows/Intel平台上,当发生一个函数调用时,数据通过如下的方式存放在栈上:
- 在函数调用之前先将函数参数压栈,参数按照从右到左的顺序。
- 由x86的call指令将函数的返回地址压栈,这个返回地址就是EIP寄存器的值。
- 上一个栈帧的栈帧指针通过EBP的值压栈
- 如果函数包含了try/catch或者其他的异常处理结构(如SEH),编译器会在栈上放置异常处理所需要的信息。
- 栈上分配局部变量
- 为临时数据分配栈缓冲区
- 最后,被调函数保存ESI, EDI, EBX寄存器的值。 对于linux/intel平台,这一步在第4步之后
栈操作
在32位系统上,ESP和EBP两个寄存器对于通过栈来进行栈上数据的操作很重要,ESP(Extended Stack Pointer)保存栈顶的指针,ESP可以直接或者间接的方式进行修改。直接方式就是通过直接的指令来改变ESP的值,(windows/Intel)如下:
add esp, 0Ch该指令导致栈收缩12个字节,而
sub esp, 0Ch是栈增长12个字节。 (注意:ESP的值越大,栈的尺寸越小,反之亦然。因为栈的增长是向下增长).
间接的方式是通过Push和Pop来进行数据的压栈和出栈。如下:
push ebp ; Save ebp, put it on the stack
pop ebp ; Restore ebp, remove it from the stack
除了指向栈顶(低地址)的栈顶指针,有一个函数帧中指向固定地址的frame point会方便很多。在上面的栈布局中,局部变量可以通过他们和ESP的偏移来进行引用。但是,当向栈中push或者pop数据时,这些偏移会发生改变,所以这种方式的引用不一致。因此,许多编译器使用另外一个称为FramePoint(FP)的寄存器,通过相对于这个寄存器保存的指针,局部变量和参数所计算出来的偏移能够在对栈进行Push和Pop时保持不变。在intel的CPU里,EBP就是这样的一个寄存器。基于栈向下增长的方式,实际参数具有正向的便宜, 局部变量具有负向的偏移。分析下面的c程序
#include <stdio.h>
int MyFunc(int parameter1, char parameter2)
{
int local1 = 9;
char local2 = ‘Z’;
return 0;
}
int main(int argc,char *argv[])
{
MyFunc(7, ‘8’);
return 0;
}
最后的内存布局应该如下:
EBP寄存器是一个指向栈帧底部的静态寄存器。栈的底部地址固定,更准确的说EBP寄存器包含了用于计算当前执行函数偏移的栈底地址(翻译的不好)。栈的尺寸依赖于函数的功能,有内核运行时动态调整。每个新的函数被调用时,当前的EBP的值先被压栈,然后将ESP的值存入EBP作为新的栈帧基址,进行局部变量访问的参考地址。前面提过,栈是向下增长的。intel,motorola, SPARC和MIPS等处理器都是这种栈的增长方式。The stack pointer (ESP) last address on the stack not the next free available address after the top of the stack.
当一个函数被调用时应该先保存上一个EBP (so it can be restored by copying into the EIP at function exit later). 然后将当前的ESP复制给EBP作为栈帧指针,将ESP前进若干个保存局部变量的空间。这个操作代码成为procedure prolog. 函数结束时,执行栈清理的操作称为 procedure epilog. intel的ENTER和LEAVE指令, Motorola的LINK和UNLINK指令,能够高效的做这些准备和清理工作.前文说过,栈操作相关的2个重要的汇编指令是PUSH和POP。PUSH进行压栈操作,POP进行出栈操作。如下图所示:
其他栈操作相关的指令如下:
Instruction | Description |
PUSH | Decrements the stack pointer and then stores the source operand on the top of the stack. |
POP | Loads the value from the top of the stack to the location specified with the destination operand and then increments the stack pointer. |
PUSHAD | Pushes the contents of the general-purpose registers onto the stack. |
POPAD | Pops doublewords from the stack into the general-purpose registers. |
PUSHFD | Pushes the contents of the EFLAGS register onto the stack. |
POPFD | Pops doublewords from the stack into the EFLAGS register |
SOME WINDOWS OS POINT OF VIEW
Microsoft Visual C++编译器会将所有的函数参数变为32位(4个字节)宽。返回值同样定位32位(4字节),存放在EAX寄存器中,除了8个字节大小结构通过EDX:EAX返回。
大的数据结构返回时,EAX中保存结构的指针。由编译器来生成函数栈的准备和清理操作代码,包括保存和还原ESI, EDI, EBX和EBP寄存器的值。
函数调用和栈帧分析
先分析一个例子来发现一个栈帧是怎么创建和销毁的。我们使用_cdecl约定,栈帧的创建和销毁都是由MSVC6.0自动实现的。_cdecl由编译器默认配置,下面的C程序代码运行在debug模式下。
OS: Windows 2000 server
Compiler: Microsoft Visual C++ 6.0
// winprocess.cpp
#include <stdio.h>
int MyFunc(int parameter1, char parameter2)
{
int local1 = 9;
char local2 = 'Z';
return 0;
}
int main(int argc, char *argv[])
{
MyFunc(7,'8');
return 0;
}
然后调试这个程序,生成汇编代码。调试步骤如下:
Debug菜单→Start (or F5) ) 如图:
然后使用Step Into(F11) , 这样可以进入函数执行的内部:
单步调试时,查看生成的汇编代码:
反汇编出来的代码摘抄如下, 一些汇编代码的行加了解释的注释:
--- e:\test\testproga\winprocess.cpp ----------------------------------
10:
11: int main(int argc, char *argv[])
12: {
00401060 push ebp
00401061 mov ebp, esp
00401063 sub esp, 40h
00401066 push ebx
00401067 push esi
00401068 push edi
00401069 lea edi, [ebp-40h]
0040106C mov ecx, 10h
00401071 mov eax, 0CCCCCCCCh
00401076 rep stos dword ptr [edi]
13: MyFunc(7,'8');
------------------jump to MyFunc()---------------------------------------
00401078 push 38h;character 8 is pushed on the stack at [ebp+12]
0040107A push 7 ;integer 7 is pushed on the stack at [ebp+8]
0040107C call @ILT+5(MyFunc) (0040100a);call MyFunc(), return
;address:00401081
;is pushed on the stack
;at [ebp+4]
-----------------------------------------------------------------------
@ILT+5(?MyFunc@@YAHHD@Z): ;function decorated name, Visual C++ .Net
0040100A jmp MyFunc (00401020)
-----------------------------------------------------------------------
--- e:\test\testproga\testproga.cpp ----------------------------------
1: //testproga.cpp
2: #include <stdio.h>
3:
4: int MyFunc(int parameter1, char parameter2)
5: {
00401020 push ebp ;save the previous frame pointer at [ebp+0]
00401021 mov ebp, esp ;the esp (top of the stack) becomes new
;ebp. The esp and ebp now are pointing to the same address.
00401023 sub esp, 48h ;subtract 72 bytes for local variables & buffer,
;where is the esp? [ebp-72]
00401026 push ebx ;save, push ebx register, [ebp-76]
00401027 push esi ;save, push esi register, [ebp-80]
00401028 push edi ;save, push edi register, [ebp-84]
00401029 lea edi, [ebp-48h] ;using the edi register…
0040102C mov ecx, 12h
00401031 mov eax, 0CCCCCCCCh
00401036 rep stos dword ptr [edi]
6: int local1 = 9;
00401038 mov dword ptr [ebp-4], 9 ;move the local variable, integer 9
;by pointer at [ebp-4]
7: char local2 = 'Z';
0040103F mov byte ptr [ebp-8], 5Ah ;move local variable, character Z
;by pointer at [ebp-8], no buffer usage in this
;program so can start dismantling the stack
8: return 0;
00401043 xor eax, eax ;clear eax register, no return data
9: }
00401045 pop edi ;restore, pop edi register, [ebp-84]
00401046 pop esi ;restore, pop esi register, [ebp-80]
00401047 pop ebx ;restore, pop ebx register, [ebp-76]
00401048 mov esp, ebp ;move ebp into esp, [ebp+0]. At this moment
;the esp and ebp are pointing at the same address
0040104A pop ebp ;then pop the saved ebp, [ebp+0] so the ebp is back
;pointing at the previous stack frame
0040104B ret ;load the saved eip, the return address: 00401081
;into the eip and start executing the instruction,
;the address is [ebp+4]
-----------------------------back to main()--------------------------------------------
00401081 add esp, 8 ;clear the parameters, 8 bytes for integer 7 and
;character 8 at [ebp+8] and [ebp+12]
;after this cleanup by the caller, main(), the
;MyFunc()’s stack is totally dismantled.
14: return 0;
00401084 xor eax, eax ;clear eax register
15: }
00401086 pop edi
00401087 pop esi
00401088 pop ebx
00401089 add esp, 40h
0040108C cmp ebp, esp
0040108E call __chkesp (004010b0) ; checking the esp corruption?
00401093 mov esp, ebp ; dismantling the stack
00401095 pop ebp
00401096 ret
-
函数参数按从右向左进行压栈
参数按照从右到左的顺序,依次压栈。函数调用代码必须记录多少个字节的代码被压栈,以便函数退出时进行栈清理
00401078 push 38h;character 8 is pushed on the stack at [ebp+12]
0040107A push 7 ;integer 7 is pushed on the stack at [ebp+8]
-
调用函数Call
处理器将EIP寄存器的值压栈,EIP指向了函数返回地址。这个操作完成后,调用函数失去程序控制权,由被调函数获得。这一步不改变EBP寄存器的值
0040107C call @ILT+5(MyFunc) (0040100a) ;call MyFunc(), return address:00401081, is
;pushed on the stack at [ebp+4]
-
保存和更新 EBP.
被调函数执行,需要一个EBP指向的新的函数栈帧。这个需要保存当前的EBP(上一个函数栈帧),将其压栈, 将ESP的值付给EBP。
00401020 push ebp ;save the previous frame pointer at [ebp+0]
00401021 mov ebp, esp ;the esp (top of the stack) becomes new ebp.
;The esp and ebp now are pointing to the same address.
一旦EBP改变,我们就可以直接通过 [ebp + 8], [ebp +12]这种方式来对函数参数进行读取。此时[ebp+0]是上一个栈帧的栈基地址,[ebp+4]是老的EIP的值,也就是函数的返回地址。
-
给局部变量和缓冲区分配空间
通过简单对ESP寄存器进行减法运算,就可以留出需要的空间,空间一般都是4个字节对齐的(32位系统)
00401023 sub esp, 48h;subtract 72 bytes for local variables & buffer,
;where is the esp? [ebp-72]
-
保存处理器的寄存器变量,以便程序退出时可以恢复
如果函数过程中用到处理器的寄存器,它必须保存寄存器中的旧值,以防止破坏依赖于这些寄存器的被调函数或者其他程序。每一个用到的寄存器都进行一次压栈操作,编译器会记住该操作,以便必要时还原这些值
00401026 push ebx ;save, push ebx register, [ebp-76]
00401027 push esi ;save, push esi register, [ebp-80]
00401028 push edi ;save, push edi register, [ebp-84]
-
将局部变量压栈
现在,局部变量被放置在EBP为基址,ESP位栈顶的栈空间上。习惯上EBP作为栈上数据计算偏移量的参考基址,这个就意味着[ebp-4]指向第一个局部变量。
6: int local1 = 9;
00401038 mov dword ptr [ebp-4], 9 ;move the local variable, integer
; 9 by pointer at [ebp-4]
7: char local2 = 'Z';
0040103F mov byte ptr [ebp-8], 5Ah ;move local variable character Z by
; pointer at [ebp-8],no buffer usage in
; this program so can start dismantling the stack
-
执行函数任务.
到此,函数栈帧已经正确建立,如下图。所有的参数和局部变量都可以通过EBP来进行定位。我们的例子程序什么都不干,所以可以开始清理函数栈了。
Figure 8: Stack frame setup.
这个函数内部可以随便使用ebx, esi和edi这几个寄存器,我们在程序开始时通过压栈对这几个寄存器的值进行了保存。 但是函数运行过程中不应该使用EBP。
-
还原保存的寄存器值.
函数的任务代码执行完毕后,要讲每一个保存起来的寄存器值进行以相反的顺序出栈还原。如果保存和还原阶段不一致,栈会被破坏。
00401045 pop edi ;restore, pop edi register, [ebp-84]
00401046 pop esi ;restore, pop esi register, [ebp-80]
00401047 pop ebx ;restore, pop ebx register, [ebp-76]
-
还原上I个栈帧基址EBP.
函数入口所做的第一件事就是保存调用者的EBP,现在可以还原它了, 我们可以丢弃掉全部的函数局部栈然后将保存的EBP还原,恢复被调的函数栈。
00401048 mov esp, ebp;move ebp into esp, [ebp+0]. At this moment
;the esp and ebp are pointing at the same address
0040104A pop ebp ;then pop the saved ebp, [ebp+0] so the ebp is
;back pointing at the previous stack frame
-
返回到调用函数.
这是函数调用的最后一步,RET指令会将老的EIP(返回地址)弹出,并且jump到该地址运行。这时程序控制权寄存器就返回给了调用者。只有栈帧指针EBP和指令EIP被函数返回修改。.
0040104B ret ;load the saved eip, the return address: 00401081
;into the eip and start executing the instruction, the address is [ebp+4]
-
清理压入栈中的参数.
在_cdecl调用预定中, 调用者来负责清理压入栈中函数参数。 这个操作可以通过POP操作或者对栈顶指针ESP进行加上参数块大小的尺寸值直接清理。
00401081 add esp, 8;clear the parameters, 8 bytes for integer
; 7 and character 8 at [ebp+8] and [ebp+12]
; after this cleanup by the caller, main(),
; the MyFunc()’s stack is totally dismantled.
从汇编代码可以看出当进行栈操作时,必须对称的压入和弹出相同过的字节数。之前讨论过栈在构造前后要保持平衡。显然,如果函数退出时栈没处理好,程序可能会在错误的地方执行,导致程序崩溃。大多数情况下,如果你压栈了多少字节数据,要保证弹出对应字节数的数据。
从汇编代码看保存寄存器的状态
用汇编代码写32位windows程序时,为了让程序和windows系统和API函数按照可预期的方式执行, 有一个寄存器的使用约定。 x86处理器的寄存器是被每个进程共享的有限资源,所以可靠的使用它们是保证程序稳定运行的基础。
前文说过,一个x86处理器由8个常规寄存器组成,分别是EAX, EBX, ECX, EDX, ESI, EDI, ESP和EBP。其中ESP和EBP主要用在进入和退出函数时,一般不做他用。所以只有剩下的6个寄存器应用程序可以使用。
...
push ebx
push esi
push edi
; here should be codes that uses
; the EBX, ESI and EDI
;
pop edi
pop esi
pop ebx
ret
当你调用一个windows API函数时, 可以假定函数自己会保存EBX,ESI和EDI的值,而EAX,ECX和EDX的值函数不会保存,如果我们需要用到这几个寄存器的值,需要自己手动的保存这几个寄存器。汇编角度来看,如果你的代码需要一个api调用,你可以使用EBX,ESI和EDI这几个寄存器,将它们作为计数器比较高效,它们在API调用内部被保留值,函数返回时能够还原回原来的值。这可以减少在循环里保存和还原寄存器值的负担。
__stdcall
__stdcall调用约定主要用在windowsAPI中, 比_cdecl更紧凑。 和_cdecl的不同主要在于一些给定的函数都是硬编码的参数集合,这个集合不会随着调用代码的不同而改变。 而C中的printf函数,是一个_cdecl的调用约定,他随着函数调用代码的不同而有不同的参数。
__stdcall中的函数参数块的大小是固定的,在栈清理这些函数参数的工作可以从调用函数(_cdecl)来负责,改为由被调函数来负责。这样做的优势在于:
-
生成代码更少, 清理函数参数的栈操作只在被调函数中出现一次, 而不会再每个函数调用时都出现一次。虽然一个函数调用只有不多的代码,但是如果函数调用多次的话,累加下来就能省下不少代码。 而且更少的代码运行速度更快.
-
参数的个数编译时可知,如果调用函数时给出了错误的函数参数会导致程序错误。但是这个问题可以由编译器在编译时克服,将函数的参数字节数编码到内部的函数名称中,那么如果使用错误的参数(尺寸错误)调用函数会产生一个编译错误。
THE GCC AND C CALLING CONVENTION - STANDARD STACK FRAME
传给C函数的参数按从右向左的顺序入栈,然后进行函数调用。被调函数做的第一件事就是保存EBP寄存器的值,拷贝ESP给EBP。此时创建了一个新的称为C stack frame的数据结构,简化的步骤示例如下:
Steps | 32-bit code/platform |
Create standard stack frame, allocate 32 bytes for local variables and buffer, save registers. | push ebp mov ebp, esp sub esp, 0x20 push edi push esi ... |
Restore registers, destroy stack frame, and return. | ... pop esi pop edi mov esp, ebp pop ebp ret |
Size of slots in stack frame, that is the stack width. | 32 bits |
Location of stack frame slots. | ... [ebp + 12] [ebp + 8] [ebp + 4] [ebp + 0] [ebp – 4] ... |
如果函数参数占用字节数比一个栈槽(stack slot)的大小多,它将会占据多个栈槽。比如一个64位的longlong值或者double, 32位程序上会占据2个栈槽,16位程序会占据4个栈槽。然后通过和EBP的正向偏移来进行函数参数访问,负向偏移用来进行局部参数的访问。前一个EBP的值存放在[ebp + 0]. 函数返回值存放在[ebp + 4].
GCC AND C calling convention – 返回值
一个C函数会将他的返回值保存在一个或者多个寄存器中。总结如下:
Size | 32-bit code/platform |
8-bit return value | AL |
16-bit return value | AX |
32-bit return value | EAX |
64-bit return value | EDX:EAX |
128-bit return value | hidden pointer |
GCC and C calling convention - 寄存器保存
GCC期望函数保存下面的寄存器:
EBX, EDI, ESI, EBP, DS, ES, SS你不需要保存下面的寄存器:
EAX, ECX, EDX, FS, GS, EFLAGS, floating point registers
在一些操作系统中,FS和GS段寄存器可能被用来保存线程局部存储的指针, 所以如果你需要使用它们,必须要先保存。