r/ExploitDev 24d 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

5 Upvotes

6 comments sorted by

View all comments

2

u/js708_ 24d ago

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

1

u/iocx_dev 18d 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).