Reversing MS VC++ Part II: Classes, Methods and RTTI
摘要
MS VC++ 是Win32平臺上最廣泛使用的編譯器,因此熟悉它的內部工作機制對于Win32逆向愛好者非常重要。能夠理解編譯器生成的附加(glue)代碼有助于快速理解程序員寫的實際代碼。同樣也有助于恢復程序的高級結構。
在Part II中,我將講到MSVC是如何實現C++的,包括類的布局,虛函數,RTTI。假設你已經熟悉C++基本知識和匯編語言。
基本的類布局
為了解釋下面的內容,讓我們看看這個簡單例子:
class A
{
int a1;
public:
virtual int A_virt1();
virtual int A_virt2();
static void A_static1();
void A_simple1();
};
class B
{
int b1;
int b2;
public:
virtual int B_virt1();
virtual int B_virt2();
};
class C: public A, public B
{
int c1;
public:
virtual int A_virt2();
virtual int B_virt2();
};
多數情形下,MSVC的類按如下格局分布:
Ÿ 指向虛函數表的指針(_vtable_或_vftable_),不過它只在類包括虛函數,以及不能從基類復用合適的函數表時才會被添加。
Ÿ 基類。
Ÿ 函數成員。
虛函數表由虛函數的地址組成,表中函數地址的順序和它們第一次出現的順序(即在類定義的順序)一致。若有重載的函數,則替換掉基類函數的地址。
因此,上面三個類的布局看起來象這樣:
class A size(8):
+---
0 | {vfptr}
4 | a1
+---
A's vftable:
0 | &A::A_virt1
4 | &A::A_virt2
class B size(12):
+---
0 | {vfptr}
4 | b1
8 | b2
+---
B's vftable:
0 | &B::B_virt1
4 | &B::B_virt2
class C size(24):
+---
| +--- (base class A)
0 | | {vfptr}
4 | | a1
| +---
| +--- (base class B)
8 | | {vfptr}
12 | | b1
16 | | b2
| +---
20 | c1
+---
C's vftable for A:
0 | &A::A_virt1
4 | &C::A_virt2
C's vftable for B:
0 | &B::B_virt1
4 | &C::B_virt2
上面的圖表是由VC8編譯器使用一個未公開的參數生成。為了看到這樣的類布局,使用編譯參數 –d1 reportSingleClassLayout,可以輸出單個類的布局。-d1 reportAllClassLayout可以輸出全部類的布局(包括內部的CRT類)。這些內容都被輸出到stdout(標準輸出)。
正如你看到的,C有兩個虛函數表vftables,因為它從兩個都有虛函數的類繼承。C::A_virt2的地址替換了A::A_virt2在類C虛函數表的地址,類似的,C::B_virt2替換了B::B_virt2。
調用慣例和類方法
MSVC中所有的類方法都默認使用_thiscall_調用慣例。類實例的地址(_this_指針)作為隱含參數傳到ecx寄存器。在函數體中,編譯器通常立刻用其它寄存器(如esi或ed),或棧中變量來指代。以后對類成員的引用都通過這個寄存器或棧變量。然而,在實現COM類時,則使用_stdcall_調用習慣。下文是對各種類型的類方法的一個概述。
1) 靜態方法
調用靜態方法不需要類的實例,所以它們和普通函數一樣的工作原理。沒有_this_指針傳入。因此也就不可能可靠的分辨靜態方法和簡單的普通函數。例如:
A::A_static1();
call A::A_static1
2) 簡單方法
簡單方法需要一個類實例,_this_指針隱式的作為第一個參數傳入,通常使用_thiscall_調用慣例,例如通過_ecx_寄存器。當基類對象沒有分配在派生類對象的開始處,在調用函數前,_this_指針需要被調整到指向基類子對象的實際開始位置。例如:
;pC->A_simple1(1);
;esi = pC
push 1
mov ecx, esi
call A::A_simple1
;pC->B_simple1(2,3);
;esi = pC
lea edi, [esi+8] ;adjust this
push 3
push 2
mov ecx, edi
call B::B_simple1
正如你看到的,在調用B的方法前,_this_指針被調整到指向B的子對象。
3) 虛方法(虛函數)
為了調用虛函數,編譯器首先需要從_vftable_取得函數地址,然后就像調用簡單方法一樣(例如,傳入_this_指針作為隱含參數)。例如:
;pC->A_virt2()
;esi = pC
mov eax, [esi] ;fetch virtual table pointer
mov ecx, esi
call [eax+4] ;call second virtual method
;pC->B_virt1()
;edi = pC
lea edi, [esi+8] ;adjust this pointer
mov eax, [edi] ;fetch virtual table pointer
mov ecx, edi
call [eax] ;call first virtual method
4) 構造函數和析構函數
構造函數和析構函數類似于簡單方法,它們取得隱式的_this_指針(例如,在_thiscall_調用慣例下通過ecx寄存器)。雖然形式上構造函數沒有返回值,但它在eax中返回_this_指針。
RTTI實現
RTTI(Run-Time Type Identification)運行時類型識別是由編譯器生成的特殊信息,用于支持像dynamic_cast<>和typeid()這樣的C++運算符,以及C++異常。基于這個本質,RTTI只為多態類生成,例如帶虛函數的類。
MSVC編譯器在vftable前設置了一個指針,指向叫做“Complete Object Locator”(完整對象定位器)的結構。這樣稱呼是因為它允許編譯器從特定的vftable指針(因為一個類可能有若干vftable)找到完整對象的位置。COL就像如下定義:
struct RTTICompleteObjectLocator
{
DWORD signature; //always zero ?
DWORD offset; //offset of this vtable in the complete class
DWORD cdOffset; //constructor displacement offset
struct TypeDescriptor* pTypeDescriptor; //TypeDescriptor of the complete class
struct RTTIClassHierarchyDescriptor* pClassDescriptor; //describes inheritance hierarchy
};
類層次描述符描述了類的繼承層次。對于一個類,所有COL共享一個。
struct RTTIClassHierarchyDescriptor
{
DWORD signature; //always zero?
DWORD attributes; //bit 0 set = multiple inheritance, bit 1 set = virtual inheritance
DWORD numBaseClasses; //number of classes in pBaseClassArray
struct RTTIBaseClassArray* pBaseClassArray;
};
基類數組描述了所有基類,并包含在執行_dynamic_cast_過程中編譯器是否允許強制轉換派生類到這些基類的信息。基類描述符中每一項都包含如下結構:
struct RTTIBaseClassDescriptor
{
struct TypeDescriptor* pTypeDescriptor; //type descriptor of the class
DWORD numContainedBases; //number of nested classes following in the Base Class Array
struct PMD where; //pointer-to-member displacement info
DWORD attributes; //flags, usually 0
};
struct PMD
{
int mdisp; //member displacement
int pdisp; //vbtable displacement
int vdisp; //displacement inside vbtable
};
PMD描述了一個基類如何放置在完整類中。在簡單的繼承體系中,它位于從整個對象起始位置的一個固定偏移處,這個偏移量就是_mdisp_。如果是一個虛基類,那需要從vbtable中取得一個額外的偏移量加上。從派生類到基類調整_this_指針的偽碼如下:
//char* pThis; struct PMD pmd;
pThis+=pmd.mdisp;
if (pmd.pdisp!=-1)
{
char *vbtable = pThis+pmd.pdisp;
pThis += *(int*)(vbtable+pmd.vdisp);
}
例如,我們的三個類的RTTI層次關系是:
RTTI hierarchy for our example classes
提取信息
1) RTTI
如果存在,RTTI對于逆向工作來說是無價的信息。從RTTI,有可能恢復類的名字,繼承層次,有時候也能恢復部分的類布局。我的RTTI掃描器腳本可以顯示大多數此類信息。(參考附錄I)
2) 靜態和全局初始化例程(initializer)
全局和靜態對象需要在main主程序開始前初始化。MSVC通過生成初始化例程函數(funclet)來實現,并把這些函數地址放入一個表中,當_cinit初始化CRT時,會調用它們。這個表通常位于.data段的開始。一個典型的初始化例程如下:
_init_gA1:
mov ecx, offset _gA1
call A::A()
push offset _term_gA1
call _atexit
pop ecx
retn
_term_gA1:
mov ecx, offset _gA1
call A::~A()
retn
從這個表我們可以找到:
· 全局/靜態對象的地址
· 它們的構造函數
· 它們的析構函數
還可以參考MSVC _#pragma_directive_init_seg_[5]。
3) Unwind Funclets
若在函數中創建了自動類型的對象,VC++編譯器會自動生成異常處理代碼以確保在異常發生時會刪除這些對象。請參看Part I以了解對C++異常實現的細節。一個典型的unwind funclet在棧上銷毀一個對象的過程是:
unwind_1tobase: ; state 1 -> -1
lea ecx, [ebp+a1]
jmp A::~A()
通過在函數體中尋找相反的狀態變化,或者是在第一次訪問相同的棧中變量時,我們也可以找到構造函數。
lea ecx, [ebp+a1]
call A::A()
mov [ebp+__$EHRec$.state], 1
對于由new創建的對象,unwind funclet確保了萬一構造失敗也能刪除分配的內存:
unwind_0tobase: ; state 0 -> -1
mov eax, [ebp+pA1]
push eax
call operator delete(void *)
pop ecx
retn
在函數體中:
;A* pA1 = new A();
push
call operator new(uint)
add esp, 4
mov [ebp+pA1], eax
test eax, eax
mov [ebp+__$EHRec$.state], 0; state 0: memory allocated but object is not yet constructed
jz short @@new_failed
mov ecx, eax
call A::A()
mov esi, eax
jmp short @@constructed_ok
@@new_failed:
xor esi, esi
@@constructed_ok:
mov [esp+14h+__$EHRec$.state], -1
;state -1: either object was constructed successfully or memory allocation failed
;in both cases further memory management is done by the programmer
另一種類型的unwind funclets用于構造函數和析構函數中。它確保了萬一發生異常時刪除類成員。這時候,funclets要使用保存在一個棧變量的_this_指針,
unwind_2to1:
mov ecx, [ebp+_this] ; state 2 -> 1
add ecx, 4Ch
jmp B1::~B1
這是funclet析構類型B1位于偏移4Ch處一個類成員的代碼。從這里我們可以找到:
· 棧變量代表了C++對象或者指向用new分配的對象的指針
· 它們的構造函數
· 它們的析構函數
· 由new創建的對象的大小
4) 構造/析構函數的遞歸調用
規則很簡單:構造函數調用其他的構造函數(其他基類和成員變量的構造函數),析構函數調用其它的析構函數。一個典型的構造函數按下列順序執行:
· 調用基類構造函數
· 調用復雜的類成員的構造函數
· 若類有虛函數,初始化vfptr
· 執行當前的構造函數代碼(即由程序員寫得構造代碼)
典型的析構函數幾乎按照反序執行:
· 若有虛函數,初始化vfptr
· 執行當前的析構函數代碼
· 調用復雜類成員的析構函數
· 調用基類的析構函數
MSVC生成的析構函數另一個獨特的特征是它們的_state_變量通常初始化為最大值,每次析構一個子對象就減一,這樣使得識別它們更容易。要注意簡單的構造/析構函數經常被MSVC內聯(inline)。那就是為什么你經常看到vftable指針在同一個函數中被不同指針重復的調用。
5) 數組的構造和析構
MSVC使用一個輔助函數來構造和析構數組。思考下面的代碼:
A* pA = new A[n];
delete [] pA
翻譯成下面的偽碼:
array = new char(sizeof(A)*n+sizeof(int))
if (array)
{
*(int*)array=n; //store array size in the beginning
'eh vector constructor iterator'(array+sizeof(int),sizeof(A),count,&A::A,&A::~A);
}
pA = array;
'eh vector destructor iterator'(pA,sizeof(A),count,&A::~A);
如果A有一個vftable,當刪除數組時,相應的會以調用一個刪除析構函數的向量來替代:
;pA->'vector deleting destructor'(3);
mov ecx, pA
push 3 ; flags: 0x2=deleting an array, 0x1=free the memory
call A::'vector deleting destructor'
若A的析構函數是虛函數,則按照調虛函數的方式調用:
mov ecx, pA
push 3
mov eax, [ecx] ;fetch vtable pointer
call [eax] ;call deleting destructor
因此,從向量構造/析構函數疊代子調用我們可以知道:
· 對象數組的地址
· 它們的構造函數
· 它們的析構函數
· 類的大小
6) 刪除析構函數
當類有虛析構函數時,編譯器生成一個輔助函數來刪除它。其目的是當析構一個類時確保_delete_操作符被調用。刪除析構函數的偽碼如下:
virtual void * A::'scalar deleting destructor'(uint flags)
{
this->~A();
if (flags&1) A::operator delete(this);
};
這個函數的地址被放入vftable替換析構函數地址。通過這種方式,如果另外一個類覆蓋了這個虛析構函數,那么它的_delete_將被調用。然而實際代碼中_delete_幾乎不會被覆蓋,所以你通常只看到調用默認的delete()。有時候,編譯器也生成一個刪除析構函數向量,就像下面一樣:
virtual void * A::'vector deleting destructor'(uint flags)
{
if (flags&2) //destructing a vector
{
array = ((int*)this)-1; //array size is stored just before the this pointer
count = array[0];
'eh vector destructor iterator'(this,sizeof(A),count,A::~A);
if (flags&1) A::operator delete(array);
}
else {
this->~A();
if (flags&1) A::operator delete(this);
}
};
我跳過了有虛基類的類的大部分實現細節,因為它們使得事情更復雜了,而且在現實生活中很少用到。請參考Jan Gray寫的文章[1]。它已經很詳盡了,只是用匈牙利命名法有點頭痛。文章[2]描述了一個MSVC實現虛繼承的實現。更多細節還可以看MS專利[3]。
附錄 I ms_rtti4.idc
這是我寫的解析RTTI和vtfable的腳本。你可以從Microsoft VC++ Reversing Helpers打包下載我的兩篇文章和腳本。這個腳本的特點包括:
- 解析RTTI結構,用對應的類名重命名vftables
- 對于某些簡單情形,識別和重命名構造函數和析構函數
- 輸出所有的虛函數表,引用的函數,及類的層次到一個文件里
使用說明:
在第一次分析結束后,加載ms_rtti4.idc。它會問你是否想要掃描exe文件來獲得vtable。注意這可能是一個漫長的過程。即使你跳過了掃描,你還是可以手工分析vtables。若你選擇了掃描,腳本將會試著識別所有的vtables,RTTI,重命名它們,識別和重命名構造函數、析構函數。有時候它會失敗,特別是存在虛繼承時。掃描結束后,它會打開記錄了結果的文本文件。
在加載腳本后,你可以用下面的快捷鍵手動分析一些MSVC結構:
- Alt-F8 - 分析vtable。光標應該位于vtable的開始處。若有RTTI,腳本會使用類名。若沒有RTTI,你可以輸入一個類名,然后腳本將重命名vtable。若有可識別的虛析構函數,腳本也會重命名它。
- Alt-F7 - 分析FuncInfo。FuncInfo是存在于有對象分配在棧中或使用了異常處理的函數中的結構體。它的地址被傳給函數異常處理程序的_CxxFrameHandler。
mov eax, offset FuncInfo1
jmp _CxxFrameHandler
大多數情況下,它可以被IDA自動識別和分析。但我的腳本提供了更豐富的信息。你也可以用我第一篇文章中的_ehseh.idc分析所有的FuncInfo。
把光標放在FuncInfo的開始處,用快捷鍵。
- Alt-F9 - 分析ThrowInfo。ThrowInfo是_CxxThrowException用來實現_throw_操作符的一個輔助結構。它的地址是_CxxThrowException的第二個參數。
lea ecx, [ebp+e]
call E::E()
push offset ThrowInfo_E
lea eax, [ebp+e]
push eax
call _CxxThrowException
把光標放在ThrowInfo的開始處,使用該快捷鍵。腳本會分析該結構體,重復添加thrown類的名字到注釋中。它還可以識別和重命名異常的析構函數和拷貝構造函數。
附錄II:恢復一個類的實踐
我們的題目是:MSN Messenger 7.5(msnmsgr.exe版本號是7.5.324,大小7094272字節)。它使用了大量的C++,含有很多RTTI信息。讓我們考慮兩個vftable,地址分別在.0040EFD8和.0040EFE0。它們完整的RTTI結構層次如下圖:
RTTI hierarchy for MSN Messenger 7.5
所以,這兩個vftables都屬于一個類-CContentMenuItem。通過查看它的基類描述符,我們看到:
- CContentMenuItem包括三個基類-CDownloader, CNativeEventSink和CNativeEventSource。
- CDownloader包含一個基類-CNativeEventSink
- 因此CContentMenuItem直接從CDownloader, CNativeEventSink和CNativeEventSource繼承,而CDownloader從CNativeEventSink繼承。
- CDownloader位于完整對象的起始處,CNativeEventSource是在0x24偏移處。
所以我們可以得出結論,第一個vftable列出了CNativeEventSource的方法,第二個列出了CDownloader或CNatvieEventSink的方法(若干二者均沒有虛方法,CContentMenuItem將復用CNativeEventSource的vftable)。現在我們看看有什么指向了這兩個表。它們都被兩個函數引用,在.052B5E0和.052B547。(這更說明了它們都屬于同一個類)。進一步,如果我們看看在函數.052B547的開始處,_state_變量初始化為6,意味著那個函數是析構函數。因為一個類只有一個析構函數,我們可以斷定.052B5E0就是它的構造函數。讓我們看得更近些:
CContentMenuItem::CContentMenuItem proc near
this = esi
push this
push edi
mov this, ecx
call sub_4CA77A
lea edi, [this+24h]
mov ecx, edi
call sub_4CBFDB
or dword ptr [this+48h], 0FFFFFFFFh
lea ecx, [this+4Ch]
mov dword ptr [this], offset const CContentMenuItem::'vftable'{for 'CContentMenuItem'}
mov dword ptr [edi], offset const CContentMenuItem::'vftable'{for 'CNativeEventSource'}
call sub_4D8000
lea ecx, [this+50h]
call sub_4D8000
lea ecx, [this+54h]
call sub_4D8000
lea ecx, [this+58h]
call sub_4D8000
lea ecx, [this+5Ch]
call sub_4D8000
xor eax, eax
mov [this+64h], eax
mov [this+68h], eax
mov [this+6Ch], eax
pop edi
mov dword ptr [this+60h], offset const CEventSinkList::'vftable'
mov eax, this
pop this
retn
sub_52B5E0 endp
編譯器在prolog后的第一件事情就是從exc拷貝_this_指針到esi,因此隨后的地址引用都是基于esi。在初始化vfptr前,它調了兩個其它函數,一定是基類的構造函數 - 我們的例子中就是CDownloader和CNativeEventSource。進到這兩個函數中,我們可以確認第一個用CDownloader::’vftable’初始化它的vfptr,第二個用CNativeEventSource::’vftable’。我們還可以進一步看看CDownloader的構造函數-它調用了基類CNativeEventSink的構造函數。
同樣,從edi中取得傳給第二個函數的_this_指針,它指向this+24h。根據我們的類結構圖,這個地址是CNativeEventSource子對象的位置。這從另一個方向確認了調用的第二個函數是CNativeEventSource的構造函數。
調用完基類構造函數后,基類對象的vfptr都被CContentMenuItem的實現重寫了,意味著CContentMenuItem覆蓋了基類的某些虛方法(或添加了它自己的)。(如果有需要,我們可以比較這些表,查看哪些指針被改變或者添加-被添加的就是CContentMenuItem新實現的。
下面我們看看幾個在地址.04D8000的函數調用,這時ecx從this+4Ch被設置到this+5Ch - 很顯然,初始化了一些成員變量。我們如何得知那是一個編譯器生成的構造函數調用還是以程序員寫的初始化函數呢?這里有幾個提示:
- 函數使用_thiscall_調用習慣,而且是第一次訪問這些域。
- 這些域的初始化是按照地址增長的方向進行的。
為了保證我們可以查看析構函數中的unwind funclet - 那里我們可以看得為這些成員變量,編譯器生成的析構函數調用。
這個新類不包括虛函數,也就沒有RTTI,所以我們不知道它的真實名字。就叫它RefCountedPtr吧。我們已經確定,4D8000是它的構造函數。析構函數我們可以從CContentMenuItem析構函數的unwind funclet找到,它在63CCB4。
回到CContentMenuItem的構造函數,我們看得3個域初始化為0,還有一個vftable指針。這看起來像是一個成員變量內聯展開的構造函數(不是基類的,因為若是基類,就應該在繼承樹中存在)。從用到的vftable的RTTI,我們看得它是CEventSinkList模板的一個實例。
現在,我們來寫一個可能的類聲明:
class CContentMenuItem: public CDownloader, public CNativeEventSource
{
/* 00 CDownloader */
/* 24 CNativeEventSource */
/* 48 */ DWORD m_unknown48;
/* 4C */ RefCountedPtr m_ptr4C;
/* 50 */ RefCountedPtr m_ptr50;
/* 54 */ RefCountedPtr m_ptr54;
/* 58 */ RefCountedPtr m_ptr58;
/* 5C */ RefCountedPtr m_ptr5C;
/* 60 */ CEventSinkList m_EventSinkList;
/* size = 70? */
};
我們不確定在偏移48處的變量是否不是CNativeEventSource的一部分,因為在CNativeEventSource的構造函數中沒有訪問過,它很可能是CContentMenuItem的一部分。包含被重命名的方法的構造函數和類結構如下:
public: __thiscall CContentMenuItem::CContentMenuItem(void) proc near
push this
push edi
mov this, ecx
call CDownloader::CDownloader(void)
lea edi, [this+CContentMenuItem._CNativeEventSource]
mov ecx, edi
call CNativeEventSource::CNativeEventSource(void)
or [this+CContentMenuItem.m_unknown48], -1
lea ecx, [this+CContentMenuItem.m_ptr
mov [this+CContentMenuItem._CDownloader._vfptr], offset const CContentMenuItem::'vftable'{for 'CContentMenuItem'}
mov [edi+CNativeEventSource._vfptr], offset const CContentMenuItem::'vftable'{for 'CNativeEventSource'}
call RefCountedPtr::RefCountedPtr(void)
lea ecx, [this+CContentMenuItem.m_ptr50]
call RefCountedPtr::RefCountedPtr(void)
lea ecx, [this+CContentMenuItem.m_ptr54]
call RefCountedPtr::RefCountedPtr(void)
lea ecx, [this+CContentMenuItem.m_ptr58]
call RefCountedPtr::RefCountedPtr(void)
lea ecx, [this+CContentMenuItem.m_ptr
call RefCountedPtr::RefCountedPtr(void)
xor eax, eax
mov [this+CContentMenuItem.m_EventSinkList.field_4], eax
mov [this+CContentMenuItem.m_EventSinkList.field_8], eax
mov [this+CContentMenuItem.m_EventSinkList.field_C], eax
pop edi
mov [this+CContentMenuItem.m_EventSinkList._vfptr], offset const CEventSinkList::'vftable'
mov eax, this
pop this
retn
public: __thiscall CContentMenuItem::CContentMenuItem(void) endp
鏈接和參考資料
[1] http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/dnarvc/html/jangrayhood.asp
with illustrations (but in Japanese): http://www.microsoft.com/japan/msdn/vs_previous/visualc/techmat/feature/jangrayhood/
C++: Under the Hood (PDF)
[2] http://www.lrdev.com/lr/c/virtual.html
[3] Microsoft patents which describe various parts of their C++ implementation. Very insightful.
- 5410705: Method for generating an object data structure layout for a class in a compiler for an object-oriented programming language
- 5617569: Method for implementing pointers to members in a compiler for an object-oriented programming language
- 5754862: http://freepatentsonline.com/5854931.html Method and system for accessing virtual base classes
- 5297284: Method and system for implementing virtual functions and virtual base classes and setting a this pointer for an object-oriented programming language
- 5371891: Method for object construction in a compiler for an object-oriented programming language
- 5603030: Method and system for destruction of objects using multiple destructor functions in an object-oriented computer system
- 6138269: Determining the actual class of an object at run time
[4] Built-in types for compiler's RTTI and exception support.
http://members.ozemail.com.au/~geoffch@ozemail.com.au/samples/programming/msvc/language/predefined/index.html
[5] #pragma init_seg
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclang/html/_predir_init_seg.asp
posted on 2009-03-11 15:37 pear_li 閱讀(567) 評論(0) 編輯 收藏 引用 所屬分類: C++