逆向工程/常見解決方案
程式設計師沒有很多有效的保護措施來防止大多數溢位漏洞。但是,可以做一些事情。
像Java和C#這樣的新語言之所以如此重視它們的“自動邊界檢查”和“記憶體管理”功能,主要是因為它們有助於防止堆疊溢位(以犧牲少量效能為代價)。然而,C程式設計師只能靠自己,需要顯式地測試每個陣列的邊界。這很乏味,但至少駭客不會破壞你的程式,你就不會被解僱。
一些編譯器透過在堆疊上內建一個稱為金絲雀或Cookie的標誌值來提供幫助,通常位於已推入的幀指標和返回地址之上(想想煤礦中用來檢測有毒氣體積聚的籠子裡的鳥,在工人中毒之前)。
push CANARY push ebp mov ebp, esp sub esp, 100
現在,當函式要返回時,我們可以執行以下操作
add esp, 100 mov esp, ebp pop ebp pop ebx ;canary value cmp ebx, CANARY jne _STACK_ERROR_FOUND ret
這樣,我們可以檢測到堆疊是否被覆蓋,因為金絲雀值已經改變。然而,一個可預測的金絲雀值是脆弱的:將該值作為溢位資料的一部分插入堆疊的攻擊者可以逃避檢測。出於這個原因,大多數金絲雀值是在執行時隨機生成的。許多金絲雀值也包含在開頭或結尾的兩個空字元:字串複製函式(如strcpy或wcscpy)在遇到和寫入空字元後停止複製資料;如果攻擊者省略了空字元,則溢位將被捕獲。
這種保護方法可以捕獲基本溢位,並防止函式返回到修改後的地址並執行任意程式碼。但是,子程式仍然會被執行——內部狀態和變數被破壞——因為溢位只有在返回時才會被檢測到。攻擊者仍然可以利用這一點:例如,記憶體指標變數可以被修改為指向任意位置。如果子程式然後使用這個指標寫入記憶體,它可能會覆蓋程式地址空間中的任何內容。
許多堆溢位透過覆蓋下一個堆塊開頭的管理資料來變得有效,該資料通常至少包含一個連結串列。分配或釋放覆蓋的塊會導致資料被寫入記憶體中的任意地址。現在,大多數堆系統都會檢查連結串列指向的資料,以確保它們指向另一個堆塊或有效資料。
這種保護方法也存在於 Microsoft Windows 的“結構化異常處理”例程中。在呼叫異常處理程式(其指標駐留在堆疊上,可以被覆蓋)之前,首先會檢查它以確保該例程駐留在記憶體的可執行部分內。如果處理程式例程沒有,那麼它就不會被呼叫。
由於標準庫字串函式是堆疊溢位的常見原因,因此出現了許多包含“安全”字串函式的庫來嘗試解決這個問題。它們中的大多數在函式引數中要求顯式地提供“字串長度”,並將複製的資料限制為該數量。
程式設計師顯然仍然必須小心,並輸入準確的字串長度值;粗心程式設計仍然會導致問題。
我們將留給讀者作為練習,編寫一組安全的字串函式,這些函式接受長度引數,並執行簡單的邊界檢查以防止溢位。另一個選擇是將指向“最大”堆疊位置的指標作為引數,並比較指標以防止溢位。