虚函数存在是为了克服类型域解决方案的缺陷,以使程序员可以在基类里声明一些能够在各个派生类里重新定义的函数。
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.0mfc40.dllcall [ebp+0x14]call [ebp+0x14]6.0mfc42.dllcall [ebp+0x14]call [ebp+0x14]7.1mfc71.dllcall [ebp+0x14]call [ebp+0x14]10.0mfc100.dllcall [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
版权归原作者 17bdw学编程 所有, 如有侵权,请联系我们删除。