首页
社区
课程
招聘
[原创]MFC框架攻防深入探讨
发表于: 2025-3-6 20:44 2295

[原创]MFC框架攻防深入探讨

2025-3-6 20:44
2295

前言

最近分析了一下MFC中的一些核心机制。写出一些心得与各位大佬前辈们探讨交流。文本主要讨论是的MFC中的RTTI机制与MFC中消息路由机制探讨与利用。(本文是探讨MFC原理和攻防心得请不要使用文中提到技术去触犯法律事情,发生后果与作者无关。如有侵权请联系作者删除。)

环境

编译器: Microsoft Visual Studio Community 2019 16.11.44
系统:Windows 10 专业版 22H2 19045.5487
MFC版本:14.29.30040.0

RTTI讨论

在MFC中是有RTTI机制的。这个可以在微软的官方文档查询到。我们在创建一个MFC程序的时候IDE会生成一个 xx(工程名)App类,并且继承与CWinApp类。主要负责程序的初始化运行退出等工作。并且会生成一个全局变量。

1
extern xxxApp xxApp;

MFC在开发过程中是不可能预测到用户写类的是什么,如果要对本身的框架内的组件进行扩展怎么办?在真正的初始化过程中是如何进行的呢?所以需要RTTI机制,让框架能识别类型,并且能动态创建对象。
在创建一个工程的时候IDE会自动给我们的xxApp类重写一个InitInstance,这里就使用了RTTI。

1
2
3
4
5
6
7
//....部分代码实现
    pDocTemplate = new CSingleDocTemplate(
        IDR_MAINFRAME,
        RUNTIME_CLASS(CTestDocDoc),
        RUNTIME_CLASS(CMainFrame),       // 主 SDI 框架窗口
        RUNTIME_CLASS(CTestDocView));
//....部分代码实现

CRuntimeClass结构体介绍

首先介绍一下CRuntimeClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct CRuntimeClass
{
// Attributes
    LPCSTR m_lpszClassName;
    int m_nObjectSize;
    UINT m_wSchema; // schema number of the loaded class
    CObject* (PASCAL* m_pfnCreateObject)(); // NULL => abstract class
#ifdef _AFXDLL
    CRuntimeClass* (PASCAL* m_pfnGetBaseClass)();
#else
    CRuntimeClass* m_pBaseClass;
#endif
 
// Operations
    CObject* CreateObject();
    BOOL IsDerivedFrom(const CRuntimeClass* pBaseClass) const;
 
    // dynamic name lookup and creation
    static CRuntimeClass* PASCAL FromName(LPCSTR lpszClassName);
    static CRuntimeClass* PASCAL FromName(LPCWSTR lpszClassName);
    static CObject* PASCAL CreateObject(LPCSTR lpszClassName);
    static CObject* PASCAL CreateObject(LPCWSTR lpszClassName);
 
// Implementation
    void Store(CArchive& ar) const;
    static CRuntimeClass* PASCAL Load(CArchive& ar, UINT* pwSchemaNum);
 
    // CRuntimeClass objects linked together in simple list
    CRuntimeClass* m_pNextClass;       // linked list of registered classes
    const AFX_CLASSINIT* m_pClassInit;
};

他是一个RTTI结构体MFC中RTTI信息都存储于这个结构体中。关键信息如下:

1
2
3
4
5
6
7
8
9
10
//类的名称
LPCSTR m_lpszClassName;
//对象的大小,用于动态内存分配
int m_nObjectSize;
//Schema 版本号
UINT m_wSchema; // schema number of the loaded class
//用于 MFC 动态对象创建
CObject* (PASCAL* m_pfnCreateObject)();
//指向基类的 CRuntimeClass 结构
CRuntimeClass* m_pBaseClass;

介绍一下这些方法

1
2
3
4
5
6
7
8
9
10
//通过类名 动态创建对象
CObject* CreateObject();\
//通过字符串断继承层次
BOOL IsDerivedFrom(const CRuntimeClass* pBaseClass) const;
// dynamic name lookup and creation
//通过类名(char* 或 wchar_t*)查找该类对应的 CRuntimeClass 结构
static CRuntimeClass* PASCAL FromName(LPCSTR lpszClassName);
static CRuntimeClass* PASCAL FromName(LPCWSTR lpszClassName);
static CObject* PASCAL CreateObject(LPCSTR lpszClassName);
static CObject* PASCAL CreateObject(LPCWSTR lpszClassName);

RTTI实现

首先MFC在设计之初就设计了类似Java一样顶层父类CObject,其它类都继承与它。这里借用一下微软的MFC框架图。
图片描述

在CObject中有一个这样的方法和和这样的一个成员。这样就具备了动态创建和获取类的能力。

1
2
virtual CRuntimeClass* GetRuntimeClass() const;
static const CRuntimeClass classCObject;

根据架构图可以得知,MFC中所有类都继承与CObjec,所以整个框架都有RTTI了。
现在有个问题?用户的类是怎么加入RTTI的呢?以CTestDocDoc这个类为例

1
2
3
4
5
class CTestDocDoc : public CDocument
{
protected: // 仅从序列化创建
    CTestDocDoc() noexcept;
    DECLARE_DYNCREATE(CTestDocDoc)

着重关注一下这个宏:

1
DECLARE_DYNCREATE(CTestDocDoc)

把它扩展开来是这样的

1
2
3
4
DECLARE_DYNCREATE
```cpp
DECLARE_DYNAMIC(CTestDocDoc)   // 提供运行时类型识别(RTTI)支持
static CObject* PASCAL CreateObject();

DECLARE_DYNAMIC 展开

1
2
3
public:
    static const CRuntimeClass classCTestDocDoc;   // 运行时类信息
    virtual CRuntimeClass* GetRuntimeClass() const;  // 获取 CRuntimeClass

这样CTestDocDoc 就具备动态识别和动态创建的能力了。
现在还有一个问题,这些RTTI是在什么时候创建的并且怎么把他们关联起来用?

在CTestDocDoc.cpp中有这样的一个宏

1
IMPLEMENT_DYNCREATE(CTestDocDoc, CDocument)

我们展开后,可以看到创建了一个CRuntimeClass,并且填写了RTTI信息,绑定了父类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CObject* PASCAL CTestDocDoc::CreateObject()
{
    return new CTestDocDoc;  // 运行时动态创建 CTestDocDoc 对象
}
 
AFX_COMDAT const CRuntimeClass CTestDocDoc::classCTestDocDoc = {
    "CTestDocDoc",              // 记录类名
    sizeof(CTestDocDoc),        // 记录对象大小
    0xFFFF,                     // 序列化 Schema 版本
    CTestDocDoc::CreateObject,  // 指向 CreateObject() 方法(用于动态创建对象)
    RUNTIME_CLASS(CDocument),   // 指向基类 CDocument 的 CRuntimeClass 结构
    NULL,                       // MFC 内部字段
    NULL                        // MFC 内部字段
};
 
CRuntimeClass* CTestDocDoc::GetRuntimeClass() const
{
    return RUNTIME_CLASS(CTestDocDoc);
}

解释一下RUNTIME_CLASS宏

1
2
#define RUNTIME_CLASS(class_name) _RUNTIME_CLASS(class_name)
#define _RUNTIME_CLASS(class_name) ((CRuntimeClass*)(&class_name::class##class_name))

展开后就是这样

1
((CRuntimeClass*)(&CTestDocDoc::classCTestDocDoc))

所以我们可以得知RTTI是一个向上绑定关系。也就是IDE窗口的类和MFC本身类中都会存在RTTI机制。

RTTI机制利用

从上源码分析中可以看到这些类都依靠自身的静态成员xx::classxx来实现。所以说明在程序编译的时候已经生产,我猜测应该是在RDATA节中存放。可以通过静态分析来显示所有RTTI机制的类。
实现思路如下:
图片描述
相关核心代码:
定义关键结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CObject {
 
 
};
struct CRuntimeClass
{
    // Attributes
    LPCSTR m_lpszClassName;
    int m_nObjectSize;
    UINT m_wSchema;
    CObject* (PASCAL* m_pfnCreateObject)();
    CRuntimeClass* m_pBaseClass;
    CRuntimeClass* m_pNextClass;
    const void* m_pClassInit;
};

工具函数实现

1
2
3
4
5
6
DWORD VaToFileOffset(MY_INT VaPoint,  MY_INT PointerToRawData, MY_INT VirtualAddress , MY_INT ImageBase) {
    return VaPoint + PointerToRawData - VirtualAddress - ImageBase;
}
DWORD FileOffsetToVa(MY_INT VaPoint,  MY_INT PointerToRawData, MY_INT VirtualAddress , MY_INT ImageBase) {
    return VaPoint - PointerToRawData + VirtualAddress + ImageBase;
}

利用内存文件共享,实现文件打开和关闭和读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 文件打开函数,返回需要的所有句柄和指针
bool OpenFile(const char* szPath, HANDLE* phFile, HANDLE* phMapFile,
    LPVOID* ppMapView, unsigned char** ppFilePoint) {
    // 打开文件
    *phFile = CreateFileA(
        szPath,                   // 文件路径
        GENERIC_READ | GENERIC_WRITE,  // 读写权限
        0,                        // 共享模式(不共享)
        NULL,                     // 安全属性
        OPEN_EXISTING,            // 只打开已存在的文件
        FILE_ATTRIBUTE_NORMAL,    // 普通文件
        NULL                      // 模板文件句柄
    );
 
    if (*phFile == INVALID_HANDLE_VALUE) {
        std::cerr << "无法打开文件,错误码:" << GetLastError() << std::endl;
        return false;
    }
 
    // 创建文件映射
    *phMapFile = CreateFileMapping(*phFile, NULL, PAGE_READWRITE, 0, 0, NULL);
    if (*phMapFile == NULL) {
        std::cerr << "无法创建文件映射,错误码:" << GetLastError() << std::endl;
        CloseHandle(*phFile);
        *phFile = INVALID_HANDLE_VALUE;
        return false;
    }
 
    // 映射视图
    *ppMapView = MapViewOfFile(
        *phMapFile,            // 文件映射对象句柄
        FILE_MAP_ALL_ACCESS,   // 读写访问权限
        0, 0,                  // 偏移量
        0                      // 映射整个文件
    );
 
    if (*ppMapView == NULL) {
        std::cerr << "无法映射文件,错误码:" << GetLastError() << std::endl;
        CloseHandle(*phMapFile);
        CloseHandle(*phFile);
        *phMapFile = NULL;
        *phFile = INVALID_HANDLE_VALUE;
        return false;
    }
 
    *ppFilePoint = static_cast<unsigned char*>(*ppMapView);
    return true;
}
 
// 关闭文件和清理资源
void CloseFile(HANDLE hFile, HANDLE hMapFile, LPVOID pMapView) {
    if (pMapView) {
        UnmapViewOfFile(pMapView);
    }
 
    if (hMapFile) {
        CloseHandle(hMapFile);
    }
 
    if (hFile != INVALID_HANDLE_VALUE) {
        CloseHandle(hFile);
    }
}

找到Cobjec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
bool FindCObject(unsigned char* pFilePoint, IMAGE_SECTION_HEADER* pRdataSectionHeader , MY_INT ImageBase, CRuntimeClass** CobjectRtti , MY_INT& CobjectAddress) {
    MY_INT CobjectCharPoint;
    if (!pFilePoint || !pRdataSectionHeader) {
        std::cerr << "无效的文件指针或Rdata段指针" << std::endl;
        return false;
    }
    MY_INT BaseAddr = (MY_INT)pFilePoint;
    for (DWORD i = 0; i < pRdataSectionHeader->SizeOfRawData; i++) {
        const char* FindAddr = (const char*)(pRdataSectionHeader->PointerToRawData + BaseAddr + i);
 
        if (strcmp(FindAddr, "CObject") == 0) {
            if (*(FindAddr - 1) == 0) {
                CobjectCharPoint = FileOffsetToVa((DWORD)(FindAddr)-BaseAddr, pRdataSectionHeader->PointerToRawData,
                    pRdataSectionHeader->VirtualAddress, ImageBase);
                std::cout << "CObject VA " << std::hex << std::showbase << CobjectCharPoint << std::endl;
                for (DWORD i = 0; i < pRdataSectionHeader->SizeOfRawData; i+=4) {
                    MY_INT* FindAddr = (MY_INT*)(pRdataSectionHeader->PointerToRawData + BaseAddr + i);
                    if (*FindAddr == CobjectCharPoint) {
                        *CobjectRtti = (CRuntimeClass * )FindAddr;
                        CobjectAddress = FileOffsetToVa((DWORD)(FindAddr)-BaseAddr, pRdataSectionHeader->PointerToRawData,
                            pRdataSectionHeader->VirtualAddress, ImageBase);
 
                        std::cout << "CObject VA " << std::hex << std::showbase << CobjectAddress << std::endl;
                        return true;
                    }
                }
                std::cout << "未找到CObject 引用" << std::endl;
                return false;
                
            }
        }
    }
    std::cout << "未找到CObject" << std::endl;
    return false;
}

Dump类关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void Dump(int CObjectAddress , MY_INT BaseAddr, unsigned char* Buf , int BufSize , MY_INT ImageBase, MY_INT PointerToRawData,
    MY_INT VirtualAddress , int Level  , const char* parentInfo) {
    
    // 如果该父类已经处理过,则直接返回
    if (g_visitedAddresses.find(CObjectAddress) != g_visitedAddresses.end())
        return;
    g_visitedAddresses.insert(CObjectAddress);
 
    for (int i = 0; i < BufSize; i+=4) {
        if (CObjectAddress == *(int*)(Buf + i)) {
            CRuntimeClass* pRtti = (CRuntimeClass *)(Buf + i - sizeof(void*) - sizeof(UINT) - sizeof(int) - sizeof(void*));
             MY_INT Va = FileOffsetToVa((MY_INT)pRtti - BaseAddr, PointerToRawData, VirtualAddress , ImageBase);
             MY_INT FileOffset = VaToFileOffset((MY_INT)pRtti->m_lpszClassName, PointerToRawData, VirtualAddress, ImageBase);
             for (int indent = 0; indent < Level; indent++) {
                 std::cout << "  ";
             }
             std::cout << "RootClassName: " << parentInfo << std::endl;
             for (int indent = 0; indent < Level; indent++) {
                 std::cout << "  ";
             }
             std::cout << "ClassName: " << (char *)(FileOffset  + BaseAddr) << std::endl;
             for (int indent = 0; indent < Level; indent++) {
                 std::cout << "  ";
             }
             std::cout << "ObjSize: " << pRtti->m_nObjectSize << std::endl;
 
             Dump(Va, BaseAddr, Buf, BufSize, ImageBase, PointerToRawData, VirtualAddress , Level + 1 , (char*)(FileOffset + BaseAddr));
        }
    }
     
}

最后的效果:
图片描述
当然这个思路仅限静态链接的MFC动态链接需要稍微修改一下。

MFC中消息路由

在MFC中用户的按钮与控件的绑定一直都是使用UI界面进行实现的。用户只需要拖动和点击就可以创建一个响应函数。这里就是使用了消息路由机制。我们可以从看到这两个比较关键的宏

1
2
3
DECLARE_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(CTestDocDoc, CDocument)
END_MESSAGE_MAP()
1
2
3
4
5
6
7
8
9
10
11
12
#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;                    \
        __pragma(warning(push))                            \
        __pragma(warning(disable: 4640)) /* message maps can only be called by single threaded message pump */ \
        static const AFX_MSGMAP_ENTRY _messageEntries[] =  \
        {
1
2
3
4
5
6
7
8
9
#define END_MESSAGE_MAP() \
        {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \
    }; \
        __pragma(warning(pop))  \
        static const AFX_MSGMAP messageMap = \
        { &TheBaseClass::GetThisMessageMap, &_messageEntries[0] }; \
        return &messageMap; \
    }                                 \
    PTM_WARNING_RESTORE

假设展开后是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 1. BEGIN_MESSAGE_MAP 宏展开
PTM_WARNING_DISABLE
 
const AFX_MSGMAP* CMyWnd::GetMessageMap() const
{
    return GetThisMessageMap();
}
 
const AFX_MSGMAP* PASCAL CMyWnd::GetThisMessageMap()
{
    typedef CMyWnd ThisClass;
    typedef CWnd TheBaseClass;
     
    __pragma(warning(push))
    __pragma(warning(disable: 4640)) // message maps can only be called by single threaded message pump
 
    static const AFX_MSGMAP_ENTRY _messageEntries[] =
    {
        { WM_LBUTTONDOWN, 0, 0, 0, AfxSig_vwii,
          (AFX_PMSG)(AFX_PMSGW)(static_cast<void (CMyWnd::*)(UINT, CPoint)>(&CMyWnd::OnLButtonDown)) },
        { WM_RBUTTONDOWN, 0, 0, 0, AfxSig_vwii,
          (AFX_PMSG)(AFX_PMSGW)(static_cast<void (CMyWnd::*)(UINT, CPoint)>(&CMyWnd::OnRButtonDown)) },
        { 0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } // 结束标记
    };
 
    __pragma(warning(pop))
 
    static const AFX_MSGMAP messageMap =
    {
        &TheBaseClass::GetThisMessageMap, &_messageEntries[0]
    };
 
    return &messageMap;
}
 
// 2. DECLARE_MESSAGE_MAP 宏展开
public:
    static const AFX_MSGMAP* PASCAL GetThisMessageMap();
    virtual const AFX_MSGMAP* GetMessageMap() const;
 
// 3. END_MESSAGE_MAP 宏已经在 GetThisMessageMap 里完成
PTM_WARNING_RESTORE

主要就是创建了这个表
static const AFX_MSGMAP_ENTRY _messageEntries[]
和重写几个虚函数,最关键是这个表结构是什么样的?

1
2
3
4
5
6
7
8
9
struct AFX_MSGMAP_ENTRY
{
    UINT nMessage;   // Windows 消息 ID
    UINT nCode;      // 控件通知代码 (WM_NOTIFY 或控件消息)
    UINT nID;        // 控件 ID(窗口消息时为 0)
    UINT nLastID;    // 控件 ID 范围的最后一个 ID(单个 ID 时等于 nID)
    UINT_PTR nSig;   // 消息签名,决定函数调用方式
    AFX_PMSG pfn;    // 处理该消息的成员函数指针
};

主要就是控件消息ID,和消息签名,还有消息成员函数指针。
这里的nSig主要是表示这个AFX_PMSG pfn函数指针的参数类型。在消息派发中处理不同控件参数不一致的问题。MFC定义了所有的可能情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum AfxSig{
    AfxSig_end = 0,     // [marks end of message map]
    AfxSig_b_D_v,               // BOOL (CDC*)
    AfxSig_b_b_v,               // BOOL (BOOL)
    AfxSig_b_u_v,               // BOOL (UINT)
    AfxSig_b_h_v,               // BOOL (HANDLE)
    AfxSig_b_W_uu,              // BOOL (CWnd*, UINT, UINT)
    AfxSig_b_W_COPYDATASTRUCT,              // BOOL (CWnd*, COPYDATASTRUCT*)
    AfxSig_b_v_HELPINFO,        // BOOL (LPHELPINFO);
    AfxSig_CTLCOLOR,            // HBRUSH (CDC*, CWnd*, UINT)
    AfxSig_CTLCOLOR_REFLECT,    // HBRUSH (CDC*, UINT)
    AfxSig_i_u_W_u,             // int (UINT, CWnd*, UINT)  // ?TOITEM
    AfxSig_i_uu_v,              // int (UINT, UINT)
    AfxSig_i_W_uu,              // int (CWnd*, UINT, UINT)
    AfxSig_i_v_s,               // int (LPTSTR)
    AfxSig_l_w_l,               // LRESULT (WPARAM, LPARAM)
    AfxSig_l_uu_M,              // LRESULT (UINT, UINT, CMenu*)
    AfxSig_v_b_h,               // void (BOOL, HANDLE)
    //... ... 还有很多省略
}

如何把父类和子类相连呢?假设子类中没有消息响应处理怎么办?所以回到宏那
我们可以看到有这样一个代码:

1
2
3
4
static const AFX_MSGMAP messageMap =
{
     &TheBaseClass::GetThisMessageMap, &_messageEntries[0]
};

AFX_MSGMAP 结构体是这样的:他有指向上一个 theClass::GetThisMessageMap(),然后根据宏DECLARE_MESSAGE_MAP ,他是一个静态函数。这样就构成一个单向向上链表,相当于这是一个元素节点。当子类没有消息的时候会直接找父类的

1
2
3
4
5
struct AFX_MSGMAP
{
    const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();
    const AFX_MSGMAP_ENTRY* lpEntries;
};

在用户视角中是这样的
图片描述

窗口绑定

在MFC中窗口绑定有点特殊所以需要讨论一下,后面用得上。窗口的创建主要在CWnd::CreateEx中实现的
这里有个特殊的地方

1
AfxHookWindowCreate(this);

这里框架自己HOOK了自己

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void AFXAPI AfxHookWindowCreate(CWnd* pWnd)
{
    _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
    if (pThreadState->m_pWndInit == pWnd)
        return;
 
    if (pThreadState->m_hHookOldCbtFilter == NULL)
    {
        pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT,
            _AfxCbtFilterHook, NULL, ::GetCurrentThreadId());
        if (pThreadState->m_hHookOldCbtFilter == NULL)
            AfxThrowMemoryException();
    }
    ASSERT(pThreadState->m_hHookOldCbtFilter != NULL);
    ASSERT(pWnd != NULL);
    ASSERT(pWnd->m_hWnd == NULL);   // only do once
 
    ASSERT(pThreadState->m_pWndInit == NULL);   // hook not already in progress
    pThreadState->m_pWndInit = pWnd;
}

内部实现可以看到SetWindowsHookEx他hook的主要还是窗口创建消息这些。转到过程函数中可以发现主要是对窗口进行过滤,因为窗口创建的这些消息是无法再消息循环中拿到的所以需要提前拦截获取。并且修改了消息循环最后绑定到MFC自己的回调中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//....
 
    CWnd* pWndInit = pThreadState->m_pWndInit;
    BOOL bContextIsDLL = afxContextIsDLL;
    if (pWndInit != NULL || (!(lpcs->style & WS_CHILD) && !bContextIsDLL))
    {
        // Note: special check to avoid subclassing the IME window
        if (_afxDBCS)
        {
            // check for cheap CS_IME style first...
            if (GetClassLong((HWND)wParam, GCL_STYLE) & CS_IME)
                goto lCallNextHook;
 
//....
            // subclass the window with standard AfxWndProc
            WNDPROC afxWndProc = AfxGetAfxWndProc();
            oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC,
                (DWORD_PTR)afxWndProc);
            ASSERT(oldWndProc != NULL);
            if (oldWndProc != afxWndProc)
                *pOldWndProc = oldWndProc;
//...

这样MFC就可以把所有子窗口都绑定到一个消息循环中来这样集中派发消息。核心函数主要就是这个AfxWndProc。

1
2
3
4
5
6
7
8
WNDPROC AFXAPI AfxGetAfxWndProc()
{
#ifdef _AFXDLL
    return AfxGetModuleState()->m_pfnAfxWndProc;
#else
    return &AfxWndProc;
#endif
}

消息派发

这里简单介绍一下消息派发。篇幅有限望各位大佬理解。在HOOK的过程有一个关键函数

1
pWndInit->Attach(hWnd);

主要作用就是hWnd和CWnd对象加入到Hash表里面来,这个表放在Tls中存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BOOL CWnd::Attach(HWND hWndNew)
{
    ASSERT(m_hWnd == NULL);     // only attach once, detach on destroy
    ASSERT(FromHandlePermanent(hWndNew) == NULL);
        // must not already be in permanent map
 
    if (hWndNew == NULL)
        return FALSE;
 
    CHandleMap* pMap = afxMapHWND(TRUE); // create map if not exist
    ASSERT(pMap != NULL);
 
    pMap->SetPermanent(m_hWnd = hWndNew, this);
 
    AttachControlSite(pMap);
 
    return TRUE;
}

MFC有一个线程管理类专门管理这些信息

1
2
3
4
5
6
7
8
9
10
11
class AFX_MODULE_THREAD_STATE : public CNoTrackObject
{
//...
    DWORD m_nTempMapLock;           // if not 0, temp maps locked
    CHandleMap* m_pmapHWND;
    CHandleMap* m_pmapHMENU;
    CHandleMap* m_pmapHDC;
    CHandleMap* m_pmapHGDIOBJ;
    CHandleMap* m_pmapHIMAGELIST;
//...
}

在消息循环中MFC会从MAP中访问这些

1
2
3
4
5
6
AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam){
//..
CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
//...
return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam);
}

拿到对应的pWnd 后就会使用消息路由机制找到相对于的处理函数,主要就是循环遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult){
//...
const AFX_MSGMAP* pMessageMap; pMessageMap = GetMessageMap();
//...
else
            {
                // registered windows message
                lpEntry = pMessageMap->lpEntries;
                while ((lpEntry = AfxFindMessageEntry(lpEntry, 0xC000, 0, 0)) != NULL)
                {
                    UINT* pnID = (UINT*)(lpEntry->nSig);
                    ASSERT(*pnID >= 0xC000 || *pnID == 0);
                        // must be successfully registered
                    if (*pnID == message)
                    {
                        pMsgCache->lpEntry = lpEntry;
                        winMsgLock.Unlock();
                        goto LDispatchRegistered;
                    }
                    lpEntry++;      // keep looking past this one
                }
            }
}
//...

消息路由的利用

我们知道了在MFC中过程函数是唯一的,并且存在一个消息路由表,可以拿到这个消息路由表并且打印这些虚函数和对应消息映射方便我们的逆向查找相关按钮和组件。实现思路如下:
图片描述
首先我们创建一个MFCDLL工程方便我们使用MFC头文件。
关键实现代码:
遍历顶层窗口函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 回调函数,用于枚举窗口
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) {
    DWORD windowPID = 0;
    GetWindowThreadProcessId(hwnd, &windowPID);
 
    // 检查窗口是否属于当前进程,并且是顶级窗口且可见
    if (windowPID == GetCurrentProcessId() &&
        GetWindow(hwnd, GW_OWNER) == NULL &&
        IsWindowVisible(hwnd)) {
        *((HWND*)lParam) = hwnd;  // 保存窗口句柄
        return FALSE;  // 找到后停止枚举
    }
    return TRUE;
}
 
HWND GetCurrentProcessMainWindow() {
    HWND hwnd = NULL;
    EnumWindows(EnumWindowsProc, (LPARAM)&hwnd);
    return hwnd;
}

拿到消息循环过程函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
LONG_PTR GetAfxWinProc(HWND Hwnd) {
    LONG_PTR  WinProc = 0;
 
    if (!IsWindow(Hwnd)) {
 
        return WinProc;
    }
    PrintWindowTitle(Hwnd);
    WinProc = GetWindowLongPtrA(Hwnd, GWLP_WNDPROC);
    if (!WinProc) {
        WinProc = GetWindowLongPtrW(Hwnd, GWLP_WNDPROC);
    }
    return WinProc;
}

替换过程函数成我们的

1
2
3
4
5
OldpFun = (pProcFun)WinProc;
if (WinProc) {
    g_oss << "WinProc acquired successfully:" << std::hex << WinProc << std::endl;
}
SetWindowLongPtr(ParentHwnd, GWLP_WNDPROC, (LONG_PTR)MyWndProc);

自己过程函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
LRESULT CALLBACK MyWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     
    /*for (int i = 0 ;i< 100;i++) {
        auto   pFnGetMsg = (const AFX_MSGMAP * (__thiscall*)(CWnd*))(vtable[0xa]);
        g_oss << pFnGetMsg << std::endl;
    }*/
    if (g_Flag) {
        CWnd* Wnd = g_pFromHandlePermanent(hwnd);
        auto WndRtti = Wnd->GetRuntimeClass();
        auto RootRtti = FindRoot(WndRtti);
        TraverseClassHierarchyWithInstances(WndRtti, g_oss);
        auto** vtable = *(void***)Wnd;
        auto   pFnGetMsg = (const AFX_MSGMAP * (__thiscall*)(CWnd*))(vtable[0xa]);
        auto   pMap = pFnGetMsg(Wnd);
        DumpMessageMapsSimple(pMap, g_oss);
        SaveToTextFile("MessageMap.txt");
        g_Flag = false;
    }
    return OldpFun(hwnd, message, wParam, lParam);
}

拿到FromHandlePermanent ,这里展示release版本,主要是通过函数偏移进行计算从而拿到地址。

1
g_pFromHandlePermanent = (pFromHandlePermanent)((WinProc + 0x1B) + *(LONG_PTR*)(WinProc + 0x17));

打印单链路上的RTTI信息

1
2
3
4
CWnd* Wnd = g_pFromHandlePermanent(hwnd);
        auto WndRtti = Wnd->GetRuntimeClass();
        auto RootRtti = FindRoot(WndRtti);
        TraverseClassHierarchyWithInstances(WndRtti, g_oss);

RTTI实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
CRuntimeClass* FindRoot(CRuntimeClass* pRuntimeClass) {
 
    // 向上遍历到最顶层的类
    CRuntimeClass* pCurrentClass = pRuntimeClass;
    while (pCurrentClass)
    {
        pCurrentClass = (CRuntimeClass*)pCurrentClass->m_pfnGetBaseClass;
    }
    return pCurrentClass;
}
 
void TraverseClassHierarchyWithInstances(const CRuntimeClass* pRuntimeClass, std::ostringstream& oss, int level = 0)
{
    // Check if the runtime class pointer is valid
    if (!pRuntimeClass)
        return;
 
    // Add indentation based on hierarchy level
    for (int i = 0; i < level; i++)
        oss << "    "; // Four spaces for each level of hierarchy
 
    // Print current class name
    oss << pRuntimeClass->m_lpszClassName;
 
    // Print instance count if available
    if (pRuntimeClass->m_pfnGetBaseClass)
        oss << " (Base: " << ((CRuntimeClass*)pRuntimeClass->m_pfnGetBaseClass)->m_lpszClassName << ")";
 
    // Print size of the class
    oss << " - Size: " << pRuntimeClass->m_nObjectSize << " bytes";
 
    // Add newline for readability
    oss << std::endl;
 
    // Recursively traverse base class if it exists
    if (pRuntimeClass->m_pfnGetBaseClass)
    {
        // Get the base class runtime information using your specific method
        const CRuntimeClass* pBaseClass = (CRuntimeClass*)pRuntimeClass->m_pfnGetBaseClass;
 
        // Recursively call to traverse the base class with increased indentation level
        TraverseClassHierarchyWithInstances(pBaseClass, oss, level + 1);
    }
}

通过虚表拿到 CWnd::FromHandlePermanent

1
2
3
auto** vtable = *(void***)Wnd;
        auto   pFnGetMsg = (const AFX_MSGMAP * (__thiscall*)(CWnd*))(vtable[0xa]);
        auto   pMap = pFnGetMsg(Wnd);

这里有个小技巧,自己计算虚表太麻烦我们可以遍历虚表并且使用VS自带的提示去判断到底是哪个。不同的版本可以下载不同的版本的mfc自行编译计算偏移位置。大致思路是这样的

1
2
3
for (int i = 0; i < 100; i++) {
            pFnGetMsg = (const AFX_MSGMAP * (__thiscall*)(CWnd*))(vtable[i]);
}

图片描述
最后在解析信息,这里就罗列主要核心代码,因为有很多字符串处理代码就简化展示一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
DumpMessageMapsSimple(pMap, g_oss);
// 获取消息描述
std::string GetMessageDescription(UINT message)
{
    switch (message)
    {
    case WM_CREATE:    return "WM_CREATE";
    case WM_DESTROY:   return "WM_DESTROY";
    case WM_SIZE:      return "WM_SIZE";
    case WM_PAINT:     return "WM_PAINT";
    default:
        // 简化处理,直接返回 16 进制值
        char buf[16];
        sprintf_s(buf, "0x%04X", message);
        return buf;
    }
}
 
// 获取通知代码描述
std::string GetNotifyCodeDescription(UINT code)
{
    switch (code)
    {
    case BN_CLICKED:  return "BN_CLICKED";
    case EN_CHANGE:   return "EN_CHANGE";
    default:
        char buf[16];
        sprintf_s(buf, "0x%04X", code);
        return buf;
    }
}
 
// 获取控件类型描述
std::string GetControlTypeDescription(UINT id)
{
    switch (id)
    {
    case IDOK:       return "IDOK";
    case IDCANCEL:   return "IDCANCEL";
    default:
        return "Control ID " + std::to_string(id);
    }
}
 
// 获取 MFC 消息签名描述
std::string GetSignatureDescription(UINT nSig)
{
    switch (nSig)
    {
    case AfxSig_vv:  return "AfxSig_vv (void OnXxx())";
    default:
        char buf[16];
        sprintf_s(buf, "0x%X", nSig);
        return buf;
    }
}
 
 
 
void DumpMessageMapsSimple(const AFX_MSGMAP* pMap, std::ostringstream& oss, void* baseAddress = nullptr)
{
    // 循环遍历当前类及其基类的消息映射
    for ( ; pMap != nullptr; pMap = pMap->pfnGetBaseMap ? pMap->pfnGetBaseMap() : nullptr)
    {
        const AFX_MSGMAP_ENTRY* pEntry = pMap->lpEntries;
 
        // 遍历当前类的所有消息映射条目
        while (pEntry->nSig != 0)
        {
            oss
                << "Message: "   << GetMessageDescription(pEntry->nMessage)
                << ", Notify: " << GetNotifyCodeDescription(pEntry->nCode)
                << ", ID: "     << GetControlTypeDescription(pEntry->nID)
                << ", Sig: "    << GetSignatureDescription(pEntry->nSig)
                << ", Func: "   << pEntry->pfn
                << std::endl;
 
            // 指向下一个消息映射条目
            ++pEntry;
        }
    }
}

最后的效果图:
图片描述

还分享一种思路

上面提到过MAP这些关键信息是从tls里面拿到的,而且是有一个类进行管理所以肯定有一个创建过程。通过逆向分析可以看到
在initterm中可以看到
图片描述
在Initerm表中
图片描述
我们回到源码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
THREAD_LOCAL(_AFX_THREAD_STATE, _afxThreadState)
 
AFX_MODULE_STATE* AFXAPI AfxGetModuleState()
{
    _AFX_THREAD_STATE* pState = _afxThreadState;
    ENSURE(pState);
    AFX_MODULE_STATE* pResult;
    if (pState->m_pModuleState != NULL)
    {
        // thread state's module state serves as override
        pResult = pState->m_pModuleState;
    }
    else
    {
        // otherwise, use global app state
        pResult = _afxBaseModuleState.GetData();
    }
    ENSURE(pResult != NULL);
    return pResult;
}

当_afxThreadState为空的时候就会创建 AFX_MODULE_STATE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CNoTrackObject* CProcessLocalObject::GetData(
    CNoTrackObject* (AFXAPI* pfnCreateObject)())
{
    if (m_pObject == NULL)
    {
        AfxLockGlobals(CRIT_PROCESSLOCAL);
        TRY
        {
            if (m_pObject == NULL)
                m_pObject = (*pfnCreateObject)();
        }
        CATCH_ALL(e)
        {
            AfxUnlockGlobals(CRIT_PROCESSLOCAL);
            THROW_LAST();
        }
        END_CATCH_ALL
        AfxUnlockGlobals(CRIT_PROCESSLOCAL);
    }
    return m_pObject;
}

在函数内部有这样的函数

1
pResult = _afxBaseModuleState.GetData();

由于编译器的优化会导致只剩下这个_afxBaseModuleState.GetData();,这个可以拿到AFX_MODULE_STATE类,并且整个进程只有一个唯一的。
而AFX_MODULE_STATE 继承与CNoTrackObject

1
class AFX_MODULE_STATE : public CNoTrackObject

并且存在一个m_pmapHWND成员在回调的时候afxMapHWND就是使用它,所以拿到他几乎就拿到所有信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CHandleMap* PASCAL afxMapHWND(BOOL bCreate)
{
    AFX_MODULE_THREAD_STATE* pState = AfxGetModuleThreadState();
    if (pState->m_pmapHWND == NULL && bCreate)
    {
        BOOL bEnable = AfxEnableMemoryTracking(FALSE);
        _PNH pnhOldHandler = AfxSetNewHandler(&AfxCriticalNewHandler);
 
        pState->m_pmapHWND = new CHandleMap(RUNTIME_CLASS(CWnd),
            ConstructDestruct<CWnd>::Construct, ConstructDestruct<CWnd>::Destruct,
            offsetof(CWnd, m_hWnd));
 
        AfxSetNewHandler(pnhOldHandler);
        AfxEnableMemoryTracking(bEnable);
    }
    return pState->m_pmapHWND;
}

我们只需要定位这个函数就行了。然后进行调用即可。
图片描述

这里对MFC框架加固的一些看法

首先个人觉得需要对CRunTime进行加密,尤其是字符串加密这样就可以防止静态分析,其次需要对注入进行检测比如对模块进行扫描。


[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!

最后于 2天前 被BitWarden编辑 ,原因:
收藏
免费 5
支持
分享
最新回复 (5)
雪    币: 4905
活跃值: (5630)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
2
你做的这些,之前有个程序叫xspy的都做了
2025-3-7 02:17
0
雪    币: 795
活跃值: (636)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
3
在最后一个思路中tls不适用于远程线程注入,tls在你注入后的远程线程中是无法拿到正确结果的因为你的线程不一样,如果要注入全程都需要写汇编进行内联注入或者用硬件断点进行注入,相较于第一种方法麻烦不少。
2025-3-7 05:19
0
雪    币: 215
活跃值: (225)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
TeddyBe4r 在最后一个思路中tls不适用于远程线程注入,tls在你注入后的远程线程中是无法拿到正确结果的因为你的线程不一样,如果要注入全程都需要写汇编进行内联注入或者用硬件断点进行注入,相较于第一种方法麻烦不少。
和第一种方法一样也可以替换过程函数拿,这样就切换到当前线程去了。
2025-3-7 09:28
0
雪    币: 215
活跃值: (225)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
sonyps 你做的这些,之前有个程序叫xspy的都做了
好的
2025-3-7 09:29
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
支持支持 顶!
2025-3-7 13:30
0
游客
登录 | 注册 方可回帖
返回