环境:Windows SP3,VC++6.0
现在很多程序都是用C++写的,要写一个安全又高效的C++程序或者逆向一个用C++编译的程序首先就要知道C++对象在内存是怎么布局的。要声明的一点是,这里的程序没有使用RTTI,所以不太复杂。
在这里首先要说的一点是,C++程序会大量得使用EXC寄存器,因为ECX是用来传递this指针的。
class A
{
public:
A(){ x = 0xAA;}
~A(){}
public:
void func(){ x+= 0xF; }
int getX(){ return x; }
virtual void vfunc1(){ printf("A::vfunc1/n"); }
virtual void vfunc2(){ printf("A::vfunc2/n"); }
private:
int x;
};
class B : public A
{
public:
B(){ x = 0xBB; }
~B(){}
virtual void vfunc1(){ printf("B::vfunc1/n"); }
virtual void fff(){}
private:
int x;
};
class C
{
public:
C(){ x = 0xCC; }
~C(){}
virtual void vfunc1(){ printf("C::vfunc1/n"); }
private:
int x;
};
class D : public C, public A
{
public:
D(){ x = 0xDD; }
~D(){}
virtual void aaa(){}
private:
int x;
};
class E
{
int x;
};
1.new函数
其实new和malloc一样,在内存分配时都调用的是_nh_malloc_dbg,所以在内存分配上效果是一样的。你可能会说,new在分配内存时会 执行类的构造函数,而malloc不会,其实VC++6.0在这主面不是通过new实现的,而是在编译时就确定好了汇编代码,当new执行完后,若成功则 直接执行该类的构造函数。
2.delete函数
我们都知道,delete在释放对象前会调用对象的析构函数,然后释放空间,看了上面new的叙述,你可能会以为delete就是编译器确定汇编代码,先 析构对象,再释放对象。其实不是这样的,在delete时,这些操作是封装在一起的,而不是像new一样是分开的(但不知道为什么不把new做得像 delete一样,看着也好看,无语......)。比如对于类A的对象a,在delete a时,编译会转换下这个样子:call @ILT+10(A::`scalar deleting destructor'),在这个函数里,先调用了A的析构函数,然后又调用的delete函数,在delete函数里,调用了free函数。
下面看一段用new和delete的代码:
195: A *a = new A;
00401B4D push 8 ;sizeof(A) = 8
00401B4F call operator new (00409310) ;调用new
00401B54 add esp,4 ;恢复栈
00401B57 mov dword ptr [ebp-18h],eax ;把new的返回值存到一个局部变量里
00401B5A mov dword ptr [ebp-4],0 ;设置SEH的trylevel
00401B61 cmp dword ptr [ebp-18h],0 ;检测new是否成功
00401B65 je main+54h (00401b74) ;new不成功的话就不执行构造函数了
00401B67 mov ecx,dword ptr [ebp-18h] ;把new得到的首地址放到eax里,实际上就是当前对象的this指针
00401B6A call @ILT+50(A::A) (00401037) ;调用A类的构造函数,this指针会通过ecx传进去
00401B6F mov dword ptr [ebp-24h],eax ;把构造函数的返回值存到一个局部变量里,其实就是this指针
00401B72 jmp main+5Bh (00401b7b) ;new成功会走这一句
00401B74 mov dword ptr [ebp-24h],0 ;new不成功会走这一句
00401B7B mov eax,dword ptr [ebp-24h] ;用eax保存构造函数的返回值
00401B7E mov dword ptr [ebp-14h],eax ;把eax放到个局部变量里
00401B81 mov dword ptr [ebp-4],0FFFFFFFFh ;设置SEH的trylevel
00401B88 mov ecx,dword ptr [ebp-14h] ;从刚才保存的那个局部变量里取出this指针
00401B8B mov dword ptr [ebp-10h],ecx ;把this指针的值赋给a
196:
197: delete a;
00401B8E mov edx,dword ptr [ebp-10h] ;取出a的值
00401B91 mov dword ptr [ebp-20h],edx
00401B94 mov eax,dword ptr [ebp-20h]
00401B97 mov dword ptr [ebp-1Ch],eax
00401B9A cmp dword ptr [ebp-1Ch],0 ;检验a是否为空
00401B9E je main+8Fh (00401baf) ;如果a为空,就不执行delete操作
00401BA0 push 1 ;只是一个标志
00401BA2 mov ecx,dword ptr [ebp-1Ch] ;this指针
00401BA5 call @ILT+10(A::`scalar deleting destructor') (0040100f) ;这个函数会先调用析构函数,再调用delete
00401BAA mov dword ptr [ebp-28h],eax ;保存delete的返回值
00401BAD jmp main+96h (00401bb6)
00401BAF mov dword ptr [ebp-28h],0
198: }
3.构造函数和析构函数
从上面的代码,我们就可以看出来,在调用构造函数前必须要把对象的this指针放到ECX里去,下面看一下类A的构造函数:
22: A::A()23: {
004012F0 push ebp
004012F1 mov ebp,esp
004012F3 sub esp,44h
004012F6 push ebx
004012F7 push esi
004012F8 push edi
004012F9 push ecx ;注意,这里会保护ECX,因为下面要用ECX当循环变量
004012FA lea edi,[ebp-44h]
004012FD mov ecx,11h
00401302 mov eax,0CCCCCCCCh
00401307 rep stos dword ptr [edi]
00401309 pop ecx ;循环完后,把ECX取出,ECX不是在A()里被初始化的,说明这个值需要外部调用函数传一个正确的值,也就是要初始化的对象的this指针
0040130A mov dword ptr [ebp-4],ecx ;把this指针放到个局部变量里,先存起来
0040130D mov eax,dword ptr [ebp-4]
00401310 mov dword ptr [eax],offset A::`vftable' (00432020) ;初始化虚函数表,如果没有虚函数,则没这一句
24: x = 0xAA;
00401316 mov ecx,dword ptr [ebp-4]
00401319 mov dword ptr [ecx+4],0AAh ;对成员变量的赋值,是通过对this指针的偏移来实现的
25: }
00401320 mov eax,dword ptr [ebp-4]
00401323 pop edi
00401324 pop esi
00401325 pop ebx
00401326 mov esp,ebp
00401328 pop ebp
00401329 ret
析构函数的实现与构造函数是类似的。
4.成员函数
成员函数的调用也需要先把该对象的this指针存入ECX里,和构造函数挺像的,所以如果只给你一个汇编出来的C++程序,很可能会出现误把成员函数当做构造函数。
196: A a;
00401B9D lea ecx,[ebp-14h]
00401BA0 call @ILT+45(A::A) (00401032) ;调用构造函数
201: a.func();
00401BD0 lea ecx,[ebp-14h]
00401BD3 call @ILT+10(A::func) (0040100f) ;如果只是call 0040100f,是不是很容易当成构造函数?
5.虚函数和虚函数表
C++为了实现继承和多态,所以使用了虚函数这个概念。当一个类没有虚函数时,它的对象里存的就只是该类的成员变量,有多少变量,这个类就占多大内存;而 如果当一个类有虚函数时,它的this指针的0偏移处存的是该对象的虚函数表的地址,然后下面才存的是变量;如果当一个类继承自一个有虚函数的基类时,它 也先有继承该基类的虚函数表的地址和变量然后才是自己的变量;如果该子类有一个自己的虚函数时,它会把该虚函数的地址跟在它继承来的虚函数表的后面;如果 当一个类多继承自多个在虚函数的大基类时,它的内存布局会是(地址由低到高):继承列表中第一个类的虚函数表的地址和变量,继承列表中第二个类的虚函数表 的地址和变量,......,自己的变量;如果该子类也有一个自己的虚函数时,它为把这个虚函数的地址跟到第一个虚函数表的后面。
看了这些,现在看看我上面写的几个类的内存是如何布局的吧:
Object a(0x12FF6C): //a的地址
0x12FF6C : 0x433020 //a的虚函数表的地址
0x12FF70 : 0xAA //a的成员变量x
Object a(0x12FF6C) _vftable:
1 0x433020 : 0x40101E //vfunc1的地址
2 0x433024 : 0x40100A //vfunc2的地址
Object b(0x12FF60):
0x12FF60 : 0x433060
0x12FF64 : 0xAA
0x12FF68 : 0xBB
Object b(0x12FF60) _vftable:
1 0x433060 : 0x401019 //B覆盖的vfunc1
2 0x433064 : 0x40100A //A原来的vfunc2
3 0x433068 : 0x40105A //B添的fff
Object c(0x12FF58):
0x12FF58 : 0x433094
0x12FF5C : 0xCC
Object c(0x12FF58) _vftable:
1 0x433094 : 0x401014 //C只有一个虚函数vfunc1
Object d(0x12FF44):
0x12FF44 : 0x4330D0 //C的虚函数表的地址
0x12FF48 : 0xCC //C的变量
0x12FF4C : 0x4330C0 //A的虚函数表的地址
0x12FF50 : 0xAA //A的变量
0x12FF54 : 0xDD
Object d(0x12FF44) _vftable:
1 0x4330D0 : 0x401014 //C原来的vfunc1
2 0x4330D4 : 0x401069 //D添的aaa
Object d(0x12FF4C) _vftable: //A原来的虚函数表
1 0x4330C0 : 0x40101E
2 0x4330C4 : 0x40100A
Object e(0x12FF40):
0x12FF40 : 0xCCCCCCCC //只有一个变量,没有虚函数
这里还要说明的一个问题是,D继承自C和A,而C和A都有一个不同的虚函数vfunc1,所以D会有两个vfunc1,如果在写代码时写成了d.vfunc1();这会在编译时产生错误:
error C2385: 'D::vfunc1' is ambiguous
不过这个是很好理解的。
至此,我们已经大致了解了基本的C++反汇编后是一个什么样子了。
附:
输出上面的内存布局的程序(类定义在最前面,程序中使用的常量都是经验值,在VC++6.0上没问题,在其它编译器上就不敢保证了 ):
void printVftableAddr(const char *varName, void *a)
{
int *x = (int *)a;
int *y = (int *)(*x);
if((int)y <= 0x0000ffff)
{
printf("Object %s(0x%X) doesn't have a _vftable/n", varName, a);
return;
}
printf("Object %s(0x%X) _vftable:/n", varName, a);
for(int i = 0; i < 10; i++)
{
if(*(y + i) == NULL || *(y + i) >= 0x500000)
{
break;
}
printf("/t%d/t0x%X : 0x%X/n", i + 1, y + i, *(y + i));
}
}
void printObjectAddr(const char *varName, void *a, int size)
{
int i = 0;
printf("Object %s(0x%X):/n", varName, a);
for(i = 0; i < size / 4; i++)
{
printf("/t0x%X : 0x%X/n", (int*)a + i, *((int*)a + i));
}
int *x = (int *)a;
for(i = 0; i < size / 4; i++, x++)
{
if(*x <= 0x400000)
{
continue;
}
printVftableAddr(varName, x);
}
}
int main(int argc, char* argv[])
{
A a;
B b;
C c;
D d;
E e;
printObjectAddr("a", &a, sizeof(a));
printObjectAddr("b", &b, sizeof(b));
printObjectAddr("c", &c, sizeof(c));
printObjectAddr("d", &d, sizeof(d));
printObjectAddr("e", &e, sizeof(e));
return 0;
}