【软件逆向-MFC框架攻防深入探讨】此文章归类为:软件逆向。
大家好,我是科锐逆向49期最菜的学员。(TeddyBe4r是大哥,开个玩笑)
最近分析了一下MFC中的一些核心机制。写出一些心得与各位大佬前辈们探讨交流。文本主要讨论是的MFC中的RTTI机制与MFC中消息路由机制探讨与利用。(本文是探讨MFC原理和攻防心得请不要使用文中提到技术去触犯法律事情,发生后果与作者无关。如有侵权请联系作者删除。)
编译器: Microsoft Visual Studio Community 2019 16.11.44
系统:Windows 10 专业版 22H2 19045.5487
MFC版本:14.29.30040.0
在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
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); |
首先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机制。
从上源码分析中可以看到这些类都依靠自身的静态成员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中用户的按钮与控件的绑定一直都是使用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; } |
我们只需要定位这个函数就行了。然后进行调用即可。
首先个人觉得需要对CRunTime进行加密,尤其是字符串加密这样就可以防止静态分析,其次需要对注入进行检测比如对模块进行扫描。
更多【软件逆向-MFC框架攻防深入探讨】相关视频教程:www.yxfzedu.com