C語言函式呼叫棧

husterzxh發表於2022-05-14

C語言函式呼叫棧

棧溢位(stack overflow)是最常見的二進位制漏洞,在介紹棧溢位之前,我們首先需要了解函式呼叫棧。

函式呼叫棧是一塊連續的用來儲存函式執行狀態的記憶體區域,呼叫函式(caller)和被呼叫函式(callee)根據呼叫關係堆疊起來。棧在記憶體區域中從高地址向低地址生長。 每個函式在棧上都有自己的棧幀,用來存放區域性變數、函式引數等資訊。當caller呼叫callee時,callee對應的棧幀就會被開闢,當呼叫結束返回caller時,callee對應的棧幀就會被銷燬。

函式呼叫棧生長

下圖展示了棧幀的結構。在32位程式中,暫存器ebp指向棧幀的底部,用來儲存當前棧幀的基址,在函式執行過程中不變,可以用來索引函式引數和區域性變數的位置。暫存器esp指向棧幀的頂部,當棧生長時,esp的值減少(向低地址生長)。暫存器eip用於儲存下一條指令的地址。在64位程式中,三個暫存器分別為rbp、rsp和rip。

棧幀結構

當函式呼叫發生時,首先需要儲存caller的狀態,以便函式呼叫結束後進行恢復,然後建立callee的狀態。具體來說:

  1. 如果是32位程式,將傳給callee的引數按照逆序依次壓入caller的棧幀中;如果是64位程式,將傳給callee的引數按照逆序依次傳入暫存器r9、r8、rcx、rdx、rsi、rdi,如果引數的個數超過了6個,將其餘引數壓入caller的棧幀中。如果callee不需要引數,則這一步驟省略。

  2. 將caller呼叫callee後的下一條指令的地址壓入棧中,作為callee的返回地址,這樣,當函式返回後可以正常執行接下來的指令。

  3. 將當前ebp暫存器的值壓入棧中,這是caller棧幀的基址,將ebp更新為當前的esp。

  4. 將callee的區域性變數壓入棧中。

  5. 函式呼叫結束後,就是上面過程的逆過程,callee棧幀中資料會出棧,恢復到caller棧幀狀態。

上面的第1步由caller完成,第2步在caller執行call指令時完成,第3、4步由callee完成。

下面看一個具體的例子,callerStack.c程式碼如下:

// callerStack.c
// C語言函式呼叫棧  

# include <stdio.h>

int func(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8)
{
    int loc1 = arg1 + 1;
    int loc2 = arg8 + 8;
    return loc1 + loc2;
}

int main(void)
{
    int ret = func(1, 2, 3, 4, 5, 6, 7, 8);
    return 0;
}

用命令gcc -m32 callerStack.c -o callerStack32生成32位程式,用gdb反彙編,得到的結果如下:

(這裡額外說一下,如果是在64位機器上執行上述命令可能會報錯: fatal error: bits/libc-header-start.h: No such file or directory #include <bits/libc-header-start.h>,需要安裝multilib庫:sudo apt install gcc-multilib

   0x565561dd <main>       endbr32 
   0x565561e1 <main+4>     push   ebp    ; 將ebp入棧,儲存caller的基址,esp -= 4
   0x565561e2 <main+5>     mov    ebp, esp    ; 將ebp更新為當前的esp
   0x565561e4 <main+7>     sub    esp, 0x10    ; esp -= 0x10
   0x565561e7 <main+10>    call   __x86.get_pc_thunk.ax                    <__x86.get_pc_thunk.ax>    ; 沒看懂
 
   0x565561ec <main+15>    add    eax, 0x2df0    ; 沒看懂
   0x565561f1 <main+20>    push   8    ; 引數入棧,esp -= 4
   0x565561f3 <main+22>    push   7
   0x565561f5 <main+24>    push   6
   0x565561f7 <main+26>    push   5
   0x565561f9 <main+28>    push   4
   0x565561fb <main+30>    push   3
   0x565561fd <main+32>    push   2
   0x565561ff <main+34>    push   1
   0x56556201 <main+36>    call   func                    <func>    ; 呼叫func,返回地址入棧
 
   0x56556206 <main+41>    add    esp, 0x20    ; 恢復棧頂
   0x56556209 <main+44>    mov    dword ptr [ebp - 4], eax    ; eax存放func的返回值
   0x5655620c <main+47>    mov    eax, 0
   0x56556211 <main+52>    leave  
   0x56556212 <main+53>    ret 


   0x565561ad <func>       endbr32 
   0x565561b1 <func+4>     push   ebp    ; 將ebp入棧,儲存caller的基址,esp -= 4
   0x565561b2 <func+5>     mov    ebp, esp    ; ebp更新為當前的esp
   0x565561b4 <func+7>     sub    esp, 0x10    ; esp -= 0x10
   0x565561b7 <func+10>    call   __x86.get_pc_thunk.ax                    <__x86.get_pc_thunk.ax>    ; 沒看懂
 
   0x565561bc <func+15>    add    eax, 0x2e20                   <func+15>    ; 沒看懂
   0x565561c1 <func+20>    mov    eax, dword ptr [ebp + 8]    ; 取出arg1(值為1),放入eax中
   0x565561c4 <func+23>    add    eax, 1    ; arg1 + 1
   0x565561c7 <func+26>    mov    dword ptr [ebp - 8], eax    ; 計算結果(區域性變數loc1)放入棧中
   0x565561ca <func+29>    mov    eax, dword ptr [ebp + 0x24]    ; 取出arg8(值為8),放入eax中
   0x565561cd <func+32>    add    eax, 8    ; arg8 + 8
   0x565561d0 <func+35>    mov    dword ptr [ebp - 4], eax    ; 計算結果(區域性變數loc8)放入棧中
   0x565561d3 <func+38>    mov    edx, dword ptr [ebp - 8]
   0x565561d6 <func+41>    mov    eax, dword ptr [ebp - 4]
   0x565561d9 <func+44>    add    eax, edx    ; eax = eax (loc8) + edx (loc1),函式返回值存放在eax中
   0x565561db <func+46>    leave      ; mov esp, ebp     pop ebp
   0x565561dc <func+47>    ret     ; pop eip

以上就是C語言函式的呼叫過程以及棧的情況,但是我還有幾點疑問沒有弄清楚,記錄一下:

  1. 為什麼在函式剛開始的地方sub esp, 0x10,從後面的程式碼來看,開闢的空間用於存放區域性變數,那為什麼不是在區域性變數定義的時候將區域性變數的值入棧,再移動esp呢?而是一次性先esp -= 0x10,這樣不會帶來空間的浪費嗎?

  2. call __x86.get_pc_thunk.ax是什麼意思?

  3. add eax, 0x2e20有什麼作用?

參考資料

星盟安全團隊課程:https://www.bilibili.com/video/BV1Uv411j7fr
CTF競賽權威指南(Pwn篇)(楊超 編著,吳石 eee戰隊 審校,電子工業出版社)
https://www.cnblogs.com/xuyaowen/p/libc-header-start.html

相關文章