Files
reverse-engineering-anything/src/domain/native/binaryLayout.ts
T
Vinícius Andrade 976c2c083e fix(contracts): reject OS-invalid local paths in binary and artifact tools (#1925)
* fix(contracts): reject OS-invalid local paths in binary and artifact tools

Enforce localPathStringSchema across inspect_pe_resources,
inspect_binary_layout, inspect_recorded_crash, inspect_wasm_artifact,
and inspect_evm_interface input schemas to reject embedded NUL bytes at
the contract boundary instead of failing later during filesystem
operations.

Add corresponding acceptance test cases to the advertised MCP contracts
matrix in localToolContracts.test.ts.

Signed-off-by: Vinícius DS Andrade <95101505+viniciusdsandrade@users.noreply.github.com>

* style(contracts): wrap pe resources path description under print width

Wrap the inspectPeResourcesInputSchema path property description to keep lines within the 80-column oxfmt limit.

Signed-off-by: Vinícius DS Andrade <95101505+viniciusdsandrade@users.noreply.github.com>

---------

Signed-off-by: Vinícius DS Andrade <95101505+viniciusdsandrade@users.noreply.github.com>
2026-10-12 08:53:03 +08:00

442 lines
15 KiB
TypeScript

import { z } from "zod";
import { localPathStringSchema } from "../localPath.js";
import { CANONICAL_BASE64_PATTERN } from "../stringPatterns.js";
const unsignedHex = z.string().regex(/^0x(?:0|[1-9a-f][0-9a-f]{0,15})$/);
const index = z.number().int().nonnegative();
const scalar = z.union([z.string(), z.number().int()]);
const range = z.strictObject({ offset: unsignedHex, bytes: unsignedHex });
const canonicalBase64 = z.string().regex(CANONICAL_BASE64_PATTERN);
const name = z.strictObject({
display: z.string(),
bytes_base64: canonicalBase64.nullable(),
location: range.nullable(),
unknown_reason: z.string().nullable(),
});
/** Explicit local object selection, independent of an active disassembler target. */
export const inspectBinaryLayoutInputSchema = z.strictObject({
path: localPathStringSchema.describe(
"Absolute filesystem path to the selected binary",
),
});
/** File and linked-image evidence; numeric ELF values never pass through unsafe JSON numbers. */
const binaryLayoutObjectSchema = z.strictObject({
artifact: z.strictObject({
path: z.string().min(1),
sha256: z.string().regex(/^[a-f0-9]{64}$/),
bytes: index,
}),
format: z.literal("elf"),
architecture: z.strictObject({
machine: z.literal("EM_X86_64"),
bits: z.literal(64),
byte_order: z.literal("little"),
}),
image_type: z.enum(["ET_EXEC", "ET_DYN", "ET_REL"]),
entry_point: z.strictObject({
reported_value: unsignedHex,
meaning: z.enum(["linked-virtual-address", "not-applicable", "absent"]),
execution_status: z.literal("unknown"),
}),
runtime_load_base: z.null(),
sections: z.array(
z.strictObject({
index,
name,
name_offset: unsignedHex,
type: scalar,
header_location: range,
address: unsignedHex,
offset: unsignedHex,
size: unsignedHex,
alignment: unsignedHex,
flags: unsignedHex,
link: index,
info: index,
entry_size: unsignedHex,
file_backing: z.enum(["file", "none"]),
}),
),
segments: z.array(
z.strictObject({
index,
type: scalar,
header_location: range,
offset: unsignedHex,
file_size: unsignedHex,
memory_size: unsignedHex,
virtual_address: unsignedHex,
physical_address: unsignedHex,
alignment: unsignedHex,
flags: unsignedHex,
file_backing: z.enum(["file", "none"]),
permissions: z
.strictObject({
read: z.boolean(),
write: z.boolean(),
execute: z.boolean(),
})
.nullable(),
}),
),
symbols: z.array(
z.strictObject({
table_index: index,
entry_index: index,
name,
name_offset: unsignedHex,
location: range,
value: unsignedHex,
value_meaning: z.enum([
"undefined",
"alignment",
"absolute-value",
"no-address",
"unknown-section-index",
"section-offset",
"tls-offset",
"linked-virtual-address",
]),
size: unsignedHex,
binding: scalar,
type: scalar,
visibility: scalar,
section_index: scalar,
}),
),
relocations: z.array(
z.strictObject({
section_index: index,
entry_index: index,
reported_offset: unsignedHex,
target: z.discriminatedUnion("kind", [
z.strictObject({
kind: z.literal("section-offset"),
section_index: index,
offset: unsignedHex,
}),
z.strictObject({
kind: z.literal("linked-virtual-address"),
address: unsignedHex,
}),
z.strictObject({
kind: z.literal("unknown-section"),
reported_section_index: index,
offset: unsignedHex,
unknown_reason: z.enum([
"undefined-section-reference",
"inactive-section-header",
]),
}),
]),
location: range,
type: index,
symbol_table_index: index,
symbol_index: index,
symbol_reference_meaning: z.enum([
"zero-symbol-value",
"symbol-table-entry",
]),
addend: z
.string()
.regex(/^-?(?:0|[1-9][0-9]{0,18})$/)
.nullable(),
}),
),
packed_relative_relocations: z.array(
z.strictObject({
section_index: index,
location: range,
encoded_bytes_base64: canonicalBase64,
entries: z.array(
z.strictObject({ decoded_index: index, reported_offset: unsignedHex }),
),
offset_meaning: z.enum(["linked-virtual-address", "unknown"]),
evidence_kind: z.literal("derived"),
entry_source_locations: z.null(),
addends: z.null(),
}),
),
relocation_inventory_completeness: z.literal("unknown"),
linkage: z.strictObject({
needed_libraries: z.array(name),
interpreters: z.array(name),
got: z.array(
z.strictObject({ display_name: z.string(), address: unsignedHex }),
),
plt: z.array(
z.strictObject({ display_name: z.string(), address: unsignedHex }),
),
convenience_maps_completeness: z.literal("unknown"),
runtime_library_paths: z.null(),
}),
mitigations: z.strictObject({
evidence_kind: z.literal("inferred"),
position_independent: z.boolean(),
nx_indicator: z.boolean().nullable(),
executable_stack_indicator: z.boolean(),
stack_canary_indicator: z.boolean(),
relro: z.enum(["Partial", "Full"]).nullable(),
}),
diagnostics: z.strictObject({
stdout: z.string(),
stderr: z.string(),
truncated: z.boolean(),
}),
limitations: z.array(z.string()),
});
/** Decoder payload excludes the artifact identity observed by the owning process boundary. */
export const binaryLayoutPayloadSchema = binaryLayoutObjectSchema.omit({
artifact: true,
diagnostics: true,
});
/** Check source ranges again at the public boundary, independently of upstream parsing. */
export const binaryLayoutSchema = binaryLayoutObjectSchema.superRefine(
(value, context) => {
const size = BigInt(value.artifact.bytes);
const check = (
offset: string,
length: string,
path: (string | number)[],
): void => {
const start = BigInt(offset);
const bytes = BigInt(length);
if (start > size || bytes > size - start)
context.addIssue({
code: "custom",
path,
message:
"Reported file-backed range lies outside the selected artifact.",
});
};
const checkLocation = (
where: z.infer<typeof range>,
path: (string | number)[],
): void => check(where.offset, where.bytes, path);
const checkName = (
reported: z.infer<typeof name>,
path: (string | number)[],
): void => {
if (reported.location !== null)
checkLocation(reported.location, [...path, "location"]);
};
const expectedEntryMeaning =
value.image_type === "ET_REL"
? "not-applicable"
: value.entry_point.reported_value === "0x0"
? "absent"
: "linked-virtual-address";
if (value.entry_point.meaning !== expectedEntryMeaning)
context.addIssue({
code: "custom",
path: ["entry_point", "meaning"],
message:
"Entry meaning must distinguish zero/unused fields from an actual linked address.",
});
for (const [index, section] of value.sections.entries()) {
if (
(section.type === "SHT_SYMTAB" || section.type === "SHT_DYNSYM") &&
value.sections[section.link]?.type !== "SHT_STRTAB"
)
context.addIssue({
code: "custom",
path: ["sections", index, "link"],
message:
"Symbol tables must link to an existing string-table section.",
});
if (section.index !== index)
context.addIssue({
code: "custom",
path: ["sections", index, "index"],
message:
"Complete section order must preserve original producer indices.",
});
checkLocation(section.header_location, [
"sections",
index,
"header_location",
]);
checkName(section.name, ["sections", index, "name"]);
if (section.file_backing === "file")
check(section.offset, section.size, ["sections", index]);
}
for (const [index, segment] of value.segments.entries()) {
if (segment.index !== index)
context.addIssue({
code: "custom",
path: ["segments", index, "index"],
message:
"Complete segment order must preserve original producer indices.",
});
checkLocation(segment.header_location, [
"segments",
index,
"header_location",
]);
const hasFileBytes =
segment.type !== "PT_NULL" && BigInt(segment.file_size) !== 0n;
if (segment.file_backing !== (hasFileBytes ? "file" : "none"))
context.addIssue({
code: "custom",
path: ["segments", index, "file_backing"],
message:
"File backing must follow the reported segment type and file size.",
});
if ((segment.type === "PT_NULL") !== (segment.permissions === null))
context.addIssue({
code: "custom",
path: ["segments", index, "permissions"],
message:
"Unused PT_NULL flags have no permission meaning; other segment flags retain their interpretation.",
});
if (hasFileBytes)
check(segment.offset, segment.file_size, ["segments", index]);
}
for (const [index, symbol] of value.symbols.entries()) {
checkLocation(symbol.location, ["symbols", index, "location"]);
checkName(symbol.name, ["symbols", index, "name"]);
}
for (const [index, relocation] of value.relocations.entries())
checkLocation(relocation.location, ["relocations", index, "location"]);
validateRelocationReferences(value, context);
validatePackedRelativeTables(value, context, checkLocation);
for (const facet of ["needed_libraries", "interpreters"] as const)
for (const [index, reported] of value.linkage[facet].entries())
checkName(reported, ["linkage", facet, index]);
},
);
export type InspectBinaryLayoutInput = z.infer<
typeof inspectBinaryLayoutInputSchema
>;
export type BinaryLayout = z.infer<typeof binaryLayoutSchema>;
const validateRelocationReferences = (
value: z.output<typeof binaryLayoutObjectSchema>,
context: z.RefinementCtx,
): void => {
const symbols = new Set(
value.symbols.map(
(symbol) => `${symbol.table_index}:${symbol.entry_index}`,
),
);
for (const [index, relocation] of value.relocations.entries()) {
const owner = value.sections[relocation.section_index];
const target = relocation.target;
const targetMatches =
value.image_type === "ET_REL"
? target.kind === "section-offset"
? target.section_index !== 0 &&
target.section_index === owner?.info &&
value.sections[target.section_index] !== undefined &&
value.sections[target.section_index]?.type !== "SHT_NULL" &&
target.offset === relocation.reported_offset
: target.kind === "unknown-section" &&
target.reported_section_index === owner?.info &&
target.offset === relocation.reported_offset &&
(target.reported_section_index === 0
? target.unknown_reason === "undefined-section-reference"
: value.sections[target.reported_section_index]?.type ===
"SHT_NULL" &&
target.unknown_reason === "inactive-section-header")
: target.kind === "linked-virtual-address" &&
target.address === relocation.reported_offset;
if (!targetMatches)
context.addIssue({
code: "custom",
path: ["relocations", index, "target"],
message:
"Relocation target must preserve its reported offset and owner reference; SHN_UNDEF and inactive headers remain unknown rather than resolved sections.",
});
const table = value.sections[relocation.symbol_table_index];
const tableExists =
relocation.symbol_table_index !== 0 &&
(table?.type === "SHT_SYMTAB" || table?.type === "SHT_DYNSYM");
const positiveReferenceExists =
tableExists &&
BigInt(table.entry_size) >= 24n &&
BigInt(relocation.symbol_index) <
BigInt(table.size) / BigInt(table.entry_size) &&
symbols.has(
`${relocation.symbol_table_index}:${relocation.symbol_index}`,
);
if (
(owner?.type !== "SHT_REL" && owner?.type !== "SHT_RELA") ||
owner.link !== relocation.symbol_table_index ||
(relocation.symbol_table_index !== 0 && !tableExists) ||
(relocation.symbol_index !== 0 && !positiveReferenceExists) ||
relocation.symbol_reference_meaning !==
(relocation.symbol_index === 0
? "zero-symbol-value"
: "symbol-table-entry")
)
context.addIssue({
code: "custom",
path: ["relocations", index],
message:
"Relocation must preserve its REL/RELA owner and resolve positive symbol indices through the linked symbol table; index zero uses a zero symbol value.",
});
}
};
const validatePackedRelativeTables = (
value: z.output<typeof binaryLayoutObjectSchema>,
context: z.RefinementCtx,
checkLocation: (
location: z.output<typeof range>,
path: (string | number)[],
) => void,
): void => {
for (const [
tableIndex,
table,
] of value.packed_relative_relocations.entries()) {
checkLocation(table.location, [
"packed_relative_relocations",
tableIndex,
"location",
]);
const encodedBytes =
(table.encoded_bytes_base64.length * 3) / 4 -
(table.encoded_bytes_base64.endsWith("==")
? 2
: table.encoded_bytes_base64.endsWith("=")
? 1
: 0);
if (
!Number.isInteger(encodedBytes) ||
BigInt(encodedBytes) !== BigInt(table.location.bytes)
)
context.addIssue({
code: "custom",
path: ["packed_relative_relocations", tableIndex],
message:
"Complete packed table bytes must match its original file range.",
});
const section = value.sections[table.section_index];
if (
section?.type !== "SHT_RELR" ||
section.offset !== table.location.offset ||
section.size !== table.location.bytes
)
context.addIssue({
code: "custom",
path: ["packed_relative_relocations", tableIndex, "section_index"],
message:
"Packed relative evidence must retain its original RELR section identity and range.",
});
for (const [decoded, entry] of table.entries.entries())
if (entry.decoded_index !== decoded)
context.addIssue({
code: "custom",
path: ["packed_relative_relocations", tableIndex, "entries", decoded],
message: "Decoded offsets must preserve upstream iterator order.",
});
}
};