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

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

2025-3-6 20:44
2585

前言

最近分析了一下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*>(*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)
    //... ... 还有很多省略
}

[注意]看雪招聘,专注安全领域的专业人才平台!

最后于 4天前 被BitWarden编辑 ,原因:
收藏
免费 5
支持
分享
赞赏记录
参与人
雪币
留言
时间
米龙·0xFFFE
你的分享对大家帮助很大,非常感谢!
2025-3-9 22:16
appview
期待更多优质内容的分享,论坛有你更精彩!
2025-3-8 13:09
xichang13
+1
非常支持你的观点!
2025-3-6 23:31
半个大西瓜
为你点赞!
2025-3-6 20:56
xle
+1
谢谢你的细致分析,受益匪浅!
2025-3-6 20:47
最新回复 (5)
雪    币: 4915
活跃值: (5645)
能力值: ( 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
雪    币: 415
活跃值: (275)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
4
TeddyBe4r 在最后一个思路中tls不适用于远程线程注入,tls在你注入后的远程线程中是无法拿到正确结果的因为你的线程不一样,如果要注入全程都需要写汇编进行内联注入或者用硬件断点进行注入,相较于第一种方法麻烦不少。
和第一种方法一样也可以替换过程函数拿,这样就切换到当前线程去了。
2025-3-7 09:28
0
雪    币: 415
活跃值: (275)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
5
sonyps 你做的这些,之前有个程序叫xspy的都做了
好的
2025-3-7 09:29
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
支持支持 顶!
2025-3-7 13:30
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册