[系统安全18]面向对象逆向-类、构造函数、析构函数

1 结构体与类

在C++中,结构体与类是非常相似的,都有默认的构造函数与析构函数,也都可以提供自己的接口函数,但是除此之外它们还有有很多不同的。

1.1 变量作用域的识别

具体来说,变量总共分为以下几种类型:

全局变量:全局变量的数据总是存储在PE文件中的数据段或代码段中,在游戏修改社群内也被称为“基址”。

局部变量:局部变量的数据保存在栈中,其生命周期与其所在的函数作用域一致,访问方式是使用ebp或esp间接访问。

静态局部变量:静态局部变量的数据保存方式及访问方式与全局变量基本一致,因此很容易被误认为是全局变量。但是在保存此变量的临近位置往往会有一个标志位控制其具体的作用域,当此标志位为1时证明此变量已经被初始化。

堆变量:堆变量的数据保存在new出来的堆空间中,其作用域由与new对应的delete控制。

代码示例

#include "stdafx.h"

int g_nNum = 1;			// 全局变量
int _tmain(int argc, _TCHAR* argv[])
{
	int        nNum = 2;		// 局部变量
	static int sNum = 3;		// 静态局部变量
	int*       pNum = new int;	// 堆变量
	*pNum = 4;

	printf("%X %X %X %X ",g_nNum,nNum,sNum,*pNum);

	delete pNum;
	return 0;
}


Debug汇编代码:

全局变量与静态变量是没有初始化代码的,因为已经初始化了。


add     esp, 4
mov     [ebp+var_8], 2
push    4
call    sub_4031B6
add     esp, 4
mov     [ebp+var_E0], eax
mov     eax, [ebp+var_E0]
mov     [ebp+var_14], eax
mov     eax, [ebp+var_14]
mov     dword ptr [eax], 4
mov     eax, [ebp+var_14]
mov     ecx, [eax]      ; 堆变量赋值压栈
push    ecx
mov     edx, dword_4B9004 ; 静态变量已经初始化了
push    edx
mov     eax, [ebp+var_8] ; 局部变量
push    eax
mov     ecx, dword_4B9000
push    ecx             ; 全局变量已经初始化了
push    offset aXXXX    ; "%X %X %X %X "
call    sub_403D2D

2 识别构造与析构函数

C++ 与 C 最大的不同就在于 C++ 多了一个类。通过由汇编反推出 C++ 的类相关代码,通过代码证明面向对象编程并不是某些语言所特有的,连汇编都可以。

2.1 快速识别出类

在VS下,一般情况会使用ecx寄存器传递this指针。因为每个成员函数在被调用时都要用this指针作为第一个参数。

C++代码

#include "stdafx.h"


class CObj
{
public:
	void ShowMsg(int nID)
	{
		printf("ID:%d Who is your God? I am!\r\n",nID);
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	CObj obj;
	obj.ShowMsg(9);
	return 0;
}

Debug反汇编代码:

  • 1)push参数后,会紧跟着把对象的地址传进ecx
  • 2)根据stdcall调用规范,由于ecx是最紧邻call指令的,那么就证明ecx是被当成第一个参数传递进去的。
0104713E  push        9  
01047140  lea         ecx,[obj]  
01047143  call        CObj::ShowMsg (01043E0Eh)  

跟踪进ShowMsg()函数。


01046F90  push        ebp  
01046F91  mov         ebp,esp  
01046F93  sub         esp,0CCh  
01046F99  push        ebx  
01046F9A  push        esi  
01046F9B  push        edi  
01046F9C  push        ecx  
01046F9D  lea         edi,[ebp-0CCh]  
01046FA3  mov         ecx,33h  
01046FA8  mov         eax,0CCCCCCCCh  
01046FAD  rep stos    dword ptr es:[edi]  
01046FAF  pop         ecx  
01046FB0  mov         dword ptr [this],ecx     ; 保存this指针
    18: 		printf("ID:%d Who is your God? I am!\r\n",nID);
01046FB3  mov         eax,dword ptr [nID]      ;  获取参数
01046FB6  push        eax  
01046FB7  push        offset string "ID:%d Who is your God? I am!\r\n" (010D5E50h)  
01046FBC  call        _printf (01043CE7h)  
01046FC1  add         esp,8  
    19: 	}
01046FC4  pop         edi  
01046FC5  pop         esi  
01046FC6  pop         ebx  
01046FC7  add         esp,0CCh  
01046FCD  cmp         ebp,esp  
    19: 	}
    

识别一个类成员函数的调用,对于简单的代码只能臆测。以下代码为类加上了构造函数,并初始化了成员函数。

#include <Windows.h>

class CObj
{
public:
	CObj():m_Obj_1(0xAAAAAAAA),m_Obj_2(0xBBBB)
	{
		printf("CObj() Constructor...\r\n");
	}
	void Show(int nID)
	{
		m_Obj_1 = 1;
		printf("ID:%d Who is your God? I am!\r\n",nID);
	}
private:
	int  m_Obj_1;
	WORD m_Obj_2;
};

int _tmain(int argc, _TCHAR* argv[])
{
	CObj obj;
	obj.Show(0);
	return 0;
}

两次函数调用的参数都用到了ecx,通过反汇编代码可知ecx没有变化。通过以上论据,判断这是两个成员函数。


002471C0 push ebp
002471C1 mov ebp,esp
002471C3 sub esp,0xD0
002471C9 push ebx
002471CA push esi                   ;  9-51.<ModuleEntryPoint>
002471CB push edi
002471CC lea edi,[local.52]
002471D2 mov ecx,0x34
002471D7 mov eax,0xCCCCCCCC
002471DC rep stos dword ptr es:[edi]
002471DE lea ecx,[local.3]          ;  获取变量地址到ecx中,保存this指针
002471E1 call 9-51.00243D73         ;  调用构造函数
002471E6 push 0x0
002471E8 lea ecx,[local.3]          ;  获取变量地址到ecx中,保存this指针
002471EB call 9-51.00243B43         ;  调用类成员函数
002471F0 xor eax,eax

跟进构造函数中的反汇编代码如下:

00246FA0  push        ebp  
00246FA1  mov         ebp,esp  
00246FA3  sub         esp,0CCh  
00246FA9  push        ebx  
00246FAA  push        esi  
00246FAB  push        edi  
00246FAC  push        ecx  
00246FAD  lea         edi,[ebp+FFFFFF34h]  
00246FB3  mov         ecx,33h  
00246FB8  mov         eax,0CCCCCCCCh  
00246FBD  rep stos    dword ptr es:[edi]  
00246FBF  pop         ecx  
00246FC0  mov         dword ptr [ebp-8],ecx            ;调用ecx,保存this指针
    13: class CObj
    14: {
    15: public:
    16: 	CObj():m_Obj_1(0xAAAAAAAA),m_Obj_2(0xBBBB)
00246FC3  mov         eax,dword ptr [ebp-8]  
00246FC6  mov         dword ptr [eax],0AAAAAAAAh   ;m_Obj_1
00246FCC  mov         eax,0BBBBh                   ;m_Obj_2
00246FD1  mov         ecx,dword ptr [ebp-8]  
00246FD4  mov         word ptr [ecx+4],ax  
    18: 		printf("CObj() Constructor...\r\n");
00246FD8  push        2D5E50h  
00246FDD  call        00243CEC  
00246FE2  add         esp,4  
    19: 	}

这里使用vs直接调试(去掉显示符号即可以对比,不用在OD和IDA里反复切换)。

通过回忆 C++ 的知识,C++的类中this指针始终指向本类结构的起始地址。

以下汇编中,ecx作为一个指针被当做参数传递了进来,并且还将ecx指向的地方当做了一个结构体,分别为其赋了两个值。

其次,保存有ecx值的[ebp-8]又被当做返回值传了回去。

    16: 	CObj():m_Obj_1(0xAAAAAAAA),m_Obj_2(0xBBBB)
00246FC3  mov         eax,dword ptr [ebp-8]  
00246FC6  mov         dword ptr [eax],0AAAAAAAAh  
00246FCC  mov         eax,0BBBBh  
00246FD1  mov         ecx,dword ptr [ebp-8]  
00246FD4  mov         word ptr [ecx+4],ax;将ecx指向的地方当做了一个结构体,分别为其赋了两个值
...

00246FE5  mov         eax,dword ptr [ebp-8] ;ecx的值又被当做返回值传了回去

因此,对this指针指向的内容进行操作,就是对类内部的成员进行操作。

跟进成员函数查看:

由ecx寻址,并给一个地方赋值,结合上下文,大致判断是类内成员变量赋值。

    20: 	void Show(int nID)
    21: 	{
00247020  push        ebp  
00247021  mov         ebp,esp  
00247023  sub         esp,0CCh  
00247029  push        ebx  
0024702A  push        esi  
0024702B  push        edi  
0024702C  push        ecx  
0024702D  lea         edi,[ebp+FFFFFF34h]  
00247033  mov         ecx,33h  
00247038  mov         eax,0CCCCCCCCh  
0024703D  rep stos    dword ptr es:[edi]  
0024703F  pop         ecx  
00247040  mov         dword ptr [ebp-8],ecx  ;保存this指针
    22: 		m_Obj_1 = 1;
00247043  mov         eax,dword ptr [ebp-8]  ; eax = nID
00247046  mov         dword ptr [eax],1  
    23: 		printf("ID:%d Who is your God? I am!\r\n",nID);
0024704C  mov         eax,dword ptr [ebp+8]  
0024704F  push        eax  
00247050  push        2D5E6Ch  
00247055  call        00243CEC  
0024705A  add         esp,8  
    24: 	}

综上所述,得出以下两条经验:

  • 紧盯采用ecx传参的函数调用
  • 留意以ecx为指针进行数据操作的代码

2.2 识别构造函数

  • 判定同一个this指针的调用。

像上一个例子一样,两次call的ecx参数都来自同一位置,那么我们就要考虑验证它的第一个call调用的是否为构造函数了

  • 构造函数内部会将this指针作为返回值

00246FE5  mov         eax,dword ptr [ebp-8] ;ecx的值又被当做返回值传了回去

2.3 识别析构函数

简单的类识别析构函数并不靠谱,因为只有一个特征,从调用上来说,同一this指针的最后一次被使用时调用的函数就有可能是析构函数。

代码示例

#include "stdafx.h"
#include <Windows.h>

class CObj
{
public:
	CObj():m_Obj_1(0xAAAAAAAA),m_Obj_2(0xBBBB)
	{
		printf("CObj() Constructor...\r\n");
	}
	~CObj()  // 新添加的析构函数
	{
		printf("CObj() Destructor...\r\n");
	}
	void Show(int nID)
	{
		m_Obj_1 = 1;
		printf("ID:%d Who is your God? I am!\r\n",nID);
	}
private:
	int  m_Obj_1;
	WORD m_Obj_2;
};

int _tmain(int argc, _TCHAR* argv[])
{
	CObj obj;
	obj.Show(0);
	return 0;
}

Debug

通过VS的反汇编发现加了析构函数后多了一个成员函数的调用。

     34: int _tmain(int argc, _TCHAR* argv[])
    35: {
00FD7230  push        ebp  
00FD7231  mov         ebp,esp  
00FD7233  push        0FFFFFFFFh  
00FD7235  push        1063528h  
00FD723A  mov         eax,dword ptr fs:[00000000h]  
00FD7240  push        eax  
00FD7241  sub         esp,0DCh  
00FD7247  push        ebx  
00FD7248  push        esi  
00FD7249  push        edi  
00FD724A  lea         edi,[ebp+FFFFFF18h]  
00FD7250  mov         ecx,37h  
00FD7255  mov         eax,0CCCCCCCCh  
00FD725A  rep stos    dword ptr es:[edi]  
00FD725C  mov         eax,dword ptr ds:[01089004h]  
00FD7261  xor         eax,ebp  
00FD7263  push        eax  
00FD7264  lea         eax,[ebp-0Ch]  
00FD7267  mov         dword ptr fs:[00000000h],eax  
    36: 	CObj obj;
00FD726D  lea         ecx,[ebp-18h]  
00FD7270  call        00FD3D78  
00FD7275  mov         dword ptr [ebp-4],0  
    37: 	obj.Show(0);
00FD727C  push        0  
00FD727E  lea         ecx,[ebp-18h]  
00FD7281  call        00FD3B43  
    38: 	return 0;
00FD7286  mov         dword ptr [ebp+FFFFFF1Ch],0  
00FD7290  mov         dword ptr [ebp-4],0FFFFFFFFh  
00FD7297  lea         ecx,[ebp-18h]  
00FD729A  call        00FD3BB1                ;成员函数调用
00FD729F  mov         eax,dword ptr [ebp+FFFFFF1Ch]  
    39: }

跟踪到其中

    20: 	~CObj()  // 新添加的析构函数
    21: 	{
00FD7030  push        ebp  
00FD7031  mov         ebp,esp  
00FD7033  sub         esp,0CCh  
00FD7039  push        ebx  
00FD703A  push        esi  
00FD703B  push        edi  
00FD703C  push        ecx  
00FD703D  lea         edi,[ebp+FFFFFF34h]  
00FD7043  mov         ecx,33h  
00FD7048  mov         eax,0CCCCCCCCh  
    20: 	~CObj()  // 新添加的析构函数
    21: 	{
00FD704D  rep stos    dword ptr es:[edi]  
00FD704F  pop         ecx  
00FD7050  mov         dword ptr [ebp-8],ecx  
    22: 		printf("CObj() Destructor...\r\n");
00FD7053  push        1065E6Ch  
00FD7058  call        00FD3CF1  
00FD705D  add         esp,4  
    23: 	}

无法在这个函数中找到任何有别于其他成员函数的特征,所以无从判断。如果想以比较可靠的方式识别出一个类的析构函数,那么需要类满足以下两个条件:

  • 包含有虚函数
  • 包含有空间申请的操作
posted @ 2018-01-21 21:20  17bdw  阅读(528)  评论(0编辑  收藏  举报