mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
refactor(pe): share optional-header facts across readers
This commit is contained in:
@@ -80,7 +80,7 @@ import {
|
||||
parseCodeViewData,
|
||||
claimsExecutableHeader,
|
||||
parseExecutableHeader,
|
||||
parsePeRecord,
|
||||
parsePeRecordFacts,
|
||||
peDebugDirectoryEntry,
|
||||
peRvaToFileOffset,
|
||||
type ExecutableMetadata,
|
||||
@@ -1304,7 +1304,8 @@ const readPeMetadata = async (
|
||||
optionalHeaderSize,
|
||||
signal,
|
||||
);
|
||||
return parsePeRecord(record, codeView);
|
||||
const parsed = parsePeRecordFacts(record, codeView);
|
||||
return parsed.ok ? ok(parsed.value.metadata) : parsed;
|
||||
};
|
||||
|
||||
const readPeCodeView = async (
|
||||
|
||||
@@ -9,7 +9,10 @@ import { setImmediate } from "node:timers/promises";
|
||||
import type { DesktopFile, DesktopFact } from "../domain/desktopApplication.js";
|
||||
import { jsonValueByteLength } from "../domain/jsonValueByteLength.js";
|
||||
import { observeIl2cppMetadataHeaderPrefix } from "../domain/il2cpp/il2cppMetadataHeaderPrefix.js";
|
||||
import { parsePeRecord, peRvaToFileOffset } from "../domain/binaryTarget.js";
|
||||
import {
|
||||
parsePeRecordFacts,
|
||||
peRvaToFileOffset,
|
||||
} from "../domain/binaryTarget.js";
|
||||
import { readManagedMetadataLayout } from "../dotnet/ManagedMetadataLayout.js";
|
||||
import { readAssemblyReferenceName } from "../dotnet/ManagedMetadataInventoryRows.js";
|
||||
import { ManagedReaderFailure } from "../dotnet/ManagedReaderFailure.js";
|
||||
@@ -221,24 +224,24 @@ const inspectPeFile = async (
|
||||
"PE optional header exceeds the supported format bound",
|
||||
);
|
||||
const optional = await read(peOffset + 24, optionalSize);
|
||||
const parsed = parsePeRecord(Buffer.concat([coff, optional]), undefined);
|
||||
const parsed = parsePeRecordFacts(Buffer.concat([coff, optional]), undefined);
|
||||
if (!parsed.ok) {
|
||||
row.issues.push(parsed.error);
|
||||
return;
|
||||
}
|
||||
if (parsed.value.format !== "pe") return;
|
||||
if (parsed.value.metadata.format !== "pe") return;
|
||||
fact({
|
||||
kind: "native-image-header",
|
||||
offset: peOffset,
|
||||
value: { format: "pe", machine: header.value.machine },
|
||||
});
|
||||
if (!parsed.value.managed) return;
|
||||
const directories = optional.readUInt16LE(0) === 0x20b ? 112 : 96;
|
||||
const cliRva = optional.readUInt32LE(directories + 14 * 8);
|
||||
const cliSize = optional.readUInt32LE(directories + 14 * 8 + 4);
|
||||
if (!parsed.value.metadata.managed) return;
|
||||
const cliDirectory = parsed.value.optionalHeader.dataDirectories[14];
|
||||
if (cliDirectory === undefined) return;
|
||||
const { address: cliRva, size: cliSize } = cliDirectory;
|
||||
fact({
|
||||
kind: "pe-cli-directory",
|
||||
offset: peOffset + 24 + directories + 14 * 8,
|
||||
offset: peOffset + 24 + cliDirectory.offset,
|
||||
value: { rva: cliRva, bytes: cliSize },
|
||||
});
|
||||
if (unityManagedDirectory === null) return;
|
||||
@@ -249,7 +252,7 @@ const inspectPeFile = async (
|
||||
rva,
|
||||
sections,
|
||||
sectionCount,
|
||||
optional.readUInt32LE(60),
|
||||
parsed.value.optionalHeader.sizeOfHeaders,
|
||||
bytes,
|
||||
);
|
||||
const cliOffset = mapped(cliRva, 16);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseExecutableHeader } from "./binaryTarget.js";
|
||||
import {
|
||||
parseExecutableHeader,
|
||||
peDebugDirectoryEntry,
|
||||
parsePeRecordFacts,
|
||||
} from "./binaryTarget.js";
|
||||
import { dosMz, elf, fat, pe, thinMach } from "./binaryTarget.fixture.js";
|
||||
|
||||
describe("executable header parsing", () => {
|
||||
@@ -377,3 +381,44 @@ const fat64 = (cpus: readonly number[], littleEndian: boolean): Buffer => {
|
||||
});
|
||||
return bytes;
|
||||
};
|
||||
|
||||
it.each([
|
||||
{ magic: 0x10b, directoryOffset: 96 },
|
||||
{ magic: 0x20b, directoryOffset: 112 },
|
||||
])(
|
||||
"retains a complete debug entry when later declared directories are unavailable ($magic)",
|
||||
({ magic, directoryOffset }) => {
|
||||
const optional = Buffer.alloc(directoryOffset + 7 * 8);
|
||||
optional.writeUInt16LE(magic);
|
||||
optional.writeUInt32LE(0x400, 60);
|
||||
optional.writeUInt32LE(0xffffffff, directoryOffset - 4);
|
||||
optional.writeUInt32LE(0x2400, directoryOffset + 6 * 8);
|
||||
optional.writeUInt32LE(28, directoryOffset + 6 * 8 + 4);
|
||||
expect(peDebugDirectoryEntry(optional)).toEqual({
|
||||
debugRva: 0x2400,
|
||||
debugSize: 28,
|
||||
sizeOfHeaders: 0x400,
|
||||
});
|
||||
expect(
|
||||
peDebugDirectoryEntry(optional.subarray(0, optional.length - 1)),
|
||||
).toBeUndefined();
|
||||
optional.writeUInt32LE(6, directoryOffset - 4);
|
||||
expect(peDebugDirectoryEntry(optional)).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps validated optional-header facts separate from executable metadata", () => {
|
||||
const bytes = pe(0x8664);
|
||||
const record = bytes.subarray(bytes.readUInt32LE(0x3c));
|
||||
const parsed = parsePeRecordFacts(record, undefined);
|
||||
if (!parsed.ok) throw new Error(parsed.error);
|
||||
expect(parsed.value.optionalHeader).toMatchObject({ magic: 0x20b, bits: 64 });
|
||||
expect(parsed.value.metadata).toEqual({
|
||||
format: "pe",
|
||||
peMachine: 0x8664,
|
||||
architecture: "x86_64",
|
||||
availableArchitectures: ["x86_64"],
|
||||
executableRole: "application",
|
||||
managed: false,
|
||||
});
|
||||
});
|
||||
|
||||
+40
-32
@@ -9,6 +9,11 @@ import type {
|
||||
UniversalMachOSlice,
|
||||
} from "./binaryTargetTypes.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import {
|
||||
readPeOptionalHeaderFixedFields,
|
||||
readPeOptionalHeaderDirectory,
|
||||
} from "./peOptionalHeader.js";
|
||||
import type { PeOptionalHeaderFacts } from "./peMachine.js";
|
||||
import { readPeCoffHeader } from "./peCoffHeader.js";
|
||||
import { readPeSectionHeader } from "./peSectionHeader.js";
|
||||
import { readElfFixedHeader } from "./elfFixedHeader.js";
|
||||
@@ -468,7 +473,8 @@ const parseMz = (
|
||||
if (windowsHeader.signature !== "PE")
|
||||
return err("invalid or truncated Windows executable header in MZ image");
|
||||
const codeView = parsePeCodeViewFromBuffer(bytes, offset);
|
||||
return parsePeRecord(bytes.subarray(offset), codeView);
|
||||
const parsed = parsePeRecordFacts(bytes.subarray(offset), codeView);
|
||||
return parsed.ok ? ok(parsed.value.metadata) : parsed;
|
||||
}
|
||||
const parsed = evidence.dos;
|
||||
return parsed.ok
|
||||
@@ -481,10 +487,16 @@ const parseMz = (
|
||||
};
|
||||
|
||||
/** Parse a bounded PE signature, COFF record, and optional header without I/O. */
|
||||
export const parsePeRecord = (
|
||||
export const parsePeRecordFacts = (
|
||||
record: Buffer,
|
||||
codeView: PeCodeViewRecord | undefined,
|
||||
): Result<ExecutableMetadata, string> => {
|
||||
): Result<
|
||||
{
|
||||
readonly metadata: ExecutableMetadata;
|
||||
readonly optionalHeader: PeOptionalHeaderFacts;
|
||||
},
|
||||
string
|
||||
> => {
|
||||
if (
|
||||
record.length < 24 ||
|
||||
record.toString("binary", 0, 4) !== "PE\u0000\u0000"
|
||||
@@ -510,18 +522,21 @@ export const parsePeRecord = (
|
||||
if (facts.value.machine === null) return err("unsupported PE architecture");
|
||||
const { architecture } = facts.value.machine;
|
||||
return ok({
|
||||
format: "pe",
|
||||
peMachine: machineType,
|
||||
architecture,
|
||||
availableArchitectures: [architecture],
|
||||
executableRole:
|
||||
(characteristics & 0x0002) === 0
|
||||
? "non-executable"
|
||||
: (characteristics & 0x2000) === 0
|
||||
? "application"
|
||||
: "shared-library",
|
||||
managed: facts.value.managed,
|
||||
...(codeView !== undefined ? { codeView } : {}),
|
||||
optionalHeader: facts.value,
|
||||
metadata: {
|
||||
format: "pe",
|
||||
peMachine: machineType,
|
||||
architecture,
|
||||
availableArchitectures: [architecture],
|
||||
executableRole:
|
||||
(characteristics & 0x0002) === 0
|
||||
? "non-executable"
|
||||
: (characteristics & 0x2000) === 0
|
||||
? "application"
|
||||
: "shared-library",
|
||||
managed: facts.value.managed,
|
||||
...(codeView !== undefined ? { codeView } : {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -570,24 +585,17 @@ export const peDebugDirectoryEntry = (
|
||||
readonly sizeOfHeaders: number;
|
||||
}
|
||||
| undefined => {
|
||||
if (optionalHeader.length < 2) return undefined;
|
||||
const magic = optionalHeader.readUInt16LE(0);
|
||||
if (magic !== 0x10b && magic !== 0x20b) return undefined;
|
||||
const directoryOffset = magic === 0x20b ? 112 : 96;
|
||||
const directoryCountOffset = directoryOffset - 4;
|
||||
if (optionalHeader.length < directoryCountOffset + 4) return undefined;
|
||||
const directoryCount = optionalHeader.readUInt32LE(directoryCountOffset);
|
||||
if (directoryCount <= 6) return undefined;
|
||||
const debugEntryOffset = directoryOffset + 6 * 8;
|
||||
if (optionalHeader.length < debugEntryOffset + 8) return undefined;
|
||||
const debugRva = optionalHeader.readUInt32LE(debugEntryOffset);
|
||||
const debugSize = optionalHeader.readUInt32LE(debugEntryOffset + 4);
|
||||
const fixed = readPeOptionalHeaderFixedFields(optionalHeader);
|
||||
if (!fixed.ok) return undefined;
|
||||
const observed = readPeOptionalHeaderDirectory(
|
||||
optionalHeader,
|
||||
fixed.value,
|
||||
6,
|
||||
);
|
||||
if (observed.state !== "present") return undefined;
|
||||
const { address: debugRva, size: debugSize } = observed.directory;
|
||||
if (debugRva === 0 || debugSize < 28) return undefined;
|
||||
return {
|
||||
debugRva,
|
||||
debugSize,
|
||||
sizeOfHeaders: optionalHeader.readUInt32LE(60),
|
||||
};
|
||||
return { debugRva, debugSize, sizeOfHeaders: fixed.value.sizeOfHeaders };
|
||||
};
|
||||
|
||||
/** Translate an RVA to an absolute file offset using the PE section table. */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readPeOptionalHeaderFixedFields } from "../peOptionalHeader.js";
|
||||
import { readPeCoffHeader } from "../peCoffHeader.js";
|
||||
import { readPeSectionHeader } from "../peSectionHeader.js";
|
||||
import type { NativeMetadataRecovery } from "./nativeMetadataRecovery.js";
|
||||
@@ -511,12 +512,14 @@ class PeImage {
|
||||
throw new Error(
|
||||
"PE optional header is too short for image mapping fields",
|
||||
);
|
||||
const magic = this.#u16(optional);
|
||||
if (magic !== 0x20b || this.#machine !== 0x8664)
|
||||
const fixed = readPeOptionalHeaderFixedFields(
|
||||
bytes.subarray(optional, optional + optionalSize),
|
||||
);
|
||||
if (!fixed.ok || fixed.value.magic !== 0x20b || this.#machine !== 0x8664)
|
||||
throw new Error("Only x64 PE32+ is supported");
|
||||
this.imageBase = this.#u64(optional + 24);
|
||||
this.sizeOfImage = this.#u32(optional + 56);
|
||||
this.#sizeOfHeaders = this.#u32(optional + 60);
|
||||
this.imageBase = fixed.value.imageBase;
|
||||
this.sizeOfImage = fixed.value.sizeOfImage;
|
||||
this.#sizeOfHeaders = fixed.value.sizeOfHeaders;
|
||||
if (count < 1 || this.sizeOfImage === 0)
|
||||
throw new Error("Unsupported PE image extent");
|
||||
if (this.imageBase + BigInt(this.sizeOfImage) > 1n << 64n)
|
||||
@@ -658,18 +661,10 @@ class PeImage {
|
||||
)
|
||||
throw new Error("PE field leaves the captured file");
|
||||
}
|
||||
#u16(offset: number): number {
|
||||
this.#range(offset, 2);
|
||||
return this.#bytes.readUInt16LE(offset);
|
||||
}
|
||||
#u32(offset: number): number {
|
||||
this.#range(offset, 4);
|
||||
return this.#bytes.readUInt32LE(offset);
|
||||
}
|
||||
#u64(offset: number): bigint {
|
||||
this.#range(offset, 8);
|
||||
return this.#bytes.readBigUInt64LE(offset);
|
||||
}
|
||||
}
|
||||
|
||||
const hex = (value: bigint): string => `0x${value.toString(16)}`;
|
||||
|
||||
@@ -64,7 +64,9 @@ it.each(formats)(
|
||||
imageBase: format.bits === 64 ? 0xfedcba9876540000n : 0x87650000n,
|
||||
sectionAlignment: 0x1000,
|
||||
fileAlignment: 0x200,
|
||||
sizeOfImage: 0,
|
||||
sizeOfHeaders: 0xffffffff,
|
||||
directoryCountOffset: format.directoryOffset - 4,
|
||||
directoryCount: 0,
|
||||
dataDirectories: [],
|
||||
},
|
||||
@@ -170,7 +172,9 @@ it("honors a Uint8Array window and detaches facts from later source changes", ()
|
||||
imageBase: 0xfedcba9876540000n,
|
||||
sectionAlignment: 0x1000,
|
||||
fileAlignment: 0x200,
|
||||
sizeOfImage: 0,
|
||||
sizeOfHeaders: 0x400,
|
||||
directoryCountOffset: formats[1].directoryOffset - 4,
|
||||
directoryCount: 1,
|
||||
dataDirectories: [{ address: 0x12345678, size: 0x90, offset: 112 }],
|
||||
});
|
||||
|
||||
@@ -9,15 +9,20 @@ export interface PeDataDirectory {
|
||||
}
|
||||
|
||||
/** Structural facts copied from one declared PE optional header. */
|
||||
export interface PeOptionalHeaderStructure {
|
||||
export interface PeOptionalHeaderFixedFields {
|
||||
readonly magic: 0x10b | 0x20b;
|
||||
readonly bits: 32 | 64;
|
||||
readonly format: "pe32" | "pe32-plus";
|
||||
readonly imageBase: bigint;
|
||||
readonly sectionAlignment: number;
|
||||
readonly fileAlignment: number;
|
||||
readonly sizeOfImage: number;
|
||||
readonly sizeOfHeaders: number;
|
||||
readonly directoryOffset: number;
|
||||
readonly directoryCountOffset: number;
|
||||
}
|
||||
|
||||
export interface PeOptionalHeaderStructure extends PeOptionalHeaderFixedFields {
|
||||
readonly directoryCount: number;
|
||||
readonly dataDirectories: readonly PeDataDirectory[];
|
||||
}
|
||||
@@ -32,10 +37,10 @@ export interface PeOptionalHeaderFailure {
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
/** Decode structural declarations without admitting a machine, file extent, or directory address. */
|
||||
export const readPeOptionalHeaderStructure = (
|
||||
/** Decode mapping fields without requiring a directory count or table. */
|
||||
export const readPeOptionalHeaderFixedFields = (
|
||||
bytes: Uint8Array,
|
||||
): Result<PeOptionalHeaderStructure, PeOptionalHeaderFailure> => {
|
||||
): Result<PeOptionalHeaderFixedFields, PeOptionalHeaderFailure> => {
|
||||
if (bytes.byteLength < 2)
|
||||
return err({
|
||||
kind: "malformed",
|
||||
@@ -52,27 +57,12 @@ export const readPeOptionalHeaderStructure = (
|
||||
});
|
||||
const bits = magic === 0x20b ? 64 : 32;
|
||||
const directoryOffset = bits === 64 ? 112 : 96;
|
||||
if (bytes.byteLength < directoryOffset)
|
||||
if (bytes.byteLength < 64)
|
||||
return err({
|
||||
kind: "malformed",
|
||||
reason: "truncated-fixed-fields",
|
||||
message: "truncated PE optional header",
|
||||
});
|
||||
const directoryCount = view.getUint32(directoryOffset - 4, true);
|
||||
if (directoryCount > Math.floor((bytes.byteLength - directoryOffset) / 8))
|
||||
return err({
|
||||
kind: "malformed",
|
||||
reason: "truncated-directory-table",
|
||||
message: "truncated PE data-directory table",
|
||||
});
|
||||
const dataDirectories = Array.from({ length: directoryCount }, (_, index) => {
|
||||
const offset = directoryOffset + index * 8;
|
||||
return {
|
||||
address: view.getUint32(offset, true),
|
||||
size: view.getUint32(offset + 4, true),
|
||||
offset,
|
||||
};
|
||||
});
|
||||
return ok({
|
||||
magic,
|
||||
bits,
|
||||
@@ -83,9 +73,74 @@ export const readPeOptionalHeaderStructure = (
|
||||
: BigInt(view.getUint32(28, true)),
|
||||
sectionAlignment: view.getUint32(32, true),
|
||||
fileAlignment: view.getUint32(36, true),
|
||||
sizeOfImage: view.getUint32(56, true),
|
||||
sizeOfHeaders: view.getUint32(60, true),
|
||||
directoryOffset,
|
||||
directoryCount,
|
||||
dataDirectories,
|
||||
directoryCountOffset: directoryOffset - 4,
|
||||
});
|
||||
};
|
||||
|
||||
const readDirectory = (view: DataView, offset: number): PeDataDirectory => ({
|
||||
address: view.getUint32(offset, true),
|
||||
size: view.getUint32(offset + 4, true),
|
||||
offset,
|
||||
});
|
||||
|
||||
export type PeOptionalHeaderDirectoryObservation =
|
||||
| { readonly state: "count-unavailable" }
|
||||
| { readonly state: "absent"; readonly count: number }
|
||||
| { readonly state: "truncated"; readonly count: number }
|
||||
| {
|
||||
readonly state: "present";
|
||||
readonly count: number;
|
||||
readonly directory: PeDataDirectory;
|
||||
};
|
||||
|
||||
/** Observe one declared entry independently of the availability of later entries. */
|
||||
export const readPeOptionalHeaderDirectory = (
|
||||
bytes: Uint8Array,
|
||||
fixed: PeOptionalHeaderFixedFields,
|
||||
index: number,
|
||||
): PeOptionalHeaderDirectoryObservation => {
|
||||
if (bytes.byteLength < fixed.directoryOffset)
|
||||
return { state: "count-unavailable" };
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const count = view.getUint32(fixed.directoryCountOffset, true);
|
||||
if (index >= count) return { state: "absent", count };
|
||||
const offset = fixed.directoryOffset + index * 8;
|
||||
if (offset > bytes.byteLength - 8) return { state: "truncated", count };
|
||||
return {
|
||||
state: "present",
|
||||
count,
|
||||
directory: readDirectory(view, offset),
|
||||
};
|
||||
};
|
||||
|
||||
/** Decode the complete declared table without admitting a machine or directory address. */
|
||||
export const readPeOptionalHeaderStructure = (
|
||||
bytes: Uint8Array,
|
||||
): Result<PeOptionalHeaderStructure, PeOptionalHeaderFailure> => {
|
||||
const parsed = readPeOptionalHeaderFixedFields(bytes);
|
||||
if (!parsed.ok) return parsed;
|
||||
const fixed = parsed.value;
|
||||
if (bytes.byteLength < fixed.directoryOffset)
|
||||
return err({
|
||||
kind: "malformed",
|
||||
reason: "truncated-fixed-fields",
|
||||
message: "truncated PE optional header",
|
||||
});
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const directoryCount = view.getUint32(fixed.directoryCountOffset, true);
|
||||
if (
|
||||
directoryCount > Math.floor((bytes.byteLength - fixed.directoryOffset) / 8)
|
||||
)
|
||||
return err({
|
||||
kind: "malformed",
|
||||
reason: "truncated-directory-table",
|
||||
message: "truncated PE data-directory table",
|
||||
});
|
||||
const dataDirectories = Array.from({ length: directoryCount }, (_, index) =>
|
||||
readDirectory(view, fixed.directoryOffset + index * 8),
|
||||
);
|
||||
return ok({ ...fixed, directoryCount, dataDirectories });
|
||||
};
|
||||
|
||||
@@ -93,6 +93,22 @@ describe("managed PE optional-header directory count", () => {
|
||||
});
|
||||
|
||||
describe("managed CLI directory availability", () => {
|
||||
it("keeps a readable CLI directory when later declared entries are truncated", () => {
|
||||
const bytes = buildManagedPeFixture();
|
||||
const optional = bytes.readUInt32LE(0x3c) + 24;
|
||||
bytes.writeUInt32LE(0xffffffff, optional + 92);
|
||||
expect(readManagedPeLayout(bytes)).toMatchObject({
|
||||
cliDirectoryState: "present",
|
||||
cliIssue: null,
|
||||
cli: expect.objectContaining({ headerSize: 72 }),
|
||||
});
|
||||
const result = inspectManagedArtifactBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
);
|
||||
expect(result.metadata.status).toBe("complete");
|
||||
});
|
||||
|
||||
it("keeps a declared but truncated CLI directory malformed", () => {
|
||||
const bytes = buildManagedPeFixture();
|
||||
const peOffset = bytes.readUInt32LE(0x3c);
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
readPeOptionalHeaderFixedFields,
|
||||
readPeOptionalHeaderDirectory,
|
||||
type PeOptionalHeaderFixedFields,
|
||||
} from "../domain/peOptionalHeader.js";
|
||||
import { readPeSectionHeader } from "../domain/peSectionHeader.js";
|
||||
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
|
||||
import { readPeCoffHeader } from "../domain/peCoffHeader.js";
|
||||
import { decodePeMachine } from "../domain/peMachine.js";
|
||||
@@ -86,8 +92,7 @@ interface CoffHeader {
|
||||
interface OptionalHeader {
|
||||
readonly optionalOffset: number;
|
||||
readonly optionalHeader: "pe32" | "pe32-plus";
|
||||
readonly directoryCountOffset: number;
|
||||
readonly directoryOffset: number;
|
||||
readonly fixed: PeOptionalHeaderFixedFields;
|
||||
}
|
||||
|
||||
type CliDirectoryRead =
|
||||
@@ -337,10 +342,10 @@ const readOptionalHeader = (
|
||||
"PE optional header is too short",
|
||||
optionalOffset,
|
||||
);
|
||||
const magic = bytes.readUInt16LE(optionalOffset);
|
||||
const optionalHeader =
|
||||
magic === 0x010b ? "pe32" : magic === 0x020b ? "pe32-plus" : undefined;
|
||||
if (optionalHeader === undefined)
|
||||
const parsed = readPeOptionalHeaderFixedFields(
|
||||
bytes.subarray(optionalOffset, optionalOffset + coff.optionalSize),
|
||||
);
|
||||
if (!parsed.ok)
|
||||
throw managedFailure(
|
||||
"invalid-directory",
|
||||
"pe.optional-header",
|
||||
@@ -349,10 +354,8 @@ const readOptionalHeader = (
|
||||
);
|
||||
return {
|
||||
optionalOffset,
|
||||
optionalHeader,
|
||||
directoryCountOffset:
|
||||
optionalOffset + (optionalHeader === "pe32" ? 92 : 108),
|
||||
directoryOffset: optionalOffset + (optionalHeader === "pe32" ? 96 : 112),
|
||||
optionalHeader: parsed.value.format,
|
||||
fixed: parsed.value,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -370,11 +373,19 @@ const readSections = (
|
||||
const sections: PeSection[] = [];
|
||||
for (let index = 0; index < coff.sectionCount; index += 1) {
|
||||
const offset = sectionOffset + index * 40;
|
||||
const parsed = readPeSectionHeader(bytes.subarray(offset, offset + 40));
|
||||
if (!parsed.ok)
|
||||
throw managedFailure(
|
||||
"invalid-directory",
|
||||
"pe.sections",
|
||||
parsed.error.message,
|
||||
offset,
|
||||
);
|
||||
const section = {
|
||||
virtualSize: bytes.readUInt32LE(offset + 8),
|
||||
virtualAddress: bytes.readUInt32LE(offset + 12),
|
||||
rawSize: bytes.readUInt32LE(offset + 16),
|
||||
rawOffset: bytes.readUInt32LE(offset + 20),
|
||||
virtualSize: parsed.value.virtual_size,
|
||||
virtualAddress: parsed.value.virtual_address,
|
||||
rawSize: parsed.value.raw_size,
|
||||
rawOffset: parsed.value.offset,
|
||||
};
|
||||
if (section.rawSize > 0)
|
||||
requireRange(
|
||||
@@ -393,34 +404,41 @@ const readCliDirectory = (
|
||||
coff: CoffHeader,
|
||||
optional: OptionalHeader,
|
||||
): CliDirectoryRead => {
|
||||
const countOffset = optional.directoryCountOffset - optional.optionalOffset;
|
||||
if (countOffset + 4 > coff.optionalSize)
|
||||
const observed = readPeOptionalHeaderDirectory(
|
||||
bytes.subarray(
|
||||
optional.optionalOffset,
|
||||
optional.optionalOffset + coff.optionalSize,
|
||||
),
|
||||
optional.fixed,
|
||||
14,
|
||||
);
|
||||
const countOffset =
|
||||
optional.optionalOffset + optional.fixed.directoryCountOffset;
|
||||
if (observed.state === "count-unavailable")
|
||||
return {
|
||||
kind: "unreadable",
|
||||
issue: {
|
||||
code: "invalid-directory",
|
||||
scope: "pe.optional-header",
|
||||
detail: "PE optional header does not contain its directory count",
|
||||
offset: optional.directoryCountOffset,
|
||||
offset: countOffset,
|
||||
},
|
||||
};
|
||||
const directoryCount = bytes.readUInt32LE(optional.directoryCountOffset);
|
||||
if (directoryCount <= 14) return { kind: "absent" };
|
||||
const availableDirectoryCount = Math.floor(
|
||||
(coff.optionalSize - (optional.directoryOffset - optional.optionalOffset)) /
|
||||
8,
|
||||
);
|
||||
if (availableDirectoryCount < 15)
|
||||
if (observed.state === "absent") return { kind: "absent" };
|
||||
if (observed.state === "truncated")
|
||||
return {
|
||||
kind: "unreadable",
|
||||
issue: {
|
||||
code: "invalid-directory",
|
||||
scope: "pe.data-directories",
|
||||
detail: "PE data-directory count exceeds the optional-header extent",
|
||||
offset: optional.directoryCountOffset,
|
||||
offset: countOffset,
|
||||
},
|
||||
};
|
||||
const directory = readDirectory(bytes, optional.directoryOffset + 14 * 8);
|
||||
const directory = {
|
||||
rva: observed.directory.address,
|
||||
size: observed.directory.size,
|
||||
};
|
||||
return directory.rva !== 0 || directory.size !== 0
|
||||
? { kind: "present", directory }
|
||||
: { kind: "absent" };
|
||||
|
||||
Reference in New Issue
Block a user