fix(windows): validate declared PE header identity

This commit is contained in:
morluto committed 2026-10-27 11:45:25 +08:00
1 parent a116f2a7ae
commit a486cfe518
3 files changed
+56 -12

No files matched your search

+20 -8
View File
@@ -8,11 +8,14 @@ export interface PeDataDirectory {
readonly offset: number;
}
/** Structural facts copied from one declared PE optional header. */
export interface PeOptionalHeaderFixedFields {
export interface PeOptionalHeaderIdentity {
readonly magic: 0x10b | 0x20b;
readonly bits: 32 | 64;
readonly format: "pe32" | "pe32-plus";
}
/** Structural facts copied from one declared PE optional header. */
export interface PeOptionalHeaderFixedFields extends PeOptionalHeaderIdentity {
readonly imageBase: bigint;
readonly sectionAlignment: number;
readonly fileAlignment: number;
@@ -37,10 +40,10 @@ export interface PeOptionalHeaderFailure {
readonly message: string;
}
/** Decode mapping fields without requiring a directory count or table. */
export const readPeOptionalHeaderFixedFields = (
/** Decode format identity without requiring mapping fields or directories. */
export const readPeOptionalHeaderIdentity = (
bytes: Uint8Array,
): Result<PeOptionalHeaderFixedFields, PeOptionalHeaderFailure> => {
): Result<PeOptionalHeaderIdentity, PeOptionalHeaderFailure> => {
if (bytes.byteLength < 2)
return err({
kind: "malformed",
@@ -56,6 +59,16 @@ export const readPeOptionalHeaderFixedFields = (
message: "Only PE32 and PE32+ image optional headers are supported",
});
const bits = magic === 0x20b ? 64 : 32;
return ok({ magic, bits, format: bits === 64 ? "pe32-plus" : "pe32" });
};
/** Decode mapping fields without requiring a directory count or table. */
export const readPeOptionalHeaderFixedFields = (
bytes: Uint8Array,
): Result<PeOptionalHeaderFixedFields, PeOptionalHeaderFailure> => {
const identity = readPeOptionalHeaderIdentity(bytes);
if (!identity.ok) return identity;
const { bits } = identity.value;
const directoryOffset = bits === 64 ? 112 : 96;
if (bytes.byteLength < 64)
return err({
@@ -63,10 +76,9 @@ export const readPeOptionalHeaderFixedFields = (
reason: "truncated-fixed-fields",
message: "truncated PE optional header",
});
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
return ok({
magic,
bits,
format: bits === 64 ? "pe32-plus" : "pe32",
...identity.value,
imageBase:
bits === 64
? view.getBigUint64(24, true)
+25 -1
View File
@@ -7,11 +7,12 @@ import {
} from "./windowsNativeArtifact.js";
const fixture = () => {
const bytes = Buffer.alloc(128);
const bytes = Buffer.alloc(152);
bytes.write("MZ");
bytes.writeUInt32LE(64, 0x3c);
bytes.write("PE\0\0", 64, "ascii");
bytes.writeUInt16LE(0x8664, 68);
bytes.writeUInt16LE(64, 84);
bytes.writeUInt16LE(0x20b, 88);
const manifest = {
packageVersion: "1.0.0",
@@ -116,3 +117,26 @@ it.each(["offset", "signature", "machine", "optional magic"])(
});
},
);
it.each([0, 1, 65, 0xffff])(
"rejects an unavailable declared optional header of %i bytes",
(length) => {
const { bytes } = fixture();
bytes.writeUInt16LE(length, 84);
expect(createWindowsNativeManifest("1.0.0", bytes)).toMatchObject({
ok: false,
error: { message: expect.stringContaining("PE32+") },
});
},
);
it.each([2, 32, 63, 64])(
"admits PE32+ identity in a present declared header of %i bytes",
(length) => {
const { bytes } = fixture();
bytes.writeUInt16LE(length, 84);
expect(createWindowsNativeManifest("1.0.0", bytes)).toMatchObject({
ok: true,
});
},
);
+11 -3
View File
@@ -1,6 +1,7 @@
import { observeMzWindowsHeader } from "./dosMz.js";
import { createHash } from "node:crypto";
import { readPeCoffHeader } from "./peCoffHeader.js";
import { readPeOptionalHeaderIdentity } from "./peOptionalHeader.js";
import { err, ok, type Result } from "./result.js";
/** Fixed compatibility contract shared by the artifact producer and readers. */
@@ -99,14 +100,21 @@ const validateWindowsX64Artifact = (
),
);
const offset = observed.offset;
if (offset > bytes.length - 26 || bytes.readUInt16LE(offset + 24) !== 0x20b)
const coff = readPeCoffHeader(bytes.subarray(offset + 4, offset + 24));
if (
!coff.ok ||
coff.value.machine !== 0x8664 ||
coff.value.optionalHeaderSize > bytes.length - offset - 24
)
return err(
new WindowsNativeArtifactError(
"Windows native artifact is not a Windows x64 PE32+ image",
),
);
const coff = readPeCoffHeader(bytes.subarray(offset + 4, offset + 24));
if (!coff.ok || coff.value.machine !== 0x8664)
const optional = readPeOptionalHeaderIdentity(
bytes.subarray(offset + 24, offset + 24 + coff.value.optionalHeaderSize),
);
if (!optional.ok || optional.value.bits !== 64)
return err(
new WindowsNativeArtifactError(
"Windows native artifact is not a Windows x64 PE32+ image",