出题——ClassReaper
前言
这次出题主要考察 RTTI 恢复以及虚函数表,本题关闭了标准 C++ RTTI(/GR-),所以 IDA 的 Class Informer 无法自动恢复类。但 MFC 自有的 CRuntimeClass 机制仍然保留,这是本题的突破口
MFC 逆向的 RTTI 恢复,就是利用 MFC 自有的 CRuntimeClass 链表及其 m_lpszClassName、m_pBaseClass、m_pfnCreateObject 等字段,从二进制中还原类名、继承关系、虚表和消息映射,把匿名结构还原成可读的 C++ 类模型
程序入口
IDA 反编译找到入口

1 | __int64 __fastcall AfxWinMain(HINSTANCE__ *hInstance, HINSTANCE__ *hPrevInstance, wchar_t *lpCmdLine, int nCmdShow) |
MFC 程序里有个全局唯一的 App 对象,就是 CChallengeApp。整个程序从头到尾只有这一个实例,它代表 “这个应用程序本身”
Thread->InitInstance(Thread) 是虚调用,Thread 的实际类型是 CChallengeApp,所以这里会跳到 CChallengeApp::InitInstance
虚调用就是函数地址不写死在代码里,而是运行时根据对象的实际类型去查虚函数表(vtable),决定调哪个版本——所以 Thread 实际是 CChallengeApp,就调 CChallengeApp::InitInstance,而不是基类的 CWinThread::InitInstance

分析解密函数
CWinApp::InitInstance(this); 是 MFC 基类初始化的,直接看 DecryptAllClassNames
遍历 MFC 主链,把业务类的 m_lpszClassName 从密文原地解密成明文,方便后续代码正常使用类名
第一步:取主链头
MFC 在 CRT 初始化阶段,通过 AFX_CLASSINIT 把所有 CRuntimeClass 静态实例自动挂进这条链。所以程序启动后,链上已经有所有注册过的类
1 | ModuleState = AfxGetModuleState(); |
AfxGetModuleState()返回当前 MFC 模块态;m_classList.m_pHead是CRuntimeClass全局链表的头指针;沿
m_pNextClass一直走,上限 4096 防死循环
第二步:筛选目标类
1 | if ( i == &CValidator::classCValidator |
命中以下任意一条,就解密
| 条件 | 含义 |
|---|---|
i == &classCValidator |
自身就是 CValidator 基类 |
IsDerivedFrom(i, &classCValidator) |
i 是 CValidator 的任意子类 |
第三步:读类名指针、扫长度
1 | m_lpszClassName = i->m_lpszClassName; |
m_lpszClassName是CRuntimeClass结构体第 0 个字段(偏移+0x00)扫到
'\0'定长度,上限 512长度必须非空且 ≤ 511 才继续
第四步:改页权限
1 | VirtualProtect(m_lpszClassName, v4, 4u, &oldProt) |
4u = PAGE_READWRITE;类名存在
.rdata(只读),必须先改成可写才能原地 XOR;解密后再
VirtualProtect恢复原保
第五步:XOR 解密循环
1 | LOBYTE(v5) = __ROR1__(26, 5) ^ 0xA7; // base = 0x77 |
从 IDA 抄出 g_NameSbox 的 256 字节

恢复 CRuntimeClass 结构
现在去找密文,前面我们知道 m_lpszClassName 是 CRuntimeClass 结构体第 0 个字段(偏移 +0x00)
所以现在去看 CRuntimeClass



| 偏移 | 字段 | 含义 |
|---|---|---|
| +0x00 | m_lpszClassName |
类名字符串指针(密文) |
| +0x08 | m_nObjectSize |
对象大小 |
| +0x0C | m_wSchema |
schema 版本 |
| +0x10 | m_pfnCreateObject |
工厂函数指针(抽象类为 NULL) |
| +0x18 | m_pBaseClass |
基类指针 |
| +0x20 | m_pNextClass |
链表下一节点 |
| +0x28 | m_pClassInit |
AFX_CLASSINIT 指针,MFC 自动注册用 |
离线解密类名
在 IDA 里打开 DecryptAllClassNames 反汇编,只有三个基类被显式引用

选中 ?classCValidator@... 符号,按 Enter 或双击。跳到 .rdata 里的结构体定义

1 | .rdata:... ?classCValidator@CValidator@@2UCRuntimeClass@@B: |
其中密文 a8_0 在下方,同时还发现其他多组

编写脚本解密
1 | SBOX = [ |
得到
1 | a8_0 -> CValidator |
当然,IDA 里已经带了 XREF 提示
1 | .rdata:... a8_3 db '8' ; DATA XREF: .data:CRuntimeClass CLengthValidator::classCLengthValidator |
读 CRuntimeClass 的三个关键字段
从密文的 XREF 跳到四个子类的 CRuntimeClass 结构体,一次读出三个关键字段:
m_lpszClassName→ 类名密文(已在上面解密)m_pBaseClass→ 基类指针,用于确认继承树m_pfnCreateObject→ 工厂函数指针
四个子类的 m_pBaseClass 都指向 ?classCValidator@CValidator@@...,据此确认继承树:
1 | CObject |

工厂函数
双击 CLengthValidator 的 m_pfnCreateObject 值,即 j_?CreateObject@CLengthValidator@@SAPEAVCObject@@XZ

读 vftable
其中 'vftable' 就是虚表地址,在反编译里双击 CLengthValidator::'vftable',跳到 .rdata

1 | .rdata:00000001403EC348 ??_7CLengthValidator@@6B@: |
定位四个 Check()
在 vftable 里双击 j_?Check@CLengthValidator@@...

检查点 1:长度 = 42
1 | if ( *(flag->m_pszData - 4) != 42 ) |
检查点 2:前缀 = flag{
1 | LODWORD(prefix) = -1157068284; // 0xBB088A04,小端 → 04 8A 08 BB |
检查点 3:后缀 = }
1 | lenBlob[0] = 24; // 0x18 |
检查点 4:第 1 段 = 目标值
1 | v17 = SplitFlagBody(flag, &a, &b, &c, &d); // 按连字符切成 4 段 |
查看解密函数

编写第一段解密脚本
1 | def xor_decode_in_place(buf: bytes, keySalt: int) -> bytes: |
同理找到第二个 Check()

1 | import struct |
第三个 Check()

1 | import struct |
第四段的脚本
1 | import struct |
最后的 flag:flag{fe55d536-eff9-4e58-ad35-7f93e1b94c69}