緩衝區溢位與指標安全 (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、檢視暫存器/記憶體 | break、stepi、x/ |
| 緩衝區溢位 | 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 |
核心因果鏈:C 無邊界檢查 → 區域緩衝區與 return address 同在 stack → 越界寫入覆寫返回位址 → ret 跳到攻擊者控制的位置。理解這件事必須下探到 machine-code 層級,單看 C 程式碼完全看不出來。
3.10.1 理解指標 (Understanding Pointers) (p.313-314)
指標是 C 產生「對不同資料結構中元素之參考」的統一機制。以下是指標與機器碼對應的關鍵原則。
- 每個指標都有 type:type 表示所指物件的種類。若物件型別為
T,指標型別為*T。特殊的void *是泛型指標(如malloc回傳值)。指標型別不存在於機器碼中,純粹是 C 提供給程式員避免定址錯誤的抽象。 - 每個指標都有 value:即某個所指型別物件的位址。特殊值
NULL(0) 表示「不指向任何地方」。 &產生指標:可作用於任何 lvalue(可放在賦值左邊的運算式,如變數、struct/union/array 元素)。機器碼常用leaq實作&,因為它就是設計來計算記憶體參考的位址。*解參考:結果具有指標關聯的型別;由一次記憶體參考(存或取指定位址)實作。- 陣列與指標密切相關:陣列名可當指標讀取(但不可更新)。
a[3]等同*(a+3);兩者都需依物件大小 scaling。
指標算術縮放公式:對指標 p(值為 p+i 得到的位址為
- Casting 改 type 不改 value,但會改變指標算術的縮放。範例(
p為char *,值): (int *) p + 7計算(先轉型再 +7,縮放 4) (int *) (p + 7)計算(先算 char 加法,cast 優先於加法)
- 函式指標:指標也可指向函式,值為該函式機器碼第一條指令的位址,提供傳遞/儲存程式碼參考的能力。
函式指標宣告解讀 (由內而外):
int (*fp)(int, int *);
|____| fp 是一個指標
|________________| 指向一個函式
(int, int *) 該函式取 (int, int*)
int 並回傳 int
註:(*fp) 的括號必要;int *fp(int,int*) 會被讀成
回傳 int* 的函式原型,而非函式指標。
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 |
當前堆疊框架 / 所有暫存器 |
print格式碼:/x十六進位、/t二進位、/d(預設)十進位。x/語法:x/nfu addr,如x/20b multstore看前 20 個 byte、x/2g看 2 個 giant(8-byte)字。- 許多程式員使用 gdb 的圖形前端 ddd。
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 中儲存的狀態 |
- 字串超過 23 字元時 return address 被覆寫,
ret會跳到完全非預期的位置。 - 改善之道:用
fgets(帶最大讀取位元組數的參數)取代gets。 - 危險函式:
gets、strcpy、strcat、sprintf都能在不知目標緩衝區大小的情況下產生位元組序列 → 溢位漏洞。
這些行為「從 C 程式碼看起來根本不可能發生」。gets 等函式越界寫入的衝擊,只有在 machine-code 層級研究才能理解。使用任何可能溢位的函式都是不良實務。
攻擊利用 (exploit):攻擊者餵入含 exploit code 位元組編碼的字串,再用額外位元組把 return address 覆寫成指向 exploit code 的指標;ret 便跳去執行。常見手法是啟動 shell 取得 OS 功能,或執行未授權任務後修好 stack 再 ret 一次假裝正常返回。
歷史案例:1988 年 11 月的 Internet worm 用四種方式散播,其一即對 finger daemon fingerd 的緩衝區溢位攻擊。啟示:任何對外介面都應「防彈」,不讓外部代理造成系統誤動作。
3.10.4 挫敗緩衝區溢位攻擊 (p.320-326)
現代編譯器與 OS 提供三道機制,皆不需程式員特別處理、效能代價極小,分開各自降低脆弱度、合併更有效。
防線一:堆疊隨機化 (Stack Randomization / ASLR)
- 問題:歷史上 stack 位址高度可預測,同一程式 + OS 版本在多台機器上 stack 位置穩定 → security monoculture(單一病毒株可攻陷大量機器)。
- 作法:程式啟動時用
alloca在 stack 上配置 0~n bytes 隨機空間(不使用),使後續所有 stack 位址每次執行都不同。 - ASLR (address-space layout randomization):把程式碼、函式庫碼、stack、全域變數、heap 每次載入不同記憶體區域。
- 實測:32 位元模式範圍約
;64 位元模式範圍近 。
ASLR 非萬靈丹。攻擊者可用暴力法反覆嘗試不同位址;常見技巧是 exploit code 前放一長串 nop(nop sled),只要猜中序列中任一位址,程式就會「滑」過 nop 直到命中 exploit code。256-byte nop sled 對
防線二:堆疊破壞偵測 (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
- canary 每次執行隨機產生,攻擊者無法得知。
- 用 segmented addressing (
%fs:40) 讀取,可標記為唯讀,攻擊者無法覆寫存的 canary。 xorq比對:相同得 0(正常),非零表示 canary 被改。- gcc 只在函式含
char型別區域緩衝區時插入此碼,效能代價小。 - 變數重排安全性(Practice 3.48):protector 會把緩衝區放到接近 canary、把純量變數放在更「安全」(不會被緩衝區溢位波及)的位置,降低溢位污染其他區域變數的機會。
先前示範溢位時需加 -fno-stack-protector 才能關掉此保護。gcc 預設會自動判斷函式是否易受溢位並插入。
防線三:限制可執行程式碼區域 (NX bit)
- 典型程式中只有編譯器產生的碼區段需要可執行,其餘可限制為僅讀寫。
- 虛擬記憶體以 page(典型 2048 或 4096 bytes)為單位,硬體支援三種存取控制:read / write / execute。
- 歷史上 x86 把 read 與 execute 合成單一 1-bit flag,可讀即可執行;stack 必須可讀寫故也可執行 → 危險。
- AMD 引入 NX (no-execute) bit(Intel 跟進),分離讀與執行權限;stack 可標記為可讀寫但不可執行,由硬體檢查,無效能代價。
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
-
固定 vs 可變:固定變數
i用-8(%rbp)參考(位置對%rbp不變);陣列p起點用%rcx/%r8動態算出。 -
leave指令(無運算元)等價於:movq %rbp, %rsp ; 堆疊指標移到 saved %rbp 位置 popq %rbp ; 彈出還原 %rbp,%rsp 指向 caller frame 末端效果是一次釋放整個 stack frame。
-
對齊邏輯(Practice 3.49):line 5-7 用
leaq 22(,%rdi,8)後andq $-16把 s2 對齊到 16 的倍數(的位元表示低 4 位為 0,遮罩即向下對齊);line 8-10 用 +7後shr $3再*8把 p 向上對齊到 8 的倍數。保證 s2 為 16 對齊、p 為 8 對齊。
frame pointer 的演進:早期 x86 每次函式呼叫都用 frame pointer;x86-64 只在框架可變大小時才用 %rbp。近版 gcc 已放棄舊慣例。混用有/無 frame pointer 的碼是可以的,只要所有函式都把 %rbp 當 callee-saved 對待。
一般化敘述的例外:多數 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*) |
前者 |
& 運算子對應哪條指令 |
leaq(計算位址) |
| 函式指標的 value 是什麼 | 函式機器碼第一條指令的位址 |
char buf[8],輸入幾字元會覆寫 return address |
24–31 字元(0–7 安全,9–23 只毀未用空間) |
| 哪些函式易致溢位 | gets、strcpy、strcat、sprintf;改用 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 隨機化 |
Related Notes
- 03-Machine-Level-Programs/04-Procedures-and-the-Stack — stack frame、return address、callee-saved 暫存器、呼叫慣例基礎
- 03-Machine-Level-Programs/05-Arrays-and-Data-Structures — 陣列/指標算術縮放、對齊 (alignment) 與本節指標原則相接
- 03-Machine-Level-Programs/03-Control-Flow —
ret、跳轉與控制流,理解溢位如何劫持控制 - 02-Information-Representation/03-Integer-Arithmetic — 除以 2 的冪、位元遮罩(
表示)支撐對齊計算 - 09-Virtual-Memory/02-VM-Management-and-Protection — page 級 read/write/execute 保護與 NX bit
- 09-Virtual-Memory/06-Garbage-Collection-and-Memory-Bugs — 記憶體 bug 與越界存取的後續延伸