處理器棧幀佈局

處理器棧幀佈局

函數的調用會導致隱式的內存分配,棧幀就是在這一過程構造的。顯式的內存分配和釋放可以使用函數malloc(),calloc(),realloc(),new,free()delete等,這時候的分配的內存是位於堆上的。典型的棧幀佈局如下,可能在不同的操作系統上有不同的組織方式:

  • 函數參數(Function parameters).

  • 函數返回值(Function’s return address).

  • 棧幀指針(Frame pointer).

  • 異常處理幀(Exception Handler frame).

  • 局部變量(Locally declared variables).

  • Buffer.

  • 保存調用者的寄存器(Callee save registers).

Typical illustration of a stack layout during the function call

Figure 1 典型的函數調用過程中棧幀的佈局


從這張圖上可以很清晰的看出,如果發生buffer overflow,將會有可能覆蓋位於Buffer地址之上的其他變量,比如 局部變量,異常處理幀,棧幀指針,返回值,以及函數參數(爲什麼覆蓋的是Buffer之上的內存?比如我們在函數內部定義int i = 0;char a[4];這個時候i的地址是高地址,a的地址是低地址。在通過for循環給a賦值的時候是從a的低地址開始,向高地址內存單元賦值。如果for循環的次數超過4,就可能破環i的值。)

Windows/Intel爲例子,一般而已,棧上面的數據存儲按照以下方式進行:

  1. 在函數調用之前按照從右到左的順序把函數參數壓入棧中。

  2. 在x86體系中通過CALL指令將函數的返回地址壓入棧中,返回地址保存的是當前EIP寄存器的值。

  3. 接着,EBP中保存的指向前一個棧幀的指針被壓入棧中。

  4. 如果一個函數包含try/catch或者其他的異常處理結構,編譯器生成的異常處理信息將會保存到棧上。

  5. 接着就是局部定義的變量。

  6. 接着在buffer中分配需要臨時存放的數據

  7. 最後,保存函數調用者的寄存器,如果這些寄存器在接下來的函數調用過程中會被用到的話。比如 ESI, EDI, 以及EBX 。對於Linux/Intel體系, 這一步緊跟着第4步。

處理器的棧操作

對於函數調用棧來說有兩個寄存器是非常重要的,因爲他們保存着訪問內存數據的信息。在32位的系統中,這兩個寄存器是ESP,EBP。ESP保存着棧頂的地址。ESP寄存器中的值可以被直接或者間接的修改。

直接修改(Windows/Intel)

add esp, 0Ch

這條指令將導致棧的大小減少12個字節。

sub esp, 0Ch

這條指令會導致棧的大小增大12字節。這點可能很讓人混淆,實際上,ESP寄存器的值越大,棧的值越小,反之亦然。因爲棧是向下增長的。

間接修改:通過PUSH或者POP指令向棧中添加或者刪除數據元素。

push   ebp    ; 保存ebp,把它壓入棧中

pop    ebp    ; 恢復ebp,從棧中刪除

除了棧指針(指向棧頂,具有較低的地址),爲了方便還有另外的一個指針:FP(棧幀指針stack frame pointer),它指向棧幀中一個固定的內存地址。通過查看棧幀結構,我們知道局部變量可以通過ESP加上偏移引用來訪問。然而隨着數據的進棧和出棧,這個offset偏移值會產生變化。這就導致對於局部變量的訪問,變的不一致。所以很多的編譯器使用另外一個寄存器FB( Frame Pointer),用來訪問局部變量和參數。因爲局部變量和參數到FB的偏移值不會隨着PUSH和POP的操作而改變。在Intel中,EBP(Extended Base Pointer)寄存器充當的就是FB的角色。因爲棧上向下增長的,所以實際參數有相對於FB有正的偏移量,而局部變量有負的偏移量。讓我們看下面的例子:

    #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;
    }

上面的例子內存佈局如下:

Function call: The memory layout

Figure 2: 函數調用的內存佈局

EBP是一個指向棧底的靜態寄存器。棧底是一個固定的地址。更準確的說法是:EBP含有棧底的地址,作爲執行函數的便宜量。依賴於函數所執行的任務,內核會動態的調整棧的大小。每當一個新的函數被調用,舊的EBP就會被壓入棧中,然後把新的ESP賦值給EBP。當在新分配的棧中尋找局部變量時EBP中的值就變爲參考基地值。之前已經提到過,棧向下增長,大多數的計算機體系棧增長都採用這種方式。

當一個函數被調用的時候,第一件事情就是保存之前的EBP(在函數退出之後,複製保存的值到EIP寄存器)。緊接着把ESP的值複製到EBP中來創建一個新的棧幀指針。然後調整ESP來爲局部變量分配空間。以上是爲了函數調用在棧中所做的準備工作(procedure prolog),當函數調用結束,需要在棧中執行清理過程,這一個過程稱爲procedure epilogIntel 提供的 ENTERLEAVE 指令還有 Motorola 提供的LINKUNLINK執行,都可以高效的完成大部分函數調用的準備工作和函數退出的清理工作。之前已經講過,棧操作中兩個重要的指令是PUSH和POP,PUSH用於在棧頂壓入一個元素,POP,相反的作用,用於從棧頂移除一個元素。

其他的用於棧操作的指令如下表所列:

Instruction

Description

PUSH

減少棧指針的值,把源操作數壓入棧中

POP

取出棧頂元素的值到目的操作數制定的地址,然後增加棧指針的值。

PUSHAD

把普通寄存器中的內容壓入棧中

POPAD

取出棧中的值到一個普通寄存器

PUSHFD

EFLAGS寄存器中的內容壓入棧中

POPFD

從棧中取出四個字節到EFLAGS寄存器中

Windows操作系統上棧的做法

Microsoft Visual C++編譯器將所有傳入函數的參數擴展爲32位,即四個字節。函數返回值也被擴展爲四個四節,被保存在EAX寄存器中。如果返回值是8個字節,就會被保存在EDX:EAX寄存器對中。

如果返回值是更大的結構,將會把返回值的地址保存在EAX中。編譯器將自動生成prolog andepilog指令,用來保存和恢復ESI, EDI, EBX, 以及 EBP寄存器。

函數調用以及棧幀分配

讓我們通過一個例子,從一般函數調用的視角看看棧幀是如何構造和銷燬的。我們使用__cdecl的參數入棧方式,這些步驟靠Microsoft Visual C++ 6.0編譯器自動生成。儘管這些步驟不是所有的函數調用都會產生,因爲有的函數沒有參數,沒有局部變量。程序的代碼之前已經列出。

通過F11開始單步調試,調出Disassembly窗口。彙編代碼如下:

9:    int main(int argc, char *argv[])
10:   {
00401060   push        ebp                          ;將上一個棧幀的FB壓入當前棧中
00401061   mov         ebp,esp                      ;調整FB爲當前esp
00401063   sub         esp,40h                      ;分配buffer
00401066   push        ebx                          ;壓入ebx
00401067   push        esi                          ;壓入esi
00401068   push        edi                          ;壓入edi
00401069   lea         edi,[ebp-40h]                ;將edi的值賦爲FB爲起始偏移40H字節
0040106C   mov         ecx,10h                      ;
00401071   mov         eax,0CCCCCCCCh               ;
00401076   rep stos    dword ptr [edi]              ;將剛剛分配的40H字節初始化爲0xCCCCCCCCh
11:       MyFunc(7,'8');
00401078   push        38h                          ;把參數'8'壓入當前棧幀
0040107A   push        7                            ;把參數 7 壓入當前棧幀
0040107C   call        @ILT+5(MyFunc) (0040100a)    ;把返回地址00401081壓入當前棧幀,然後調用MyFunc(0040100a)
00401081   add         esp,8                        ;清除傳入參數
12:       return 0;
00401084   xor         eax,eax
13:   }
00401086   pop         edi
00401087   pop         esi
00401088   pop         ebx
00401089   add         esp,40h
0040108C   cmp         ebp,esp
0040108E   call        __chkesp (004010b0)
00401093   mov         esp,ebp
00401095   pop         ebp
00401096   ret

1:    #include <stdio.h>
2:    int MyFunc(int parameter1, char parameter2)
3:    {
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,48h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-48h]
0040102C   mov         ecx,12h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]
4:        int local1 = 9;
00401038   mov         dword ptr [ebp-4],9          ;複製局部參數到棧中
5:        char local2 = 'Z';
0040103F   mov         byte ptr [ebp-8],5Ah
6:        return 0;
00401043   xor         eax,eax
7:    }
00401045   pop         edi                          ;恢復edi
00401046   pop         esi                          ;恢復esi
00401047   pop         ebx                          ;恢復ebx
00401048   mov         esp,ebp                      ;恢復上一個棧幀的esp指針
0040104A   pop         ebp                          ;恢復上一個棧幀的ebp
0040104B   ret                                      ;回到返回地址00401081

下圖是以上代碼調試過程中的棧幀佈局。藍色和黃色部分分別代表一個完整的棧幀。



參考資料:

http://www.tenouk.com/Bufferoverflowc/Bufferoverflow2a.html



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章