叛逆修女 / RaidyHD
1809 字
9 分鐘
🎓【C++】多型與Vtable|六周目

目錄
- 一、多型概念速查
- 二、虛函式、vptr 與 vtable
- 三、編譯器實作差異
- 四、多重繼承與虛擬繼承
- 五、RTTI 與動態轉型
- 六、匯編觀察:動態呼叫流程
- 七、攻擊面:vtable 劫持
- 八、防禦技術與最佳實踐
- 九、常見問答與誤區
- 十、參考資源
多型概念速查
類型 | 說明 | 例子 |
---|---|---|
編譯期多型 | 透過 函式多載 與 模板 (Templates),在編譯階段解析呼叫。 | void print(int); void print(std::string); |
執行期多型 | 透過 虛函式 (virtual function) 與 動態繫結 (dynamic dispatch) 在執行期決定呼叫目標。 | Base* p = new Derived; p->foo(); |
一句話記憶:虛函式 + 指標/參考 ⇒ 執行期多型。
提示:虛函式是實現執行期多型的關鍵。
為何需要執行期多型?
執行期多型是 C++ 物件導向設計的基石,因為它能:
- 抽象接口:讓使用者僅依賴基底類別,降低耦合。
- 開放/封閉原則:允許在不修改既有程式碼的情況下擴充行為。
- 策略模式、工廠模式 等 OO 設計模式核心。
虛函式、vptr 與 vtable
名詞定義
名詞 | 作用 | 位址範圍 | 記憶體位置 |
---|---|---|---|
vtable (virtual table) | 儲存此類別所有虛函式的函式指標陣列。 | 文字區 (R/O) 或資料區,依 ABI 而定。 | 通常位於唯讀區 |
vptr (virtual pointer) | 每個物件的隱藏成員,指向對應類別的 vtable。 | 物件首部 (GCC/Clang Itanium);MSVC 可在首部或尾部視繼承情況。 | 物件內部 |
簡易範例:單一繼承
class Base {public: virtual void speak() { std::cout << "Base\n"; } virtual ~Base() {} // 虛擬解構子,確保資源正確釋放,避免洩漏};class Derived : public Base {public: void speak() override { std::cout << "Derived\n"; }};
- 編譯器生成
Derived
的 vtable:[Derived::speak, Base::~Base]
。 Derived
物件:[vptr] [其餘資料成員…]
。- 呼叫
p->speak()
底層步驟:- 讀取
p
第一欄位 vptr。 - 讀取 vptr[0] 的函式位址。
jmp/rjmp
至該位址。
- 讀取
性能成本:多一次間接尋址(指標解參照);現代 CPU 分支預測佳,開銷約 1–2ns。若函式本身複雜,影響可忽略。
編譯器實作差異
編譯器 | ABI | vtable 位置 | vptr 名稱 | 其他特點 |
---|---|---|---|---|
GCC / Clang | Itanium C++ ABI | .rodata (只讀區) | __vptr$Class (ID 尾綴) | 支援 thunk 修正 this 指標偏移。 |
MSVC | Microsoft ABI | .rdata or .data | ??_7Class@@6B@ | 每個虛基類可能額外有 vbtable 虛基表。 |
建議:跨編譯器逆向時,先確認 ABI 差異,再找 vtable。IDA/Ghidra 的 auto-analysis 會標示
vftable
,vftable_ref
等符號。
ABI 重要性:ABI(Application Binary Interface)定義了二進位層級的相容性,逆向工程時必須關注 ABI 差異以正確解析 vtable 和 vptr。
多重繼承與虛擬繼承
注意:這部分較為複雜,建議初學者先跳過,待熟悉單一繼承後再回頭學習。
class A { virtual void fa(); };class B { virtual void fb(); };class C : public A, public B { void fa() override; void fb() override; };
C
物件內部佈局(Itanium ABI 簡化示意):
+0 vptr_A -> vtable_C_for_A (offset-to-top = 0)+8 vptr_B -> vtable_C_for_B (offset-to-top = -8)
offset-to-top
是 thunk 修正this
回到物件實際起始位址。MSVC 以兩張獨立 vtable + vfptr adjustor 實現。
虛擬繼承 (virtual inheritance) 與菱形問題
class VBase { ... };class A : virtual public VBase { ... };class B : virtual public VBase { ... };class C : public A, public B { ... };
- 編譯器增設 vbptr (虛基指標) 指向 vbtable,用於計算虛基類的偏移。
- 物件大小增長,存取虛基成員多一次間接級。
鑽石問題:多重繼承可能導致重複繼承,虛擬繼承能解決此問題(建議搭配圖示說明)。
RTTI 與動態轉型
功能 | 說明 | 重要細節 | 使用時機 |
---|---|---|---|
typeid(expr) | 執行期取得 std::type_info & 類名。 | 需至少一個虛函式 (有 vtable),否則回傳靜態類型。 | 檢查物件實際型別 |
dynamic_cast<T*>(ptr) | 安全向下轉型,失敗回傳 nullptr 。 | 透過 vtable 內的 RTTI 結構 比對。 | 在繼承層次中安全轉型 |
逆向小技巧:觀察 vtable + RTTI 結構 (符號如
__RTTI_Type_Descriptor
) 可以迅速鎖定類名(建議搭配 IDA Pro 截圖展示)。
匯編觀察:動態呼叫流程
以 GCC 14 x86-64 為例:
mov rax, QWORD PTR [rdi] ; 讀取 vptr (rdi 是 this 指標)mov rax, QWORD PTR [rax] ; 從 vtable 取出 speak() 的位址call rax ; 呼叫 Derived::speak
- 若多重繼承,可能有 thunk:
add rdi, -0x8 ; 修正 this 指標偏移jmp Derived::fb
步驟解說:
[rdi]
取出 vptr。[rax]
從 vtable 取函式位址。call
跳轉執行。
thunk 作用:在多重繼承中調整 this 指標,確保指向正確子物件。
攻擊面:vtable 劫持
警告:本節內容僅用於教育目的,切勿用於非法活動。
手法 | 條件 | 成功效果 |
---|---|---|
Use‑After‑Free (UAF) | 可控制已釋放物件,再分配攻擊者資料覆寫 vptr。 | 呼叫虛函式跳到惡意程式碼。 |
Buffer Overflow | vptr 與其他資料在同一 buffer 內,溢出覆寫。 | 類似劫持。 |
Type Confusion | 錯誤 cast 造成以錯誤 vtable 呼叫。 | arbitrary code execution / logic flaw |
POC 簡例
struct Cmd { virtual void run() { puts("run"); } };void exploit() { Cmd* p = new Cmd(); delete p; // 釋放物件,造成 UAF char* buf = new char[sizeof(Cmd)]; // 重新分配記憶體 *(uintptr_t*)buf = (uintptr_t)evil_vtable; // 覆寫 vptr p->run(); // 呼叫虛函式,跳轉到惡意程式碼}
註解:
delete p
釋放記憶體但未置空指標。buf
覆蓋原物件位置,控制 vptr。p->run()
觸發劫持。
防禦技術與最佳實踐
類型 | 技術 | 說明 | 適用場景 |
---|---|---|---|
編譯期 | -fstack-protector , -fcontrol-flow-guard , /guard:cf | 生成 CFG 表;執行期檢查 vtable 目標是否在許可集。 | 保護堆疊、控制流 |
執行期 | ASLR, DEP, CET (Shadow Stack) | 分散位址、阻止可寫可執行、保護返回位址。 | 系統級防護 |
語言層 | final , override , 智能指標 (std::unique_ptr ) | 防止未預期覆寫;自動管理生命週期,避免 UAF。 | 代碼級防護 |
實務建議:
- 使用 RAII 和智能指標(如
std::unique_ptr
),自動管理資源,有效防止 UAF 漏洞。- 開啟所有硬體/編譯器防禦(
/guard:cf
,-fcf-protection
)。- 多型僅用於必要抽象;效能敏感路徑考慮
final
或 CRTP (Curiously Recurring Template Pattern)。
常見問答與誤區
問題 | 解答 |
---|---|
Q: virtual 只在 header 宣告就好嗎? | 是。virtual 關鍵字僅需在第一處宣告即可,定義處可省略。 |
Q: 物件沒有虛函式就沒有 vtable? | 正確。無虛函式則無 vptr/vtable。 |
Q: 關閉 RTTI 可增添安全? | 部分。減少類名曝光,但無法阻止 vtable 劫持。 |
Q: dynamic_cast 很慢? | 取決於繼承複雜度;常數次 table lookup,微測約 10–50ns,通常可接受。 |
Q: 虛函式和純虛函式的區別是什麼? | 純虛函式必須在子類別中實作,否則該類別為抽象類別。 |
Q: 為何不建議在建構式中呼叫虛函式? | 建構期間物件型別尚未完全形成,呼叫虛函式可能導致未定義行為。 |
參考資源
- Itanium C++ ABI:https://itanium-cxx-abi.github.io/cxx-abi/abi.html
- Microsoft PE/COFF ABI:MSDN 文件
- 《Deep C++》Chapter 6 — Virtual Table
- IDA Pro / Ghidra 官方文件:RTTI 與 Class Structure Recovery
- 安全研究:《VTable Hijacking Revisited》(BlackHat 2023)
- 入門資源:
- 線上課程:Coursera 的「C++ For C Programmers」
- 教學影片:YouTube 上的「C++ Polymorphism Explained」
IMPORTANT再次感謝,馬斯克創立的AI〖Grok〗幫我整理文章。(時代的進步真好ಥ_ಥ)
🎓【C++】多型與Vtable|六周目
https://illumi.love/posts/指南向/多型與vtable完全指南/