Files
rea/src/domain/native/peImports.ts
T
4ef916da03 feat(pe): inspect import and delay-load tables (#6509)
* feat(pe): inspect import and delay-load tables

Add inspect_pe_imports / inspect-pe-imports to read PE32/PE32+ import
and delay-load directories without executing the image. The report keeps
DLL names, named and ordinal functions, ILT/IAT RVAs, and a pefile
import hash of the standard import table. A missing directory is
observed absence, not proof the image never imported APIs.

* fix(pe): default PE import fixture options

Allow peImportImage() without an argument so CLI and filesystem tests
can use the KERNEL32 sample without repeating an empty options object.

* test(pe): keep import path cases on the dedicated MCP contract

inspect_pe_imports already rejects relative MCP paths in its own
boundary test. Leave the shared absolute-path matrix to tools whose
handler issue projection matches that suite's expected shape.

* fix(native): index virtual-to-file mapping lookups

mappedFileOffset scans every mapping twice per span, so a parser that
translates one span per table entry costs sections x entries. Add
fileMappingLookup, which builds an O(n log n) interval index once and
answers each span in O(log n) with the same accept, reject and
translation results. A differential test checks it against
mappedFileOffset over randomized overlapping mappings and zero fills.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g

* fix(pe): report partial import evidence and exact pefile hashes

- Withhold imphash with an explicit reason whenever the standard import
  table is partial (unreadable names or thunks, missing tables), or when
  pefile 2024.8.26 would hash a different row sequence: invalid or
  truncated names, ordinal 0, descriptors without functions, tables its
  bounds or repeat/spread heuristics discard, and its 8192-thunk import
  symbol cap. Delay-load issues do not withhold it.
- Hash ordinal imports with pefile's complete ordlookup tables and its
  ord<decimal> fallback instead of declining unmapped ordinals.
- Report a nonterminal descriptor whose FirstThunk is zero, and a
  delay-load descriptor whose IAT or INT RVA is zero, as malformed_entry
  partial coverage instead of a complete empty inventory.
- Directory evidence lists the file spans actually consumed for the
  descriptor array and terminator; discontiguous mappings produce
  separate spans and separate Evidence ranges.
- Carry exhausted limits as PeImportLimitFailure.limit and derive
  recovery from it; the fixed 16 MiB retained-text ceiling reports a
  fixed allowance instead of max_file_bytes advice.
- Translate RVAs through the indexed mapping lookup and yield with a
  cancellation check every 1024 table entries.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g

* fix(pe): read only own ordinal tables for DLL names

DLL names such as constructor and __proto__ pass pefile's name rule but
resolved inherited object members, throwing before the ord<n> fallback.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g

* fix(native): preserve PE import facts and prove hash compatibility

* test(native): verify PE import workflows and measured costs

* refactor(native): narrow PE facts across the container union

* docs(native): record integrated PE import validation

* fix(native): bound every PE import RVA span

* docs(native): measure RVA guard completed workflows

* fix(pe): reconcile import coverage with retained rows

* fix(pe): retain partial imports at descriptor mapping gaps

---------

Co-authored-by: Matt Van Horn <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: gensokyo-dev <[email protected]>
2026-10-24 11:44:12 +08:00

154 lines
4.8 KiB
TypeScript

import { z } from "zod";
import { digestSchema } from "../digests.js";
import { absoluteLocalPathStringSchema } from "../localPath.js";
const uint = z.number().int().min(0).max(0xffffffff);
const range = z.strictObject({ offset: uint, bytes: uint });
const md5Schema = z.string().regex(/^[a-f0-9]{32}$/u);
/** Hard ceiling on the caller-selected input read budget. */
export const MAX_INSPECT_PE_IMPORT_FILE_BYTES = 536870912;
/** Hard ceiling on import and delay-load descriptors examined together. */
export const MAX_INSPECT_PE_IMPORT_DLLS = 4096;
/** Hard ceiling on named and ordinal functions across every table. */
export const MAX_INSPECT_PE_IMPORT_FUNCTIONS = 262144;
/** Fixed ceiling on decoded DLL and function name text retained in one report. */
export const MAX_INSPECT_PE_IMPORT_RETAINED_TEXT_BYTES = 16 * 1024 * 1024;
/** Complete bounded inspection of PE import and delay-load tables. */
export const inspectPeImportsInputSchema = z.strictObject({
path: absoluteLocalPathStringSchema.describe(
"Absolute filesystem path to a PE image",
),
max_file_bytes: z
.number()
.int()
.min(64)
.max(MAX_INSPECT_PE_IMPORT_FILE_BYTES)
.default(67108864)
.describe("Maximum stable artifact bytes to read (default 64 MiB)"),
max_dlls: z
.number()
.int()
.min(1)
.max(MAX_INSPECT_PE_IMPORT_DLLS)
.default(256)
.describe(
"Maximum import and delay-load descriptors to examine; exceeding the budget fails without partial success",
),
max_functions: z
.number()
.int()
.min(1)
.max(MAX_INSPECT_PE_IMPORT_FUNCTIONS)
.default(8192)
.describe(
"Maximum imported functions to examine across every table; exceeding the budget fails without partial success",
),
});
export type InspectPeImportsInput = z.infer<typeof inspectPeImportsInputSchema>;
const peImportFunctionSchema = z.strictObject({
hint: uint.nullable(),
name: z.string().min(1).nullable(),
ordinal: z.number().int().min(0).max(0xffff).nullable(),
ilt_rva: uint.nullable(),
iat_rva: uint,
thunk_hex: z.string().regex(/^[0-9a-f]+$/u),
});
const peImportDllSchema = z.strictObject({
index: uint,
name: z.string().min(1).nullable(),
name_rva: uint,
original_first_thunk_rva: uint,
first_thunk_rva: uint,
time_date_stamp: uint,
forwarder_chain: uint,
location: range,
functions: z.array(peImportFunctionSchema),
});
const peDelayImportDllSchema = z.strictObject({
index: uint,
name: z.string().min(1).nullable(),
name_rva: uint,
attributes: uint,
import_address_table_rva: uint,
import_name_table_rva: uint,
bound_import_rva: uint,
unload_information_table_rva: uint,
module_handle_rva: uint,
time_date_stamp: uint,
location: range,
functions: z.array(peImportFunctionSchema),
});
/**
* `locations` are the file spans the parser actually consumed for the
* descriptor array and its terminator, in RVA order. Consecutive descriptors
* that map to discontiguous file bytes produce separate spans.
* No bytes are consumed when the first slot cannot be examined; the declared
* directory remains present and coverage is partial.
*/
const directorySchema = z
.strictObject({
rva: uint,
size: uint,
locations: z.array(range),
})
.nullable();
const peImportIssueSchema = z.strictObject({
code: z.enum([
"malformed_entry",
"descriptor_unreadable",
"name_unreadable",
"thunk_unreadable",
"unsupported_form",
]),
location: z.string().min(1).nullable(),
message: z.string().min(1),
});
const coverageSchema = z.strictObject({
status: z.enum(["complete", "partial"]),
reason: z.string().min(1).nullable(),
examined_dlls: uint,
imports: uint,
delay_imports: uint,
});
const imphashSchema = z.strictObject({
algorithm: z.literal("pefile"),
md5: md5Schema.nullable(),
reason: z.string().min(1).nullable(),
});
/** Static import inventory of one PE32/PE32+ image, read without execution. */
export const peImportsSchema = z.strictObject({
artifact: z.strictObject({
path: z.string().min(1),
sha256: digestSchema,
bytes: uint,
}),
format: z.enum(["pe32", "pe32-plus"]),
machine: uint,
bits: z.union([z.literal(32), z.literal(64)]),
architecture: z.string().min(1),
import_directory: directorySchema,
delay_import_directory: directorySchema,
imports: z.array(peImportDllSchema),
delay_imports: z.array(peDelayImportDllSchema),
imphash: imphashSchema,
coverage: coverageSchema,
issues: z.array(peImportIssueSchema),
limitations: z.array(z.string().min(1)),
});
export type PeImports = z.infer<typeof peImportsSchema>;
export type PeImportDll = z.infer<typeof peImportDllSchema>;
export type PeDelayImportDll = z.infer<typeof peDelayImportDllSchema>;
export type PeImportFunction = z.infer<typeof peImportFunctionSchema>;
export type PeImportIssue = z.infer<typeof peImportIssueSchema>;