mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
ca4ca65c5e
* feat(pe): inspect Authenticode certificate tables Add inspect_pe_certificates for IMAGE_DIRECTORY_ENTRY_SECURITY WIN_CERTIFICATE entries and structurally decoded PKCS#7 signer and X.509 identity fields. The workflow reads a stable PE image without executing it or verifying signatures, chains, timestamps, or Authenticode digest coverage. * fix(pe): bound and validate certificate DER decoding - Cap decoded DER values with a shared node budget so sibling fan-out cannot exhaust memory before the PKCS#7 check - Require SignedData version, encapContentInfo and a final signerInfos SET in RFC 5652 order, and a signature OCTET STRING in each SignerInfo - Decode OBJECT IDENTIFIER arcs as unbounded base-128 values, including a multi-byte first combined arc - Decode UniversalString names as UTF-32BE and reject invalid values - Share PE header admission with the resource layout so a section table that does not fit SizeOfHeaders is rejected * docs: list PE certificates in the translated READMEs * fix(pe): cap OBJECT IDENTIFIER length and arc size A crafted type-2 certificate with a 262,144-byte contentType OID stalled decodeOid for about 20 seconds, because one arc kept growing a BigInt before the signedData check ran. decodeOid now rejects an OID longer than 256 bytes and an arc longer than 32 bytes before decoding it. Certificate OIDs, including 2.25 UUID arcs, fit under both caps. * fix(pe): reject truncated certificate tables and PKCS#7 SignedData now requires one SignerInfo with a non-empty signature and a complete issuerAndSerialNumber. Certificate names share a 64 KiB decoded-byte budget and are copied once. A security directory that ends in a short WIN_CERTIFICATE header fails instead of reporting a complete table. The certificate CLI forwards the caller abort signal into cleanup. Co-authored-by: morluto <morluto@users.noreply.github.com> * chore(ci): keep fork workflow files unchanged The certificate restack follows current main. Workflow files stay at the fork tip so this branch update does not rewrite .github/workflows. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(pe): charge serial and validity text to the decoded budget Certificate and signer serial numbers and validity times now count against the shared decoded-text budget before they are converted, so a crafted certificate cannot push megabytes of text into the hashed evidence. The budget error now names certificate text rather than only names. Restore the CI workflow to match main: the CI_TIMING_TIER input, the tier-named timing artifacts and their publication conditions, the Windows typecheck, and the path and snapshot suites in the curated Windows lane. * fix(pe): require certificate fields and preserve signer key ids Reject X.509 records that omit subjectPublicKeyInfo or signatureValue, return subjectKeyIdentifier bytes as bounded hex, and advertise an absolute path in the PE certificate input schema. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(pe): reject extra PKCS#7 wrappers and invalid UTF-8 names ContentInfo and its signedData wrapper no longer ignore trailing values, and directory strings must round-trip as UTF-8 instead of becoming U+FFFD. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
263 lines
9.5 KiB
TypeScript
263 lines
9.5 KiB
TypeScript
import { inspectByteEntropyInputSchema } from "../domain/native/byteEntropy.js";
|
|
import { z } from "incur";
|
|
import {
|
|
createByteEntropyService,
|
|
createRecordedCrashService,
|
|
createBinaryLayoutService,
|
|
createElfNotesService,
|
|
createPeResourcesService,
|
|
createPeCertificatesService,
|
|
createNativeExportsService,
|
|
} from "../composition/binaryDiagnostics.js";
|
|
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
|
import { logCliCommand } from "../cliLogging.js";
|
|
import type { Logger } from "pino";
|
|
import type { CliInstance } from "./types.js";
|
|
import { withCommandCancellation } from "./commandCancellation.js";
|
|
import { inspectBinaryLayoutInputSchema } from "../domain/native/binaryLayout.js";
|
|
import { inspectRecordedCrashInputSchema } from "../domain/native/recordedCrash.js";
|
|
import { inspectPeResourcesInputSchema } from "../domain/native/peResources.js";
|
|
import { inspectPeCertificatesInputSchema } from "../domain/native/peCertificates.js";
|
|
import { inspectNativeExportsInputSchema } from "../domain/native/nativeExports.js";
|
|
import { inspectElfNotesInputSchema } from "../domain/native/elfNotes.js";
|
|
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
|
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
|
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
|
|
|
/** Inspect operator-selected files through the same shared workflow as MCP. */
|
|
export const registerBinaryDiagnosticsCommands = (
|
|
cli: CliInstance,
|
|
logger: Logger,
|
|
environment: Readonly<NodeJS.ProcessEnv>,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.inspectElfNotes, {
|
|
description:
|
|
"Inspect ELF PT_NOTE and SHT_NOTE identities without executing the image",
|
|
args: z.object({ path: z.string().describe("Local ELF32/ELF64 image") }),
|
|
options: z.object({
|
|
maxFileBytes: inspectElfNotesInputSchema.shape.max_file_bytes,
|
|
maxEntries: inspectElfNotesInputSchema.shape.max_entries,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
withCommandCancellation(async (signal) => {
|
|
const elf = createElfNotesService();
|
|
return await logCliCommand(
|
|
logger,
|
|
CLI_COMMANDS.inspectElfNotes,
|
|
async () =>
|
|
finishAnalysisCommand(
|
|
() =>
|
|
elf.inspect(
|
|
{
|
|
path: absoluteCallerPath(args.path),
|
|
max_file_bytes: options.maxFileBytes,
|
|
max_entries: options.maxEntries,
|
|
},
|
|
{ signal },
|
|
),
|
|
elf,
|
|
"rea-elf-notes",
|
|
"inspect_elf_notes",
|
|
signal,
|
|
),
|
|
);
|
|
}),
|
|
});
|
|
const entropy = createByteEntropyService();
|
|
cli.command(CLI_COMMANDS.inspectByteEntropy, {
|
|
description:
|
|
"Inspect file-wide and block byte entropy without executing the file",
|
|
args: z.object({ path: z.string().describe("Local file") }),
|
|
options: z.object({
|
|
blockSize: inspectByteEntropyInputSchema.shape.block_size.describe(
|
|
"Power-of-two block size from 256 to 65536 bytes (default 4096)",
|
|
),
|
|
offset: inspectByteEntropyInputSchema.shape.offset.describe(
|
|
"Window start as a decimal file byte offset (default 0)",
|
|
),
|
|
length: inspectByteEntropyInputSchema.shape.length.describe(
|
|
"Window length in bytes; omit to extend through EOF",
|
|
),
|
|
}),
|
|
run: ({ args, options }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(logger, CLI_COMMANDS.inspectByteEntropy, async () => {
|
|
return await finishAnalysisCommand(
|
|
() =>
|
|
entropy.inspect(
|
|
{
|
|
path: absoluteCallerPath(args.path),
|
|
block_size: options.blockSize,
|
|
offset: options.offset,
|
|
length: options.length,
|
|
},
|
|
{ signal },
|
|
),
|
|
entropy,
|
|
"rea-byte-entropy",
|
|
"inspect_byte_entropy",
|
|
signal,
|
|
);
|
|
}),
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.inspectPeCertificates, {
|
|
description:
|
|
"Inspect PE Authenticode certificate table identities without verifying signatures",
|
|
args: z.object({ path: z.string().describe("Local PE32/PE32+ image") }),
|
|
options: z.object({
|
|
maxFileBytes: inspectPeCertificatesInputSchema.shape.max_file_bytes,
|
|
maxEntries: inspectPeCertificatesInputSchema.shape.max_entries,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
withCommandCancellation(async (signal) => {
|
|
const pe = createPeCertificatesService();
|
|
return await logCliCommand(
|
|
logger,
|
|
CLI_COMMANDS.inspectPeCertificates,
|
|
async () =>
|
|
finishAnalysisCommand(
|
|
() =>
|
|
pe.inspect(
|
|
{
|
|
path: absoluteCallerPath(args.path),
|
|
max_file_bytes: options.maxFileBytes,
|
|
max_entries: options.maxEntries,
|
|
},
|
|
{ signal },
|
|
),
|
|
pe,
|
|
"rea-pe-certificates",
|
|
"inspect_pe_certificates",
|
|
signal,
|
|
),
|
|
);
|
|
}),
|
|
});
|
|
cli.command(CLI_COMMANDS.inspectPeResources, {
|
|
description:
|
|
"Inspect PE resource identities, payload ranges, hashes and icon-group relationships",
|
|
args: z.object({ path: z.string().describe("Local PE32/PE32+ image") }),
|
|
options: z.object({
|
|
maxFileBytes: inspectPeResourcesInputSchema.shape.max_file_bytes,
|
|
maxEntries: inspectPeResourcesInputSchema.shape.max_entries,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
withCommandCancellation(async (signal) => {
|
|
const pe = createPeResourcesService();
|
|
return await logCliCommand(
|
|
logger,
|
|
CLI_COMMANDS.inspectPeResources,
|
|
async () =>
|
|
finishAnalysisCommand(
|
|
() =>
|
|
pe.inspect(
|
|
{
|
|
path: absoluteCallerPath(args.path),
|
|
max_file_bytes: options.maxFileBytes,
|
|
max_entries: options.maxEntries,
|
|
},
|
|
{ signal },
|
|
),
|
|
pe,
|
|
"rea-pe-resources",
|
|
"inspect_pe_resources",
|
|
signal,
|
|
),
|
|
);
|
|
}),
|
|
});
|
|
cli.command(CLI_COMMANDS.inspectNativeExports, {
|
|
description:
|
|
"Inspect a PE or ELF image's export table without executing it",
|
|
args: z.object({ path: z.string().describe("Local PE or ELF image") }),
|
|
options: z.object({
|
|
maxFileBytes: inspectNativeExportsInputSchema.shape.max_file_bytes,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
withCommandCancellation(async (signal) => {
|
|
const exports = createNativeExportsService();
|
|
return await logCliCommand(
|
|
logger,
|
|
CLI_COMMANDS.inspectNativeExports,
|
|
async () =>
|
|
finishAnalysisCommand(
|
|
() =>
|
|
exports.inspect(
|
|
{
|
|
path: absoluteCallerPath(args.path),
|
|
max_file_bytes: options.maxFileBytes,
|
|
},
|
|
{ signal },
|
|
),
|
|
exports,
|
|
"rea-native-exports",
|
|
"inspect_native_exports",
|
|
signal,
|
|
),
|
|
);
|
|
}),
|
|
});
|
|
const service = createBinaryLayoutService(environment);
|
|
cli.command(CLI_COMMANDS.inspectBinaryLayout, {
|
|
description:
|
|
"Inspect binary file/linked layout without executing the selected object",
|
|
args: z.object({
|
|
path: z.string().describe("Local ELF binary or relocatable object"),
|
|
}),
|
|
options: z.object({
|
|
pwntoolsTimeoutMs:
|
|
inspectBinaryLayoutInputSchema.shape.pwntools_timeout_ms,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(logger, CLI_COMMANDS.inspectBinaryLayout, async () => {
|
|
const result = await service.inspectAndClose(
|
|
{
|
|
path: absoluteCallerPath(args.path),
|
|
...(options.pwntoolsTimeoutMs === undefined
|
|
? {}
|
|
: { pwntools_timeout_ms: options.pwntoolsTimeoutMs }),
|
|
},
|
|
{ signal },
|
|
);
|
|
return result.ok ? result.value : projectAnalysisError(result.error);
|
|
}),
|
|
),
|
|
});
|
|
const recorded = createRecordedCrashService(environment);
|
|
cli.command(CLI_COMMANDS.inspectRecordedCrash, {
|
|
description:
|
|
"Inspect supplied core registers/signals and optional debugger mapping evidence",
|
|
args: z.object({
|
|
path: z
|
|
.string()
|
|
.describe("Local Linux ELF64 amd64 or AArch64 little-endian core"),
|
|
}),
|
|
options: z.object({
|
|
debuggerContext: z
|
|
.boolean()
|
|
.default(false)
|
|
.describe("Add BYO core-only GDB/pwndbg mapping candidates"),
|
|
pwntoolsTimeoutMs:
|
|
inspectRecordedCrashInputSchema.shape.pwntools_timeout_ms,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(logger, CLI_COMMANDS.inspectRecordedCrash, async () => {
|
|
const result = await recorded.inspectAndClose(
|
|
{
|
|
path: absoluteCallerPath(args.path),
|
|
include_debugger_context: options.debuggerContext,
|
|
...(options.pwntoolsTimeoutMs === undefined
|
|
? {}
|
|
: { pwntools_timeout_ms: options.pwntoolsTimeoutMs }),
|
|
},
|
|
{ signal },
|
|
);
|
|
return result.ok ? result.value : projectAnalysisError(result.error);
|
|
}),
|
|
),
|
|
});
|
|
};
|