Skip to content

feat(pe): inspect Rich header linker records - #8923

Open
Plastic-time wants to merge 6 commits into
morluto:mainfrom
Plastic-time:rrfz/inspect-pe-rich-header
Open

Plastic-time wants to merge 6 commits into
morluto:mainfrom
Plastic-time:rrfz/inspect-pe-rich-header

Conversation

@Plastic-time

@Plastic-time Plastic-time commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This adds inspect_pe_rich_header and rea inspect-pe-rich-header. They read the Rich header of a local PE32 or PE32+ image with no provider, and never run the target.

The Rich header is the record Microsoft's linker writes between the DOS stub and the PE signature. It lists which compiler, assembler, linker and import-library builds produced the image, and how many objects each contributed. Today REA has no reader for it.

No issue is linked. This is a proposed new read-only tool, and whether REA wants it is for maintainers to decide.

Problem and expected behavior

Use case: an analyst wants to know which Microsoft toolchain built a Windows binary, or whether its Rich header was copied or edited after linking. The binary has no DWARF to ask, so #8523 does not cover it.

Observable outcome:

  • rich_header.state is present, malformed or absent.
  • A present record reports its file range, key, decoded padding, and each entry's product_id, build and count.
  • checksum.computed is the linker checksum recomputed from the DOS header, stub and entries. checksum.matches says whether it reproduces the stored key.
  • A footer without its DanS marker, or a record that ends inside an entry, is malformed with a reason. A missing record is absent and carries a limitation saying absence does not show how the image was built.
  • A PE header that starts inside the 64-byte DOS header leaves no room for a record, so the report is absent.

Change and scope

  • src/native/pe/PeRichHeaderParser.ts does the reading. It takes the last DWORD-aligned Rich footer whose key ends at or before the PE signature, then walks back to the nearest DanS encoded with that key.
  • src/domain/native/peRichHeader.ts owns the report schema. The provider, service, CLI command and MCP registration follow inspect_pe_load_config and inspect_pe_certificates.
  • PE header admission reuses readPeHeaders. No existing reader changes.

Two choices differ from pefile, on purpose:

  • pefile assumes DanS sits at 0x80. On this host it is at 0x58 in two vk*.sys drivers and at 0x200 in WeGameDriverXP.sys.
  • pefile takes the first Rich marker. tcblaunch.exe has a custom DOS stub that is itself a PE image, with its own record at 0xC0. The image's record is the last one, at 0x2D20.

Not included: names for product IDs or Visual Studio releases, the "rich hash", or any edit of the record.

Follow the contribution scope.
Unresolved scope, authority, side effects, or required verification block
integration. Link the required maintainer scope decision for new providers or
capabilities involving target execution, injection, mutation, or protection
recovery; ordinary fixes within existing boundaries may state "not applicable".
Draft PRs may leave the scope decision pending and the readiness and merge checklists
unchecked. Maintainers can help resolve open questions.

Contract and boundary impact

  • Semantic owner and earliest changed stage: new src/native/pe/PeRichHeaderParser.ts. It consumes the existing readPeHeaders.
  • Contribution scope fit and linked maintainer decision: format inspection of a local file, with no execution, injection, mutation or protection recovery. A required decision is not applicable. I have not assumed the tool is wanted.
  • CLI and MCP/tool-catalog contract: additive. One new tool, inspect_pe_rich_header, and one new command, inspect-pe-rich-header.
  • Provider, bridge, target-format, or platform compatibility: no provider or bridge. PE32 and PE32+ on any supported host.
  • Evidence, artifact, provenance, or reconstruction contract: new Evidence with artifact SHA-256, the record's file range (or only the footer when malformed), and observed confidence. Existing Evidence is unchanged.
  • Target selection, authority, upstream behavior, and cleanup details (if applicable): the caller selects one local path, read through readStableArtifact. No resources are retained.
  • Generated metadata (docs/public/product-catalog.json), package, or installation impact (CI generates and validates ignored catalogs and skills/; edit authored instructions in .agents/skills/reverse-engineer-anything/ and never commit build outputs): one new verify:pe:rich-header script in package.json. No runtime dependency and no build output.

Expected effects (check known effects; use documentation-only on its own).
Unchecked items do not establish absence; leave uncertainties visible.

  • Read existing files or captured data.
  • Launch or attach to a process.
  • Inject instrumentation or run scripts/code.
  • Change process memory, registers, files, or configuration.
  • Contact network services.
  • Recover protected code or work around protections.
  • Documentation or usability only; no target interaction.
  • Not sure yet; investigation or maintainer guidance needed.

Evidence and regression coverage

  • Tests added or updated:
    • src/native/pe/PeRichHeaderParser.test.ts: 17 cases on synthetic PE32 and PE32+ images. The expected key comes from a separate BigInt checksum in the fixture. One case fills a 4 MiB stub with unaligned Rich strings and checks that the scan yields and observes cancellation.
    • src/contracts/tupleSchemaParity.test.ts: the padding words reject empty, short, overlong, null-element and out-of-range values in both JSON Schema drafts and both projections.
    • src/application/binaryDiagnostics/PeRichHeaderService.test.ts: input validation, cancellation, Evidence locations, an absent report for a PE header at 0x30 inside the DOS header, and 5 kinds of malformed provider output rejected.
    • src/native/pe/PeRichHeaderProvider.test.ts: once the read completes, an exhausted entry budget and a malformed PE header both keep the file's path, SHA-256 and byte count as the partial observation. Cancellation during decoding takes the same path.
    • CLI acceptance, MCP boundary and filesystem boundary tests, plus the shared localToolContracts, stableArtifactConsumers and toolEffects lists.
  • Base reproduction or other evidence:
    • On main, rea inspect-pe-rich-header is COMMAND_NOT_FOUND, and git grep finds no DanS or Rich header reader under src.
    • scripts/verify-pe-rich-header.mjs compares key and entries with pefile 2024.8.26 parse_rich_header. It leaves a file out only on pefile's own result, and fails on any other state difference, a file only REA rejects, or a run that compares nothing. Over 17,896 PE files in C:\Windows\System32, C:\Program Files and C:\Program Files (x86), 17,890 agree, 0 differ, and 5 fall outside pefile's 0x80 assumption. One 0-byte file is rejected by both REA and pefile. docs/verification/pe-rich-header-2026-11-04.md holds the record.
    • Of the 10,915 records found, the recomputed checksum reproduces the key for 10,901. The other 14 are NVIDIA driver files, three vk*.sys drivers, three Riot Vanguard executables and tcblaunch.exe.
  • User-visible CLI/MCP output (if applicable): reduced from rea inspect-pe-rich-header ... --json on this commit.
C:\Windows\System32\notepad.exe (10.0.26100.8457, pe32-plus)
pe_header_offset 248
state present  location 128+112  key 2756908916
checksum computed 2756908916  matches true  padding 0,0,0
entries 11: (147, 30729, 94) (0, 0, 1) (260, 33145, 11) ...

C:\Windows\System32\drivers\WeGameDriverXP.sys
pe_header_offset 600  DanS at 512  entries 5  matches true
entries: (1, 0, 15) (93, 4035, 5) (131, 30729, 2) (148, 30729, 1) (145, 30729, 1)
pefile on the same file: 53 pairs, each (0x184a, 61001, 407563849),
  warning "Rich Header is not in Microsoft format, possibly malformed"
  • Remaining proof gaps:
    • Windows host only. Linux and macOS were not run, though the parser has no host-specific code.
    • The filesystem boundary case that creates a symlink fails here with EPERM. The existing peCertificates boundary test fails the same way on this host.
    • The checksum algorithm is confirmed empirically on the 10,901 matching records, not against a Microsoft document. Microsoft does not document the Rich header.
    • I did not find why the 14 mismatching images differ.

For boundary fixes, identify the applicable boundary regressions
and lifecycle regressions
covered by the evidence above, and explain relevant gaps.

For evidence-bearing changes:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

Run on Windows 11 10.0.26200 with Node 24.22.1. The branch starts from main 63b66fa49 and was not rebased. c1db5e5ec merges main 0a99c2a58. That adds two main commits. They share only three docs files with this PR. The source tests, the provider comparison and the pefile run are on 155b2c700. Some other runs predate the merge. CI passes on the head 01aa387c5, an empty commit with the same tree as 155b2c700.

  • npm run test:local on the 5 new or changed source tests, including tupleSchemaParity and the provider test: 72 passed.
  • PeRichHeaderProvider.test.ts with the provider from c1db5e5ec: 2 failed. On this commit: 2 passed.
  • The unaligned-marker parser case on the first commit's parser: 1 failed, 16 passed. On this commit: 17 passed.
  • The overlapping-header case in PeRichHeaderService.test.ts with the service from 4af9a5243: 1 failed, 10 passed. On this commit: 11 passed.
  • The same image through the CLI (512 bytes, PE32+, PE signature at 0x30): on 4af9a5243, inspect-pe-rich-header --json exits 1 with unreadable_output. On this commit it exits 0 with absent and pe_header_offset 48. inspect-pe-resources reads the same file with exit 0.
  • npm run test:focused on the new CLI, MCP and filesystem tests plus localToolContracts and stableArtifactConsumers: 173 passed, 1 failed (the symlink EPERM above). toolSchemaValidity, clientSchemaPatterns and cliInputSchemaRequired: passed.
  • npm run test:local on the 59 test files under src/contracts, src/composition, src/server, src/cli, src/native/pe and src/application/binaryDiagnostics: 823 passed, 1 failed, 2 skipped. The failure is a ByteEntropyService case that renames a parent directory while streaming, the Windows class from [Bug] Windows: five artifact tests fail because they rename a parent directory while its admitted file is open #7846. This PR does not touch that service or its test.
  • node scripts/verify-pe-rich-header.mjs with pefile 2024.8.26: passed on this commit, numbers above.
  • typecheck, lint (0 errors), lint:typed (0 errors), format:check, metadata:check, verify:architecture-guards, verify:test-discovery, knip, knip:production and docs:check: passed.

Compatibility, safety, and release

  • Breaking changes or migration steps: none. The tool is additive.
  • Real Hopper/Ghidra, browser, or OS coverage: none needed. Real images on Windows 11 only.
  • Package or release metadata impact: one new package.json script. No new dependency.
  • Security, privacy, process, or containment review: the image is parsed and never run. The read is bounded by max_file_bytes (default 64 MiB), decoding by max_entries (default 1024), and long scans yield and check cancellation every 1 MiB. Exhausted budgets fail without partial success.

Author readiness

Human owner: @Plastic-time

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

AI was used for assistance.

Add inspect_pe_rich_header and the inspect-pe-rich-header command. They
read the Rich header between the DOS stub and the PE signature without a
provider.

- Take the last DWORD-aligned Rich footer before the PE signature and
  walk back to its DanS marker.
- Report each product ID, build and use count, the decoded padding and
  the record's file range.
- Recompute the linker checksum and report whether it reproduces the
  stored key.
- Report an absent record, and a footer without DanS or with a partial
  entry as malformed.

The verifier compares key and entries with pefile 2024.8.26.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-11-04T16:33:29.424749Z
We couldn’t complete this review. Comment @codex review to try again.
82d3f76 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@greptile-apps

greptile-apps Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5 Tier: apex

[Medium impact] The PR appears safe to merge; no blocking code issue remains from this review.

Summary

Adds inspect_pe_rich_header and inspect-pe-rich-header to read PE linker records without running the target. Reports include entries, file ranges, checksum results, and artifact identity.

  • PE Rich records now show decoded entries and a checksum comparison.
  • The CLI and MCP now expose PE Rich header inspection.
  • A verification script compares Rich header reports with pefile.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A["CLI or MCP: explicit local path"] --> B["PeRichHeaderService: check input"]
  B --> C["PeRichHeaderProvider: readStableArtifact"]
  C --> D["Keep path, SHA-256 and byte count"]
  D --> E["parsePeRichHeader: admit PE headers"]
  E --> F["Find last aligned Rich footer"]
  F --> G["Find encoded DanS and decode entries"]
  G --> H["Recompute checksum"]
  H --> I["Check report and create Evidence"]
  E --> J["On failure, retain completed file identity"]
  F --> J
  G --> J
Loading

Reviews (5) · Last reviewed commit: "chore: rerun CI after an unrelated test ..." · Reviewed by Greptile

Comment thread src/domain/native/peRichHeader.ts Resolved
Comment thread src/native/pe/PeRichHeaderParser.ts Resolved
Comment thread scripts/verify-pe-rich-header.mjs Resolved
Comment thread scripts/verify-pe-rich-header.mjs Resolved
- Scan aligned DWORDs for the Rich footer and DanS marker in one loop
  that yields and checks cancellation every MiB. A stub full of
  unaligned Rich strings no longer holds the event loop.
- Declare the padding words with fixedTupleSchema and cover them in the
  tuple parity tests.
- Import the binary diagnostics contract family in the focused CLI and
  MCP tests.
- In the pefile verifier, exclude files only by pefile's own layout
  result, fail any other state difference, refuse a run that compares
  nothing, and give the CLI check the same read budget.
A PE whose e_lfanew points inside the 64-byte DOS header passes
readPeHeaders, and the parser reports no Rich record for it. The
service then refused the report as malformed provider output, because
it required pe_header_offset to be at least 64.

Drop that lower bound. The footer checks still reject any record
reported before 0x40.
@morluto

morluto commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Very interesting!

Comment thread src/native/pe/PeRichHeaderProvider.ts Resolved
Comment thread scripts/verify-pe-rich-header.mjs Resolved
- Once the stable read completes, a later decoding failure or
  cancellation keeps the file's path, SHA-256 and byte count as a
  partial observation.
- The pefile verifier now fails on a file that pefile reads but REA
  rejects. An input both reject, such as an empty file, is listed
  apart.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants