系統安全與漏洞利用

影子堆疊(shadow stack)

想像你把待辦清單的私人第二份副本鎖在別人碰不到的抽屜裡。每當你完成一項任務想知道下一項時,你同時查看公開清單「與」你鎖起來的副本;若兩者不一致,就是有人動過公開那份。影子堆疊就是返回位址的那份上鎖副本:一個獨立、受保護的堆疊,記錄每個函式應該返回到哪裡,使一般堆疊上被損毀的返回位址能被抓到。

其機制如下。一般堆疊把返回位址就地存放,正好在緩衝區溢位搆得到之處。影子堆疊另外維護一個平行堆疊,「只」含返回位址,位於程式無法透過一般指標意外或惡意寫入的記憶體中。每次呼叫時,返回位址會被推進一般堆疊「與」影子堆疊。每次返回時,處理器(或編譯器插入的程式碼)把即將從一般堆疊使用的位址,與影子堆疊的頂端比較;若相符,返回照常進行;若不同——因為溢位或任意寫改了一般堆疊那份副本——程式就中止。這是控制流完整性的「後向邊」:它保證函式只返回到它真正的呼叫者,而那正是 ROP 與堆疊破壞所違反的。Intel CET 提供硬體影子堆疊;ARM 與各編譯器提供軟體影子呼叫堆疊。

它之所以重要,是因為它擊敗了整個後向邊攻擊面——返回位址覆蓋與 ROP 鏈,它們都依賴對返回位址說謊——並且比堆疊金絲雀更穩健,因為沒有秘密可洩漏、也不假設是連續溢位。誠實的提醒:影子堆疊只保護「返回」,不保護前向邊的間接呼叫(那需要 CFI 的其餘部分),而它的安全性建立在影子區段對攻擊者的寫入原語真正不可觸及之上;一個強到能搆到影子堆疊本身的任意寫,會損及它,這正是為何硬體實施較受青睞。

call f: 把 return_addr 推入 -> 一般堆疊「與」影子堆疊 ret 在 f:addr = pop(一般堆疊) if addr != pop(影子堆疊) -> 中止 // 返回位址被竄改過 else 跳到 addr

每次返回都對受保護的影子副本交叉核對;不符就代表返回位址被損毀了。

影子堆疊只守護「後向邊」(返回);前向邊的間接呼叫仍需 CFI 或 PAC,而它的安全性假設攻擊者的寫入原語搆不到影子區段本身。

又稱
shadow call stackreturn-address shadowbackward-edge CFI影子呼叫堆疊返回位址影子