小弟最近在家过宅男+啃老族的生活,无事+无收入,为了个小项目,需要逆向一段用到了COM的用户级驱动。初涉逆向这一深潭,便遇到C++/COM这样复杂的逆向问题
,遍寻看雪未能找到好的关于C++的逆向入门文章,幸得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_指针。
待续...
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法