緩衝區溢位與指標安全 (Buffer Overflow & Pointer Safety)

Overview Table

本節 (3.10) 把前面分開講的「控制」與「資料」結合起來,聚焦在指標、gdb 除錯、緩衝區溢位攻擊與防禦,以及可變大小的堆疊框架。

主題 小節 核心觀念 關鍵字
指標 (Pointers) 3.10.1 每個指標有 type 與 value;& 取址、* 解參考;陣列即指標算術 leaq、scaling L
gdb 除錯 3.10.2 設 breakpoint、single-step、檢視暫存器/記憶體 breakstepix/
緩衝區溢位 3.10.3 C 不做邊界檢查,越界寫入可覆寫 return address gets、exploit code
溢位防禦 3.10.4 ASLR + 金絲雀 (canary) + NX 三道防線 stack protector、NX bit
可變大小框架 3.10.5 %rbp 當 frame pointer,leave 還原 alloca、VLA
Important

核心因果鏈:C 無邊界檢查 → 區域緩衝區與 return address 同在 stack → 越界寫入覆寫返回位址 → ret 跳到攻擊者控制的位置。理解這件事必須下探到 machine-code 層級,單看 C 程式碼完全看不出來。


3.10.1 理解指標 (Understanding Pointers) (p.313-314)

指標是 C 產生「對不同資料結構中元素之參考」的統一機制。以下是指標與機器碼對應的關鍵原則。

Important

指標算術縮放公式:對指標 p(值為 p,關聯型別大小 L),運算式 p+i 得到的位址為

p+Li
函式指標宣告解讀 (由內而外):
  int (*fp)(int, int *);
        |____|             fp 是一個指標
        |________________| 指向一個函式
             (int, int *)  該函式取 (int, int*)
  int                      並回傳 int

  註:(*fp) 的括號必要;int *fp(int,int*) 會被讀成
      回傳 int* 的函式原型,而非函式指標。
Tip

int (*f)(int*)f 開始向外讀:(*f) → f 是指標;(*f)(int*) → 指向取 int* 的函式;最外層 int → 回傳 int


3.10.2 使用 gdb 除錯器 (p.315-316)

gdb 讓我們能在程式執行時觀察其行為,而非僅靠讀碼推論。一般流程:先用 objdump 反組譯,再用 gdb 在關注點設 breakpoint,命中後檢視暫存器與記憶體,並可 single-step。

類別 指令範例 效果
啟停 run / kill / quit 執行 / 停止 / 離開
中斷點 break multstore / break *0x400540 在函式入口 / 位址設中斷點
執行 stepi / nexti / continue / finish 執行一條指令 / 跨過呼叫 / 續跑 / 跑到函式返回
看碼 disas / print /x $rip 反組譯當前函式 / 印 PC(hex)
看資料 print /x $rax / x/2g 0x... 印暫存器(hex)/ 檢視兩個 8-byte 字
資訊 info frame / info registers 當前堆疊框架 / 所有暫存器

3.10.3 越界記憶體參考與緩衝區溢位 (p.315-320)

C 不做任何陣列邊界檢查,而區域變數與 return address、saved register 一起存在 stack。越界寫入某陣列元素會破壞 stack 上的狀態;之後重載暫存器或執行 ret 就會出大問題。最常見來源即 buffer overflow:某字元陣列放在 stack 存字串,但字串長度超過配置空間。

echo 範例(char buf[8] 太小),編譯後可推知 stack 佈局:

echo:
  subq $24, %rsp     ; 在 stack 配 24 bytes
  movq %rsp, %rdi    ; buf 就在 %rsp (堆疊頂端)
  call gets          ; gets 無法得知緩衝區大小!
  ...
  addq $24, %rsp
  ret
 高位址
 +------------------------+
 |  caller 的 stack frame  |
 +------------------------+
 |     Return address     |  <- %rsp+24
 +------------------------+
 |      (16 bytes 未用)    |
 +------------------------+
 | [7][6][5][4][3][2][1][0]|  <- buf = %rsp
 +------------------------+
 低位址

gets 越界寫入時,隨字串變長逐步破壞的狀態:

輸入字元數 被破壞的狀態
0–7 無(含結尾 null 剛好放得下)
9–23 未用的 stack 空間
24–31 Return address
32+ caller 中儲存的狀態
Warning

這些行為「從 C 程式碼看起來根本不可能發生」。gets 等函式越界寫入的衝擊,只有在 machine-code 層級研究才能理解。使用任何可能溢位的函式都是不良實務。

攻擊利用 (exploit):攻擊者餵入含 exploit code 位元組編碼的字串,再用額外位元組把 return address 覆寫成指向 exploit code 的指標;ret 便跳去執行。常見手法是啟動 shell 取得 OS 功能,或執行未授權任務後修好 stack 再 ret 一次假裝正常返回。

Important

歷史案例:1988 年 11 月的 Internet worm 用四種方式散播,其一即對 finger daemon fingerd 的緩衝區溢位攻擊。啟示:任何對外介面都應「防彈」,不讓外部代理造成系統誤動作。


3.10.4 挫敗緩衝區溢位攻擊 (p.320-326)

現代編譯器與 OS 提供三道機制,皆不需程式員特別處理、效能代價極小,分開各自降低脆弱度、合併更有效。

防線一:堆疊隨機化 (Stack Randomization / ASLR)

Warning

ASLR 非萬靈丹。攻擊者可用暴力法反覆嘗試不同位址;常見技巧是 exploit code 前放一長串 nop(nop sled),只要猜中序列中任一位址,程式就會「滑」過 nop 直到命中 exploit code。256-byte nop sled 對 n=223 只需列舉 215=32768 個起始位址,對堅決攻擊者完全可行;64 位元的 224 則較費事。

防線二:堆疊破壞偵測 (Stack Corruption Detection / Canary)

在區域緩衝區與其餘 stack 狀態之間存入一個隨機的金絲雀值 (canary / guard value),返回前檢查它是否被更動;若被改則呼叫 __stack_chk_fail 中止。

 +------------------------+
 |     Return address     |  <- %rsp+24
 +------------------------+
 |        Canary          |  <- 隨機保護值 (位於 8(%rsp))
 +------------------------+
 | [7][6][5][4][3][2][1][0]|  <- buf = %rsp
 +------------------------+
echo (啟用 stack protector):
  subq $24, %rsp
  movq %fs:40, %rax   ; 從特殊 segment 取 canary (只讀)
  movq %rax, 8(%rsp)  ; 存到 buf 之外
  xorl %eax, %eax     ; 清掉暫存器(避免洩漏 canary)
  ...call gets / puts...
  movq 8(%rsp), %rax
  xorq %fs:40, %rax   ; 與存的 canary 比對
  je   .L9            ; 相等(=0)則正常
  call __stack_chk_fail ; 否則 stack 被破壞!
.L9:
  addq $24, %rsp
  ret
Tip

先前示範溢位時需加 -fno-stack-protector 才能關掉此保護。gcc 預設會自動判斷函式是否易受溢位並插入。

防線三:限制可執行程式碼區域 (NX bit)

Warning

JIT(just-in-time)編譯等技術需動態產生並執行碼(如 Java),能否只把可執行權限限於編譯器原生產物,取決於語言與 OS。三道防線都非完整保障,worm/virus 仍持續存在。


3.10.5 支援可變大小的堆疊框架 (p.326-329)

前面函式的 stack frame 大小編譯期即可決定;但某些函式需可變區域儲存,例如呼叫 alloca,或宣告可變大小陣列 (VLA)(如 long *p[n],需 8n bytes,n 每次呼叫不同)。編譯器無法預知框架大小。

解法:用 %rbp 當 frame pointer(base pointer)。它是 callee-saved 暫存器;函式先存舊 %rbp,再讓 %rbp 固定指向該位置,對固定長度區域變數(如 i)用相對 %rbp 的偏移量參考。

vframe 的 stack frame (Figure 3.44):
 高位址
 +----------------------+
 |    Return address    |  +8 (相對 %rbp)
 +----------------------+
 |     Saved %rbp       |   0  <- %rbp (frame pointer)
 +----------------------+
 |         i            |  -8   固定偏移量參考
 +----------------------+
 |      (Unused)        |  -16  (= s1)
 +----------------------+
 |                      |
 |    8n bytes: p[ ]    |  可變大小
 |                      |
 +----------------------+  <- %rsp (= s2)  低位址
vframe:
  pushq %rbp           ; 存舊 %rbp
  movq  %rsp, %rbp     ; 設 frame pointer
  subq  $16, %rsp      ; 配 i 空間 (%rsp = s1)
  leaq  22(,%rdi,8),%rax
  andq  $-16, %rax     ; 對齊到 16 的倍數
  subq  %rax, %rsp     ; 配陣列 p (%rsp = s2)
  leaq  7(%rsp), %rax
  shrq  $3, %rax
  leaq  0(,%rax,8),%r8 ; &p[0]
  ...
  movq  %rax, -8(%rbp) ; 用 -8(%rbp) 存取固定變數 i
  ...
  leave                ; 還原 %rbp 與 %rsp
  ret
Important

frame pointer 的演進:早期 x86 每次函式呼叫都用 frame pointer;x86-64 只在框架可變大小時才用 %rbp。近版 gcc 已放棄舊慣例。混用有/無 frame pointer 的碼是可以的,只要所有函式都把 %rbp 當 callee-saved 對待。

Warning

一般化敘述的例外:多數 x86-64 函式的 stack frame 大小固定、不用 %rbp;本節的 frame pointer 屬於「可變框架」這個例外情境。相關固定框架與呼叫慣例見 03-Machine-Level-Programs/04-Procedures-and-the-Stack


Exam/Test Patterns

情境 / 關鍵字 答案
(int *) p + 7 vs (int *) (p+7)(p 為 char*) 前者 p+28(cast 優先、縮放 4);後者 p+7
& 運算子對應哪條指令 leaq(計算位址)
函式指標的 value 是什麼 函式機器碼第一條指令的位址
char buf[8],輸入幾字元會覆寫 return address 24–31 字元(0–7 安全,9–23 只毀未用空間)
哪些函式易致溢位 getsstrcpystrcatsprintf;改用 fgets
canary 存哪、怎麼取 存 buf 與 saved state 之間;movq %fs:40,%rax(segmented,只讀)
canary 比對用哪指令、何時觸發 fail xorq %fs:40,%rax;非零則 call __stack_chk_fail
gcc 何時插入 stack protector 函式含 char 型別區域緩衝區時
nop sled 作用 讓程式「滑」過一串 nop 到 exploit code,對抗位址隨機化
NX bit 作用 分離 read/execute,stack 可讀寫但不可執行
何時用 %rbp frame pointer 框架大小可變時(alloca、VLA)
leave 等價於 movq %rbp,%rsp + popq %rbp
andq $-16, %rax 目的 向下對齊到 16 的倍數(16-byte 對齊)
ASLR 屬於哪類技術 address-space layout randomization,含 stack 隨機化