MFC Under the Hood: RTTI, Message Routing, and Class Hierarchy Recovery

About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.

This research is published for educational and defensive purposes. The target sample is anonymized throughout.

Preface

I recently spent some time analyzing a few of the core mechanisms in MFC, and I’m writing up what I found to discuss with the community. This post is mainly about MFC’s RTTI mechanism and its message-routing mechanism — how they work and how to make use of them. (This article explores MFC internals for educational purposes only; use these techniques only on software you own or are authorized to analyze.)

Environment

  • Compiler: Microsoft Visual Studio Community 2019 16.11.44
  • OS: Windows 10 Pro 22H2 19045.5487
  • MFC version: 14.29.30040.0

RTTI in MFC

MFC has an RTTI mechanism, documented in Microsoft’s official docs. When you create an MFC program, the IDE generates an xxxApp class (named after your project) that inherits from CWinApp, responsible for initializing, running, and exiting the program. It also generates a global variable:

extern xxxApp xxApp;

MFC can’t predict what classes the developer will write, so how does it extend its own framework components, and how does the real initialization work? That’s what RTTI is for — it lets the framework recognize types and dynamically create objects.

When you create a project, the IDE overrides InitInstance on your xxApp class, and RTTI is used there:

// ...partial implementation
    pDocTemplate = new CSingleDocTemplate(
        IDR_MAINFRAME,
        RUNTIME_CLASS(CTestDocDoc),
        RUNTIME_CLASS(CMainFrame),       // main SDI frame window
        RUNTIME_CLASS(CTestDocView));
// ...partial implementation

The CRuntimeClass structure

First, CRuntimeClass:

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;
};

All of MFC’s RTTI information lives in this structure. The key fields:

// class name
LPCSTR m_lpszClassName;
// object size, used for dynamic allocation
int m_nObjectSize;
// schema version number
UINT m_wSchema; // schema number of the loaded class
// used for MFC dynamic object creation
CObject* (PASCAL* m_pfnCreateObject)();
// points to the base class's CRuntimeClass structure
CRuntimeClass* m_pBaseClass;

The methods:

// dynamically create an object by class name
CObject* CreateObject();
// determine the inheritance hierarchy by string
BOOL IsDerivedFrom(const CRuntimeClass* pBaseClass) const;
// dynamic name lookup and creation
// look up the CRuntimeClass structure by class name (char* or wchar_t*)
static CRuntimeClass* PASCAL FromName(LPCSTR lpszClassName);
static CRuntimeClass* PASCAL FromName(LPCWSTR lpszClassName);
static CObject* PASCAL CreateObject(LPCSTR lpszClassName);
static CObject* PASCAL CreateObject(LPCWSTR lpszClassName);

How RTTI is implemented

From the start, MFC designed a top-level base class CObject (like Java’s Object), from which everything else inherits. Here’s Microsoft’s MFC framework diagram:

MFC framework diagram

CObject has this method and this member, giving it dynamic creation and type lookup:

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

Per the diagram, every MFC class inherits from CObject, so the whole framework has RTTI. But how does a user class get added to RTTI? Take CTestDocDoc:

class CTestDocDoc : public CDocument
{
protected: // created only from serialization
    CTestDocDoc() noexcept;
    DECLARE_DYNCREATE(CTestDocDoc)

Look at this macro:

DECLARE_DYNCREATE(CTestDocDoc)

Expanded:

DECLARE_DYNAMIC(CTestDocDoc)   // provide runtime type identification (RTTI) support
static CObject* PASCAL CreateObject();

DECLARE_DYNAMIC expands to:

public:
    static const CRuntimeClass classCTestDocDoc;   // runtime class information
    virtual CRuntimeClass* GetRuntimeClass() const;  // get the CRuntimeClass

Now CTestDocDoc has dynamic identification and dynamic creation. But when are these RTTI records created, and how are they linked together and used?

In CTestDocDoc.cpp there’s this macro:

IMPLEMENT_DYNCREATE(CTestDocDoc, CDocument)

Expanded, it creates a CRuntimeClass, fills in the RTTI information, and binds the base class:

CObject* PASCAL CTestDocDoc::CreateObject()
{
    return new CTestDocDoc;  // dynamically create a CTestDocDoc at runtime
}

AFX_COMDAT const CRuntimeClass CTestDocDoc::classCTestDocDoc = {
    "CTestDocDoc",              // class name
    sizeof(CTestDocDoc),        // object size
    0xFFFF,                     // serialization schema version
    CTestDocDoc::CreateObject,  // points to CreateObject() (for dynamic creation)
    RUNTIME_CLASS(CDocument),   // points to base class CDocument's CRuntimeClass
    NULL,                       // MFC internal field
    NULL                        // MFC internal field
};

CRuntimeClass* CTestDocDoc::GetRuntimeClass() const
{
    return RUNTIME_CLASS(CTestDocDoc);
}

The RUNTIME_CLASS macro:

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

Expanded:

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

So RTTI is an upward (base-class) binding relationship. Both the IDE-generated classes and MFC’s own classes have this RTTI mechanism.

Making use of the RTTI mechanism

From the source analysis, these classes rely on their static member xx::classxx, which means the records are produced at compile time and (I’d guess) stored in the .rdata section. You can enumerate all RTTI classes through static analysis.

The approach:

RTTI dump approach

Core code. First the key structures:

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;
};

Utility functions:

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;
}

Open/close and read the file using memory-mapped file sharing:

// file-open helper; returns all the handles and pointers needed
bool OpenFile(const char* szPath, HANDLE* phFile, HANDLE* phMapFile,
    LPVOID* ppMapView, unsigned char** ppFilePoint) {
    // open the file
    *phFile = CreateFileA(
        szPath,                   // file path
        GENERIC_READ | GENERIC_WRITE,  // read/write access
        0,                        // share mode (no sharing)
        NULL,                     // security attributes
        OPEN_EXISTING,            // only open existing files
        FILE_ATTRIBUTE_NORMAL,    // normal file
        NULL                      // template file handle
    );

    if (*phFile == INVALID_HANDLE_VALUE) {
        std::cerr << "cannot open file, error: " << GetLastError() << std::endl;
        return false;
    }

    // create the file mapping
    *phMapFile = CreateFileMapping(*phFile, NULL, PAGE_READWRITE, 0, 0, NULL);
    if (*phMapFile == NULL) {
        std::cerr << "cannot create file mapping, error: " << GetLastError() << std::endl;
        CloseHandle(*phFile);
        *phFile = INVALID_HANDLE_VALUE;
        return false;
    }

    // map the view
    *ppMapView = MapViewOfFile(
        *phMapFile,            // file mapping object handle
        FILE_MAP_ALL_ACCESS,   // read/write access
        0, 0,                  // offset
        0                      // map the whole file
    );

    if (*ppMapView == NULL) {
        std::cerr << "cannot map file, error: " << GetLastError() << std::endl;
        CloseHandle(*phMapFile);
        CloseHandle(*phFile);
        *phMapFile = NULL;
        *phFile = INVALID_HANDLE_VALUE;
        return false;
    }

    *ppFilePoint = static_cast<unsigned char*>(*ppMapView);
    return true;
}

// close the file and clean up
void CloseFile(HANDLE hFile, HANDLE hMapFile, LPVOID pMapView) {
    if (pMapView) {
        UnmapViewOfFile(pMapView);
    }

    if (hMapFile) {
        CloseHandle(hMapFile);
    }

    if (hFile != INVALID_HANDLE_VALUE) {
        CloseHandle(hFile);
    }
}

Locate CObject:

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 << "invalid file or .rdata section pointer" << 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 reference not found" << std::endl;
                return false;

            }
        }
    }
    std::cout << "CObject not found" << std::endl;
    return false;
}

Dump the class relationships:

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 this parent has already been processed, return
    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));
        }
    }

}

The result:

Dumped class hierarchy

Note that this approach only works for statically-linked MFC; dynamically-linked MFC needs slight modification.

Message routing in MFC

In MFC, binding buttons and controls to handlers is normally done through the UI designer — you drag, you click, and a response function is created. That’s the message-routing mechanism at work. You’ll see two key macros:

DECLARE_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(CTestDocDoc, CDocument)
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;                       \
        __pragma(warning(push))                               \
        __pragma(warning(disable: 4640)) /* message maps can only be called by single threaded message pump */ \
        static const AFX_MSGMAP_ENTRY _messageEntries[] =  \
        {
#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

Fully expanded, it looks like this:

// 1. BEGIN_MESSAGE_MAP expansion
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 } // end marker
    };

    __pragma(warning(pop))

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

    return &messageMap;
}

// 2. DECLARE_MESSAGE_MAP expansion
public:
    static const AFX_MSGMAP* PASCAL GetThisMessageMap();
    virtual const AFX_MSGMAP* GetMessageMap() const;

// 3. END_MESSAGE_MAP is already completed inside GetThisMessageMap
PTM_WARNING_RESTORE

The main thing produced is this table — static const AFX_MSGMAP_ENTRY _messageEntries[] — plus a few overridden virtual functions. The key question is what that table structure looks like:

struct AFX_MSGMAP_ENTRY
{
    UINT nMessage;   // Windows message ID
    UINT nCode;      // control notification code (WM_NOTIFY or control message)
    UINT nID;        // control ID (0 for window messages)
    UINT nLastID;    // last ID of a control ID range (equals nID for a single ID)
    UINT_PTR nSig;   // message signature, determines how the function is called
    AFX_PMSG pfn;    // pointer to the member function handling this message
};

The essentials are the control message ID, the message signature, and the member-function pointer. nSig describes the parameter types of the AFX_PMSG pfn function pointer, so dispatch can handle different control parameters consistently. MFC enumerates every possibility:

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)
    // ...many more omitted
}

How are parent and child classes linked? What if a child class has no message handler? Back to the macro — this code:

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

AFX_MSGMAP has a pointer to the previous theClass::GetThisMessageMap(), and (per DECLARE_MESSAGE_MAP) it’s a static function. This forms a one-directional, upward-linked list — each entry is a node. When a child class has no handler for a message, it falls through to the base class:

struct AFX_MSGMAP
{
    const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();
    const AFX_MSGMAP_ENTRY* lpEntries;
};

From the user’s point of view:

Message map linked list

Window binding

Window binding in MFC is a bit special, so it’s worth discussing (it’ll be useful later). Window creation happens mainly in CWnd::CreateEx. There’s a special spot here:

    AfxHookWindowCreate(this);

The framework hooks itself:

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;
}

The implementation calls SetWindowsHookEx to hook window-creation messages. In the hook procedure, it filters windows — because window-creation messages can’t be received in the normal message loop, MFC intercepts them early, then rebinds the window procedure to its own callback:

//....

    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;
//...

This is how MFC binds all child windows into a single message loop for centralized dispatch. The core function is AfxWndProc:

WNDPROC AFXAPI AfxGetAfxWndProc()
{
#ifdef _AFXDLL
    return AfxGetModuleState()->m_pfnAfxWndProc;
#else
    return &AfxWndProc;
#endif
}

Message dispatch

A quick overview of message dispatch (keeping it short). During the hook there’s a key function:

pWndInit->Attach(hWnd);

Its job is to add the hWnd/CWnd pair into a hash table stored in TLS:

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 has a thread-state class dedicated to managing this:

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;
//...
}

In the message loop, MFC looks these up from the map:

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

After getting the pWnd, it uses the message-routing mechanism to find the handler — mainly by iterating:

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
                }
            }
}
//...

Making use of message routing

We know MFC has a single window procedure and a message-routing table. We can retrieve that table and print the virtual functions and their message mappings to make reverse engineering buttons and controls easier. The approach:

Message-map dump approach

First, create an MFC DLL project so we can use the MFC headers. Key implementation code — enumerate top-level windows:

// callback to enumerate windows
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) {
    DWORD windowPID = 0;
    GetWindowThreadProcessId(hwnd, &windowPID);

    // check whether the window belongs to the current process, is top-level, and visible
    if (windowPID == GetCurrentProcessId() &&
        GetWindow(hwnd, GW_OWNER) == NULL &&
        IsWindowVisible(hwnd)) {
        *((HWND*)lParam) = hwnd;  // save the window handle
        return FALSE;  // stop enumerating once found
    }
    return TRUE;
}

HWND GetCurrentProcessMainWindow() {
    HWND hwnd = NULL;
    EnumWindows(EnumWindowsProc, (LPARAM)&hwnd);
    return hwnd;
}

Get the message-loop window procedure:

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;
}

Replace the window procedure with ours:

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

Our window procedure:

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);
}

Get FromHandlePermanent — here’s the Release version, computing the address from a function offset:

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

Print the RTTI information along a single chain:

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

RTTI implementation:

CRuntimeClass* FindRoot(CRuntimeClass* pRuntimeClass) {

    // traverse upward to the topmost class
    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);
    }
}

Get CWnd::FromHandlePermanent via the vtable:

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

A small trick: computing the vtable offset by hand is tedious, so iterate the vtable and use the IDE’s own hints to tell which entry is which. For different versions you can download and build the matching MFC yourself to compute the offsets. Roughly:

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

Finally, parse the information. Here’s the core of it (lots of string handling is simplified):

DumpMessageMapsSimple(pMap, g_oss);
// get message description
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:
        // simplified: return the hex value directly
        char buf[16];
        sprintf_s(buf, "0x%04X", message);
        return buf;
    }
}

// get notification code description
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;
    }
}

// get control type description
std::string GetControlTypeDescription(UINT id)
{
    switch (id)
    {
    case IDOK:       return "IDOK";
    case IDCANCEL:   return "IDCANCEL";
    default:
        return "Control ID " + std::to_string(id);
    }
}

// get MFC message signature description
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)
{
    // iterate the message maps of the current class and its base classes
    for ( ; pMap != nullptr; pMap = pMap->pfnGetBaseMap ? pMap->pfnGetBaseMap() : nullptr)
    {
        const AFX_MSGMAP_ENTRY* pEntry = pMap->lpEntries;

        // iterate all message-map entries of the current class
        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;

            // advance to the next entry
            ++pEntry;
        }
    }
}

The result:

Dumped message map

Another approach

As mentioned above, those key maps are stored in TLS and managed by a class, so there must be a creation process. Reverse engineering shows it in initterm:

initterm

In the initterm table:

initterm table

Back in the source:

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;
}

When _afxThreadState is empty, AFX_MODULE_STATE is created:

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;
}

Inside, there’s this call:

pResult = _afxBaseModuleState.GetData();

Compiler optimization reduces this to just _afxBaseModuleState.GetData(). It returns the AFX_MODULE_STATE, which is unique process-wide. AFX_MODULE_STATE inherits from CNoTrackObject:

class AFX_MODULE_STATE : public CNoTrackObject

It has an m_pmapHWND member used by afxMapHWND during the callback, so getting it gets you almost everything:

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;
}

You just need to locate that function and call it:

Locating afxMapHWND

Some thoughts on hardening MFC

First, I think CRuntimeClass should be encrypted — especially string encryption, to block static analysis. Second, injection should be detected, for example by scanning loaded modules.

Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).

Get in touch