mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(pe): inspect resource records at their declared offsets
Remove unsupported DWORD alignment refusals from resource data entries and adjacent directory tables. Preserve original record locations, UTF-16 name alignment, bounds, mapping, overlap checks and payload hashes. Validation: 49 focused parser/provider/filesystem/lifecycle and compiled CLI/stdio MCP tests; check:ci; independent pefile agreement for the reported layout, padded control and PE32/PE32+ word-aligned directories. Public CLI and SDK-validated MCP retain truncated-input refusals and unchanged source hashes. Synthetic static inspection only; Windows loading was not tested. Fixes #5750
This commit is contained in:
1 parent
264862bf2e
commit
22d5a40a2e
4 files changed
+120
-4
No files matched your search
@@ -22,6 +22,59 @@ const parse = (bytes: Buffer, maxEntries = 4096, signal?: AbortSignal) =>
|
||||
signal,
|
||||
);
|
||||
|
||||
describe("word-aligned PE resource records", () => {
|
||||
it.each([false, true])(
|
||||
"preserves word-aligned resource records after UTF-16 names (PE32+ = %s)",
|
||||
async (plus) => {
|
||||
for (const identities of [
|
||||
{ type: 10, name: 1, language: "AB" },
|
||||
{ type: "AB", name: 1, language: 1033 },
|
||||
]) {
|
||||
const payload = Buffer.from("opaque resource");
|
||||
const fixture = peResourceFixture(
|
||||
plus,
|
||||
[{ ...identities, payload }],
|
||||
65536,
|
||||
2,
|
||||
);
|
||||
const dataOffset = fixture.dataOffsets[0];
|
||||
if (dataOffset === undefined)
|
||||
throw new Error("Missing fixture data entry");
|
||||
expect(fixture.dataOffsets.some((at) => at % 4 === 2)).toBe(true);
|
||||
if (typeof identities.type === "string")
|
||||
expect(fixture.directoryOffsets.some((at) => at % 4 === 2)).toBe(
|
||||
true,
|
||||
);
|
||||
const result = peResourcesSchema.parse(await parse(fixture.bytes));
|
||||
expect(
|
||||
result.directories.map(({ location }) => location.offset),
|
||||
).toEqual(fixture.directoryOffsets);
|
||||
expect(result.coverage).toMatchObject({
|
||||
status: "complete",
|
||||
resources: 1,
|
||||
});
|
||||
expect(result.resources[0]?.data_entry_location).toEqual({
|
||||
offset: dataOffset,
|
||||
bytes: 16,
|
||||
});
|
||||
expect(result.resources[0]?.payload.sha256).toBe(
|
||||
createHash("sha256").update(payload).digest("hex"),
|
||||
);
|
||||
expect(result.resources[0]?.payload.location.offset).toBe(
|
||||
fixture.bytes.readUInt32LE(dataOffset) - 0x1c00,
|
||||
);
|
||||
fixture.bytes.writeUInt32LE(
|
||||
dataOffset - 0x400 + 15,
|
||||
fixture.directoryAt + 4,
|
||||
);
|
||||
await expect(parse(fixture.bytes)).rejects.toMatchObject({
|
||||
reason: "format",
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("portable PE resource parsing", () => {
|
||||
it.each([false, true])(
|
||||
"preserves original identities, ranges, hashes and icon candidates (PE32+ = %s)",
|
||||
|
||||
@@ -120,7 +120,6 @@ export const parsePeResources = async (
|
||||
ancestors: ReadonlySet<number>,
|
||||
): Promise<void> => {
|
||||
if (ancestors.has(at)) peFailure("Cyclic PE resource directory.");
|
||||
if (at % 4 !== 0) peFailure("Unaligned PE resource directory.");
|
||||
const header = relative(at, 16);
|
||||
const named = bytes.readUInt16LE(header + 12);
|
||||
const ids = bytes.readUInt16LE(header + 14);
|
||||
@@ -177,7 +176,6 @@ export const parsePeResources = async (
|
||||
"Unsupported resource leaf outside the type/name/language profile.",
|
||||
);
|
||||
const data = relative(offset, 16);
|
||||
if (offset % 4 !== 0) peFailure("Unaligned resource data entry.");
|
||||
const rva = bytes.readUInt32LE(data);
|
||||
const size = bytes.readUInt32LE(data + 4);
|
||||
const payloadOffset = layout.map(rva, size);
|
||||
|
||||
@@ -38,6 +38,7 @@ export const peResourceFixture = (
|
||||
{ type: 14, name: 42, language: 1033, payload: iconGroup() },
|
||||
],
|
||||
sectionBytes = 65536,
|
||||
recordAlignment: 2 | 4 = 4,
|
||||
) => {
|
||||
const tree: Tree = { children: new Map() };
|
||||
for (const resource of resources) {
|
||||
@@ -61,7 +62,7 @@ export const peResourceFixture = (
|
||||
}
|
||||
const section = Buffer.alloc(sectionBytes);
|
||||
let cursor = 0;
|
||||
const allocate = (size: number, alignment = 4): number => {
|
||||
const allocate = (size: number, alignment = recordAlignment): number => {
|
||||
cursor = Math.ceil(cursor / alignment) * alignment;
|
||||
const at = cursor;
|
||||
cursor += size;
|
||||
@@ -110,7 +111,7 @@ export const peResourceFixture = (
|
||||
};
|
||||
emit(tree);
|
||||
for (const { dataAt, resource } of leaves) {
|
||||
const payloadAt = allocate(resource.payload.length);
|
||||
const payloadAt = allocate(resource.payload.length, 4);
|
||||
resource.payload.copy(section, payloadAt);
|
||||
section.writeUInt32LE(0x2000 + payloadAt, dataAt);
|
||||
section.writeUInt32LE(resource.payload.length, dataAt + 4);
|
||||
|
||||
@@ -92,6 +92,59 @@ const expectPeInputFailuresAcrossAdapters = async (input: {
|
||||
|
||||
const contract = toolContract("inspect_pe_resources");
|
||||
|
||||
const expectPeWordAlignedRecordsAcrossAdapters = async (input: {
|
||||
readonly cli: TestCli;
|
||||
readonly client: Client;
|
||||
readonly root: string;
|
||||
readonly definition: Awaited<
|
||||
ReturnType<Client["listTools"]>
|
||||
>["tools"][number];
|
||||
}): Promise<void> => {
|
||||
const { cli, client, root, definition } = input;
|
||||
for (const plus of [false, true]) {
|
||||
for (const [index, identities] of [
|
||||
{ type: 10, name: 1, language: "AB" },
|
||||
{ type: "AB", name: 1, language: 1033 },
|
||||
].entries()) {
|
||||
const fixture = peResourceFixture(
|
||||
plus,
|
||||
[{ ...identities, payload: Buffer.from("opaque resource") }],
|
||||
65536,
|
||||
2,
|
||||
);
|
||||
const path = join(root, `word-aligned-${plus}-${index}.exe`);
|
||||
await writeFile(path, fixture.bytes);
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-pe-resources", path, "--json"],
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const evidence = contract.outputSchema.parse(result.json);
|
||||
const report = peResourcesSchema.parse(evidence.normalized_result);
|
||||
expect(report.coverage).toMatchObject({
|
||||
status: "complete",
|
||||
resources: 1,
|
||||
});
|
||||
expect(report.resources[0]?.data_entry_location).toEqual({
|
||||
offset: fixture.dataOffsets[0],
|
||||
bytes: 16,
|
||||
});
|
||||
expect(report.resources[0]?.payload.sha256).toBe(
|
||||
createHash("sha256").update("opaque resource").digest("hex"),
|
||||
);
|
||||
const response = await client.callTool(
|
||||
{ name: definition.name, arguments: { path } },
|
||||
{ toolDefinition: definition },
|
||||
);
|
||||
expect(response.isError).not.toBe(true);
|
||||
expect(
|
||||
contract.outputSchema.parse(response.structuredContent)
|
||||
.normalized_result,
|
||||
).toEqual(evidence.normalized_result);
|
||||
expect(await readFile(path)).toEqual(fixture.bytes);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const expectPeMappingOwnershipAcrossAdapters = async (
|
||||
cli: TestCli,
|
||||
client: Client,
|
||||
@@ -284,6 +337,11 @@ cliTest(
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const inventory = await client.listTools();
|
||||
const definition = inventory.tools.find(
|
||||
({ name }) => name === contract.name,
|
||||
);
|
||||
if (definition === undefined)
|
||||
throw new Error("Missing PE resource tool contract");
|
||||
expect(inventory.tools).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: contract.name,
|
||||
@@ -301,6 +359,12 @@ cliTest(
|
||||
"inspect_pe_resources",
|
||||
);
|
||||
await expectPeInventoriesAcrossAdapters(cli, client, root);
|
||||
await expectPeWordAlignedRecordsAcrossAdapters({
|
||||
cli,
|
||||
client,
|
||||
root,
|
||||
definition,
|
||||
});
|
||||
await expectPeMappingOwnershipAcrossAdapters(cli, client, root);
|
||||
await expectPePaddingFailureAcrossAdapters({
|
||||
cli,
|
||||
|
||||
Reference in new issue
Block a user