mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
fix(pe): retain TLS templates without complete file backing
Signed-off-by: Rudy Celekli <rudy@gradiahq.com> Fixes #7400 Supersedes #7529 Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
1 parent
a7398cc9eb
commit
85198fd5cb
5 files changed
+114
-12
No files matched your search
@@ -70,6 +70,7 @@ export interface PeLoadConfigFixtureOptions {
|
||||
/** In-section callbacks when `callbacks` is present. Defaults to the rich trio. */
|
||||
readonly callbackCount?: number;
|
||||
readonly zeroFillCallback?: boolean;
|
||||
readonly rawDataRange?: readonly [number, number];
|
||||
readonly securityCookie?: "mapped" | "zero";
|
||||
readonly directoryCount?: number;
|
||||
readonly malformed?: PeLoadConfigFixtureMalformed;
|
||||
@@ -146,11 +147,12 @@ export const peLoadConfigImage = (
|
||||
const rawStart =
|
||||
options.malformed === "raw-data-inverted"
|
||||
? layout.rawDataRva + layout.rawDataSize
|
||||
: layout.rawDataRva;
|
||||
: (options.rawDataRange?.[0] ?? layout.rawDataRva);
|
||||
const rawEnd =
|
||||
options.malformed === "raw-data-inverted"
|
||||
? layout.rawDataRva
|
||||
: layout.rawDataRva + layout.rawDataSize;
|
||||
: (options.rawDataRange?.[1] ??
|
||||
layout.rawDataRva + layout.rawDataSize);
|
||||
writeVa(tlsAt, vaOf(rawStart));
|
||||
writeVa(tlsAt + pointerWidth, vaOf(rawEnd));
|
||||
writeVa(tlsAt + pointerWidth * 2, vaOf(layout.indexRva));
|
||||
|
||||
@@ -52,6 +52,35 @@ describe.each([32, 64] as const)("PE%s load configuration", (bits) => {
|
||||
expect(report.load_config?.inferences[0]?.evidence_kind).toBe("inferred");
|
||||
expect(report.load_config?.se_handler_count).toBe(bits === 32 ? 2 : null);
|
||||
});
|
||||
it.each([
|
||||
[0x2100, 0x2200, 0x1700],
|
||||
[0x2180, 0x2280, null],
|
||||
[0x2200, 0x2300, null],
|
||||
] as const)(
|
||||
"retains a TLS template at [%s, %s) without requiring file backing",
|
||||
async (start, end, fileOffset) => {
|
||||
const bytes = peLoadConfigImage({
|
||||
bits,
|
||||
zeroFillCallback: true,
|
||||
rawDataRange: [start, end],
|
||||
});
|
||||
const base = bits === 64 ? 0x180000000n : 0x10000000n;
|
||||
const report = await parsePeLoadConfig(
|
||||
bytes,
|
||||
{ path: "/sample.exe", sha256: "a".repeat(64), bytes: bytes.length },
|
||||
inspectPeLoadConfigInputSchema.parse({ path: "/sample.exe" }),
|
||||
);
|
||||
expect(report.tls).toMatchObject({
|
||||
start_address_of_raw_data: `0x${(base + BigInt(start)).toString(16)}`,
|
||||
end_address_of_raw_data: `0x${(base + BigInt(end)).toString(16)}`,
|
||||
raw_data_size: 256,
|
||||
raw_data_file_offset: fileOffset,
|
||||
raw_data_section: ".text",
|
||||
});
|
||||
expect(report.tls?.callbacks).toHaveLength(1);
|
||||
expect(report.load_config?.guard_flags?.raw).toBe(0x500);
|
||||
},
|
||||
);
|
||||
it("honors Size even when directory bytes contain newer fields", async () => {
|
||||
const report = await parse({
|
||||
bits,
|
||||
|
||||
@@ -36,6 +36,21 @@ const PE_LOAD_CONFIG_DIRECTORY = 10;
|
||||
/** Callback slots decoded between event-loop yields and cancellation checks. */
|
||||
const ENTRIES_PER_YIELD = 1024;
|
||||
|
||||
/** Derived locations may be unmapped; ambiguous mappings remain malformed. */
|
||||
const optionalFileOffset = (
|
||||
lookup: { offset(address: bigint, size: number): number },
|
||||
address: bigint,
|
||||
size: number,
|
||||
): number | null => {
|
||||
try {
|
||||
return lookup.offset(address, size);
|
||||
} catch (cause) {
|
||||
if (!(cause instanceof BinaryMappingFailure) || cause.reason !== "unmapped")
|
||||
throw cause;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/** The parser-owned allowance an image exhausted. */
|
||||
export type PeLoadConfigLimit = "callbacks";
|
||||
|
||||
@@ -181,15 +196,7 @@ export const parsePeLoadConfig = async (
|
||||
const rva = rvaOf(va);
|
||||
let fileOffset: number | null = null;
|
||||
if (rva !== null) {
|
||||
try {
|
||||
fileOffset = lookup.offset(va, 1);
|
||||
} catch (cause) {
|
||||
if (
|
||||
!(cause instanceof BinaryMappingFailure) ||
|
||||
cause.reason !== "unmapped"
|
||||
)
|
||||
throw cause;
|
||||
}
|
||||
fileOffset = optionalFileOffset(lookup, va, 1);
|
||||
}
|
||||
return {
|
||||
va: hex(va),
|
||||
@@ -309,7 +316,7 @@ const readTls = async ({
|
||||
const rva = rvaOf(start);
|
||||
rawSection = rva === null ? null : sectionName(rva);
|
||||
} else {
|
||||
rawFile = lookup.offset(start, rawSize);
|
||||
rawFile = optionalFileOffset(lookup, start, rawSize);
|
||||
const startRva = rvaOf(start);
|
||||
const last = startRva === null ? null : startRva + rawSize - 1;
|
||||
if (startRva !== null && last !== null && last <= 0xffffffff) {
|
||||
|
||||
@@ -58,6 +58,43 @@ for (const bits of [32, 64] as const) {
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
`retains PE${bits} TLS template metadata in virtual zero padding`,
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-pe-config-padding-cli-");
|
||||
for (const range of [
|
||||
[0x2180, 0x2280],
|
||||
[0x2200, 0x2300],
|
||||
] as const) {
|
||||
await writeFile(
|
||||
join(root, "app.exe"),
|
||||
peLoadConfigImage({
|
||||
bits,
|
||||
zeroFillCallback: true,
|
||||
rawDataRange: range,
|
||||
}),
|
||||
);
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-pe-load-config", "app.exe", "--json"],
|
||||
cwd: root,
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const evidence = toolContract(
|
||||
"inspect_pe_load_config",
|
||||
).outputSchema.parse(result.json);
|
||||
expect(evidence.normalized_result.tls).toMatchObject({
|
||||
raw_data_size: 256,
|
||||
raw_data_file_offset: null,
|
||||
raw_data_section: ".text",
|
||||
});
|
||||
expect(evidence.normalized_result.tls?.callbacks).toHaveLength(1);
|
||||
expect(evidence.normalized_result.load_config?.guard_flags?.raw).toBe(
|
||||
0x500,
|
||||
);
|
||||
expect(evidence.subject?.local_path).toBe(join(root, "app.exe"));
|
||||
}
|
||||
},
|
||||
);
|
||||
cliTest(`reads PE${bits} TLS callbacks through the CLI`, async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-pe-config-cli-");
|
||||
await writeFile(join(root, "app.exe"), peLoadConfigImage({ bits }));
|
||||
|
||||
@@ -58,6 +58,33 @@ mcpTest(
|
||||
PE_LOAD_CONFIG_FIXTURE.dynamicRelocSection,
|
||||
});
|
||||
expect(Buffer.compare(await readFile(path), bytes)).toBe(0);
|
||||
for (const range of [
|
||||
[0x2180, 0x2280],
|
||||
[0x2200, 0x2300],
|
||||
] as const) {
|
||||
const padded = peLoadConfigImage({
|
||||
zeroFillCallback: true,
|
||||
rawDataRange: range,
|
||||
});
|
||||
await writeFile(path, padded);
|
||||
const reply = await client.callTool(
|
||||
{ name: "inspect_pe_load_config", arguments: { path } },
|
||||
{ toolDefinition: definition },
|
||||
);
|
||||
expect(reply.isError, JSON.stringify(reply.content)).not.toBe(true);
|
||||
const retained = toolContract(
|
||||
"inspect_pe_load_config",
|
||||
).outputSchema.parse(reply.structuredContent);
|
||||
expect(retained.normalized_result.tls).toMatchObject({
|
||||
raw_data_size: 256,
|
||||
raw_data_file_offset: null,
|
||||
raw_data_section: ".text",
|
||||
});
|
||||
expect(retained.normalized_result.load_config?.guard_flags?.raw).toBe(
|
||||
0x500,
|
||||
);
|
||||
expect(Buffer.compare(await readFile(path), padded)).toBe(0);
|
||||
}
|
||||
const bad = await client.callTool(
|
||||
{
|
||||
name: "inspect_pe_load_config",
|
||||
|
||||
Reference in new issue
Block a user