深入淺出CPU眼中的函式呼叫——棧溢位攻擊
原理解讀
函式呼叫,大家再耳熟能詳了,我們先看一個最簡單的函式:
#include <stdio.h>
#include <stdlib.h>
int func1(int a, int b){
int c = a + b;
return c;
}
int main(){
int res = func1();
printf("%d", res);
}
函式呼叫前,呼叫者會先把需要傳遞的引數儲存在對應的暫存器中,然後就會呼叫call函式,call函式會做兩件事情,第一件事情是把下一條指令的地址入棧(對應的上圖22行的指令地址),第二件事情是跳轉到fun1所在的位置執行;跳轉過去之後,首先要開闢當前函式的棧幀,對應push rbp; mov rbp, rsp
兩條指令;再往後便是
我們知道,區域性變數都是儲存在棧上的,每個函式也有自己的棧幀,在整個函式呼叫的過程中,我們回顧一下棧幀的變化;
整個函式呼叫過程如上圖
-
call指令先將呼叫者函式的下一條指令入棧,之後跳轉到被呼叫函式執行。
-
被呼叫者函式先將呼叫者函式的棧幀基地址入棧。
-
然後開闢自己的棧幀,主要是將ebp設定為自己的棧幀基地址。
...執行相關的函式操作...
-
pop rbp將ebp恢復為main函式棧幀。
-
ret將下一條指令程式計數器PC,然後該指令出棧。(有點像pop)。
棧溢位攻擊
實驗環境:
經過上面的介紹,我們會發現,棧是從高地址向低地址增長的,並且區域性變數都儲存在當前棧幀的基地址之下;當前棧幀的基地址之上則包含了當前函式的返回地址,那麼是不是可以透過某種方式,去修改這個返回地址,來實現棧溢位攻擊呢,答案是可以的;
#include <stdio.h>
#include <stdlib.h>
void func2(){
printf("☠️☠️☠️☠️☠️");
exit(4);
}
void func1(){
long a[2];
a[1] = 1;
a[0] = 2;
// 修改返回地址
a[4] = (long)func2;
}
int main(){
func1();
printf("hello");
}
這段程式碼我們透過陣列越界寫,使用a[3]修改當前函式的返回地址,使得其去執行fun2。程式碼執行如下:
tackAttack.cpp:14:5: warning: array index 4 is past the end of the array (which contains 2 elements) [-Warray-bounds]
a[4] = (long)func2;
^ ~
stackAttack.cpp:10:5: note: array 'a' declared here
long a[2];
^
1 warning generated.
☠️☠️☠️☠️☠️
[Done] exited with code=4 in 0.231 seconds
我們可以發現雖然編譯器提示我們陣列發生越界,但是並沒有阻止,將返回地址修改後,程式執行了我們的惡意程式碼,並且沒有輸出hello
;