未定義行為與安全

緩衝區溢位

想像把兩公升的水倒進一公升的水壺。多出來的那一公升不會消失——它會從壺口溢出,淋到壺旁邊碰巧擺著的任何東西上。緩衝區是記憶體裡一個固定大小的容器,緩衝區溢位正是這種溢出:寫進緩衝區的資料超過它能容納的量,多出來的部分就覆寫了剛好位在它後面的記憶體。它是越界存取一個特別、且特別危險的情形。

溢出落在哪裡,決定了它有多糟。堆疊上的緩衝區緊鄰函式的區域變數,再過去是儲存的返回位址——函式結束時 CPU 會跳去的那個地方。讓堆疊緩衝區溢位,你就可能覆寫那個返回位址;函式接著會「返回」到被覆寫的位元組所指的地方。能控制溢出輸入的攻擊者可以把那次跳轉瞄準他選定的程式碼。這個經典攻擊叫做砸毀堆疊(smashing the stack)。堆積上的緩衝區溢位則改為覆寫相鄰的堆積配置、或配置器自己的記帳資料(存在每個區塊旁的大小與自由串列連結),這會毀掉堆積,並在小心操作下同樣被導向接管。兩種情況裡,立即的症狀可能是當掉、一個垃圾值、或完全看不出來——這種毀損可能默默潛伏到很久之後。

為什麼重要:緩衝區溢位數十年來一直是嚴重安全漏洞的頭號來源,正因為 C 不做任何自動邊界檢查、信任程式設計師把大小算對。防禦是分層的:在程式碼層面,永遠用目的地大小來限制你的複製(用 snprintf 而非 sprintf,並把任何無界複製都當作可疑);在設計層面,把緩衝區大小和長度放在一起、並在複製前驗證輸入長度;在建構層面,啟用編譯器與作業系統的保護(堆疊金絲雀、不可執行堆疊、ASLR),並用 AddressSanitizer 測試,它能在溢位發生的當下就偵測到大多數情形。

void greet(const char *name) { char buf[16]; strcpy(buf, name); /* 若 name 長於 15,溢位 */ printf("Hi %s\n", buf); } /* 較安全:用目的地大小限制複製 */ void greet_safe(const char *name) { char buf[16]; snprintf(buf, sizeof buf, "%s", name); /* 絕不寫超過 buf */ printf("Hi %s\n", buf); }

strcpy 一路複製到來源的結尾字元、無視目的地大小;snprintf 用 sizeof buf 限制寫入並總是補上結尾字元。

溢位造成的當掉是一個症狀,常出現在離臭蟲很遠的地方,而且許多溢位根本不會當掉——它們默默毀損。「沒當掉」不等於「沒溢位」;請用 AddressSanitizer 測試把它們攤在陽光下。

又称
buffer overrunstack overflow (smashing)heap overflow緩衝區溢位緩衝區溢出