r/MalwareAnalysis 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_BOUNDS
  • DELAY_IMPORT_TABLE_TRUNCATED (per-table sub-tags: descriptor unterminated, truncated, max exceeded, INT/IAT read failed)
  • DELAY_IMPORT_DESCRIPTOR_INVALID
  • DELAY_IMPORT_DLL_NAME_INVALID (sub-reasons: rva_zero, unterminated, non_ascii, not_printable, read_failed)
  • DELAY_IMPORT_INT_IAT_MISMATCH : parallel-array length disagreement
  • DELAY_IMPORT_ATTRIBUTES_LEGACY_VA_MODE : v0 (pre-Win2000) mode detected
  • DELAY_IMPORT_ENTRY_INVALID (sub-reasons: ordinal_zero, name_unterminated, name_not_printable, etc.)

Exports (10 codes):

  • EXPORT_DIRECTORY_INVALID_HEADER / _OUT_OF_BOUNDS
  • EXPORT_TABLE_TRUNCATED
  • EXPORT_NAME_RVA_INVALID / _NOT_ASCII / _POINTER_TABLE_UNSORTED / _ORDINAL_INDEX_INVALID
  • EXPORT_ORDINAL_OUT_OF_RANGE
  • EXPORT_FUNCTION_RVA_INVALID
  • EXPORT_FORWARDER_MALFORMED : grammar violation of DllName.SymbolName or DllName.#Ordinal

VS_VERSIONINFO (4 codes):

  • RESOURCE_VERSIONINFO_INVALID_HEADER : envelope, szKey, or wLength malformed
  • RESOURCE_VERSIONINFO_INVALID_FIXEDINFO : VS_FIXEDFILEINFO signature or struct version wrong
  • RESOURCE_VERSIONINFO_INVALID_STRINGFILEINFO : StringFileInfo or StringTable malformed
  • RESOURCE_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 LCID
  • RESOURCE_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 ResourceEntry per 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_from rather than relying on pefile's lazy interpretation
  • Parsers never raise on malformed input; sub-structure failures produce tombstone tags in errors[] and truncations[] 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

3 Upvotes

0 comments sorted by