fix(native): bound NativeAOT reads to declared PE headers
Validate the consumed optional-header extent before interpreting mapping fields. Require complete header-backed read intervals and prevent failed header reads from falling through to section mappings. Advance NativeAOT profile interpretation identity to v4. Refs #4750
This commit is contained in:
1 parent
d9099190ab
commit
4a076f368c
4 files changed
+150
-3
No files matched your search
@@ -0,0 +1,56 @@
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
parseNativeAotPe,
|
||||
NATIVE_AOT_MAX_REPORT_BYTES,
|
||||
} from "./nativeAotPe.js";
|
||||
import {
|
||||
nativeAotPeHeaderIndirectionFixture,
|
||||
nativeAotPeOptionalHeaderFixture,
|
||||
nativeAotPeDigest,
|
||||
} from "../../../tests/fixtures/nativeaotPe.js";
|
||||
|
||||
it.each([
|
||||
{ rva: 0x3f8, admitted: true },
|
||||
{ rva: 0x3fc, admitted: false },
|
||||
{ rva: 0x400, admitted: false },
|
||||
])(
|
||||
"admits a header-backed TypeManager pointer only when all eight bytes are mapped ($rva)",
|
||||
({ rva, admitted }) => {
|
||||
const bytes = nativeAotPeHeaderIndirectionFixture(rva);
|
||||
const result = parseNativeAotPe(
|
||||
bytes,
|
||||
nativeAotPeDigest(bytes),
|
||||
NATIVE_AOT_MAX_REPORT_BYTES,
|
||||
);
|
||||
expect(result.summary.header_address !== null).toBe(admitted);
|
||||
if (!admitted)
|
||||
expect(result.summary).toMatchObject({
|
||||
status: "not_applicable",
|
||||
method_tables: 0,
|
||||
reason:
|
||||
"No supported ReadyToRun directory signature was found in initialized non-executable PE sections.",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ optionalBytes: 56, admitted: false },
|
||||
{ optionalBytes: 64, admitted: true },
|
||||
])(
|
||||
"reads mapping fields only from the declared optional header ($optionalBytes bytes)",
|
||||
({ optionalBytes, admitted }) => {
|
||||
const bytes = nativeAotPeOptionalHeaderFixture(optionalBytes);
|
||||
const result = parseNativeAotPe(
|
||||
bytes,
|
||||
nativeAotPeDigest(bytes),
|
||||
NATIVE_AOT_MAX_REPORT_BYTES,
|
||||
);
|
||||
expect(result.summary.header_address !== null).toBe(admitted);
|
||||
if (!admitted)
|
||||
expect(result.summary).toMatchObject({
|
||||
status: "not_applicable",
|
||||
method_tables: 0,
|
||||
reason: "PE optional header is too short for image mapping fields",
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -13,7 +13,7 @@ export const NATIVE_AOT_MAX_WORK_UNITS = 64 * 1024 * 1024;
|
||||
export const NATIVE_AOT_MAX_WORKING_MEMORY_BYTES = 64 * 1024 * 1024;
|
||||
/** Version of the built-in read-only PE metadata interpretation in profiles. */
|
||||
export const NATIVE_AOT_PROFILE_CONTRACT_REVISION =
|
||||
"rtr-9.1-x64-pe-read-only-v3";
|
||||
"rtr-9.1-x64-pe-read-only-v4";
|
||||
|
||||
export type NativeAotParserLimits = {
|
||||
readonly sourceBytes: number;
|
||||
@@ -399,6 +399,12 @@ class PeImage {
|
||||
const optionalSize = this.#u16(peOffset + 20);
|
||||
const optional = peOffset + 24;
|
||||
this.#range(optional, optionalSize);
|
||||
// Mapping fields through SizeOfHeaders must belong to this header,
|
||||
// independently of whether the following file bytes are available.
|
||||
if (optionalSize < 64)
|
||||
throw new Error(
|
||||
"PE optional header is too short for image mapping fields",
|
||||
);
|
||||
const magic = this.#u16(optional);
|
||||
if (magic !== 0x20b || this.#machine !== 0x8664)
|
||||
throw new Error("Only x64 PE32+ is supported");
|
||||
@@ -454,8 +460,14 @@ class PeImage {
|
||||
const rva = address - this.imageBase;
|
||||
if (rva > BigInt(Number.MAX_SAFE_INTEGER)) return null;
|
||||
const relative = Number(rva);
|
||||
if (relative < this.#sizeOfHeaders && relative <= this.#bytes.length - size)
|
||||
if (relative < this.#sizeOfHeaders) {
|
||||
if (
|
||||
size > this.#sizeOfHeaders - relative ||
|
||||
relative > this.#bytes.length - size
|
||||
)
|
||||
return null;
|
||||
return this.#bytes.subarray(relative, relative + size);
|
||||
}
|
||||
for (const section of this.sections) {
|
||||
if (this.#budget !== undefined && !this.#budget.consumeWork(1))
|
||||
return null;
|
||||
|
||||
@@ -19,6 +19,8 @@ import { fixtureRawBinaryLoadImage } from "./GhidraRawBinaryLoadImage.fixture.js
|
||||
import {
|
||||
nativeAotPeDigest,
|
||||
nativeAotPeFixture,
|
||||
nativeAotPeHeaderIndirectionFixture,
|
||||
nativeAotPeOptionalHeaderFixture,
|
||||
} from "../../tests/fixtures/nativeaotPe.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
|
||||
@@ -599,7 +601,7 @@ describe("Ghidra platform support", () => {
|
||||
executable_role: target.executableRole,
|
||||
managed: target.managed,
|
||||
native_aot_metadata: {
|
||||
contract_revision: "rtr-9.1-x64-pe-read-only-v3",
|
||||
contract_revision: "rtr-9.1-x64-pe-read-only-v4",
|
||||
source_bytes: 128 * 1024 * 1024,
|
||||
work_units: 64 * 1024 * 1024,
|
||||
working_memory_bytes: 64 * 1024 * 1024,
|
||||
@@ -3237,3 +3239,61 @@ it("rejects an absent profile before probing Java and attributes it to the reque
|
||||
await ghidra.closeOwnedResources();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
layout: "crossed header interval",
|
||||
fixture: () => nativeAotPeHeaderIndirectionFixture(0x3fc),
|
||||
},
|
||||
{
|
||||
layout: "short optional header",
|
||||
fixture: () => nativeAotPeOptionalHeaderFixture(56),
|
||||
},
|
||||
])(
|
||||
"retains NativeAOT unsupported-layout facts for a $layout through the provider",
|
||||
async ({ fixture }) => {
|
||||
const bytes = fixture();
|
||||
const observations = fixtureDosLoadImage().observation;
|
||||
observations.image_base = "0x140000000";
|
||||
const factory: GhidraProviderClientFactory = () => ({
|
||||
start: () =>
|
||||
Promise.resolve(
|
||||
ok({
|
||||
...sessionInfo(),
|
||||
target: { ...sessionInfo().target, image_base: "0x140000000" },
|
||||
}),
|
||||
),
|
||||
callTool: () => Promise.resolve(ok(jsonValueSchema.parse(observations))),
|
||||
readTargetSnapshot: async () => ok({ kind: "captured" as const, bytes }),
|
||||
close: () => Promise.resolve(ok(null)),
|
||||
});
|
||||
const ghidra = provider(installationHost(), factory);
|
||||
const target = { ...peTarget("x86_64"), sha256: nativeAotPeDigest(bytes) };
|
||||
const profile = await ghidra.resolveAnalysisProfile(target);
|
||||
if (!profile.ok || profile.value.profile === null)
|
||||
throw new Error("Expected admitted PE profile");
|
||||
const client = ghidra.createClient(target, profile.value.profile);
|
||||
try {
|
||||
const result = await client.execute("inspect_native_load_image", {});
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
result: {
|
||||
observations: {
|
||||
metadata_recovery: [
|
||||
{
|
||||
status: "not_applicable",
|
||||
header_address: null,
|
||||
method_tables: 0,
|
||||
analysis_artifact_sha256: target.sha256,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
Vendored
+19
@@ -89,3 +89,22 @@ export const nativeAotPeFixture = (major = 9, minor = 1): Buffer => {
|
||||
bytes.writeUInt16LE(0, frozen + 12 + value.length);
|
||||
return bytes;
|
||||
};
|
||||
|
||||
/** An RTR indirection into PE headers; its whole pointer interval must map. */
|
||||
export const nativeAotPeHeaderIndirectionFixture = (rva: number): Buffer => {
|
||||
const bytes = nativeAotPeFixture();
|
||||
bytes.writeUInt32LE(204, 0x600 + 40);
|
||||
bytes.writeBigUInt64LE(IMAGE_BASE + BigInt(rva), 0x600 + 48);
|
||||
bytes.writeBigUInt64LE(0n, 0x600 + 56);
|
||||
return bytes;
|
||||
};
|
||||
|
||||
/** Keep section-table placement consistent with the selected optional header. */
|
||||
export const nativeAotPeOptionalHeaderFixture = (
|
||||
optionalBytes: number,
|
||||
): Buffer => {
|
||||
const bytes = nativeAotPeFixture();
|
||||
bytes.copy(bytes, 0x98 + optionalBytes, 0x98 + 0xf0, 0x98 + 0xf0 + 80);
|
||||
bytes.writeUInt16LE(optionalBytes, 0x94);
|
||||
return bytes;
|
||||
};
|
||||
Reference in new issue
Block a user