Files
rea/src/application/binaryDiagnostics/PeLoadConfigService.ts
T
3d84215bb2 feat(pe): inspect TLS callbacks and load configuration (#7008)
* feat(pe): inspect TLS callbacks and load configuration

Adds inspect-pe-load-config and inspect_pe_load_config for PE32 and
PE32+ images, with artifact hashes and file ranges in the returned
evidence. Reports callback addresses and mappings, plus size-covered
security-cookie, SafeSEH, CFG, EH-continuation and XFG fields;
flag-derived mitigation claims are explicitly labeled inferred. Missing
directories remain null, and malformed storage or exhausted read budgets
return errors without partial success. Adds documentation and tests for
service validation, CLI/MCP access, malformed inputs and stable-file
handling.

x86 images that declare the legacy 64-byte load-config directory are
read to the structure's own Size when those bytes are file-backed, as
the Windows loader does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe

* fix(pe): retain snapshot facts and bound load-config section lookup

Keep the completed stable-read path, SHA-256 and byte count as a
file-snapshot-hash partial observation when descriptor close fails or is
cancelled. Report the checked Size span as structure_location and add it
to Evidence locations beside the declared directory range. Resolve
callback sections through a sorted segment index built once per image,
keeping the ambiguity failure. Pin every load-config field to offsets
transcribed from the PE/COFF spec and llvm COFF.h at its exact Size
boundary for PE32 and PE32+.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:06:41 +08:00

186 lines
6.0 KiB
TypeScript

import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { PeLoadConfigPort } from "./PeLoadConfigPort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import {
inspectPeLoadConfigInputSchema,
peLoadConfigSchema,
type PeLoadConfig,
} from "../../domain/native/peLoadConfig.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import {
decodePeMachine,
peOptionalHeaderMagic,
type PeArchitecture,
} from "../../domain/peMachine.js";
import { err, type Result } from "../../domain/result.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
const OPERATION = "inspect_pe_load_config";
/** Shared caller validation, output parsing and Evidence provenance for CLI/MCP. */
export class PeLoadConfigService {
constructor(readonly provider: PeLoadConfigPort) {}
close(): Promise<void> {
return this.provider.close?.() ?? Promise.resolve();
}
async inspect(
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
const input = inspectPeLoadConfigInputSchema.safeParse(rawInput);
if (!input.success)
return err(
analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, {
cause: input.error,
}),
);
if (!isAbsolute(input.data.path))
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["path"],
reason: "invalid_format",
message: "Expected an absolute filesystem path on this host.",
},
]),
);
const result = await this.provider.inspect(input.data, options);
if (!result.ok) return result;
const report = peLoadConfigSchema.safeParse(result.value);
if (
!report.success ||
report.data.artifact.path !== input.data.path ||
!validReport(report.data, input.data)
)
return err(
new AnalysisOutputError(
OPERATION,
"PE TLS/load-config provider returned malformed output or changed artifact identity.",
),
);
const value = report.data;
const architecture = subjectArchitecture(value);
const locations = [
{ kind: "artifact-path" as const, path: value.artifact.path },
...fileRange(value.tls?.directory ?? null),
...fileRange(value.load_config?.directory ?? null),
...structureRange(value.load_config),
...(value.tls?.callback_array_locations ?? []).map(
({ offset, bytes }) => ({
kind: "file-offset-range" as const,
start: offset,
end: offset + bytes,
}),
),
];
return completeProviderEvidence(
createEvidence(
{
path: value.artifact.path,
format: "pe",
sha256: value.artifact.sha256,
...(architecture === undefined ? {} : { architecture }),
},
this.provider.identity,
{
operation: OPERATION,
parameters: input.data,
result: value,
confidence: "observed",
limitations: value.limitations,
locations,
},
),
options?.signal,
);
}
}
const subjectArchitecture = (
report: PeLoadConfig,
): PeArchitecture | undefined => {
const machine = decodePeMachine(report.machine);
if (machine === undefined || machine.nativeOsOverride) return undefined;
const magic = report.format === "pe32-plus" ? 0x20b : 0x10b;
return magic === peOptionalHeaderMagic(machine.architecture)
? machine.architecture
: undefined;
};
const fileRange = (
directory: { file_offset: number; size: number } | null,
): { kind: "file-offset-range"; start: number; end: number }[] =>
directory === null
? []
: [
{
kind: "file-offset-range",
start: directory.file_offset,
end: directory.file_offset + directory.size,
},
];
/** The checked `Size` span, when it differs from the declared directory range. */
const structureRange = (
loadConfig: PeLoadConfig["load_config"],
): { kind: "file-offset-range"; start: number; end: number }[] =>
loadConfig === null ||
loadConfig.structure_location.bytes === loadConfig.directory.size
? []
: [
{
kind: "file-offset-range",
start: loadConfig.structure_location.offset,
end:
loadConfig.structure_location.offset +
loadConfig.structure_location.bytes,
},
];
const validReport = (
report: PeLoadConfig,
input: { max_callbacks: number; max_file_bytes: number },
): boolean => {
if (
report.artifact.bytes > input.max_file_bytes ||
report.examined_callbacks !== (report.tls?.callbacks.length ?? 0) ||
report.examined_callbacks > input.max_callbacks ||
report.bits !== (report.format === "pe32" ? 32 : 64)
)
return false;
const ranges = [
...fileRange(report.tls?.directory ?? null),
...fileRange(report.load_config?.directory ?? null),
];
const structure = report.load_config?.structure_location;
return (
(structure === undefined ||
(structure.offset === report.load_config?.directory.file_offset &&
structure.bytes === report.load_config.size &&
structure.offset <= report.artifact.bytes - structure.bytes)) &&
ranges.every(
({ start, end }) => start <= end && end <= report.artifact.bytes,
) &&
(report.tls?.callback_array_locations ?? []).every(
({ offset, bytes }) => offset <= report.artifact.bytes - bytes,
) &&
(report.tls?.callbacks ?? []).every(
({ file_offset }) =>
file_offset === null || file_offset < report.artifact.bytes,
)
);
};