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

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

【黑客免杀攻防】读书笔记14 - 面向对象逆向-虚函数、MFC逆向的更多相关文章

  1. 【黑客免杀攻防】读书笔记5 - PE格式讲解

    0x01 MS-DOS头 MS-DOS头部的字段重点关注e_magic与最后一个e_lfanew是需要关注的. 第一个e_magic字段的值为4D5A,作用是可以作为判断这个文件是否是PE文件. 最后 ...

  2. 【黑客免杀攻防】读书笔记8 - 软件逆向工程基础2(if-else,三目运算符)

    0x1 if-else分支 if-else分支4种状态 1.1 以常量为判断条件的简单if-else分支 C源代码: 单层if-else判断,常量为判断条件 int _tmain(int argc, ...

  3. 【黑客免杀攻防】读书笔记7 - 软件逆向工程基础1(函数调用约定、Main函数查找)

    0x1 准备工作 1.1.准备工具 IDA:交互式反汇编工具 OllyDbg:用户层调试工具 Visual Studio:微软开发工具 1.2.基础知识 C++开发 汇编语言 0x2 查找真正的mai ...

  4. 【黑客免杀攻防】读书笔记6 - PE文件知识在免杀中的应用

    0x1 PE文件与免杀思路 基于PE文件结构知识的免杀技术主要用于对抗启发式扫描. 通过修改PE文件中的一些关键点来达到欺骗反病毒软件的目的. 修改区段名 1.1 移动PE文件头位置免杀 工具:PeC ...

  5. 【黑客免杀攻防】读书笔记18-最终章Anti Rootkit

    1.免杀技巧的遏制 1.1.PE文件 入口点不在第一个区段或在最后一个区段 入口点处代码附近只有一小段代码 入口点在正常范围之外 入口点为一个无效的值,实际入口点为TLS的入口点 区段名重复或者不属于 ...

  6. 【黑客免杀攻防】读书笔记2 - 免杀与特征码、其他免杀技术、PE进阶介绍

    第3章 免杀与特征码 这一章主要讲了一些操作过程.介绍了MyCCL脚本木马免杀的操作,对于定位特征码在FreeBuf也曾发表过类似工具. VirTest5.0特征码定位器 http://www.fre ...

  7. 【黑客免杀攻防】读书笔记15 - 源码免杀、C++壳的编写

    1.源码免杀 1.1 定位产生特征的源码 定位文件特征 1.根据MyCCL的特征码定位工具,定位出有特征的地址 2.根据VS的反汇编窗口,输入有特征的地址得到特征地址与源码的关系 3.插入Messag ...

  8. 【黑客免杀攻防】读书笔记10 - switch-case分支

    0x1 switch-case分支 switch-case其实就是if-else语句的另一种体现形式.但大于3之后的switchc-case.编译器会对代码进行优化. 1.1 简单switch-cas ...

  9. 【黑客免杀攻防】读书笔记17 - Rootkit基础

    1.构建Rootkit基础环境 1.1.构建开发环境 VS2012+WDK8 1.2.构建基于VS2012的调试环境 将目标机.调试机配置在同一个工作组内 sVS2012配置->DRIVER-& ...

随机推荐

  1. 【转】ibatis 中isNull, isNotNull与isEmpty, isNotEmpty区别

    转自:http://blog.csdn.net/fanfanjin/article/details/6676566 在iBATIS中 isNull用于判断参数是否为Null,isNotNull相反 i ...

  2. 安装GourdScanV2的踩坑过程

    环境:ubuntu 16.04.1 1.安装dcoker sudo apt-get install docker.io 坑:sudo apt-get install docker 2.下载关于dock ...

  3. 洛谷P4233 射命丸文的笔记 【多项式求逆】

    题目链接 洛谷P4233 题解 我们只需求出总的哈密顿回路个数和总的强联通竞赛图个数 对于每条哈密顿回路,我们统计其贡献 一条哈密顿回路就是一个圆排列,有\(\frac{n!}{n}\)种,剩余边随便 ...

  4. centos详细安装redis步骤

    1. 从官网(http://redis.io)下载最新稳定版2. 使用命令解压下载的tar包:tar –zxvf redis-3.2.0.tar.gz3. 通过命令cd redis-3.2.0进入源码 ...

  5. 题解【bzoj2002 [Hnoi2010]Bounce 弹飞绵羊】

    Description 给 \(n\) 个点以及它们的弹力系数 \(k_i\) ,含义为 可以弹到 \(i + k_i\) 的位置. 支持两个东西,修改一个点的弹力系数:求一个点要弹多少次弹出 \(n ...

  6. Java入门:基础算法之线性搜索

    本程序使用线性搜索算法从n个数中查找一个数. /* Program: 线性搜索示例 * @author: 理工云课堂 * Input: 元素个数,每个元素值,待查找数据的值 * Output:待查找数 ...

  7. Docker图形界面管理之Portainer

    介绍 Portainer是一个开源.轻量级Docker管理用户界面,基于Docker API,可管理Docker主机或Swarm集群,支持最新版Docker和Swarm模式.官方文档 https:// ...

  8. vue写template的4种形式

    1.template标签(非单文件组件) <template id="t1"> <h2>66666666</h2> </template& ...

  9. memset函数使用详解

    1.void *memset(void *s,int c,size_t n) 总的作用:将已开辟内存空间 s 的首 n 个字节的值设为值 c. 2.例子#include void main(){cha ...

  10. webapi框架搭建-安全机制(二)-身份验证

    webapi框架搭建系列博客 身份验证(authentication)的责任是识别出http请求者的身份,除此之外尽量不要管其它的事.webapi的authentication我用authentica ...