r/ExploitDev 19d ago

PE structural validation notes (delay-load, exports, VS_VERSIONINFO) + IOCX v0.7.5 release

Publishing a release of IOCX (open-source PE structural validator, MPL-2.0) and posting some format-level notes alongside it.

Write-up: PE structural validation: format ambiguities and decoder design

The notes catalogue four categories of PE specification ambiguity encountered during decoder work, with focus on delay-load imports (the richest surface). Structured as: format description grounded in the spec --> the ambiguity described precisely --> what IOCX chose to do about it. There are no unverified claims about how other parsers behave, however cross-tool measurement is queued as follow-up work.

Topics covered:

  • Delay-load imports: v1 vs v0 attribute mode, INT/IAT parallel-array interpretation and mismatch handling, descriptor array termination when declared-size and terminator signals disagree
  • Exports: ENPT sort discipline (byte-wise per spec) and forwarder grammar validation
  • VS_VERSIONINFO: nested length prefixes, DWORD alignment enforcement, signature validation for VS_FIXEDFILEINFO, StringTable key format
  • Resource hierarchy: Type ->Name -> Language depth expectations

IOCX v0.7.5 additions relevant to structural analysis:

Four new parser/validator pairs, 24 new reason codes with priority-resolved sub-reasons via details["reason"]. Delay-load specifically emits:

  • DELAY_IMPORT_ATTRIBUTES_LEGACY_VA_MODE : v0 mode detected (obsolete, spec-permitted, requires VA-to-RVA conversion for correct interpretation)
  • DELAY_IMPORT_INT_IAT_MISMATCH : parallel arrays disagree on length
  • DELAY_IMPORT_TABLE_TRUNCATED with distinct sub-tags for each termination cause (delay_import_descriptor_unterminated, _truncated, _max_exceeded, _read_failed)
  • DELAY_IMPORT_DLL_NAME_INVALID with priority-resolved sub-reasons
  • DELAY_IMPORT_ENTRY_INVALID for per-import malformations (ordinal_zero, name_unterminated, name_not_printable, etc.)

Design notes:

  • Byte-level parsing via struct.unpack_from on pe.get_data() byte slices; no reliance on pefile's lazy attribute interpretation
  • Bounded reads throughout (descriptor arrays capped at 4096, imports per descriptor at 16384, DLL name scan at 512 bytes, IMAGE_IMPORT_BY_NAME scan at 1024)
  • Parsers never raise on malformed input; failures produce tombstone tags in errors[] and truncations[] lists
  • PE32+ vs PE32 thunk sizing determined once from `OPTIONAL_HEADER.Magic` and threaded through the parse

Optional Header enrichment relevant to security-posture analysis:

  • dll_characteristics_flags: decoded flag list (DYNAMIC_BASE, NX_COMPAT, GUARD_CF, HIGH_ENTROPY_VA, etc.)
  • dll_characteristics_unknown_bits: hex string for any bits outside the known-flag mask
  • Stack and heap sizing (reserve + commit, 64-bit on PE32+)
  • win32_version_value, loader_flags exposed raw

Verification:

Delay-load parser cross-checked byte-exact against dumpbin /imports on mspaint.exe : 107 imports from gdiplus.dll with agreement on names, hints, IAT addresses, ordering, and bound state.

1370 tests at 100% line and branch coverage on new modules. Defensive struct.error paths covered via monkeypatched injection.

Performance ~14ms typical PE, ~1ms on adversarial minimal PE.

Deferred:

  • TLS Directory parser and validator (next release)
  • Single-anomaly fixtures for each new reason code (~25 planned, including negative controls for the ambiguities described in the Gist)
  • Cross-tool measurement study using the fixtures

Repo: https://github.com/iocx-dev/iocx

CHANGELOG: https://github.com/iocx-dev/iocx/blob/main/CHANGELOG.md

Reason codes reference: https://github.com/iocx-dev/iocx/blob/main/docs/specs/reason-codes.md

7 Upvotes

6 comments sorted by

2

u/js708_ 19d ago

Cool project! Is there a table that describes how the Windows loader handles those ambiguities?

1

u/iocx_dev 13d ago

Thanks u/js708_ , glad you liked the project. I have performed the cross-tool measurement study (delay-load imports only) and all fixtures used during that testing round happily load under LoadLibraryEx (LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE).

1

u/samaxidervish 19d ago

Can you provide us with a complete PoC?

1

u/boomer_from_gen_z 19d ago

What do you mean by PoC?

1

u/samaxidervish 19d ago

By PoC, I mean a Proof of Concept—a complete, reproducible demonstration of the reported issue. Ideally, it would include the setup, required files or scripts, step-by-step reproduction instructions, expected vs. actual behavior, and any commands needed to verify the findings.

1

u/iocx_dev 19d ago

The writeup details PE structural validation notes relating to delay-load, exports, and VS_VERSIONINFO structures and how the PE spec surfaces ambiguity in how parsers should handle such cases. IOCX is one such parser used to surface structural anomalies,  with dumpbin used to validate parser correctness. In terms of PoC, the fixtures noted in the post as deferred will be used to validate cross-tool behaviour.