mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
refactor(dos): centralize MZ admission and load-image evidence
This commit is contained in:
1 parent
aa6c1d49fd
commit
a30ffd27a6
10 files changed
+491
-96
No files matched your search
@@ -57,9 +57,10 @@ import { admitGamecubeDol } from "../domain/gamecubeDolAdmission.js";
|
||||
import { DOL_HEADER_SIZE } from "../domain/gamecubeDol.js";
|
||||
import {
|
||||
mzWindowsHeaderLayoutLimitation,
|
||||
observeMzWindowsHeader,
|
||||
parseMzHeaderEvidence,
|
||||
withMzWindowsHeaderSignature,
|
||||
type DosMzHeader,
|
||||
type MzHeaderEvidence,
|
||||
type MzWindowsHeaderObservation,
|
||||
} from "../domain/dosMz.js";
|
||||
import {
|
||||
@@ -80,6 +81,7 @@ import {
|
||||
parseCodeViewData,
|
||||
claimsExecutableHeader,
|
||||
parseExecutableHeader,
|
||||
parseMzExecutableMetadata,
|
||||
parsePeRecordFacts,
|
||||
peDebugDirectoryEntry,
|
||||
peCodeViewDataRanges,
|
||||
@@ -877,7 +879,12 @@ const readExecutableMetadata = async (
|
||||
offset,
|
||||
bytes: signature.subarray(0, observedSignature.bytesRead),
|
||||
};
|
||||
evidence = parseMzHeaderEvidence(bytes, fileSize, signatureEvidence);
|
||||
evidence = withMzWindowsHeaderSignature(
|
||||
bytes,
|
||||
fileSize,
|
||||
evidence,
|
||||
signatureEvidence,
|
||||
);
|
||||
}
|
||||
const { windowsHeader } = evidence;
|
||||
if (windowsHeader.state === "declared") {
|
||||
@@ -890,31 +897,28 @@ const readExecutableMetadata = async (
|
||||
mz_windows_header: { ...windowsHeader },
|
||||
},
|
||||
});
|
||||
return readPeMetadata(
|
||||
handle,
|
||||
windowsHeader.offset,
|
||||
bytes,
|
||||
return readPeMetadata(handle, windowsHeader.offset, {
|
||||
mzPrefix: bytes,
|
||||
fileSize,
|
||||
mzEvidence: evidence,
|
||||
signal,
|
||||
);
|
||||
});
|
||||
}
|
||||
// MZ relocation records may extend past the initial probe; their format
|
||||
// bounds are at most 65535 records inside a 65535-paragraph header.
|
||||
const tableEnd =
|
||||
bytes.length >= 28
|
||||
? bytes.readUInt16LE(24) + bytes.readUInt16LE(6) * 4
|
||||
: 0;
|
||||
const headerBytes = bytes.length >= 28 ? bytes.readUInt16LE(8) * 16 : 0;
|
||||
if (
|
||||
tableEnd > bytes.length &&
|
||||
tableEnd <= headerBytes &&
|
||||
tableEnd <= fileSize
|
||||
) {
|
||||
const table = Buffer.alloc(tableEnd);
|
||||
const observed = await handle.read(table, 0, table.length, 0);
|
||||
const { relocationTableRead } = evidence;
|
||||
if (relocationTableRead !== undefined) {
|
||||
const table = Buffer.alloc(relocationTableRead.byteLength);
|
||||
const observed = await handle.read(
|
||||
table,
|
||||
0,
|
||||
table.length,
|
||||
relocationTableRead.offset,
|
||||
);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const observedBytes = table.subarray(0, observed.bytesRead);
|
||||
evidence = parseMzHeaderEvidence(
|
||||
table.subarray(0, observed.bytesRead),
|
||||
observedBytes,
|
||||
fileSize,
|
||||
signatureEvidence,
|
||||
);
|
||||
@@ -922,21 +926,11 @@ const readExecutableMetadata = async (
|
||||
return err(
|
||||
unresolvedMzHeaderFailure(evidence.windowsHeader, evidence.dos),
|
||||
);
|
||||
return parseExecutableHeader(
|
||||
table.subarray(0, observed.bytesRead),
|
||||
hostArchitecture,
|
||||
fileSize,
|
||||
signatureEvidence,
|
||||
);
|
||||
return parseMzExecutableMetadata(observedBytes, evidence);
|
||||
}
|
||||
if (windowsHeader.state === "candidate")
|
||||
return err(unresolvedMzHeaderFailure(windowsHeader, evidence.dos));
|
||||
return parseExecutableHeader(
|
||||
bytes,
|
||||
hostArchitecture,
|
||||
fileSize,
|
||||
signatureEvidence,
|
||||
);
|
||||
return parseMzExecutableMetadata(bytes, evidence);
|
||||
}
|
||||
if (bytes.length >= 4 && bytes.toString("hex", 0, 4) === "7f454c46")
|
||||
return readElfMetadata(handle, bytes, {
|
||||
@@ -1264,9 +1258,17 @@ const readFatSliceHeaderEvidence = async (
|
||||
const readPeMetadata = async (
|
||||
handle: FileHandle,
|
||||
offset: number,
|
||||
mzPrefix: Buffer,
|
||||
fileSize: number,
|
||||
signal?: AbortSignal,
|
||||
{
|
||||
mzPrefix,
|
||||
fileSize,
|
||||
mzEvidence,
|
||||
signal,
|
||||
}: {
|
||||
readonly mzPrefix: Buffer;
|
||||
readonly fileSize: number;
|
||||
readonly mzEvidence: MzHeaderEvidence;
|
||||
readonly signal: AbortSignal | undefined;
|
||||
},
|
||||
): Promise<Result<ExecutableMetadata, string>> => {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const fileHeader = Buffer.alloc(24);
|
||||
@@ -1279,10 +1281,12 @@ const readPeMetadata = async (
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (fileHeaderRead.bytesRead !== fileHeader.length)
|
||||
return err("invalid or truncated PE header");
|
||||
const windowsHeader = observeMzWindowsHeader(mzPrefix, fileSize, {
|
||||
offset,
|
||||
bytes: fileHeader.subarray(0, 4),
|
||||
});
|
||||
const { windowsHeader } = withMzWindowsHeaderSignature(
|
||||
mzPrefix,
|
||||
fileSize,
|
||||
mzEvidence,
|
||||
{ offset, bytes: fileHeader.subarray(0, 4) },
|
||||
);
|
||||
if (windowsHeader.state !== "declared")
|
||||
return err("invalid Windows new-header signature in MZ image");
|
||||
if (["NE", "LE", "LX"].includes(windowsHeader.signature))
|
||||
|
||||
@@ -20,6 +20,7 @@ import { readElfFixedHeader } from "./elfFixedHeader.js";
|
||||
import {
|
||||
mzWindowsHeaderLayoutLimitation,
|
||||
parseMzHeaderEvidence,
|
||||
type MzHeaderEvidence,
|
||||
type MzWindowsHeaderSignatureEvidence,
|
||||
} from "./dosMz.js";
|
||||
import { decodePeMachine, readPeOptionalHeader } from "./peMachine.js";
|
||||
@@ -126,7 +127,10 @@ export const parseExecutableHeader = (
|
||||
)
|
||||
return parseElf(bytes, fileSize);
|
||||
if (bytes.length >= 2 && bytes[0] === 0x4d && bytes[1] === 0x5a)
|
||||
return parseMz(bytes, fileSize, mzWindowsHeaderSignature);
|
||||
return parseMzExecutableMetadata(
|
||||
bytes,
|
||||
parseMzHeaderEvidence(bytes, fileSize, mzWindowsHeaderSignature),
|
||||
);
|
||||
if (bytes.length < 8) return err("truncated or unsupported binary header");
|
||||
const magic = bytes.readUInt32BE(0);
|
||||
if ([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(magic))
|
||||
@@ -434,12 +438,11 @@ const parseElf = (
|
||||
});
|
||||
};
|
||||
|
||||
const parseMz = (
|
||||
/** Select executable metadata from the reader's completed MZ observations. */
|
||||
export const parseMzExecutableMetadata = (
|
||||
bytes: Buffer,
|
||||
fileSize: number,
|
||||
signatureAtOffset?: MzWindowsHeaderSignatureEvidence,
|
||||
evidence: MzHeaderEvidence,
|
||||
): Result<ExecutableMetadata, string> => {
|
||||
const evidence = parseMzHeaderEvidence(bytes, fileSize, signatureAtOffset);
|
||||
const { windowsHeader } = evidence;
|
||||
if (
|
||||
windowsHeader.state === "header-needed" ||
|
||||
|
||||
@@ -4,6 +4,10 @@ import { dosMz, pe } from "./binaryTarget.fixture.js";
|
||||
import { parseExecutableHeader } from "./binaryTarget.js";
|
||||
import {
|
||||
observeMzWindowsHeader,
|
||||
readDosMzDeclarations,
|
||||
dosMzDeclaredImageByteLength,
|
||||
parseDeclaredMzHeaderEvidence,
|
||||
withMzWindowsHeaderSignature,
|
||||
parseDosMzHeader,
|
||||
parseMzHeaderEvidence,
|
||||
} from "./dosMz.js";
|
||||
@@ -419,3 +423,166 @@ it("retains ordered DOS relocation aliases and their source/module target coordi
|
||||
error: "DOS MZ relocation 1 points outside its load module",
|
||||
});
|
||||
});
|
||||
|
||||
const extendedRelocationImage = (): Buffer => {
|
||||
const bytes = dosMz(8192);
|
||||
bytes.writeUInt16LE(257, 8); // 4112-byte header.
|
||||
bytes.writeUInt16LE(2, 6);
|
||||
bytes.writeUInt16LE(4096, 24);
|
||||
return bytes;
|
||||
};
|
||||
|
||||
it("retains fixed declarations while requesting only a source-bounded relocation prefix", () => {
|
||||
const bytes = extendedRelocationImage();
|
||||
const prefix = bytes.subarray(0, 64);
|
||||
const evidence = parseMzHeaderEvidence(prefix, bytes.length);
|
||||
expect(evidence.declarations).toEqual(readDosMzDeclarations(prefix));
|
||||
expect(evidence.declarations).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
headerBytes: 4112,
|
||||
relocationCount: 2,
|
||||
relocationTableOffset: 4096,
|
||||
relocationTableEnd: 4104,
|
||||
entryOffset: 0,
|
||||
entrySegment: 0,
|
||||
},
|
||||
});
|
||||
expect(evidence.relocationTableRead).toEqual({ offset: 0, byteLength: 4104 });
|
||||
expect(evidence.dos).toEqual({
|
||||
ok: false,
|
||||
error: "truncated DOS MZ relocation table",
|
||||
});
|
||||
const observed = parseMzHeaderEvidence(bytes.subarray(0, 4104), bytes.length);
|
||||
expect(observed.relocationTableRead).toBeUndefined();
|
||||
expect(observed.dos).toMatchObject({
|
||||
ok: true,
|
||||
value: { relocationCount: 2, headerBytes: 4112 },
|
||||
});
|
||||
});
|
||||
|
||||
it("does not request absent or out-of-bounds relocation records", () => {
|
||||
const zero = extendedRelocationImage();
|
||||
zero.writeUInt16LE(512, 8);
|
||||
zero.writeUInt16LE(0, 6);
|
||||
zero.writeUInt16LE(6000, 24);
|
||||
expect(
|
||||
parseMzHeaderEvidence(zero.subarray(0, 64), zero.length)
|
||||
.relocationTableRead,
|
||||
).toBeUndefined();
|
||||
expect(parseDosMzHeader(zero).ok).toBe(true);
|
||||
const outsideHeader = extendedRelocationImage();
|
||||
outsideHeader.writeUInt16LE(5000, 24);
|
||||
const headerEvidence = parseMzHeaderEvidence(
|
||||
outsideHeader.subarray(0, 64),
|
||||
outsideHeader.length,
|
||||
);
|
||||
expect(headerEvidence.relocationTableRead).toBeUndefined();
|
||||
expect(headerEvidence.dos).toEqual({
|
||||
ok: false,
|
||||
error: "DOS MZ relocation table lies outside its header",
|
||||
});
|
||||
const outsideFile = extendedRelocationImage();
|
||||
outsideFile.writeUInt16LE(1024, 8);
|
||||
outsideFile.writeUInt16LE(9000, 24);
|
||||
expect(
|
||||
parseMzHeaderEvidence(outsideFile.subarray(0, 64), outsideFile.length)
|
||||
.relocationTableRead,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps independent malformed declarations and the established DOS error precedence", () => {
|
||||
const bytes = extendedRelocationImage();
|
||||
bytes.writeUInt16LE(0, 4);
|
||||
bytes.writeUInt16LE(65535, 20);
|
||||
const evidence = parseMzHeaderEvidence(bytes.subarray(0, 64), bytes.length);
|
||||
expect(evidence.declarations).toMatchObject({
|
||||
ok: true,
|
||||
value: { pages: 0, entryOffset: 65535 },
|
||||
});
|
||||
expect(evidence.relocationTableRead?.byteLength).toBe(4104);
|
||||
expect(evidence.dos).toEqual({
|
||||
ok: false,
|
||||
error: "invalid DOS MZ page count or final-page size",
|
||||
});
|
||||
expect(parseDosMzHeader(bytes, bytes.length - 1)).toEqual({
|
||||
ok: false,
|
||||
error: "invalid DOS MZ file length",
|
||||
});
|
||||
bytes.writeUInt16LE(17, 4);
|
||||
expect(parseDosMzHeader(bytes.subarray(0, 64), bytes.length)).toEqual({
|
||||
ok: false,
|
||||
error: "truncated DOS MZ relocation table",
|
||||
});
|
||||
expect(parseDosMzHeader(bytes)).toEqual({
|
||||
ok: false,
|
||||
error: "DOS MZ entry point lies outside its initialized load module",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps declared Windows signatures independent of incomplete DOS relocations", () => {
|
||||
const bytes = extendedRelocationImage();
|
||||
bytes.writeUInt32LE(128, 60);
|
||||
const evidence = parseMzHeaderEvidence(bytes.subarray(0, 64), bytes.length, {
|
||||
offset: 128,
|
||||
bytes: Buffer.from("PE\0\0"),
|
||||
});
|
||||
expect(evidence.windowsHeader).toEqual({
|
||||
state: "declared",
|
||||
signature: "PE",
|
||||
offset: 128,
|
||||
});
|
||||
expect(evidence.dos).toEqual({
|
||||
ok: false,
|
||||
error: "truncated DOS MZ relocation table",
|
||||
});
|
||||
expect(evidence.relocationTableRead?.byteLength).toBe(4104);
|
||||
});
|
||||
|
||||
it("completes captured evidence from the original admitted fixed declarations", () => {
|
||||
const bytes = extendedRelocationImage();
|
||||
const declarations = readDosMzDeclarations(bytes.subarray(0, 64));
|
||||
if (!declarations.ok) throw new Error(declarations.error);
|
||||
expect(dosMzDeclaredImageByteLength(declarations.value)).toEqual({
|
||||
ok: true,
|
||||
value: bytes.length,
|
||||
});
|
||||
const evidence = parseDeclaredMzHeaderEvidence(
|
||||
bytes.subarray(0, 4104),
|
||||
bytes.length,
|
||||
declarations,
|
||||
);
|
||||
expect(evidence.declarations).toBe(declarations);
|
||||
expect(evidence).toEqual(
|
||||
parseMzHeaderEvidence(bytes.subarray(0, 4104), bytes.length),
|
||||
);
|
||||
expect(evidence.dos.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("adds distant Windows signatures while retaining prior DOS observations", () => {
|
||||
const bytes = extendedRelocationImage();
|
||||
bytes.writeUInt32LE(6000, 60);
|
||||
const prefix = bytes.subarray(0, 4104);
|
||||
const evidence = parseMzHeaderEvidence(prefix, bytes.length);
|
||||
expect(evidence.windowsHeader).toEqual({
|
||||
state: "signature-needed",
|
||||
offset: 6000,
|
||||
});
|
||||
const signature = { offset: 6000, bytes: Buffer.from("PE\0\0") };
|
||||
const completed = withMzWindowsHeaderSignature(
|
||||
prefix,
|
||||
bytes.length,
|
||||
evidence,
|
||||
signature,
|
||||
);
|
||||
expect(completed.dos).toBe(evidence.dos);
|
||||
expect(completed.declarations).toBe(evidence.declarations);
|
||||
expect(completed.windowsHeader).toEqual({
|
||||
state: "declared",
|
||||
signature: "PE",
|
||||
offset: 6000,
|
||||
});
|
||||
expect(completed).toEqual(
|
||||
parseMzHeaderEvidence(prefix, bytes.length, signature),
|
||||
);
|
||||
});
|
||||
+134
-21
@@ -1,11 +1,45 @@
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
|
||||
/** The unsigned 16-bit page count bounds the initialized image, not overlays. */
|
||||
export const dosMzImageByteLength = (bytes: Buffer): Result<number, string> => {
|
||||
if (bytes.length < 28 || bytes.toString("latin1", 0, 2) !== "MZ")
|
||||
return err("invalid or truncated DOS MZ header");
|
||||
const lastPageBytes = bytes.readUInt16LE(2);
|
||||
const pages = bytes.readUInt16LE(4);
|
||||
/** Source declarations in the fixed DOS header, independent of load-module validity. */
|
||||
export interface DosMzDeclarations {
|
||||
readonly lastPageBytes: number;
|
||||
readonly pages: number;
|
||||
readonly headerBytes: number;
|
||||
readonly relocationCount: number;
|
||||
readonly relocationTableOffset: number;
|
||||
readonly relocationTableEnd: number;
|
||||
readonly entrySegment: number;
|
||||
readonly entryOffset: number;
|
||||
}
|
||||
|
||||
const fixedDosMzDeclarations = (bytes: Buffer): DosMzDeclarations => {
|
||||
const relocationCount = bytes.readUInt16LE(6);
|
||||
const relocationTableOffset = bytes.readUInt16LE(24);
|
||||
return {
|
||||
lastPageBytes: bytes.readUInt16LE(2),
|
||||
pages: bytes.readUInt16LE(4),
|
||||
headerBytes: bytes.readUInt16LE(8) * 16,
|
||||
relocationCount,
|
||||
relocationTableOffset,
|
||||
relocationTableEnd: relocationTableOffset + relocationCount * 4,
|
||||
entrySegment: bytes.readUInt16LE(22),
|
||||
entryOffset: bytes.readUInt16LE(20),
|
||||
};
|
||||
};
|
||||
|
||||
/** Admit only the fixed source header; malformed page and load declarations remain observable. */
|
||||
export const readDosMzDeclarations = (
|
||||
bytes: Buffer,
|
||||
): Result<DosMzDeclarations, string> =>
|
||||
bytes.length < 28 || bytes.toString("latin1", 0, 2) !== "MZ"
|
||||
? err("invalid or truncated DOS MZ header")
|
||||
: ok(fixedDosMzDeclarations(bytes));
|
||||
|
||||
/** Derive the initialized source extent from already admitted fixed declarations. */
|
||||
export const dosMzDeclaredImageByteLength = ({
|
||||
pages,
|
||||
lastPageBytes,
|
||||
}: DosMzDeclarations): Result<number, string> => {
|
||||
if (pages === 0 || lastPageBytes > 511)
|
||||
return err("invalid DOS MZ page count or final-page size");
|
||||
return ok((pages - 1) * 512 + (lastPageBytes === 0 ? 512 : lastPageBytes));
|
||||
@@ -75,13 +109,23 @@ export const observeMzWindowsHeader = (
|
||||
bytes: Buffer,
|
||||
fileSize = bytes.length,
|
||||
signatureEvidence?: MzWindowsHeaderSignatureEvidence,
|
||||
): MzWindowsHeaderObservation =>
|
||||
observeWindowsHeader(bytes, fileSize, signatureEvidence);
|
||||
|
||||
const observeWindowsHeader = (
|
||||
bytes: Buffer,
|
||||
fileSize: number,
|
||||
signatureEvidence?: MzWindowsHeaderSignatureEvidence,
|
||||
declarations?: DosMzDeclarations,
|
||||
): MzWindowsHeaderObservation => {
|
||||
if (bytes.length < 64)
|
||||
return fileSize >= 64 ? { state: "header-needed" } : { state: "absent" };
|
||||
const headerBytes = bytes.readUInt16LE(8) * 16;
|
||||
const relocationCount = bytes.readUInt16LE(6);
|
||||
const tableStart = bytes.readUInt16LE(24);
|
||||
const tableEnd = tableStart + relocationCount * 4;
|
||||
const {
|
||||
headerBytes,
|
||||
relocationCount,
|
||||
relocationTableOffset: tableStart,
|
||||
relocationTableEnd: tableEnd,
|
||||
} = declarations ?? fixedDosMzDeclarations(bytes);
|
||||
if (
|
||||
headerBytes !== 0 &&
|
||||
relocationCount > 0 &&
|
||||
@@ -159,22 +203,32 @@ const windowsHeaderSignature = (
|
||||
export const parseDosMzHeader = (
|
||||
bytes: Buffer,
|
||||
fileSize = bytes.length,
|
||||
): Result<DosMzHeader, string> =>
|
||||
parseDeclaredDosMzHeader(bytes, fileSize, readDosMzDeclarations(bytes));
|
||||
|
||||
const parseDeclaredDosMzHeader = (
|
||||
bytes: Buffer,
|
||||
fileSize: number,
|
||||
declarations: Result<DosMzDeclarations, string>,
|
||||
): Result<DosMzHeader, string> => {
|
||||
if (bytes.length < 28 || bytes.toString("latin1", 0, 2) !== "MZ")
|
||||
return err("invalid or truncated DOS MZ header");
|
||||
if (!declarations.ok) return declarations;
|
||||
if (!Number.isSafeInteger(fileSize) || fileSize < bytes.length)
|
||||
return err("invalid DOS MZ file length");
|
||||
// DOS structure is parsed independently from the meaning of bytes at 0x3c.
|
||||
const extent = dosMzImageByteLength(bytes);
|
||||
const extent = dosMzDeclaredImageByteLength(declarations.value);
|
||||
if (!extent.ok) return extent;
|
||||
const imageBytes = extent.value;
|
||||
const headerBytes = bytes.readUInt16LE(8) * 16;
|
||||
const {
|
||||
headerBytes,
|
||||
relocationCount,
|
||||
relocationTableOffset,
|
||||
relocationTableEnd: tableEnd,
|
||||
entryOffset,
|
||||
entrySegment,
|
||||
} = declarations.value;
|
||||
if (headerBytes < 28 || headerBytes >= imageBytes)
|
||||
return err("invalid DOS MZ header size or empty load module");
|
||||
if (imageBytes > fileSize) return err("truncated DOS MZ load module");
|
||||
const relocationCount = bytes.readUInt16LE(6);
|
||||
const relocationTableOffset = bytes.readUInt16LE(24);
|
||||
const tableEnd = relocationTableOffset + relocationCount * 4;
|
||||
if (
|
||||
relocationCount > 0 &&
|
||||
(relocationTableOffset < 28 || tableEnd > headerBytes)
|
||||
@@ -191,8 +245,6 @@ export const parseDosMzHeader = (
|
||||
moduleBytes,
|
||||
);
|
||||
if (!relocations.ok) return relocations;
|
||||
const entryOffset = bytes.readUInt16LE(20);
|
||||
const entrySegment = bytes.readUInt16LE(22);
|
||||
if (entrySegment * 16 + entryOffset >= moduleBytes)
|
||||
return err("DOS MZ entry point lies outside its initialized load module");
|
||||
return ok({
|
||||
@@ -210,6 +262,12 @@ export const parseDosMzHeader = (
|
||||
|
||||
/** Preserve DOS structural facts and Windows-pointer evidence independently. */
|
||||
export interface MzHeaderEvidence {
|
||||
readonly declarations: Result<DosMzDeclarations, string>;
|
||||
/** A source-bounded prefix read, independent of later DOS module validity. */
|
||||
readonly relocationTableRead?: {
|
||||
readonly offset: 0;
|
||||
readonly byteLength: number;
|
||||
};
|
||||
readonly dos: Result<DosMzHeader, string>;
|
||||
readonly windowsHeader: MzWindowsHeaderObservation;
|
||||
}
|
||||
@@ -218,9 +276,64 @@ export const parseMzHeaderEvidence = (
|
||||
bytes: Buffer,
|
||||
fileSize = bytes.length,
|
||||
signatureEvidence?: MzWindowsHeaderSignatureEvidence,
|
||||
): MzHeaderEvidence =>
|
||||
parseDeclaredMzHeaderEvidence(
|
||||
bytes,
|
||||
fileSize,
|
||||
readDosMzDeclarations(bytes),
|
||||
signatureEvidence,
|
||||
);
|
||||
|
||||
/** Complete evidence using fixed declarations from the same captured source. */
|
||||
export const parseDeclaredMzHeaderEvidence = (
|
||||
bytes: Buffer,
|
||||
fileSize: number,
|
||||
declarations: Result<DosMzDeclarations, string>,
|
||||
signatureEvidence?: MzWindowsHeaderSignatureEvidence,
|
||||
): MzHeaderEvidence => {
|
||||
const fields = declarations.ok ? declarations.value : undefined;
|
||||
const needed =
|
||||
fields !== undefined &&
|
||||
Number.isSafeInteger(fileSize) &&
|
||||
fileSize >= bytes.length &&
|
||||
fields.relocationCount > 0 &&
|
||||
fields.relocationTableEnd > bytes.length &&
|
||||
fields.relocationTableEnd <= fields.headerBytes &&
|
||||
fields.relocationTableEnd <= fileSize;
|
||||
return {
|
||||
declarations,
|
||||
...(needed
|
||||
? {
|
||||
relocationTableRead: {
|
||||
offset: 0 as const,
|
||||
byteLength: fields.relocationTableEnd,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
dos: parseDeclaredDosMzHeader(bytes, fileSize, declarations),
|
||||
windowsHeader: observeWindowsHeader(
|
||||
bytes,
|
||||
fileSize,
|
||||
signatureEvidence,
|
||||
fields,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/** Add a signature observation without reinterpreting the captured DOS structure. */
|
||||
export const withMzWindowsHeaderSignature = (
|
||||
bytes: Buffer,
|
||||
fileSize: number,
|
||||
evidence: MzHeaderEvidence,
|
||||
signature: MzWindowsHeaderSignatureEvidence,
|
||||
): MzHeaderEvidence => ({
|
||||
dos: parseDosMzHeader(bytes, fileSize),
|
||||
windowsHeader: observeMzWindowsHeader(bytes, fileSize, signatureEvidence),
|
||||
...evidence,
|
||||
windowsHeader: observeWindowsHeader(
|
||||
bytes,
|
||||
fileSize,
|
||||
signature,
|
||||
evidence.declarations.ok ? evidence.declarations.value : undefined,
|
||||
),
|
||||
});
|
||||
|
||||
const readRelocations = (
|
||||
|
||||
@@ -189,3 +189,33 @@ it("preserves exact cancellation after an overlay descriptor read", async () =>
|
||||
await handle.close();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
[0, "invalid DOS MZ page count or final-page size"],
|
||||
[4, "truncated DOS MZ load module"],
|
||||
] as const)(
|
||||
"preserves early extent failure before Windows inspection (pages=%s)",
|
||||
async (pages, error) => {
|
||||
const fixture = fixtureDosLoadImage(Buffer.alloc(1024));
|
||||
fixture.bytes.writeUInt16LE(pages, 4);
|
||||
fixture.bytes.writeUInt32LE(512, 60);
|
||||
fixture.bytes.write("NE", 512, "ascii");
|
||||
const directory = await createTestTempDirectory("rea-mz-extent-");
|
||||
const path = join(directory, "inert.exe");
|
||||
await writeFile(path, fixture.bytes);
|
||||
const handle = await open(path, "r");
|
||||
try {
|
||||
await expect(
|
||||
attestGhidraDosSnapshot(
|
||||
handle,
|
||||
fixture.bytes.length,
|
||||
path,
|
||||
fixture.sha256,
|
||||
fixture.observation,
|
||||
),
|
||||
).resolves.toEqual({ ok: false, error });
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -3,8 +3,10 @@ import type { FileHandle } from "node:fs/promises";
|
||||
import { readArtifactFileChunks } from "../artifacts/readArtifactFileChunks.js";
|
||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import {
|
||||
dosMzImageByteLength,
|
||||
observeMzWindowsHeader,
|
||||
dosMzDeclaredImageByteLength,
|
||||
readDosMzDeclarations,
|
||||
parseDeclaredMzHeaderEvidence,
|
||||
withMzWindowsHeaderSignature,
|
||||
} from "../domain/dosMz.js";
|
||||
import { err } from "../domain/result.js";
|
||||
import type { NativeLoadImageObservation } from "../domain/native/nativeLoadImage.js";
|
||||
@@ -37,7 +39,9 @@ export const attestGhidraDosSnapshot = async (
|
||||
});
|
||||
if (header === undefined || header.length !== headerBytes)
|
||||
throw new RegularFileChangedError(path);
|
||||
const extent = dosMzImageByteLength(header);
|
||||
const declarations = readDosMzDeclarations(header);
|
||||
if (!declarations.ok) return declarations;
|
||||
const extent = dosMzDeclaredImageByteLength(declarations.value);
|
||||
if (!extent.ok) return extent;
|
||||
if (extent.value > fileBytes) return err("truncated DOS MZ load module");
|
||||
// Retain the complete initialized image and the Windows-pointer header only.
|
||||
@@ -57,7 +61,8 @@ export const attestGhidraDosSnapshot = async (
|
||||
}
|
||||
}
|
||||
if (captured !== captureBytes) throw new RegularFileChangedError(path);
|
||||
const windowsHeader = observeMzWindowsHeader(prefix, fileBytes);
|
||||
let evidence = parseDeclaredMzHeaderEvidence(prefix, fileBytes, declarations);
|
||||
const { windowsHeader } = evidence;
|
||||
let signature:
|
||||
| { readonly offset: number; readonly bytes: Buffer }
|
||||
| undefined;
|
||||
@@ -81,7 +86,14 @@ export const attestGhidraDosSnapshot = async (
|
||||
bytes: signatureBytes,
|
||||
};
|
||||
}
|
||||
const prepared = prepareGhidraDosLoadImage(prefix, fileBytes, signature);
|
||||
if (signature !== undefined)
|
||||
evidence = withMzWindowsHeaderSignature(
|
||||
prefix,
|
||||
fileBytes,
|
||||
evidence,
|
||||
signature,
|
||||
);
|
||||
const prepared = prepareGhidraDosLoadImage(prefix, evidence);
|
||||
if (!prepared.ok) return prepared;
|
||||
const originalHash = createHash("sha256").update(prefix);
|
||||
const modifiedHash = createHash("sha256")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { parseMzHeaderEvidence } from "../domain/dosMz.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { dosMz } from "../domain/binaryTarget.fixture.js";
|
||||
import type { NativeLoadImageObservation } from "../domain/native/nativeLoadImage.js";
|
||||
@@ -21,7 +22,7 @@ export const compareDosFixture = (
|
||||
return err(
|
||||
"Ghidra load-image snapshot digest does not match the admitted target",
|
||||
);
|
||||
const image = prepareGhidraDosLoadImage(bytes, bytes.length);
|
||||
const image = prepareGhidraDosLoadImage(bytes, parseMzHeaderEvidence(bytes));
|
||||
if (!image.ok) return image;
|
||||
return attestGhidraDosLoadImage(
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { parseMzHeaderEvidence } from "../domain/dosMz.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
@@ -22,7 +23,9 @@ it("accepts DOS header padding after a complete relocation table", () => {
|
||||
bytes.writeUInt16LE(4, 8);
|
||||
bytes.writeUInt16LE(28, 24);
|
||||
bytes.write("ABCD", 60, "ascii");
|
||||
expect(prepareGhidraDosLoadImage(bytes, bytes.length)).toMatchObject({
|
||||
expect(
|
||||
prepareGhidraDosLoadImage(bytes, parseMzHeaderEvidence(bytes)),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
header: {
|
||||
@@ -38,20 +41,25 @@ it("accepts DOS header padding after a complete relocation table", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not interpret an unresolved Windows-header candidate as a DOS image", () => {
|
||||
const bytes = Buffer.alloc(128);
|
||||
bytes.write("MZ", 0, "ascii");
|
||||
bytes.writeUInt16LE(128, 2);
|
||||
bytes.writeUInt16LE(1, 4);
|
||||
bytes.writeUInt16LE(0, 6);
|
||||
bytes.writeUInt16LE(4, 8);
|
||||
bytes.writeUInt16LE(28, 24);
|
||||
bytes.write("ABCD", 60, "ascii");
|
||||
expect(prepareGhidraDosLoadImage(bytes, bytes.length)).toEqual({
|
||||
ok: false,
|
||||
error: "MZ Windows new-header candidate is unresolved",
|
||||
});
|
||||
});
|
||||
it.each([0, 1])(
|
||||
"prioritizes an unresolved Windows-header candidate over DOS page validity (pages=%s)",
|
||||
(pages) => {
|
||||
const bytes = Buffer.alloc(128);
|
||||
bytes.write("MZ", 0, "ascii");
|
||||
bytes.writeUInt16LE(128, 2);
|
||||
bytes.writeUInt16LE(pages, 4);
|
||||
bytes.writeUInt16LE(0, 6);
|
||||
bytes.writeUInt16LE(4, 8);
|
||||
bytes.writeUInt16LE(28, 24);
|
||||
bytes.write("ABCD", 60, "ascii");
|
||||
expect(
|
||||
prepareGhidraDosLoadImage(bytes, parseMzHeaderEvidence(bytes)),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
error: "MZ Windows new-header candidate is unresolved",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const damage: ReadonlyArray<
|
||||
readonly [string, (observation: NativeLoadImageObservation) => void]
|
||||
@@ -505,7 +513,10 @@ it("applies duplicate aliases and overlapping DOS fixups in declared order with
|
||||
bytes.writeUInt16LE(0, 42);
|
||||
bytes.set([1, 0, 2], 80);
|
||||
const before = Buffer.from(bytes);
|
||||
const prepared = prepareGhidraDosLoadImage(bytes, bytes.length);
|
||||
const prepared = prepareGhidraDosLoadImage(
|
||||
bytes,
|
||||
parseMzHeaderEvidence(bytes),
|
||||
);
|
||||
if (!prepared.ok) throw new Error(prepared.error);
|
||||
expect(bytes.equals(before)).toBe(true);
|
||||
expect(
|
||||
|
||||
@@ -5,11 +5,7 @@ import {
|
||||
type NativeLoadImageCheck,
|
||||
type NativeLoadImageObservation,
|
||||
} from "../domain/native/nativeLoadImage.js";
|
||||
import {
|
||||
parseMzHeaderEvidence,
|
||||
type DosMzHeader,
|
||||
type MzWindowsHeaderSignatureEvidence,
|
||||
} from "../domain/dosMz.js";
|
||||
import type { DosMzHeader, MzHeaderEvidence } from "../domain/dosMz.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { validateDosComLength } from "../domain/dosCom.js";
|
||||
@@ -58,10 +54,8 @@ export type GhidraDosImage = {
|
||||
/** Prepare initialized bytes and relocation facts without copying overlays. */
|
||||
export const prepareGhidraDosLoadImage = (
|
||||
bytes: Buffer,
|
||||
fileBytes: number,
|
||||
signatureAtOffset?: MzWindowsHeaderSignatureEvidence,
|
||||
evidence: MzHeaderEvidence,
|
||||
): Result<GhidraDosImage, string> => {
|
||||
const evidence = parseMzHeaderEvidence(bytes, fileBytes, signatureAtOffset);
|
||||
if (evidence.windowsHeader.state === "candidate")
|
||||
return err("MZ Windows new-header candidate is unresolved");
|
||||
if (evidence.windowsHeader.state === "header-needed")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import {
|
||||
open,
|
||||
type FileHandle,
|
||||
chmod,
|
||||
mkdir,
|
||||
readFile,
|
||||
@@ -12,7 +13,7 @@ import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { buildBinary } from "plist";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
||||
import {
|
||||
@@ -1418,3 +1419,62 @@ describe("binary target I/O: PE CodeView header ranges", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("does not read a relocation table offset when DOS declares zero records", async () => {
|
||||
const directory = await createTestTempDirectory("rea-empty-dos-relocations-");
|
||||
const path = join(directory, "legacy.exe");
|
||||
const bytes = dosMz(8192);
|
||||
bytes.writeUInt16LE(257, 8);
|
||||
bytes.writeUInt16LE(4104, 24);
|
||||
bytes.writeUInt32LE(0, 60);
|
||||
await writeFile(path, bytes);
|
||||
const probe = await open(path, "r");
|
||||
const prototype = Object.getPrototypeOf(probe) as FileHandle;
|
||||
await probe.close();
|
||||
const reads = vi.spyOn(prototype, "read");
|
||||
try {
|
||||
expect(await parseBinaryTarget(path)).toMatchObject({
|
||||
ok: true,
|
||||
value: { format: "dos-mz", architecture: "x86" },
|
||||
});
|
||||
const lengths = reads.mock.calls.map(
|
||||
(args) => (args as readonly unknown[])[2],
|
||||
);
|
||||
expect(lengths).toContain(4096);
|
||||
expect(lengths).not.toContain(4104);
|
||||
} finally {
|
||||
reads.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("recognizes Windows metadata independently of invalid DOS load declarations", async () => {
|
||||
const directory = await createTestTempDirectory("rea-invalid-dos-valid-pe-");
|
||||
const path = join(directory, "windows.exe");
|
||||
const bytes = pe(0x8664, 8192);
|
||||
bytes.writeUInt16LE(0, 4);
|
||||
bytes.writeUInt16LE(1010, 6);
|
||||
bytes.writeUInt16LE(257, 8);
|
||||
bytes.writeUInt16LE(64, 24);
|
||||
await writeFile(path, bytes);
|
||||
expect(await parseBinaryTarget(path)).toMatchObject({
|
||||
ok: true,
|
||||
value: { format: "pe", architecture: "x86_64" },
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses a declared DOS relocation header that is not present in the file", async () => {
|
||||
const directory = await createTestTempDirectory(
|
||||
"rea-truncated-dos-relocations-",
|
||||
);
|
||||
const path = join(directory, "legacy.exe");
|
||||
const bytes = dosMz(8192);
|
||||
bytes.writeUInt16LE(1010, 6);
|
||||
bytes.writeUInt16LE(257, 8);
|
||||
bytes.writeUInt16LE(64, 24);
|
||||
bytes.writeUInt32LE(0, 60);
|
||||
await writeFile(path, bytes.subarray(0, 4096));
|
||||
expect(await parseBinaryTarget(path)).toMatchObject({
|
||||
ok: false,
|
||||
error: { reason: "truncated DOS MZ load module" },
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user