为了解决虚函数过多而造成的虚表过大的现象,微软提出了消息映射机制。对指定的消息回调函数进行重写
消息映射需要在.h文件的类声明中用DECLARE_MESSAGE_MAP标识消息映射。
#define DECLARE_MESSAGE_MAP() \
protected: \
static const AFX_MSGMAP* PASCAL GetThisMessageMap(); \
virtual const AFX_MSGMAP* GetMessageMap() const; \
GetMessageMap函数调用GetThisMessageMap函数,最终返回一个函数映射数组结构。
struct AFX_MSGMAP
{
const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();//父类的GetThisMessageMap函数,
//可以通过该函数获取父类的消息映射表
const AFX_MSGMAP_ENTRY* lpEntries;//子类到的消息映射表
};
struct AFX_MSGMAP_ENTRY
{
UINT nMessage; // 窗口消息
UINT nCode; // 控件代码或WM_NOTIFY代码
UINT nID; // 控件ID (windows消息为0)
UINT nLastID; // 用于指定控件id范围的条目
UINT_PTR nSig; // 签名类型(动作)或指向消息#的指针
AFX_PMSG pfn; // 回调函数
};
在.cpp文件中标识需要重写的消息。
BEGIN_MESSAGE_MAP(CMFC2Dlg, CDialogEx)//声明映射哪个类对象
ON_WM_PAINT()//声明映射绘图消息
END_MESSAGE_MAP()//结束消息映射
#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
PTM_WARNING_DISABLE \
const AFX_MSGMAP* theClass::GetMessageMap() const \
{ return GetThisMessageMap(); } \
const AFX_MSGMAP* PASCAL theClass::GetThisMessageMap() \
{ \
typedef theClass ThisClass; \
typedef baseClass TheBaseClass; \
static const AFX_MSGMAP_ENTRY _messageEntries[] = \
{
#define END_MESSAGE_MAP() \
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \
}; \
static const AFX_MSGMAP messageMap = \
{ &TheBaseClass::GetThisMessageMap, &_messageEntries[0] }; \
return &messageMap; \
} \
PTM_WARNING_RESTORE
将上述宏进行整理展开。
//=============BEGIN_MESSAGE_MAP宏展开开始==========
//PTM_WARNING_DISABLE宏展开是如下两句
__pragma(warning( push )) //保存当前警告
__pragma(warning( disable : 4867 )) //禁用4867警告:错误地初始化了指向成员函数的指针
//子类函数的GetMessageMap
const AFX_MSGMAP* theClass::GetMessageMap() const
{
//嵌套的调用
return GetThisMessageMap();
}
//子类函数重写的GetThisMessageMap,获取当前子类的消息映射和集成的父类的消息映射获取函数到的地址
const AFX_MSGMAP* PASCAL theClass::GetThisMessageMap()
{
//类型声明
typedef theClass ThisClass;
typedef baseClass TheBaseClass;
//AFX_MSGMAP_ENTRY类型中记录了窗口消息、控制(通知)码、控制ID、回调函数等系列信息
static const AFX_MSGMAP_ENTRY _messageEntries[] =
{
//=============BEGIN_MESSAGE_MAP宏展开结束==========
/*函数映射声明开始*/
//ON_WM_PAINT()消息映射
{ WM_PAINT, 0, 0, 0, AfxSig_vv,
(AFX_PMSG)(AFX_PMSGW)
(static_cast<void (AFX_MSG_CALL CWnd::*)(void)>( &ThisClass :: OnPaint))},
/*函数映射声明结束*/
//=============END_MESSAGE_MAP宏展开开始==========
//最后这个数组成员用于标识消息映射表的结束
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } //24字节空间,标识结束
};
//将结果组成消息映射表返回
static const AFX_MSGMAP messageMap =
{
&TheBaseClass::GetThisMessageMap, //父类的消息映射函数
&_messageEntries[0] //消息映射表首地址
};
//上述_messageEntries结构数组和messageMap结构都是声明的静态
//静态局部变量,可以理解为伪全局变量,内存空间在全局,因此函数结束,空间数据不销毁,不丢失
//因此在函数结尾能返回结构地址
return &messageMap;
}
//PTM_WARNING_RESTORE是下述语句,用于恢复警告
__pragma(warning( pop ))
//=============END_MESSAGE_MAP宏展开结束==========
对宏定义分析结束后,我们可以看到一个关系。如果一个A窗口重写了某个消息,那么该对象中就存在这个GetMessageMap函数的重写。反之不存在消息映射,那么父类的GetMessageMap函数就变为子类的私有函数。最后猜测,消息函数在被调用前,会先访问这个消息映射表,之后根据表项进行函数调用,接下来对猜测进行测试。
实例程序代码:
//MFC7.h
#include <afxwin.h>
class CMyApp :public CWinApp
{
public:
virtual BOOL InitInstance();
};
class CMainWindow :public CFrameWnd
{
public:
CMainWindow();
private:
//声明函数映射
DECLARE_MESSAGE_MAP();
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
};
//MFC7.cpp
#include "MFC7.h"
BOOL CMyApp::InitInstance()
{
//创建窗口实例
this->m_pMainWnd = new CMainWindow();
//显示窗口
this->m_pMainWnd->ShowWindow(this->m_nCmdShow);
//更新窗口
this->m_pMainWnd->UpdateWindow();
return TRUE;
}
CMainWindow::CMainWindow()
{
this->Create(NULL, TEXT("MFC"));
}
BEGIN_MESSAGE_MAP(CMainWindow,CFrameWnd)
ON_WM_CREATE()
END_MESSAGE_MAP()
int CMainWindow::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
this->MessageBox(TEXT("Test"));
return 0;
}
CMyApp cMyApp;
使用x64dbg对重点函数调用下断:
bp CMainWindow::OnCreate //被映射的消息回调函数
bp CMainWindow::GetMessageMap //获取当前类的消息映射表
bp CFrameWnd::Create //未被重写的窗口创建函数,该函数回调OnCreate函数
程序运行后首先断在CFrameWnd::Create位置,开始进行窗口的创建的一系列操作。而后对CMainWindow::OnCreate回调函数进行堆栈追踪,定位到了一个类似于中转操作的函数内。
//函数原型
virtual BOOL OnWndMsg(
UINT message, //指定要发送的消息。
WPARAM wParam, //指定其他消息相关信息。
LPARAM lParam, //指定其他消息相关信息。
LRESULT* pResult //WindowProc 的返回值。取决于消息;可为 NULL。
);
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
LRESULT lResult = 0;
// 将命令消息和通知消息交给指定的函数处理
if(message == WM_COMMAND)
{
if(OnCommand(wParam, lParam))
{
lResult = 1;
goto LReturnTrue;
}
return FALSE;
}
if(message == WM_NOTIFY)
{
NMHDR* pHeader = (NMHDR*)lParam;
if(pHeader->hwndFrom != NULL && OnNotify(wParam, lParam, &lResult))
goto LReturnTrue;
return FALSE;
}
// 在各类的消息映射表中查找合适的消息处理函数,找到的话就调用它
const AFX_MSGMAP* pMessageMap;
const AFX_MSGMAP_ENTRY* lpEntry;
for(pMessageMap = GetMessageMap(); pMessageMap != NULL; pMessageMap = pMessageMap->pBaseMap)
{
ASSERT(pMessageMap != pMessageMap->pBaseMap);
if((lpEntry = AfxFindMessageEntry(pMessageMap->pEntries, message, 0, 0)) != NULL)
goto LDispatch;
}
return FALSE;
LDispatch:
union MessageMapFunctions mmf;
mmf.pfn = lpEntry->pfn;
switch(lpEntry->nSig)
{
default:
return FALSE;
case AfxSig_vw:
(this->*mmf.pfn_vw)(wParam);
break;
case AfxSig_vv:
(this->*mmf.pfn_vv)();
break;
case AfxSig_is:
(this->*mmf.pfn_is)((LPTSTR)lParam);
break;
}
LReturnTrue:
if(pResult != NULL)
*pResult = lResult;
return TRUE;
}
在该函数内部,会出现大量的消息(message)对比操作,如果遇见所需处理的消息,则调用对应的消息函数。执行结束后,跳出当前函数。
如果上述消息比对操作,都没有成功。接下来就开始获取用户自定义的消息映射表。
而该函数会调用GetThisMessageMap函数,函数原理和最开始宏定义规定的函数结构相同,最终函数返回一个消息映射表地址。
看第二个地址中存储的数据信息。
#define ON_WM_CREATE() \
{ WM_CREATE, 0, 0, 0, AfxSig_is, \
(AFX_PMSG) (AFX_PMSGW) \
(static_cast< int (AFX_MSG_CALL CWnd::*)(LPCREATESTRUCT) > ( &ThisClass :: OnCreate)) },
//将上述宏定义转换成数值如下
{0x1,0x0,0x0,0x0,0xD,0x函数地址}
在多次执行OnWndMsg函数后,该函数终于收到了ON_WM_CREATE消息。
在OnWndMsg函数中,函数调用AfxFindMessageEntry函数,查找消息对应的函数映射信息,检索到对应地址后进行跳转执行。
// 声明函数的代码在_AFXWIN.H文件中(CWnd类下面),实现代码在WINCORE.CPP文件中
const AFX_MSGMAP_ENTRY* AfxFindMessageEntry(const AFX_MSGMAP_ENTRY* lpEntry,
UINT nMsg, UINT nCode, UINT nID)
{
while(lpEntry->nSig != AfxSig_end)
{
if(lpEntry->nMessage == nMsg && lpEntry->nCode == nCode &&
(nID >= lpEntry->nID && nID <= lpEntry->nLastID))
return lpEntry;
lpEntry++;
}
return NULL;
}
综上可以看到,微软通过自建小表的形式,解决了虚函数表过大的问题。同理,在逆向分析时,如果定位得到用户的消息映射表,那么相当于捕获到了全部的用户消息映射函数。对于程序功能逆向分析也是个不错的突破点。
本文源码参考了《Windows 程序设计(第3版)》一书。