一、类继承逆向
在C++中使用到继承,主要是为了实现多态,那么多态就必须会用到虚函数,即会产生虚表指针。
(1)父类和子类中有没用到虚函数的四种情形
1)父类和子类中都没有用到虚函数
如果父类和子类中都没有用到虚函数,那么子类中就只是继承了父类中的成员变量和成员函数,当然还得视父类中成员变量和成员函数的公有私有性质和继承方式而定,在此继承中有一种特殊形式,当父类和子类中含有同名同参数同返回值的函数时,用父类对象指针调该函数,则调的是父类中的该函数,用子类对象指针调该函数时,调的是子类中的而该函数,类似的当父类子类中都含有同名同类型的成员变量时,用各自的类型指针调各自的成员变量,这里严格上说就没用上继承的了,因为子类自己也有,对于父类中和子类中同名的成员变量,不会是合并,而是各自存放在各自的对象范畴中,当然父类对象包含在子类对象内。
2)父类有申明虚函数、子类中没申明虚函数
这种情形下,子类对象中自然也是有虚表的,调试时可发现有虚表覆盖的过程,如果子类中有同名同参同返回值的函数,那么子类虚表中相应偏移处函数指针就是子类中的那个同名同参同返回值的函数指针,如果没有同名同参同返回值的函数,那么子类虚表中相应偏移处函数指针就是父类中相应函数的指针。子类中没析构函数,父类中没析构函数,那么父类中没有默认析构函数,父类中有析构函数,子类中会有默认析构函数。对于构造函数也是一样,子类中有,父类中没默认,父类中,子类中会有默认析构函数。当一个父类中有虚函数时,并且没有构造和析构函数时,子类对象定义时会有默认构造,无默认析构。
3)父类没有申明虚函数、子类中有申明虚函数
这种情形下,父类对象中会没有虚表指针。
4)父类和子类中都有用到虚函数
类似2),父类和子类中都会有虚表指针。
(2)类的构造和析构函数中是否调用虚函数
类的构造和析构函数中一般不调用虚函数,因为父类和子类各自构造和析构自己,所以没必要使用虚函数。从粗略的角度分析,当在父类的构造函数中调用虚函数时,会调到子函数的成员函数去了,但这时子函数还没有构造,但析构父类时,又回调到子函数的析构函数,这时子类已经析构掉了,当然从编译器的操作上是表面了这种情形发生的,因为在父类构造和析构时都会填一次自己的虚表指针,即不会出现隐患,这是编译器做的防止隐患的一招。当构造和析构函数中调用虚函数时,会直接使用虚函数的指针,不会经过虚表指针。
class CParent { public: CParent() { Show(); } ~CParent(); virtual void Show() { printf("class CParent"); } int m_nInt; }; //汇编代码 15: Show(); 004010D0 mov ecx,dword ptr [ebp-4] 004010D3 call @ILT+35(CParent::Show) (00401028) //普通指针调用虚函数Show(); 11: pobj->Show(); 0040109E mov edx,dword ptr [ebp-10h] 004010A1 mov eax,dword ptr [edx] 004010A3 mov esi,esp 004010A5 mov ecx,dword ptr [ebp-10h] 004010A8 call dword ptr [eax] 004010AA cmp esi,esp
(3)父类和成员类的区别
如果父类、成员类和子类中都没有虚表,则当结构体处理。对于有虚表的情形,得从以下三点进行识别:1)虚表指针个数,2)初始化时机,3)各虚表覆盖的情况。汗一个父类、一个成员类的情形各类主要情况如下:
1)父类、成员类和子类中都有虚表
父类的虚表指针最先初始化,再次是成员类初始化,初始偏移从紧接父类和成员类在子类中前边成员偏移开始,紧接的是其它的子类成员,父类的虚表指针被覆盖,析构时反向。
2)父类中无虚表、子类中都有虚表
构造时,对象首四个字节会腾出来给子类填虚表指针,父类构造时,从子类对象地址的后四个字节开始。
3)其它的情形都比较好认识,当有多重继承和多个成员类时,直接用递归的方法。在对象首四个字节下写入断点时,可以看到虚表指针被覆盖多少次,被覆盖多少次就有多少重继承。有的时候,父类和成员类没法分清,那么这是还原成哪一种都行。在逆向C++时,得想对类成员函数,不管是虚函数和是非成员函数(野成员函数), 进行建模,建模好之后,再分发逆向。建模也是一个很重要的过程,加上这一过程,就相当于是逆向工程。
二、运算符重载和模板
运算符重载和模板是分辨不出来的,只能还原成相应的函数,当然可以根据自己分析的情况,进行还原成运算符重载和模板。
int operator+(CChild1 obj1, CChild1 obj2) { return obj1.m_nInt + obj2.m_nInt; }
;17: int m = obj1 + obj2; 004011DF sub esp,0Ch 004011E2 mov ecx,esp 004011E4 mov dword ptr [ebp-38h],esp 004011E7 lea edx,[ebp-28h] 004011EA push edx 004011EB call @ILT+45(CChild1::CChild1) (00401032) ;18: m = operator+(obj1, obj2); 00401226 sub esp,0Ch 00401229 mov ecx,esp 0040122B mov dword ptr [ebp-40h],esp 0040122E lea edx,[ebp-28h] 00401231 push edx 00401232 call @ILT+45(CChild1::CChild1) (00401032)
template<typename T> T My_Add(T m, T n) { return m + n; } 20: My_Add(1, 2); //两个不同的函数 00401158 push 2 0040115A push 1 0040115C call @ILT+25(My_Add) (0040101e) 00401161 add esp,8 21: My_Add(1.0, 2.0); //两个不同的函数 00401164 push 40000000h 00401169 push 0 0040116B push 3FF00000h 00401170 push 0 00401172 call @ILT+10(My_Add) (0040100f) 00401177 fstp st(0)
对于运算符的书写顺序分中缀式、波兰式、逆波兰式,中缀式在数学书中公式常用,波兰式在编译器中常用,逆波兰式在公式文字描述中用的较多,各式转换方式如下:
1.中缀式 a + b/c - d*e; 2.中缀式转波兰式,先按序转换成指令 sub(add(a, div(b,c)), mul(d,e)) -+a/bc*de 即为波兰式 3.文字描述 (a与(b、c之商)之和)与(d、e之积)的差 abc/+de*- 即为逆波兰式
三.纯虚函数怎么实现
VC6.0中通过19号错误来实现:
;__purecall proc near ; DATA XREF: .rdata:const CParent::`vftable‘o .text:004018A0 push ebp .text:004018A1 mov ebp, esp .text:004018A3 push 19h .text:004018A5 call __amsg_exit .text:004018A5 __purecall endp 34: { 004018A0 push ebp 004018A1 mov ebp,esp 35: _amsg_exit(_RT_PUREVIRT); 004018A3 push 19h 004018A5 call _amsg_exit (004019e0) 004018AA add esp,4 36: }
;VS2013中虚函数编译器操作,用了DecodePointer和_abort sub_40115E proc near ; DATA XREF: .rdata:off_40D154o .text:0040115E push Ptr ; Ptr .text:00401164 call ds:DecodePointer .text:0040116A test eax, eax .text:0040116C jz short loc_401170 .text:0040116E call eax .text:00401170 .text:00401170 loc_401170: ; CODE XREF: sub_40115E+Ej .text:00401170 push 1 .text:00401172 push 0 .text:00401174 call sub_402CCD .text:00401179 pop ecx .text:0040117A pop ecx .text:0040117B jmp _abort .text:0040117B sub_40115E endp