MFC事件逆向分析

选用测试软件:家庭账本3.5.9

相较于Win32窗口编程,拿到消息后可以在消息处理回调函数里面寻找对应的消息处理函数不同

MFC程序的运行常常令我摸不着头脑,有时候根本就不知道如何去找到对应的处理函数,例如按钮事件的事件处理函数。

最近遇到一些MFC的ctf题目和一些MFC写的软件想要尝试破解,但是无奈按钮事件非常难找,一旦字符串加密或者没有弹窗,很难去找到对应函数体

所以特地花了很多事件去整理了下MFC程序应该如何去逆向

这篇文章前面会讲理论,后边会结合实际逆向分析例子。

MFC消息映射机制

在 Windows 编程中,窗口与系统、用户交互主要是通过消息传递的。每个窗口都有一个消息队列,系统通过向消息队列发送消息来通知窗口事件(如鼠标点击、键盘输入、窗口重绘等)。MFC 消息映射机制使得处理这些消息变得更加结构化和便捷。

MFC 中,每个支持消息映射的类都必须使用 DECLARE_MESSAGE_MAP 宏(在类声明中)和 BEGIN_MESSAGE_MAPEND_MESSAGE_MAP 宏(在类实现中)来定义消息映射。

DECLARE_MESSAGE_MAP

DECLARE_MESSAGE_MAP 是 MFC(Microsoft Foundation Classes)框架中的一个宏,它在类的声明中使用,用于声明该类支持消息映射机制。此宏与 BEGIN_MESSAGE_MAPEND_MESSAGE_MAP 一起使用,使得类可以响应 Windows 消息(如鼠标点击、键盘输入等)并将这些消息映射到特定的成员函数。

1
2
3
4
5
6
7
#define DECLARE_MESSAGE_MAP() \
public: \
static const AFX_MSGMAP* PASCAL GetThisMessageMap(); \
virtual const AFX_MSGMAP* GetMessageMap() const; \
private: \
static const AFX_MSGMAP_ENTRY _messageEntries[]; \
static const AFX_MSGMAP messageMap;
  • GetThisMessageMap(): 这是一个静态函数,它返回一个指向该类消息映射表的指针。因为它是静态的,所以它与特定的实例无关,可以在类层次结构中调用以获取消息映射信息。
  • GetMessageMap(): 这是一个虚函数,它在运行时返回一个指向消息映射表的指针。因为它是虚函数,所以在继承层次结构中可以被覆盖,以确保消息映射能够正确地在派生类和基类之间传播。
  • _messageEntries: 这是一个静态的 AFX_MSGMAP_ENTRY 数组的声明,数组中包含了消息到函数的映射条目。具体的映射条目是在类的实现文件中定义的。
  • messageMap: 这是一个静态的 AFX_MSGMAP 结构体的声明,它包含了消息映射表的元数据,包括一个指向基类消息映射表的指针和一个指向 _messageEntries 的指针。

BEGIN_MESSAGE_MAP,END_MESSAGE_MAP

那么BEGIN_MESSAGE_MAPEND_MESSAGE_MAP 这俩宏做了什么呢?

这是BEGIN_MESSAGE_MAP

1
2
3
4
5
6
#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
const AFX_MSGMAP* theClass::GetMessageMap() const { return &theClass::messageMap; } \
const AFX_MSGMAP theClass::messageMap = \
{ &baseClass::messageMap, theClass::_messageEntries }; \
const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \
{

theClass::GetMessageMap(): 定义了一个返回类的消息映射表指针的函数。

theClass::messageMap: 声明了一个 AFX_MSGMAP 结构体,包含基类的消息映射表指针和当前类的消息映射表条目指针。

theClass::_messageEntries[]: 声明了一个 AFX_MSGMAP_ENTRY 数组,用于存储消息与对应处理函数的映射。

这是END_MESSAGE_MAP

1
2
3
#define END_MESSAGE_MAP() \
{ 0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \
};

{ 0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 }: 这是一个终止项,表示消息映射表的结束。

}: 关闭数组的定义。

1724830179641

所以以上就是对上面几个消息进行了消息映射。

上面没有参数的宏是消息交给了父类进行消息处理,并未进行消息处理的重载
1724830331321

如何获取AFX_MSGMAP表

由上面的分析我们可以知道,一旦掌握了这个AFX_MSGMAP表,我们就可以找到所有事件的消息处理函数

那么问题来了,如何获取到这个AFX_MSGMAP表呢?

这里我给出两种方法:

第一种:内存搜索法

缺点:只能碰运气/(ㄒoㄒ)/~~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct AFX_MSGMAP
{
const AFX_MSGMAP* pBaseMessageMap;
const AFX_MSGMAP_ENTRY* lpEntries;
};

struct AFX_MSGMAP_ENTRY
{
UINT nMessage; // 消息类型
UINT nCode; // 消息代码(例如通知代码)
UINT nID; // 控件 ID,或命令 ID
UINT nLastID; // 最后一个控件 ID(用于范围命令)
UINT nSig; // 函数签名类型
AFX_PMSG pfn; // 指向成员函数的指针
};

首先先要知道,AFX_MSGMAP是存在于.rdata段的,然后它有很大概率是这个结构:

1724831022662

如果是这个结构,我们就可以写一个IDA脚本去自动排除,因为AFX_MSGMAP+4的位置存的是指向AFX_MSGMAP_ENTRY数组的指针,如果指向的结构符合AFX_MSGMAP_ENTRY结构体,那么就可以基本判定为找到AFX_MSGMAP了

但是实际逆向的时候会发现有长这样的:(凑合看吧,滑稽翻转,总之就是AFX_MSGMAP在高地址,AFX_MSGMAP_ENTRY数组在低地址)

1724831246648

这样的话,内存搜索法就很容易失效,当然如果你能搞清楚AFX_MSGMAP_ENTRY数组有多少项,那还是可以编写出脚本的,只是我尚未探究如何查到有多少项

第二种:虚表查找法

查找资料发现MFC程序在启动的时候,自己也是需要获取到这个AFX_MSGMAP表的,如何获取呢?

通过一个叫GetMessageMap的函数去获取,这是一个虚函数,每个类自己会重载这个虚函数,从而获取到属于自己的AFX_MSGMAP表的地址,所有我们只需要找到这个GetMessageMap函数,就可以找到例如对话框窗口类的AFX_MSGMAP表,从而找到对应事件的消息处理函数!

我们在这里下断点,宏展开相当于在GetMessageMap下断点

1724832000148

通过查看调用堆栈可以发现,上一级是CWnd::OnWndMsg(…)

1724832045066

然后一顿分析发现,在AfxWndProc的时候,就已经获取到MyDlg类的this指针

1724834347526

所以我们只要定位到了CWnd::OnWndMsg或CWnd::WindowsProc,就可以获取到MyDlg的this指针

可喜可贺的是,IDA能够识别出CWnd这种比较基础的类,所以我们可以在CWnd::OnWndMsg下断

只要断下来,我们就可以获取到MyDlg的this指针

我们只需要IDA载入,然后找到这个函数导出的编号,例如这里的是6374

1724835139517

然后我们就可以在x64dbg下断点了,这样断下来的就是CWnd::WindowProc,然后拿到ecx便是MyDlg的this指针

1724835207668

实战

获取到this指针可以干啥?
this指针指向的是MyDlg这个类的虚函数表

然后我们在源码中仔细观察这个函数

1
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)

1724834651250

发现一直到GetMessageMap函数之前,没有一个函数被我们写的MyDlg重载过

那么就可以通过IDA,在虚函数表里面,没有被识别出来的函数下断点

IDA:

如下图,叫sub开头的都是没有符号的,因为是咱自己写的,不是MFC自带的

1724835487293

或者x96dbg:

这种没有被识别的,没导入的就是MyDlg的函数,都下断点即可

1724835617358

第一个被断下的,就是MyDlg::GetMessageMap();

至此,我们找到了AFX_MSG结构(以下是带pdb的)

1724836307762

为了超详细,还是讲讲构建结构体吧:

先创建好一个头文件,然后用IDA导入

1724840168187

1724840250136

1724840318464

点击 Add standard structure导入我们刚刚加进去的结构体

1724840344876

选中我们找到的AFX_MSGMAP,导入结构体

1724840507312

效果如图:
1724840534623

接下来就是导入AFX_MSGMAP_ENTRY:
导入好一个以后,点击Array可以建立结构体数组

1724840585971

例如有10个这样的数组

1724840626768

至此,就全部消息映射表的信息就全找到了

1724840774165

在AFX_MSGMAP_ENTRY最后一个成员函数指针下断,可以找到对应的消息处理函数

1724840814923

但是有一个问题,就是“注册”这个按钮的消息处理函数居然不在这个结构体数组里面。

但是最终我在MyDlg的虚函数表找到了对应的消息处理函数

1724841005652

从而我们得出一个结论,如果某个消息处理事件没有在消息映射表里面,那么就去虚函数表里面找,不过好在这个事情概率不大,至今我也没搞清楚为什么会这样🤦‍

有了解的小伙伴可以联系我讨论 qq:1196925782

也欢迎对游戏安全,逆向开发有兴趣的志同道合的小伙伴联系我,一起学习!