系統安全與漏洞利用

堆疊金絲雀(stack canary)

煤礦工人曾在籠裡帶著一隻金絲雀:若有危險氣體滲入,這隻鳥會先倒下,在礦工受害之前示警。堆疊金絲雀就是記憶體裡同樣的預警點子:編譯器在被儲存的返回位址「之前」放一個秘密守衛值,並在函式返回「之前」檢查它。如果一次緩衝區溢位在通往返回位址的路上輾過了那個值,守衛值就會出錯——程式於是在那個被損毀的返回位址被使用之前就中止。

它如何運作,一步步是這樣。當一個受保護的函式開始時(它的序言),編譯器插入的程式碼會把一個秘密金絲雀值複製到堆疊上,位置就在區域緩衝區與被儲存的返回位址之間——正是溢位必經之路。在函式返回之前(它的收尾),被插入的程式碼重新載入那個堆疊槽位,並與原本的秘密值比較;若兩者不同,它就呼叫一個處理常式,印出「stack smashing detected」並中止。因為一次會覆蓋返回位址的連續溢位「必然」也會覆蓋坐在它前面的金絲雀,這個不符就被抓到。金絲雀通常每次執行都隨機(讓攻擊者無法預測),且常含一個 null 位元組以阻止字串複製式溢位洩漏它或越過它。GCC 與 Clang 以 -fstack-protector-strong 之類的選項啟用它。

它之所以重要,是作為一道便宜、自動、由編譯器套用的防禦,在劫持完成之前就抓到經典的堆疊破壞樣式。誠實的提醒很要緊:金絲雀只保護「被儲存的返回位址」這條路——它對堆積溢位、釋放後使用、型別混淆、或覆蓋一個坐在金絲雀「之前」的區域函式指標,毫無作用。而且若攻擊者「洩漏」了金絲雀值(接著用正確的值覆蓋它),或在「不跨越」金絲雀的情況下覆蓋返回位址(例如透過任意寫而非連續溢位),它就會被擊敗。它是一層,不是保證。

// 編譯器插入的東西(概念上): fn() { long canary = __stack_chk_guard; // 序言:把秘密放上堆疊 char buf[16]; ... /* buf 的溢位會在返回位址之前先覆蓋 canary */ if (canary != __stack_chk_guard) // 收尾:在 ret 之前驗證 __stack_chk_fail(); // -> 'stack smashing detected',中止 }

金絲雀坐在溢位的路徑上;收尾處的不符會在信任返回位址之前中止。

金絲雀只守護返回位址這條路、只防連續溢位;洩漏金絲雀、或透過任意寫直接改寫返回位址,它就被繞過——它對堆積漏洞或釋放後使用毫無作用。

又称
stack protectorstack cookiecanary value堆疊保護器堆疊餅乾