Stack 基礎知識
在撰寫程式時,特別是 C 語言程式,經常需要一塊暫存的記憶體空間,這通常被稱為 buffer。獲取 buffer 的方式主要有兩種:
- 透過
malloc()
從 heap 取得記憶體。 - 在 stack 上宣告陣列,例如
char array[16]
。
一般 C 程式的記憶體配置如上圖所示,主要區塊包括:
.text
:存放機器碼.data
:存放已初始化的全域變數.bss
:存放尚未初始化的全域變數- heap:主要用來存放需長期使用的資料
- stack:主要用於函式呼叫時的暫存資料
Stack 的運作
本文以下部分將使用 x86 組合語言來解析 stack 的運作。
在 C 語言中,每個函式執行時都會建立自己的 stack frame,用於儲存區域變數與其他相關資訊。為了確保函式執行完畢後能夠回到正確的位置,程式需要保存當前的執行狀態,包括程式計數器(Program Counter, PC)以及 stack frame 起始位址(%rbp
暫存器)。
以下是一個簡單的範例:
#include <string.h>
#include <stdio.h>
void copy(char *str) {
char buffer[16];
strcpy(buffer, str);
printf("%s\n", buffer);
}
int main (int argc, char **argv) {
copy(argv[1]);
return 0;
}
這段程式碼的主要行為是將傳入的字串複製到 copy()
函式內的 buffer
,然後印出其內容。接著我們來分析 main()
呼叫 copy()
時,stack 的行為:
main:
# ... 此時 argv 位於 %rdi
# 根據規格,第一個參數必須放在 %rdi
# 將 %rax 64 位元的資料複製到 %rdi
movq %rax, %rdi
# 1. 呼叫 copy() 同時把 return address (PC) 推到 stack
call copy(char*)
# 結束 main()
leave
ret
copy(char*):
# function prologue
# 2. 記錄 main() 的 stack frame 起始位址
pushq %rbp
# 把 %rbp 拿來存 copy() 的 stack frame 起始位址
movq %rsp, %rbp
# 3. 在 stack 上空出一些空間,額外的空間是為了記憶體對齊,實際上只需要 24 bytes
subq $32, %rsp
# 4. 放 str
movq %rdi, -24(%rbp)
# 5. 剩下空間是 buffer 的,這裡沒有指令是因為 buffer 不需初始化
# 準備呼叫 strcpy(),把 buffer 位址放到第一個參數(%rdi)
leaq -16(%rbp), %rdi
# 把 str 放到第二個參數(%rsi),注意 str 存的已經是記憶體位址了所以不使用 leaq 複製位址的指令
movq -24(%rbp), %rsi
# 呼叫 strcpy()
call strcpy
# 呼叫 puts()
leaq -16(%rbp), %rdi
call puts
# 結束 copy() 並恢復 main() 的 PC 與 %rbp
leave
ret
根據以上指令,在 call()
回傳前的 stack 狀態如下圖所示:
什麼是 Buffer Overflow 攻擊?
Buffer Overflow(緩衝區溢位) 指的是將超過 buffer 容量的資料寫入,導致 buffer 後方的記憶體被覆寫。在上述範例中,若傳入 copy()
的字串長度超過 16 bytes,可能會覆寫 main()
的 %rbp
及 return address。
這帶來一個危險的可能性:如果我們能夠修改 return address,當函式回傳時,PC 就會跳轉至我們控制的記憶體區域。進一步地,若該記憶體區段包含我們設計的指令,就能夠執行惡意程式碼,這種技術即為 Buffer Overflow 攻擊。
典型攻擊方式如下:
- 將惡意指令(shellcode)以 buffer overflow 的方式存入 buffer 與其後方的記憶體區段。
- 覆寫 return address,讓 PC 指向 shellcode。
- 當函式回傳時,惡意程式碼被執行。
由於惡意程式碼通常會先開啟 shell,後續攻擊者可透過 shell 進行進一步的操作,所以這段惡意程式碼又被稱為 shellcode。
本文省略了諸多攻擊細節,例如如何找到 buffer 的記憶體位址、如何觸發系統呼叫等,若有興趣深入了解,請參閱參考資料。
現代防禦手段
為了防範緩衝區溢位攻擊,現代系統採取了多種安全措施,以下介紹幾種常見的防禦機制。
Stack Canary
Stack Canary 是一種檢測緩衝區溢位的技術,在 return address 前方插入一段特殊值(canary)。函式回傳前會檢查該值是否被修改,若被更動則程式會立即終止,避免執行惡意程式碼。
Canary(金絲雀)這個名稱源於早期礦工會攜帶金絲雀進入礦坑,以偵測有毒氣體。若金絲雀死亡,表示礦坑內可能存在危險,礦工便會立即撤離。在軟體開發領域,canary 也扮演類似的「試毒」角色,常見於軟體的版本號中。
Write XOR Execute(W^X)
此技術強制記憶體區塊只能是可寫(Write)或可執行(Execute),但不能同時具備這兩種屬性。這樣一來就算攻擊者能夠寫入惡意程式碼,也無法執行,從而降低攻擊成功的可能性。
位址空間配置隨機化(ASLR)
位址空間配置隨機化(Address-Space Layout Randomization, ASLR)透過隨機化程式記憶體區域(如 .text
、stack、heap 等),使攻擊者難以預測緩衝區的位址,從而增加發動攻擊的難度。