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:

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:

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:

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:

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:

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:

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:

In the 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:

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