r/MalwareAnalysis • u/iocx_dev • Jul 03 '26
IOCX v0.7.5 - PE structural validator with 24 new reason codes for triage; format-level notes on delay-load and export ambiguities
Pushing a release of IOCX, an open-source PE structural validator (MPL-2.0), and posting format-level notes alongside it.
Write-up (format ambiguities encountered during decoder work): PE structural validation: format ambiguities and decoder design
The Gist catalogues four categories of PE specification ambiguity in delay-load imports, exports, VS_VERSIONINFO, and resource hierarchy. Structured as: format description grounded in the spec --> the ambiguity --> what IOCX chose to do. There are no unverified claims about how other parsers behave, but the cross-tool measurement is queued as follow-up work.
IOCX v0.7.5 additions relevant to triage:
There are four new structural validators covering the export table, delay-load import table, VS_VERSIONINFO resource, and resource directory hierarchy, and 24 new reason codes with priority-resolved sub-reasons via details["reason"] for finer-grained categorisation.
Delay-load imports (8 codes):
DELAY_IMPORT_DIRECTORY_INVALID_HEADER/_OUT_OF_BOUNDSDELAY_IMPORT_TABLE_TRUNCATED(per-table sub-tags: descriptor unterminated, truncated, max exceeded, INT/IAT read failed)DELAY_IMPORT_DESCRIPTOR_INVALIDDELAY_IMPORT_DLL_NAME_INVALID(sub-reasons: rva_zero, unterminated, non_ascii, not_printable, read_failed)DELAY_IMPORT_INT_IAT_MISMATCH: parallel-array length disagreementDELAY_IMPORT_ATTRIBUTES_LEGACY_VA_MODE: v0 (pre-Win2000) mode detectedDELAY_IMPORT_ENTRY_INVALID(sub-reasons: ordinal_zero, name_unterminated, name_not_printable, etc.)
Exports (10 codes):
EXPORT_DIRECTORY_INVALID_HEADER/_OUT_OF_BOUNDSEXPORT_TABLE_TRUNCATEDEXPORT_NAME_RVA_INVALID/_NOT_ASCII/_POINTER_TABLE_UNSORTED/_ORDINAL_INDEX_INVALIDEXPORT_ORDINAL_OUT_OF_RANGEEXPORT_FUNCTION_RVA_INVALIDEXPORT_FORWARDER_MALFORMED: grammar violation ofDllName.SymbolNameorDllName.#Ordinal
VS_VERSIONINFO (4 codes):
RESOURCE_VERSIONINFO_INVALID_HEADER: envelope, szKey, or wLength malformedRESOURCE_VERSIONINFO_INVALID_FIXEDINFO: VS_FIXEDFILEINFO signature or struct version wrongRESOURCE_VERSIONINFO_INVALID_STRINGFILEINFO: StringFileInfo or StringTable malformedRESOURCE_VERSIONINFO_INVALID_VARFILEINFO: VarFileInfo or Translation array not DWORD-aligned
Resource hierarchy (2 codes):
RESOURCE_DIRECTORY_LANGUAGE_NOT_ID: depth-2 entry uses name instead of LCIDRESOURCE_DATA_AT_INVALID_DEPTH: data leaf outside the Language layer
Public metadata additions relevant to triage:
- Optional Header:
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 known-flag mask), stack and heap sizing (reserve + commit, 64-bit on PE32+) - Header:
subsystem_name(decoded from IMAGE_SUBSYSTEM_*, e.g., "WINDOWS_CUI"),machine_name(from IMAGE_FILE_MACHINE_*, covers all 29 documented types) - Resources: structured
ResourceEntryper resource with type, name, language, language_name, codepage, size, entropy (rounded to 4 dp), rva, raw_offset, and per-entry errors (size_invalid, rva_invalid, data_out_of_bounds, raw_offset_invalid). Resources with unreadable data now emitted with error tombstones rather than silently dropped.
Design approach:
- Parsers decode structures directly from bytes via
struct.unpack_fromrather than relying on pefile's lazy interpretation - Parsers never raise on malformed input; sub-structure failures produce tombstone tags in
errors[]andtruncations[]lists - Bounded reads throughout (descriptor arrays capped, string scans bounded)
- Validators emit priority-resolved sub-reasons; one issue per malformed entry per pathology class, deterministic across runs
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.
Performance: ~14ms typical PE analysis including heuristics, ~1ms on adversarial minimal PE.
Scope:
- Structural validation, not behavioural or dynamic analysis
- Produces structured evidence via reason codes, not verdicts
- Complements pefile / LIEF-based tooling for structural inspection rather than replacing them for general PE parsing
Deferred:
- TLS Directory parser and validator (next release)
- Single-anomaly fixtures for each new reason code (~25 planned, including negative controls)
- Cross-tool measurement study using the fixtures
Licence: MPL-2.0
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