您的位置:首页 > 编程语言 > C语言/C++

Reversing Microsoft Visual C++ Part II: Classes, Methods and RTTI

2012-11-10 20:57 661 查看
[b]逆向Microsoft Visual C++第二部分:类、方法及RTTI Reversing Microsoft Visual C++ Part II: Classes, Methods and RTTI[/b]
http://bbs.pediy.com/showthread.php?threadid=36539
Reversing enginerring with IDA pro,提及了OpenRCE.org的这的两篇文章,觉其甚好,欲译之。

原文地址:http://www.openrce.org/articles/full_view/23

逆向Microsoft Visual C++第二部分:类、方法及RTTI

摘要:省略

(第一部分:异常的处理http://www.openrce.org/articles/full_view/21)

正文:类的基本组织
在叙述后面的东西之前,先让我们看看下面的代码:

代码:
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以如下的方式组织类:

1.虚函数表指针(_vtable_ 或者 _vftable_)。只有当类中包含virtual函数或者基类中没有合适的函数表可以重用才会增加此指针

2.基类

3.类成员

虚函数表以虚方法首先出现的顺序组织函数地址。重载的函数地址替换了基类中被重载函数的地址。

由此,我们那三个函数将被组织成下面的样子:

代码:
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的一个未公开(undocumented)的开关(参数)生成的。

开关这样用:

要想看单个类组织用:-d1reportSingleClassLayout (译注:这个我没试出来

)

要想看所有类(包括内部的CRT中的类)的组织:-d1reportAllClassLayout(译注:这个是可以的)。类的组织被输出到stdout上。

如你所见,C类有两个虚函数表,因为它所继承的两个类都有虚函数,在C类的A的虚函数表中,C::A_virt2的地址替换了A::A_virt2的地址;同样在另一表中,C::B_virt2替换了B::B_virt2

调用规约(译注:convention怎么翻译好?

)和类方法

默认情况下,所有的类方法都使用_thiscall_规约。类的实例地址(_this_指针)通过ecx寄存器作为隐含参数被传递(译注:之前我RE的时候大量发现用ecx传递某个指针,不知其为何物,疑VC使用ECX传递指针以优化,幸得高人指点:ecx经常被作为this指针传递)。在函数体中,它经常又被隐藏于寄存器(如esi,edi)中,和/或堆栈变量中。其他的任何类成员的寻址都通过那个寄存器和/或堆栈变量完成。然而,当实现一个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]  ;得到虚函数表指针
mov ecx, esi
call [eax+4]  ;调用第二个虚方法

;pC->B_virt1()
;edi = pC
lea edi, [esi+8] ;调整这个指针
mov eax, [edi]   ;获取虚函数表
mov ecx, edi
call [eax]       ;调用第一个虚方法


4)构造与析构函数

构造函数与析构函数与简单函数类似:它们得到一个隐含的_this_指针作为参数(例如在_thiscall_规约下,通过ecx得到)。尽管正常情况下,构造函数没有返回值,但构造函数的确通过eax返回_this_指针。


RTTI(运行时类型鉴别)的实现


RTTI实现

RTTI是一种由编译器生成的特殊的信息,它用来支持像dynamic_cast<> 和typeid()这样的C++操作符,以及C++的异常。由于它的成因,所以,编译器只为多态的类,也就是有虚函数的类,生成RTTI信息。

MSVC编译器将一个结构体指针放在虚函数表之前,称之为“完整对象定位符(Complete Object Locator,COL)”。之所以这样称谓,是因为它允许编译器从特定的vftable指针(因为一个类可能有多个)找到完整的对象的位置。COL看起来像下面这样:

代码:
struct RTTICompleteObjectLocator
{
DWORD signature; //总是0 ?
DWORD offset;    //这个vftable在整个类中的偏移
DWORD cdOffset;  //构造函数位移的偏移
struct TypeDescriptor* pTypeDescriptor; //整个类的类型描述符
struct RTTIClassHierarchyDescriptor* pClassDescriptor; //描述继承关系(hierarchy)
};


类关系描述符描述了类的继承关系。它由类的所有COL所共享。

代码:
struct RTTIClassHierarchyDescriptor
{
DWORD signature;      //总是 zero?
DWORD attributes;     //位0置位 = 多重继承(multiple inheritance), 位1置位 = 虚继承(virtual inheritance)
DWORD numBaseClasses; //pBaseClassArray中类的数量
struct RTTIBaseClassArray* pBaseClassArray;
};


基类数组描述所有的基类及一些信息,这些信息,在执行_dynamic_cast_操作符的时候让编译得以将继承类转成任一基类。每一个入口(基类描述符)有如下的结构:

代码:
struct RTTIBaseClassDescriptor
{
struct TypeDescriptor* pTypeDescriptor; //类的类型描述符
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;  //成员偏移
int pdisp;  //vbtable偏移
int vdisp;  //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关系看起来像这样:

原图地址:http://www.openrce.org/articles/img/igor2_rtti1.gif



提取信息

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创建的对象,unwindfunclet确保了万一构造失败也能删除分配的内存:

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 memoryallocation 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
;

delete [] pA

翻译成下面的伪码:

array = new char(sizeof(A)*n+sizeof(int))

if (array)

{

*(int*)array=n; //store array size in the beginning

'eh vector constructoriterator'(array+sizeof(int),sizeof(A),count,&A::A,&A::~A);

}

pA = array;

'eh vectordestructor 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

push3

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 thispointer

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]。

附录 Ims_rtti4.idc

这是我写的解析RTTI和vtfable的脚本。你可以从Microsoft VC++ ReversingHelpers打包下载我的两篇文章和脚本。这个脚本的特点包括:

解析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], offsetconst CContentMenuItem::'vftable'{for 'CContentMenuItem'}

mov dword ptr [edi], offsetconst 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: __thiscallCContentMenuItem::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_ptr4C]

mov [this+CContentMenuItem._CDownloader._vfptr], offset constCContentMenuItem::'vftable'{for 'CContentMenuItem'}

mov [edi+CNativeEventSource._vfptr], offset constCContentMenuItem::'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_ptr5C]

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 constCEventSinkList::'vftable'

mov eax, this

pop this

retn

public: __thiscallCContentMenuItem::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 andexception 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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: