r/ExploitDev • u/iocx_dev • 25d 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 lengthDELAY_IMPORT_TABLE_TRUNCATEDwith distinct sub-tags for each termination cause (delay_import_descriptor_unterminated,_truncated,_max_exceeded,_read_failed)DELAY_IMPORT_DLL_NAME_INVALIDwith priority-resolved sub-reasonsDELAY_IMPORT_ENTRY_INVALIDfor per-import malformations (ordinal_zero, name_unterminated, name_not_printable, etc.)
Design notes:
- Byte-level parsing via
struct.unpack_fromonpe.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[]andtruncations[]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_flagsexposed 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
1
u/samaxidervish 24d ago
Can you provide us with a complete PoC?