型別混淆漏洞(type confusion)
想像一張表單,它可能是「貓」表單也可能是「狗」表單,而辦事員依最上方的勾選框決定要讀哪些欄位。如果有人遞來一張貓表單,但勾選框寫著狗,辦事員就會從實際代表「鬍鬚長度」的位元組裡,讀出「吠叫次數」這個欄位。型別混淆漏洞正是這樣:程式把一塊記憶體當成某型別,但它其實是另一型別,於是用錯誤的佈局去詮釋同一批位元組。
在像 C++ 這樣的語言裡,物件的意義來自它宣告的型別:哪些欄位位於哪些偏移、以及(對多型類別而言)vtable 指標在哪裡。當程式在沒有有效執行期檢查的情況下,把物件向下轉型或重新詮釋成不相容的型別時,就發生型別混淆——例如把一個其實指向 SmallObject 的 Base*,轉成期待更大佈局、有額外欄位與方法的 Derived*。程式接著去讀或寫真實物件裡並不存在的欄位,或者更危險地,沿著它「以為」位於某偏移的 vtable 指標走,那其實是攻擊者控制的資料。因為這個型別謊言讓你能讀過真實物件、或把資料當成程式碼指標,型別混淆常常產出任意讀/寫或直接的控制流劫持,而且根本沒有任何緩衝區被「溢出」。
它在那些大量使用動態分派與不受信任反序列化的地方特別要緊:JavaScript 引擎、PDF 與文件剖析器、語言執行期。誠實的提醒:根本原因是缺少或可被繞過的「型別」檢查,而非長度檢查,所以面向長度的防禦(金絲雀、邊界檢查)在這裡毫無作用;真正的防禦是執行期型別標記、會驗證的安全向下轉型運算子,以及避免對不受信任資料做原始的 reinterpret 式轉型。
struct Base { int tag; }; struct Derived { int tag; void (*handler)(void); int big[64]; }; Base *b = get_object(); // 其實指向一個小的 Base Derived *d = (Derived *)b; // 未檢查的轉型:對型別說了謊 d->handler(); // 讀取一個根本不存在的「函式指標」
轉型聲稱 b 是 Derived;程式接著從意義完全不同的位元組裡讀出 handler。
型別混淆根本不需要溢位——它損毀的是「意義」而非「邊界」——因此會繞過邊界檢查與金絲雀防禦;只有真正的型別檢查(或記憶體安全語言)能擋住它。