掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
push 操作先將 棧頂(sp指針) 向下移動一個位置,然后將數(shù)據(jù)寫入到新的棧頂;而 pop 操作會從 棧頂 讀取數(shù)據(jù),并且將 棧頂(sp指針) 向上移動一個位置。

例如,將 0x100 壓入棧,過程如下圖所示:
我們再來看看 出棧 操作,如下圖所示:
棧幀,也就是 Sack Frame,其本質(zhì)就是一種棧,只是這種棧專門用于保存函數(shù)調(diào)用過程中的各種信息(參數(shù),返回地址,本地變量等)。
棧幀 有 棧頂 和 棧底 之分,其中棧頂?shù)牡刂纷畹停瑮5椎牡刂纷罡?。SP(棧指針) 就是一直指向棧頂?shù)?。?x86 的 32 位 CPU 中,我們用 %ebp 寄存器指向棧底,也就是基址指針;用 %esp 寄存器指向棧頂,也就是棧指針。下面是一個棧幀的示意圖:
一般來說,我們將 %ebp 到 %esp 之間區(qū)域當做棧幀。并不是整個??臻g只有一個棧幀,每調(diào)用一個函數(shù),就會生成一個新的棧幀。
在函數(shù)調(diào)用過程中,我們將調(diào)用函數(shù)的函數(shù)稱為:調(diào)用者(caller),將被調(diào)用的函數(shù)稱為:被調(diào)用者(callee)。在這個過程中:
現(xiàn)在,我們來看看函數(shù)調(diào)用時,棧幀是如何變化的。
我們以一個函數(shù)調(diào)用的實例來解說,代碼如下:
// stack.c
int add_func(int a, int b)
{
int c, d;
c = a;
d = b;
return c + d;
}
int main(int argc, char *argv[])
{
int total;
total = add_func(1, 2);
return 0;
}
我們使用命令 gcc -S -m32 stack.c 來編譯上面的代碼,獲取的匯編代碼如下所示(去掉一些無關(guān)緊要的信息):
add_func:
pushl %ebp // 保存ebp寄存器到棧
movl %esp, %ebp // 把ebp進程設(shè)置為esp的值
subl $16, %esp // 為局部變量申請空間
movl 8(%ebp), %eax // 把參數(shù)a保存到eax寄存器中
movl %eax, -8(%ebp) // 把eax寄存器的值保存到局部變量c中(c = a)
movl 12(%ebp), %eax // 把參數(shù)b保存到eax寄存器中
movl %eax, -4(%ebp) // 把eax寄存器到值保存到局部變量d中(d = b)
movl -8(%ebp), %edx // 把d的值保存到edx寄存器中
movl -4(%ebp), %eax // 把c的值保存到eax寄存器中
addl %edx, %eax // 將eax寄存器與edx寄存器的值相加,保存到eax中(返回值)
leave
ret // 函數(shù)返回
...
可能匯編代碼比較難看懂,我們用下面的插圖來說明這個調(diào)用過程:
如上圖所示,調(diào)用過程如下:
上面介紹了 函數(shù)調(diào)用 的過程,現(xiàn)在我們來介紹一下函數(shù)調(diào)用完畢后,從被調(diào)用函數(shù)返回到原來的函數(shù)過程是如何處理的。
從 add_func() 函數(shù)的匯編代碼可以看到,當被調(diào)用函數(shù)執(zhí)行完畢返回到調(diào)用函數(shù)前,會執(zhí)行 leave 指令,這條指令等價于:
movl %ebp, %esp
popl %ebp
這兩條匯編指令的意思是,將 esp寄存器 和 ebp寄存器 恢復(fù)到調(diào)用函數(shù)前的值。
然后,調(diào)用 ret 指令返回到原來的函數(shù)。ret 指令會從棧頂獲取 返回地址,然后跳轉(zhuǎn)到(jmp指令)此地址繼續(xù)執(zhí)行。這時的 棧幀 的結(jié)構(gòu)如下圖所示:
前面說了那么,都是為了 棧溢出攻擊 這節(jié)作鋪墊的。通過前面的學(xué)習(xí),我們知道調(diào)用函數(shù)的 參數(shù) 、執(zhí)行完函數(shù)后的 返回地址 和被調(diào)用函數(shù)的 局部變量 都是存放在棧中的。
如果在調(diào)用函數(shù)時,不小心將 返回地址 覆蓋了,那么調(diào)用完函數(shù)后,將不會跳轉(zhuǎn)到原來的函數(shù)繼續(xù)執(zhí)行,而是跳轉(zhuǎn)到覆蓋后的地址執(zhí)行。如下圖所示:
那么,怎樣才能把 返回地址 覆蓋呢?我們可以通過下面的例子來說明:
#include
#include
#include
#include
#include
#define PTR_SIZE 8 // 指針的大小
#define EBP_SIZE 8 // ebp寄存器的大小
void inject_callback()
{
printf("inject_callback called...\n");
exit(0);
}
void func_call(char *addr, int len)
{
char tmpBuf[16] = {0xff};
memcpy(tmpBuf + 16 + EBP_SIZE, addr, len);
printf("func_call called...\n");
}
int main(int argc, char** argv)
{
uint64_t injectPtr = (uint64_t)&inject_callback;
func_call(&injectPtr, PTR_SIZE);
printf("main exited...\n");
return 0;
}
我們使用以下命令編譯上面代碼,并且執(zhí)行:
$ gcc stack-overflow.c -fno-stack-protector -o stack-overflow
$ ./stack-overflow
func_call called...
inject_callback called...
在編譯上面程序時,一定要加上 -fno-stack-protector 參數(shù),否則將會觸發(fā)棧溢出保護,導(dǎo)致執(zhí)行失敗。
在上面的代碼中,我們并沒有直接調(diào)用 inject_callback() 函數(shù),而是通過把 inject_callback() 函數(shù)的地址復(fù)制到 func_call() 函數(shù)的局部變量 tmpBuf 中。
由于局部變量 tmpBuf 的類型為字符串數(shù)組,而且大小為 16 個字節(jié)。但我們復(fù)制數(shù)據(jù)是從 24(16 + 8)處開始復(fù)制,已經(jīng)超出了局部變量 tmpBuf 的大小,如下圖所示:
從上圖可以看出,func_call() 函數(shù)在調(diào)用 memcpy() 函數(shù)復(fù)制數(shù)據(jù)時,由于不小心用 inject_callback() 函數(shù)的地址覆蓋了返回地址,導(dǎo)致 func_call() 函數(shù)執(zhí)行完畢后,跳轉(zhuǎn)到 inject_callback() 函數(shù)處執(zhí)行。
這就是 棧溢出攻擊 的原理,而導(dǎo)致 棧溢出攻擊 的原因就是:調(diào)用 memcpy()、strcpy() 等函數(shù)復(fù)制數(shù)據(jù)時,沒有對數(shù)據(jù)的長度進行驗證,從而 返回地址 被復(fù)制的數(shù)據(jù)覆蓋了。
黑客可以利用 棧溢出攻擊 來把函數(shù)的返回地址修改成入侵代碼的地址,從而實現(xiàn)攻擊的目的。

我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流