[系统安全19] 面向对象逆向-虚函数、MFC逆向

虚函数存在是为了克服类型域解决方案的缺陷,以使程序员可以在基类里声明一些能够在各个派生类里重新定义的函数。

1 识别简单的虚函数

代码示例:

#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");
	}
	virtual 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;
};

class CPeople : public CObj
{
public:
	CPeople():m_People_1(0xCCCCCCCC),m_People_2(0xDDDD)
	{
		printf("CPeople() Constructor...\r\n");
	}
	~CPeople()
	{
		printf("CPeople() Destructor...\r\n");
	}
	void Show(int nID)
	{
		printf("ID:%d People!\r\n",nID);
	}
private:
	int  m_People_1;
	WORD m_People_2;
};


int _tmain(int argc, _TCHAR* argv[])
{
	CObj obj;
	CPeople people;
	CObj *pobj;

	pobj = &obj;
	pobj->Show(0);
	pobj = &people;
	pobj->Show(1);
	return 0;
}
// ---------- 输出结果 ----------
// CObj() Constructor...
// CObj() Constructor...
// CPeople() Constructor...
// ID:0 Who is your God? I am!
// ID:1 People!
// CPeople() Destructor...
// CObj() Destructor...
// CObj() Destructor...
// ----------------------------

反汇编代码:

int _tmain(int argc, _TCHAR* argv[])
{
001273B0  push        ebp  
001273B1  mov         ebp,esp  
001273B3  push        0FFFFFFFFh  
001273B5  push        1B3730h  
001273BA  mov         eax,dword ptr fs:[00000000h]    
001273C0  push        eax  
001273C1  sub         esp,108h  
001273C7  push        ebx  
001273C8  push        esi  
001273C9  push        edi  
001273CA  lea         edi,[ebp+FFFFFEECh]  
001273D0  mov         ecx,42h  
001273D5  mov         eax,0CCCCCCCCh  
001273DA  rep stos    dword ptr es:[edi]  
001273DC  mov         eax,dword ptr ds:[001D9004h]
001273E1  xor         eax,ebp  
001273E3  push        eax  
001273E4  lea         eax,[ebp-0Ch]  
001273E7  mov         dword ptr fs:[00000000h],eax              ; 栈保护基址相关代码
	CObj obj;
001273ED  lea         ecx,[ebp-1Ch]                             ; this 指针
001273F0  call        00123D87                                  ; CObj::CObj (0123D87h)  
001273F5  mov         dword ptr [ebp-4],0                       ; 异常处理的辅助标志,以-1为结尾
	CPeople people;
001273FC  lea         ecx,[ebp-38h]                             ; this指针
001273FF  call        001211DB                                  ; CPeople::CPeople (01211DBh)  
00127404  mov         byte ptr [ebp-4],1  
	CObj *pobj;
	pobj = &obj;
00127408  lea         eax,[ebp-1Ch]                             ; 将obj的this指针给eax
0012740B  mov         dword ptr [ebp-44h],eax                   ; 将this指针给pobj的指针
	pobj->Show(0);
0012740E  mov         esi,esp  
00127410  push        0                                         ; 参数压栈
00127412  mov         eax,dword ptr [ebp-44h]  
00127415  mov         edx,dword ptr [eax]  
00127417  mov         ecx,dword ptr [ebp-44h]                   ; 将Obj的指针(指向的是   Obj的this指针)给ecx
0012741A  mov         eax,dword ptr [edx]                       ; 将Obj的this指针所指向的第一项的内容(即Vtbl的第一个元素)给eax 
0012741C  call        eax  
0012741C  ; 在调用完CPeople的构造后,程序采用如下步骤实现
0012741C  ; pobj = &obj;
0012741C  ; pobj ->Show(0);
0012741C  ; 
0012741C  ; 1、将创建完的Obj对象的this指针传递给pobj
0012741C  ; 2、将指this指针给eax
0012741C  ; 3、将this指针第一项(即虚函数表指针)传递给edx
0012741C  ; 4、将pobj的值传递给ecx(注意此步)
0012741C  ; 5、将既虚函数表数组的地址传递给eax
0012741C  ; 6、调用eax
0012741E  cmp         esi,esp  
00127420  call        00122329  
	pobj = &people;
00127425  lea         eax,[ebp-38h]            ; 将People的this指针传给eax
00127428  mov         dword ptr [ebp-44h],eax  ; 将People的this指针给Obj
	pobj->Show(1);
0012742B  mov         esi,esp       
0012742D  push        1                        ; 参数压栈
0012742F  mov         eax,dword ptr [ebp-44h]  ; 将People的this指针给eax
00127432  mov         edx,dword ptr [eax]      ; 将this指针中的第一项,即Vptr给edx
00127434  mov         ecx,dword ptr [ebp-44h]  ; 将People的this指针给ecx
00127437  mov         eax,dword ptr [edx]      ; 将Vptr指向的Vtbl给eax
00127439  call        eax                      ; 调用eax
0012743B  cmp         esi,esp  
0012743D  call        00122329                 ; __RTC_CheckEsp
	return 0;
00127442  mov         dword ptr [ebp+FFFFFEF0h],0  
0012744C  mov         byte ptr [ebp-4],0  
00127450  lea         ecx,[ebp-38h]  
00127453  call        00121E10  
00127458  mov         dword ptr [ebp-4],0FFFFFFFFh  
0012745F  lea         ecx,[ebp-1Ch]  
00127462  call        00123BC0  
00127467  mov         eax,dword ptr [ebp+FFFFFEF0h]  
}

如果没有Debug的符号文件,或者逆向过程中代码不是我们自己写的,那就要先判断它是否是一个类的应用。

跟进函数内部情况:

class CObj
{
00126FD0  push        ebp  
00126FD1  mov         ebp,esp  
00126FD3  sub         esp,0CCh  
00126FD9  push        ebx  
00126FDA  push        esi  
00126FDB  push        edi  
00126FDC  push        ecx  
00126FDD  lea         edi,[ebp-0CCh]  
00126FE3  mov         ecx,33h  
00126FE8  mov         eax,0CCCCCCCCh  
00126FED  rep stos    dword ptr es:[edi]  
00126FEF  pop         ecx  
00126FF0  mov         dword ptr [this],ecx        ; 取this指针 this == [ebp-8] 
00126FF3  mov         eax,dword ptr [this]        ; 取this指针
00126FF6  mov         dword ptr [eax],offset CObj::`vftable' (01B5E54h)  
public:
	CObj():m_Obj_1(0xAAAAAAAA),m_Obj_2(0xBBBB)
00126FFC  mov         eax,dword ptr [this]  
00126FFF  mov         dword ptr [eax+4],0AAAAAAAAh  ; 初始化m_Obj_1为0xAAAAAAAA
00127006  mov         eax,0BBBBh                    ; 初始化m_Obj_2为0xBBBB
0012700B  mov         ecx,dword ptr [this]          ; this指针 this == ecx-8
0012700E  mov         word ptr [ecx+8],ax  
		printf("CObj() Constructor...\r\n");
00127012  push        offset string "CObj() Constructor...\r\n" (01B5E5Ch)  
		printf("CObj() Constructor...\r\n");
00127017  call        _printf (0123D00h)  
0012701C  add         esp,4  
	}
0012701F  mov         eax,dword ptr [this]         ; 将this指针作为返回值 this == ebp-8
00127022  pop         edi  
00127023  pop         esi  
00127024  pop         ebx  
00127025  add         esp,0CCh  
0012702B  cmp         ebp,esp  
0012702D  call        __RTC_CheckEsp (0122329h)  
00127032  mov         esp,ebp  
00127034  pop         ebp  
00127035  ret  

通过阅读以上代码可以得出以下过程:

1)找出虚表位置,以及操作的流程

  • 代码里的例子操作了虚表 00126FF6 mov dword ptr [eax],offset CObj::`vftable' (01B5E54h)

这是一个保存函数地址的指针,再通过汇编上下文的猜测,则可大致确定这就是一个虚表,且将值传到了寄存器参数ecx记录地址的第一项。

  • 以寄存器参数ecx为首地址,分别给其4偏移与8偏移处赋值

  • 寄存器参数ecx又作为返回值传了回去。

  • 通过调用函数的分析,ecx里保存的是this指针,并且根据类的内存结构可知,this里的第一项是Vptr。

2)识别构造函数

  • 由于此成员函数是第一个被调用的,通过代码看出汇编函数中的第二件事是初始化数据成员。最后一件事是将this指针当做返回值返回,所以推测该函数为构造函数。

3)逐步分析函数

  • 构造函数与析构函数会对Vptr操作。
  • 在VS默认设置下,构造与析构前都会有相应的异常处理标记置位操作。
  • 虚函数的调用一般采用eax。

2 识别较复杂的虚函数

经验小结:

  • new出来的对象会以其在堆中申请空间的指针作为this指针传入参与构造。
  • new出来的对象其虚函数调用的寻址方式与普通构造出来的不同。
  • delete对象时会先析构自己,再析构父类,最后再执行delete。
  • new出来的对象如果其成员函数派生于纯虚函数,在delete时只调用父类的析构。
  • 如果此类为抽象类(包含纯虚函数),那么其虚表的对应项会填充指向库函数__purecall的函数指针。

虚函数调用的固定模式,紧盯对各个虚表的操作。从而根据上下文即可大致确定虚函数的调用与类的析构与构造。

3 识别类的继承关系

  • 根据构造函数内的构造顺序分辨此函数所属类的继承情况
  • 总结并记录分析结果
  • VS的release版中存在同时使用ecx、esi寄存器传递this指针的情况。

4 逆向MFC程序

MFC程序关键特征点

版本 对应动态库 静态库中使用MFC时的特征 动态库中使用MFC的特征
4.0 mfc40.dll call [ebp+0x14] call [ebp+0x14]
6.0 mfc42.dll call [ebp+0x14] call [ebp+0x14]
7.1 mfc71.dll call [ebp+0x14] call [ebp+0x14]
10.0 mfc100.dll call [ebp+0x14] mov edx,[ebp+0x14]

分析核心重点

1)判断目标程序是不是MFC程序,如果是,判断其MFC版本

OD快捷键:Ctrl+E 打开模块窗口,并在模块窗口寻找类似于mfc*.dll这样的模块。

如果找到了就可以根据DLL的名称判定程序所用的MFC版本,如果找不到则证明这是一个在静态库中使用MFC的程序。

2)根据目标程序调用MFC方式的不同而采取不同的方式搜索特征

OD快捷键:Ctrl+F 搜索特征 call [ebp+0x14]

由于搜索的特征位于消息分发函数里,因此特征指令所在的位置应该是一个非常大的switch-case。

3)在合适的地方下断点,并跟进到相应消息的函数中。

设置按钮点击事件下断点,即可跟进到达相应消息的函数中。

这里可以参考:

看雪《MFC程序逆向》
https://bbs.pediy.com/thread-54150.htm

posted @ 2018-04-16 00:20  17bdw  阅读(358)  评论(0编辑  收藏  举报