堆疊金絲雀(stack canary)
煤礦工人從前會帶一隻關在籠裡的金絲雀下到礦坑。這隻鳥對毒氣比人敏感,所以一旦牠停止歌唱或倒下,礦工就知道危險已至,還來得及逃。堆疊金絲雀借用了這個名字與點子:把一個脆弱的小哨兵,放在攻擊會最先造成損害的地方,再檢查它是否受傷。若金絲雀死了,你就知道攻擊發生了,並在它得逞之前停下。
回想一下堆疊緩衝區溢位是怎麼造成損害的:它寫過區域緩衝區、覆蓋掉函式被保存的返回位址(它就坐在緩衝區之後、堆疊上不遠處),使函式返回時 CPU 跳到攻擊者選定的程式碼。堆疊金絲雀防的正是這一點。當函式開始時,編譯器在堆疊上、就在被保存的返回位址之前,插入一個祕密的守衛值——金絲雀。就在函式返回之前,它插入一道檢查:金絲雀還是我們放進去的那個值嗎?一個大到足以搆到並破壞返回位址的溢位,途中勢必會覆蓋掉坐在它前面的金絲雀。所以若金絲雀變了,程式就知道堆疊被砸了、立刻中止,拒絕返回到攻擊者植入的任何東西。為了阻止攻擊者乾脆把正確的金絲雀值寫回去,金絲雀是隨機的(每個行程重新挑選)且祕密的,並常包含一個零位元組,以絆倒字串複製類的攻擊。
堆疊金絲雀是一種便宜、自動且被廣泛使用的防禦——現代編譯器用一個旗標就加上它,並把許多原本可達成程式碼執行的攻擊,變成單純的當機。但誠實地說它是一個有針對性的守衛,而非通用的守衛,精明的攻擊者有繞過它的辦法。若某個臭蟲洩漏了金絲雀值,攻擊者就能把它原封不動寫回、輕鬆通過檢查。不是連續地蓋過金絲雀的那種溢位——例如寫到攻擊者選定的某個偏移、或覆蓋一個不同的指標或區域變數而非返回位址——能完全繞開它。而且金絲雀保護的是返回位址,並非其他敏感的堆疊資料。這正是為什麼金絲雀會與 ASLR、不可執行記憶體、以及最終的記憶體安全寫法一起部署,作為縱深防禦的一層,而非一個完整的修法。
進入函式時,它在返回位址下方存入一個祕密的 8 位元組金絲雀。一個對 buf 的溢位寫過了它,在通往返回位址的途中破壞了金絲雀。就在返回前,被插入的檢查看到金絲雀不再相符,便呼叫一個中止常式——程式乾淨地死去,而非跳進攻擊者的程式碼。
在通往返回位址的途中破壞了金絲雀,檢查便逮到它。
金絲雀守的是返回位址這個特定位置。若某個臭蟲洩漏了金絲雀值,攻擊者就能把它重寫回去、通過檢查;而越過它(或打到其他資料)的溢位能繞過它——所以它是一層防線,不是完整的修法。