格式字串漏洞(format-string vulnerability)
當你呼叫 printf 時,第一個引數是格式字串——一個帶有 %d、%s 等佔位符的範本,它告訴 printf 要再取幾個引數、又該如何詮釋每一個。格式字串漏洞發生於程式讓「攻擊者」控制的文字變成那個格式字串,於是是攻擊者、而非程式設計師,在決定那些佔位符。
這之所以如此危險,原因如下。printf 是可變引數的:它並不知道實際被傳入幾個引數;它只是信任格式字串,沿著該次呼叫的引數區(暫存器與堆疊)行走,每遇一個指示符就取出一個值。如果攻擊者送入滿是 %x 的字串,printf 就樂呵呵地從堆疊上一個接一個讀出字組並印出——這是一種資訊洩漏,能傾倒位址、擊敗 ASLR、或揭露堆疊金絲雀。更強大的是 %n,一個「不印出」任何東西卻會「寫入」的指示符:它把目前為止已印出的字元數,存進 printf 期待作為相應指標引數的那個 int 裡。透過控制格式字串(它以資料形式坐在堆疊上),攻擊者可以同時安排「位址」與——借助 %100x 之類的寬度指定符做墊充——那個「值」,把 printf 變成任意記憶體寫入。教科書式的觸發點就是省略了格式字串:寫成 printf(user_input) 而非 printf("%s", user_input)。
它之所以重要,是因為這個漏洞看起來毫無威脅——只不過是一行日誌或錯誤輸出——卻能從單一一個誤用的呼叫,同時產出任意讀(透過洩漏)與任意寫(透過 %n)。誠實的說法:修法既簡單又徹底——永遠傳入一個常數格式字串,把不受信任的資料放進引數,就像 printf("%s", user_input)。現代編譯器會對非常值格式發出警告(gcc -Wformat -Wformat-security),許多 libc 版本也在可寫入的格式字串中停用或限制 %n,但真正的防禦是這份紀律。
printf(user_input); // 漏洞:user_input 成了格式字串 // 若使用者輸入 "%x %x %x %x" -> 洩漏四個堆疊字組 // 若使用者輸入 "...%n" -> 把某個值寫到選定的位址 printf("%s", user_input); // 修法:格式為常數,資料當引數
唯一的差別是誰控制了格式字串——而那個差別就是整個漏洞。
%n 是少數會「寫入」而非印出的 printf 指示符;即使 %n 被加固掉,光是 %x 式的讀取洩漏就可能足以擊敗 ASLR。