Files
rea/src/native/pe/PeLoadConfig.fixture.ts
T

383 lines
13 KiB
TypeScript

import { loadConfigFieldLayout } from "../../domain/native/peLoadConfig.js";
/** Authored fixture directory sizes through GuardMemcpyFunctionPointer. */
const LOAD_CONFIG_KNOWN_STRUCT_BYTES = { 32: 192, 64: 320 } as const;
/** File and RVA layout shared by the synthetic PE32 and PE32+ images. */
export const PE_LOAD_CONFIG_FIXTURE = {
dataRva: 0x1000,
dataFile: 0x600,
dataBytes: 0x1000,
textRva: 0x2000,
textFile: 0x1600,
textBytes: 0x200,
tlsRva: 0x1000,
loadConfigRva: 0x1040,
callbackArrayRva: 0x1200,
indexRva: 0x1300,
cookieRva: 0x1310,
sehTableRva: 0x1320,
cfTableRva: 0x1330,
iatTableRva: 0x1340,
longjmpTableRva: 0x1350,
ehTableRva: 0x1360,
rawDataRva: 0x1400,
rawDataSize: 0x80,
cfCheckRva: 0x2040,
cfDispatchRva: 0x2050,
xfgRva: 0x2060,
outsideRva: 0x9000,
zeroFillRva: 0x2200,
callbackRvas: [0x2000, 0x2010, 0x9000],
chpe: 0x33333333,
dynamicReloc: 0x22222222,
dynamicRelocOffset: 0x40,
dynamicRelocSection: 1,
guardFlags: 0x500,
sehCount: 2,
ignoredSeh: 0x44444444,
cfCount: 3,
iatCount: 1,
longjmpCount: 0,
ehCount: 1,
zeroFill: 0x20,
characteristics: 4 << 20,
} as const;
const FILE_BYTES =
PE_LOAD_CONFIG_FIXTURE.textFile + PE_LOAD_CONFIG_FIXTURE.textBytes;
export type PeLoadConfigFixtureMalformed =
| "tls-unmapped"
| "tls-truncated"
| "tls-half"
| "load-config-oversize"
| "load-config-truncated"
| "load-config-half"
| "raw-data-inverted"
| "callback-unterminated"
| "ambiguous-section";
export interface PeLoadConfigFixtureOptions {
readonly bits?: 32 | 64;
readonly tls?: boolean;
readonly loadConfig?: boolean;
/** Declared IMAGE_LOAD_CONFIG_DIRECTORY.Size. Defaults to the known struct. */
readonly loadConfigSize?: number;
/** Data-directory size. Defaults to Size; x86 images may declare less. */
readonly loadConfigDirectorySize?: number;
readonly callbacks?: "present" | "empty" | "absent";
/** In-section callbacks when `callbacks` is present. Defaults to the rich trio. */
readonly callbackCount?: number;
readonly zeroFillCallback?: boolean;
readonly securityCookie?: "mapped" | "zero";
readonly directoryCount?: number;
readonly malformed?: PeLoadConfigFixtureMalformed;
}
const imageBaseFor = (bits: 32 | 64): bigint =>
bits === 64 ? 0x180000000n : 0x10000000n;
/** Synthetic PE32/PE32+ image with a TLS directory and a sized load config. */
export const peLoadConfigImage = (
options: PeLoadConfigFixtureOptions = {},
): Buffer => {
const bits = options.bits ?? 64;
const wide = bits === 64;
const layout = PE_LOAD_CONFIG_FIXTURE;
const bytes = Buffer.alloc(FILE_BYTES);
const base = imageBaseFor(bits);
const pointerWidth = wide ? 8 : 4;
const directoriesAt = wide ? 112 : 96;
const directoryCount = options.directoryCount ?? 16;
const optionalSize = directoriesAt + 16 * 8;
bytes.write("MZ", 0, "ascii");
bytes.writeUInt32LE(128, 60);
bytes.write("PE\0\0", 128, "ascii");
bytes.writeUInt16LE(wide ? 0x8664 : 0x14c, 132);
bytes.writeUInt16LE(2, 134);
bytes.writeUInt16LE(optionalSize, 148);
const optional = 152;
bytes.writeUInt16LE(wide ? 0x20b : 0x10b, optional);
if (wide) bytes.writeBigUInt64LE(base, optional + 24);
else bytes.writeUInt32LE(Number(base), optional + 28);
bytes.writeUInt32LE(0x1000, optional + 32);
bytes.writeUInt32LE(0x200, optional + 36);
bytes.writeUInt32LE(layout.dataFile, optional + 60);
bytes.writeUInt32LE(directoryCount, optional + directoriesAt - 4);
const section = optional + optionalSize;
const textVirtual = options.zeroFillCallback ? 0x400 : layout.textBytes;
writeSection(bytes, section, ".data", {
virtualSize: layout.dataBytes,
virtualAddress: layout.dataRva,
rawSize: layout.dataBytes,
rawOffset: layout.dataFile,
characteristics: 0xc0000040,
});
writeSection(bytes, section + 40, ".text", {
virtualSize: textVirtual,
virtualAddress:
options.malformed === "ambiguous-section"
? layout.dataRva
: layout.textRva,
rawSize: layout.textBytes,
rawOffset: layout.textFile,
characteristics: 0x60000020,
});
const at = (rva: number): number => rva - layout.dataRva + layout.dataFile;
const writeVa = (fileOffset: number, va: bigint): void => {
if (wide) bytes.writeBigUInt64LE(va, fileOffset);
else bytes.writeUInt32LE(Number(va), fileOffset);
};
const vaOf = (rva: number): bigint => base + BigInt(rva);
if (options.tls !== false) {
const tlsSize = options.malformed === "tls-truncated" ? 8 : wide ? 40 : 24;
const tlsRva =
options.malformed === "tls-unmapped" ? 0xffff0000 : layout.tlsRva;
const tlsDirectorySize = tlsSize;
const tlsDirectoryRva = options.malformed === "tls-half" ? 0 : tlsRva;
if (
options.malformed !== "tls-unmapped" &&
options.malformed !== "tls-half"
) {
const tlsAt = at(layout.tlsRva);
const rawStart =
options.malformed === "raw-data-inverted"
? layout.rawDataRva + layout.rawDataSize
: layout.rawDataRva;
const rawEnd =
options.malformed === "raw-data-inverted"
? layout.rawDataRva
: layout.rawDataRva + layout.rawDataSize;
writeVa(tlsAt, vaOf(rawStart));
writeVa(tlsAt + pointerWidth, vaOf(rawEnd));
writeVa(tlsAt + pointerWidth * 2, vaOf(layout.indexRva));
const callbacks = options.callbacks ?? "present";
let callbackVa = 0n;
if (options.malformed === "callback-unterminated") {
const slotRva = layout.textRva + layout.textBytes - pointerWidth;
callbackVa = vaOf(slotRva);
writeVa(
layout.textFile + layout.textBytes - pointerWidth,
vaOf(layout.textRva),
);
} else if (callbacks === "present" || options.zeroFillCallback === true) {
const targets =
options.zeroFillCallback === true
? [layout.zeroFillRva]
: options.callbackCount === undefined
? [...layout.callbackRvas]
: Array.from(
{ length: options.callbackCount },
() => layout.textRva,
);
callbackVa = vaOf(layout.callbackArrayRva);
targets.forEach((rva, index) => {
writeVa(
at(layout.callbackArrayRva) + index * pointerWidth,
vaOf(rva),
);
});
} else if (callbacks === "empty") {
callbackVa = vaOf(layout.callbackArrayRva);
}
writeVa(tlsAt + pointerWidth * 3, callbackVa);
bytes.writeUInt32LE(layout.zeroFill, tlsAt + pointerWidth * 4);
bytes.writeUInt32LE(layout.characteristics, tlsAt + pointerWidth * 4 + 4);
bytes[layout.textFile] = 0xc3;
bytes[layout.textFile + 0x10] = 0xc3;
}
writeDirectory(
bytes,
optional + directoriesAt,
9,
tlsDirectoryRva,
options.malformed === "tls-unmapped" ? 40 : tlsDirectorySize,
);
}
writeLoadConfig(bytes, options, {
bits,
base,
directories: optional + directoriesAt,
});
return bytes;
};
const writeSection = (
bytes: Buffer,
offset: number,
name: string,
section: {
readonly virtualSize: number;
readonly virtualAddress: number;
readonly rawSize: number;
readonly rawOffset: number;
readonly characteristics: number;
},
): void => {
bytes.write(name, offset, "ascii");
bytes.writeUInt32LE(section.virtualSize, offset + 8);
bytes.writeUInt32LE(section.virtualAddress, offset + 12);
bytes.writeUInt32LE(section.rawSize, offset + 16);
bytes.writeUInt32LE(section.rawOffset, offset + 20);
bytes.writeUInt32LE(section.characteristics, offset + 36);
};
const writeDirectory = (
bytes: Buffer,
directories: number,
index: number,
rva: number,
size: number,
): void => {
bytes.writeUInt32LE(rva, directories + index * 8);
bytes.writeUInt32LE(size, directories + index * 8 + 4);
};
const writeLoadConfig = (
bytes: Buffer,
options: PeLoadConfigFixtureOptions,
{
bits,
base,
directories,
}: { bits: 32 | 64; base: bigint; directories: number },
): void => {
const wide = bits === 64;
const layout = PE_LOAD_CONFIG_FIXTURE;
const at = (rva: number): number => rva - layout.dataRva + layout.dataFile;
const vaOf = (rva: number): bigint => base + BigInt(rva);
if (options.loadConfig !== false) {
const known = LOAD_CONFIG_KNOWN_STRUCT_BYTES[bits];
const declaredSize =
options.malformed === "load-config-oversize"
? 0x10000
: (options.loadConfigSize ?? known);
const directorySize =
options.malformed === "load-config-oversize"
? 16
: options.malformed === "load-config-truncated"
? 2
: options.malformed === "load-config-half"
? 0
: (options.loadConfigDirectorySize ?? declaredSize);
const directoryRva = layout.loadConfigRva;
if (directorySize >= 4 && options.malformed !== "load-config-half") {
const configAt = at(layout.loadConfigRva);
bytes.writeUInt32LE(declaredSize, configAt);
const writeField = (
name: Parameters<typeof loadConfigFieldLayout>[1],
value: bigint,
): void => {
const field = loadConfigFieldLayout(bits, name);
const written = Math.max(directorySize, Math.min(declaredSize, known));
if (field.offset + field.width > written) return;
const atField = configAt + field.offset;
if (field.width === 8) bytes.writeBigUInt64LE(value, atField);
else if (field.width === 4) bytes.writeUInt32LE(Number(value), atField);
else bytes.writeUInt16LE(Number(value), atField);
};
const cookie =
options.securityCookie === "zero" ? 0n : vaOf(layout.cookieRva);
writeField("security_cookie", cookie);
writeField("se_handler_table", vaOf(layout.sehTableRva));
writeField("se_handler_count", BigInt(layout.sehCount));
if (wide) {
writeField("se_handler_table", BigInt(layout.ignoredSeh));
writeField("se_handler_count", 7n);
}
writeField("guard_cf_check_function_pointer", vaOf(layout.cfCheckRva));
writeField(
"guard_cf_dispatch_function_pointer",
vaOf(layout.cfDispatchRva),
);
writeField("guard_cf_function_table", vaOf(layout.cfTableRva));
writeField("guard_cf_function_count", BigInt(layout.cfCount));
writeField("guard_flags", BigInt(layout.guardFlags));
writeField(
"guard_address_taken_iat_entry_table",
vaOf(layout.iatTableRva),
);
writeField(
"guard_address_taken_iat_entry_count",
BigInt(layout.iatCount),
);
writeField("guard_long_jump_target_table", vaOf(layout.longjmpTableRva));
writeField("guard_long_jump_target_count", BigInt(layout.longjmpCount));
writeField("dynamic_value_reloc_table", BigInt(layout.dynamicReloc));
writeField("chpe_metadata_pointer", BigInt(layout.chpe));
writeField(
"dynamic_value_reloc_table_offset",
BigInt(layout.dynamicRelocOffset),
);
writeField(
"dynamic_value_reloc_table_section",
BigInt(layout.dynamicRelocSection),
);
writeField("guard_eh_continuation_table", vaOf(layout.ehTableRva));
writeField("guard_eh_continuation_count", BigInt(layout.ehCount));
writeField("guard_xfg_check_function_pointer", vaOf(layout.xfgRva));
}
writeDirectory(bytes, directories, 10, directoryRva, directorySize);
}
};
/**
* PE32+ image with `sectionCount` sections and `callbackCount` TLS callbacks.
* `.data` holds the TLS directory and callback array; every other section is
* virtual-only, and every callback targets the last one.
*/
export const peManySectionsImage = (
sectionCount: number,
callbackCount: number,
): Buffer => {
const optional = 152;
const optionalSize = 112 + 16 * 8;
const table = optional + optionalSize;
const headers = Math.ceil((table + sectionCount * 40) / 0x200) * 0x200;
// Sections start past the header mapping so it cannot overlap `.data`.
const dataRva = Math.ceil(headers / 0x1000) * 0x1000;
const dataBytes =
Math.ceil(((callbackCount + 1) * 8 + 0x100) / 0x200) * 0x200;
const virtualRva = dataRva + Math.ceil(dataBytes / 0x1000) * 0x1000;
const base = 0x180000000n;
const bytes = Buffer.alloc(headers + dataBytes);
bytes.write("MZ", 0, "ascii");
bytes.writeUInt32LE(128, 60);
bytes.write("PE\0\0", 128, "ascii");
bytes.writeUInt16LE(0x8664, 132);
bytes.writeUInt16LE(sectionCount, 134);
bytes.writeUInt16LE(optionalSize, 148);
bytes.writeUInt16LE(0x20b, optional);
bytes.writeBigUInt64LE(base, optional + 24);
bytes.writeUInt32LE(0x1000, optional + 32);
bytes.writeUInt32LE(0x200, optional + 36);
bytes.writeUInt32LE(headers, optional + 60);
bytes.writeUInt32LE(16, optional + 108);
writeSection(bytes, table, ".data", {
virtualSize: dataBytes,
virtualAddress: dataRva,
rawSize: dataBytes,
rawOffset: headers,
characteristics: 0xc0000040,
});
for (let index = 1; index < sectionCount; index++)
writeSection(bytes, table + index * 40, ".v", {
virtualSize: 0x1000,
virtualAddress: virtualRva + (index - 1) * 0x1000,
rawSize: 0,
rawOffset: 0,
characteristics: 0x60000020,
});
const target = base + BigInt(virtualRva + (sectionCount - 2) * 0x1000);
const callbacksRva = dataRva + 0x100;
bytes.writeBigUInt64LE(base + BigInt(callbacksRva), headers + 24);
for (let index = 0; index < callbackCount; index++)
bytes.writeBigUInt64LE(target, headers + 0x100 + index * 8);
writeDirectory(bytes, optional + 112, 9, dataRva, 40);
return bytes;
};