[GH-ISSUE #6408] [Feature] Inspect MFC CRuntimeClass metadata in PE binaries and link each serializable class to its Serialize(CArchive&) function #3840

Open
opened 2026-10-31 00:44:31 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 22, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6408

Before submitting

  • I searched the open and closed issues and pull requests for a related proposal.
  • I understand the contribution scope must be reviewed before integration; open questions are welcome here.
  • This request does not include credentials, private samples, or sensitive target data.

Area

Native analysis or decompilation

Current limitation

REA has no reader for MFC's own runtime type system. Examined at REA main e83e3ceadd (source reading):

  • inspect_native_dispatch_metadata, REA's static class-metadata reader for native code, reads only Objective-C and Swift metadata, preferring a validated macOS Mach-O byte reader (docs/native-investigation.md).
  • src/ and docs/ contain no reference to CRuntimeClass, DECLARE_SERIAL or MFC class metadata. The "CArchive" code in PyInstaller support under src/artifacts/ is PyInstaller's unrelated archive format.
  • #3624 (open) proposes MSVC/Itanium RTTI, vftables and class hierarchies, but does not cover MFC CRuntimeClass, which is a separate table that MFC binaries carry even when built without RTTI (/GR-).

For an MFC application, nothing tells an analyst which classes are serializable, at what schema version, with what object size and base class, or which function implements each class's Serialize(CArchive&) — the code that defines the application's document file format.

Use cases

  • Documenting or migrating a legacy MFC application's saved-file format: find every DECLARE_SERIAL class, its schema, and the Serialize body to analyze.
  • Interoperability work on proprietary document formats produced by MFC desktop software (CAD, industrial control and lab tools), where #3494 shows a reporter hand-decoding MFC archive class tags.
  • Naming classes in a /GR- build where RTTI recovery (#3624) has nothing to work with.

Desired outcome and success criteria

A read-only inspection of x86 and x64 PE images, preferably usable without a deep-analysis provider, that returns, for each CRuntimeClass record:

  1. Record virtual address and file offset, class name and its address, m_nObjectSize, and m_wSchema (low 16 bits plus the VERSIONABLE_SCHEMA flag).
  2. m_pfnCreateObject (null for abstract or DECLARE_DYNAMIC-only classes).
  3. The base class: a direct pointer in static MFC builds, or the GetBaseClass stub target in _AFXDLL builds. A base imported from the MFC DLL is named by its import; an ordinal-only import keeps its ordinal and reports the name as unknown.
  4. The vftable tied to the record through its GetRuntimeClass slot, and for serializable classes the Serialize slot and body address, following thunks and forwarders.
  5. Every facet is reported as decoded, partial or unknown. When several vftables return the same CRuntimeClass (a subclass without its own DECLARE_DYNAMIC), the link is reported as ambiguous with all candidates, not resolved to the first.

Success check: a self-built fixture compiled with MSVC for x86 and x64, with /MD and /MT, with /GR and /GR-, containing one DECLARE_SERIAL hierarchy using VERSIONABLE_SCHEMA, one DECLARE_DYNCREATE class and one DECLARE_DYNAMIC class. Names, sizes (equal to sizeof) and schemas match the source; CreateObject is non-null only for DYNCREATE and SERIAL classes; each Serialize address equals the one in the fixture's PDB, used only as a test oracle; truncated or malformed records produce partial results without a crash, and input admission is bounded.

Expected interaction with targets

  • Read existing files or captured data

Target and scope details (optional)

Static parsing of PE files only; the target is not launched and nothing is written. Non-goals: decoding saved MFC archive files (a possible follow-up, see #3494), tracing field layouts inside Serialize (existing analyze_function / trace_native_values work from the returned address), and general MSVC RTTI recovery (#3624). Emitting Ghidra seeds such as Class::Serialize could be a later option.

Verification available so far

I can suggest a sample or manual check

Verification details (optional)

The fixture above needs an MSVC toolchain with MFC; the Microsoft MFC runtime DLLs must not be committed. I have not built it or run any check for this report.

Alternatives considered

  • Ghidra's default PE analysis: its RTTI analyzer recovers MSVC RTTI structures, but I found no indication that it recovers CRuntimeClass records or links Serialize (not verified on a sample).
  • Finding records by hand with search_strings and read_bytes: possible per class, but requires knowing the struct layout and decoding stubs manually.

Possible direction

REA already ships built-in read-only metadata parsers; whether this one reads PE bytes directly or an admitted provider snapshot (as the NativeAOT reader in docs/ghidra-nativeaot.md does) is an implementation choice. Plausibility filters that keep the scan cheap: object size in a sane range, schema bits within the documented flag set, CreateObject pointing into executable code, name pointer resolving to an identifier, and only CObject allowed without a base.

References

  • Microsoft documentation for CRuntimeClass and MFC serialization (TN002, "Persistent Object Data Format") describes the record and the archive format.
  • Related: #3624, #3494.
Originally created by @coygeek on GitHub (Oct 22, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6408 ### Before submitting - [X] I searched the open and closed issues and pull requests for a related proposal. - [X] I understand the [contribution scope](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#contribution-scope) must be reviewed before integration; open questions are welcome here. - [X] This request does not include credentials, private samples, or sensitive target data. ### Area Native analysis or decompilation ### Current limitation REA has no reader for MFC's own runtime type system. Examined at REA `main` e83e3ceadd97770a50b8afa97f4077916e4d2b81 (source reading): - `inspect_native_dispatch_metadata`, REA's static class-metadata reader for native code, reads only Objective-C and Swift metadata, preferring a validated macOS Mach-O byte reader (`docs/native-investigation.md`). - `src/` and `docs/` contain no reference to `CRuntimeClass`, `DECLARE_SERIAL` or MFC class metadata. The "CArchive" code in PyInstaller support under `src/artifacts/` is PyInstaller's unrelated archive format. - #3624 (open) proposes MSVC/Itanium RTTI, vftables and class hierarchies, but does not cover MFC `CRuntimeClass`, which is a separate table that MFC binaries carry even when built without RTTI (`/GR-`). For an MFC application, nothing tells an analyst which classes are serializable, at what schema version, with what object size and base class, or which function implements each class's `Serialize(CArchive&)` — the code that defines the application's document file format. ### Use cases - Documenting or migrating a legacy MFC application's saved-file format: find every `DECLARE_SERIAL` class, its schema, and the `Serialize` body to analyze. - Interoperability work on proprietary document formats produced by MFC desktop software (CAD, industrial control and lab tools), where #3494 shows a reporter hand-decoding MFC archive class tags. - Naming classes in a `/GR-` build where RTTI recovery (#3624) has nothing to work with. ### Desired outcome and success criteria A read-only inspection of x86 and x64 PE images, preferably usable without a deep-analysis provider, that returns, for each `CRuntimeClass` record: 1. Record virtual address and file offset, class name and its address, `m_nObjectSize`, and `m_wSchema` (low 16 bits plus the `VERSIONABLE_SCHEMA` flag). 2. `m_pfnCreateObject` (null for abstract or `DECLARE_DYNAMIC`-only classes). 3. The base class: a direct pointer in static MFC builds, or the `GetBaseClass` stub target in `_AFXDLL` builds. A base imported from the MFC DLL is named by its import; an ordinal-only import keeps its ordinal and reports the name as unknown. 4. The vftable tied to the record through its `GetRuntimeClass` slot, and for serializable classes the `Serialize` slot and body address, following thunks and forwarders. 5. Every facet is reported as decoded, partial or unknown. When several vftables return the same `CRuntimeClass` (a subclass without its own `DECLARE_DYNAMIC`), the link is reported as ambiguous with all candidates, not resolved to the first. Success check: a self-built fixture compiled with MSVC for x86 and x64, with `/MD` and `/MT`, with `/GR` and `/GR-`, containing one `DECLARE_SERIAL` hierarchy using `VERSIONABLE_SCHEMA`, one `DECLARE_DYNCREATE` class and one `DECLARE_DYNAMIC` class. Names, sizes (equal to `sizeof`) and schemas match the source; `CreateObject` is non-null only for DYNCREATE and SERIAL classes; each `Serialize` address equals the one in the fixture's PDB, used only as a test oracle; truncated or malformed records produce partial results without a crash, and input admission is bounded. ### Expected interaction with targets - Read existing files or captured data ### Target and scope details (optional) Static parsing of PE files only; the target is not launched and nothing is written. Non-goals: decoding saved MFC archive files (a possible follow-up, see #3494), tracing field layouts inside `Serialize` (existing `analyze_function` / `trace_native_values` work from the returned address), and general MSVC RTTI recovery (#3624). Emitting Ghidra seeds such as `Class::Serialize` could be a later option. ### Verification available so far I can suggest a sample or manual check ### Verification details (optional) The fixture above needs an MSVC toolchain with MFC; the Microsoft MFC runtime DLLs must not be committed. I have not built it or run any check for this report. ### Alternatives considered - Ghidra's default PE analysis: its RTTI analyzer recovers MSVC RTTI structures, but I found no indication that it recovers `CRuntimeClass` records or links `Serialize` (not verified on a sample). - Finding records by hand with `search_strings` and `read_bytes`: possible per class, but requires knowing the struct layout and decoding stubs manually. ### Possible direction REA already ships built-in read-only metadata parsers; whether this one reads PE bytes directly or an admitted provider snapshot (as the NativeAOT reader in `docs/ghidra-nativeaot.md` does) is an implementation choice. Plausibility filters that keep the scan cheap: object size in a sane range, schema bits within the documented flag set, `CreateObject` pointing into executable code, name pointer resolving to an identifier, and only `CObject` allowed without a base. ### References - Microsoft documentation for `CRuntimeClass` and MFC serialization (TN002, "Persistent Object Data Format") describes the record and the archive format. - Related: #3624, #3494.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3840
No description provided.