rea/tests/fixtures/managedPe.ts

422 lines
12 KiB
TypeScript

interface ManagedPeFixtureOptions {
readonly cliFlags?: number;
readonly corruptMetadataSignature?: boolean;
readonly fieldName?: string;
readonly fieldSignature?: Buffer;
readonly ilBody?: Buffer;
readonly metadataValidMaskExtra?: bigint;
readonly methodName?: string;
readonly methodSignature?: Buffer;
readonly mvid?: Buffer;
readonly pinvoke?: {
readonly importName?: string;
readonly mappingFlags?: number;
readonly moduleName?: string;
};
readonly readyToRun?: boolean;
readonly references?: readonly string[];
readonly resourceData?: Buffer;
readonly targetFramework?: string;
readonly typeName?: string;
readonly typeNamespace?: string;
}
const textEncoder = new TextEncoder();
const align4 = (value: number): number => (value + 3) & ~3;
const cString = (value: string): Buffer => Buffer.from(`${value}\0`, "utf8");
class StringHeap {
readonly #chunks: Buffer[] = [Buffer.from([0])];
#size = 1;
add(value: string): number {
if (value.length === 0) return 0;
const index = this.#size;
const chunk = cString(value);
this.#chunks.push(chunk);
this.#size += chunk.length;
return index;
}
toBuffer(): Buffer {
return Buffer.concat(this.#chunks);
}
}
class BlobHeap {
readonly #chunks: Buffer[] = [Buffer.from([0])];
#size = 1;
add(value: Buffer): number {
if (value.length === 0) return 0;
if (value.length > 0x7f)
throw new RangeError("Managed PE fixture blob is too large");
const index = this.#size;
this.#chunks.push(Buffer.from([value.length]), value);
this.#size += value.length + 1;
return index;
}
toBuffer(): Buffer {
return Buffer.concat(this.#chunks);
}
}
const DEFAULT_MVID = Buffer.from([
0x33, 0x22, 0x11, 0x00, 0x55, 0x44, 0x77, 0x66, 0x88, 0x99, 0xaa, 0xbb, 0xcc,
0xdd, 0xee, 0xff,
]);
const guidHeap = (mvid: Buffer = DEFAULT_MVID): Buffer => {
if (mvid.length !== 16)
throw new RangeError("Managed PE fixture MVID must be 16 bytes");
return Buffer.from(mvid);
};
const u16 = (value: number): Buffer => {
const bytes = Buffer.alloc(2);
bytes.writeUInt16LE(value, 0);
return bytes;
};
const u32 = (value: number): Buffer => {
const bytes = Buffer.alloc(4);
bytes.writeUInt32LE(value, 0);
return bytes;
};
const moduleRow = (name: number): Buffer =>
Buffer.concat([u16(0), u16(name), u16(1), u16(0), u16(0)]);
const typeRefRow = (name: number, namespace: number): Buffer =>
Buffer.concat([u16(0), u16(name), u16(namespace)]);
const typeDefRow = (
name: number,
namespace: number,
fieldList: number,
methodList: number,
): Buffer =>
Buffer.concat([
u32(0x0010_0001),
u16(name),
u16(namespace),
u16((1 << 2) | 1),
u16(fieldList),
u16(methodList),
]);
const fieldRow = (name: number, signature: number): Buffer =>
Buffer.concat([u16(0x0001), u16(name), u16(signature)]);
const methodDefRow = (
name: number,
signature: number,
rva = 0,
flags = 0x0016,
implFlags = 0,
): Buffer =>
Buffer.concat([
u32(rva),
u16(implFlags),
u16(flags),
u16(name),
u16(signature),
u16(0),
]);
const memberRefRow = (name: number, signature: number): Buffer =>
Buffer.concat([u16((1 << 3) | 1), u16(name), u16(signature)]);
const moduleRefRow = (name: number): Buffer => Buffer.concat([u16(name)]);
const implMapRow = (
importName: number,
importScope: number,
mappingFlags: number,
): Buffer =>
Buffer.concat([
u16(mappingFlags),
u16((1 << 1) | 1),
u16(importName),
u16(importScope),
]);
const assemblyRow = (name: number): Buffer =>
Buffer.concat([
u32(0x0000_8004),
u16(1),
u16(2),
u16(3),
u16(4),
u32(0),
u16(0),
u16(name),
u16(0),
]);
const assemblyRefRow = (name: number, keyOrToken: number): Buffer =>
Buffer.concat([
u16(8),
u16(0),
u16(0),
u16(0),
u32(0),
u16(keyOrToken),
u16(name),
u16(0),
u16(0),
]);
const customAttributeRow = (value: number): Buffer =>
Buffer.concat([u16((1 << 5) | 14), u16((1 << 3) | 3), u16(value)]);
const manifestResourceRow = (name: number): Buffer =>
Buffer.concat([u32(0), u32(2), u16(name), u16(0)]);
const fixedStringAttributeBlob = (value: string): Buffer => {
const bytes = Buffer.from(textEncoder.encode(value));
if (bytes.length > 0x7f)
throw new RangeError("Managed PE fixture target framework is too long");
return Buffer.concat([Buffer.from([1, 0, bytes.length]), bytes, u16(0)]);
};
const metadataStreamHeader = (
relativeOffset: number,
size: number,
name: string,
): Buffer => {
const named = cString(name);
const paddedName = Buffer.alloc(align4(named.length));
named.copy(paddedName);
return Buffer.concat([u32(relativeOffset), u32(size), paddedName]);
};
const metadataRoot = (
tables: Buffer,
strings: Buffer,
guid: Buffer,
blob: Buffer,
): Buffer => {
const version = Buffer.from("v4.0.30319\0\0", "utf8");
const header = Buffer.concat([
u32(0x424a_5342),
u16(1),
u16(1),
u32(0),
u32(version.length),
version,
u16(0),
u16(4),
]);
const streamHeaderSize =
metadataStreamHeader(0, 0, "#~").length +
metadataStreamHeader(0, 0, "#Strings").length +
metadataStreamHeader(0, 0, "#GUID").length +
metadataStreamHeader(0, 0, "#Blob").length;
let offset = align4(header.length + streamHeaderSize);
const tablesOffset = offset;
offset = align4(offset + tables.length);
const stringsOffset = offset;
offset = align4(offset + strings.length);
const guidOffset = offset;
offset = align4(offset + guid.length);
const blobOffset = offset;
const headers = Buffer.concat([
metadataStreamHeader(tablesOffset, tables.length, "#~"),
metadataStreamHeader(stringsOffset, strings.length, "#Strings"),
metadataStreamHeader(guidOffset, guid.length, "#GUID"),
metadataStreamHeader(blobOffset, blob.length, "#Blob"),
]);
const root = Buffer.alloc(align4(blobOffset + blob.length));
header.copy(root, 0);
headers.copy(root, header.length);
tables.copy(root, tablesOffset);
strings.copy(root, stringsOffset);
guid.copy(root, guidOffset);
blob.copy(root, blobOffset);
return root;
};
const tablesStream = (
rows: ReadonlyMap<number, readonly Buffer[]>,
extraValidMask: bigint,
): Buffer => {
let valid = extraValidMask;
for (const table of rows.keys()) valid |= 1n << BigInt(table);
const header = Buffer.alloc(24);
header.writeUInt8(2, 4);
header.writeUInt8(1, 7);
header.writeBigUInt64LE(valid, 8);
const counts: Buffer[] = [];
for (let index = 0; index <= 44; index += 1) {
const tableRows = rows.get(index);
if (tableRows === undefined) continue;
counts.push(u32(tableRows.length));
}
const data: Buffer[] = [];
for (let index = 0; index <= 44; index += 1) {
const tableRows = rows.get(index);
if (tableRows === undefined) continue;
data.push(...tableRows);
}
return Buffer.concat([header, ...counts, ...data]);
};
/** Build a source-owned PE32/CLI fixture without committing compiled binaries. */
export const buildManagedPeFixture = (
options: ManagedPeFixtureOptions = {},
): Buffer => {
const strings = new StringHeap();
const blobs = new BlobHeap();
const moduleName = strings.add("Fixture.dll");
const assemblyName = strings.add("Fixture.Managed");
const attributeName = strings.add("TargetFrameworkAttribute");
const attributeNamespace = strings.add("System.Runtime.Versioning");
const constructorName = strings.add(".ctor");
const typeName = strings.add(options.typeName ?? "Program");
const typeNamespace = strings.add(options.typeNamespace ?? "Fixture");
const fieldName = strings.add(options.fieldName ?? "counter");
const methodName = strings.add(options.methodName ?? "Main");
const pinvokeModuleName =
options.pinvoke === undefined
? null
: strings.add(options.pinvoke.moduleName ?? "user32.dll");
const pinvokeImportName =
options.pinvoke === undefined
? null
: strings.add(options.pinvoke.importName ?? "MessageBoxW");
const resourceName = strings.add("Fixture.resources");
const referenceNames = options.references ?? ["System.Runtime"];
const referenceStringIndexes = referenceNames.map((name) =>
strings.add(name),
);
const tokenBlob = blobs.add(Buffer.from("b77a5c561934e089", "hex"));
const fieldSignature = blobs.add(
options.fieldSignature ?? Buffer.from([0x06, 0x08]),
);
const methodSignature = blobs.add(
options.methodSignature ?? Buffer.from([0x00, 0x00, 0x01]),
);
const constructorSignature = blobs.add(Buffer.from([0x20, 0x01, 0x01, 0x0e]));
const attributeBlob = blobs.add(
fixedStringAttributeBlob(
options.targetFramework ?? ".NETCoreApp,Version=v8.0",
),
);
const rows = new Map<number, readonly Buffer[]>([
[0, [moduleRow(moduleName)]],
[1, [typeRefRow(attributeName, attributeNamespace)]],
[2, [typeDefRow(typeName, typeNamespace, 1, 1)]],
[4, [fieldRow(fieldName, fieldSignature)]],
[
6,
[
methodDefRow(
methodName,
methodSignature,
0x2800,
options.pinvoke === undefined ? 0x0016 : 0x2016,
),
],
],
[10, [memberRefRow(constructorName, constructorSignature)]],
[12, [customAttributeRow(attributeBlob)]],
...(pinvokeModuleName === null
? []
: ([[26, [moduleRefRow(pinvokeModuleName)]]] as const)),
...(pinvokeImportName === null
? []
: ([
[
28,
[
implMapRow(
pinvokeImportName,
1,
options.pinvoke?.mappingFlags ?? 0x0344,
),
],
],
] as const)),
[32, [assemblyRow(assemblyName)]],
[35, referenceStringIndexes.map((name) => assemblyRefRow(name, tokenBlob))],
[40, [manifestResourceRow(resourceName)]],
]);
const metadata = metadataRoot(
tablesStream(rows, options.metadataValidMaskExtra ?? 0n),
strings.toBuffer(),
guidHeap(options.mvid),
blobs.toBuffer(),
);
if (options.corruptMetadataSignature === true) metadata.writeUInt32LE(0, 0);
const resourceData = options.resourceData ?? Buffer.from("resource-data");
const resourceDirectory = Buffer.concat([
u32(resourceData.length),
resourceData,
]);
const image = Buffer.alloc(0x1000);
image.write("MZ", 0, "ascii");
image.writeUInt32LE(0x80, 0x3c);
image.writeUInt32LE(0x0000_4550, 0x80);
const coff = 0x84;
image.writeUInt16LE(0x014c, coff);
image.writeUInt16LE(1, coff + 2);
image.writeUInt16LE(0x00e0, coff + 16);
image.writeUInt16LE(0x0102, coff + 18);
const optional = coff + 20;
image.writeUInt16LE(0x010b, optional);
image.writeUInt32LE(0x2000, optional + 20);
image.writeUInt32LE(0x0040_0000, optional + 28);
image.writeUInt32LE(0x1000, optional + 32);
image.writeUInt32LE(0x200, optional + 36);
image.writeUInt32LE(0x3000, optional + 56);
image.writeUInt32LE(0x200, optional + 60);
image.writeUInt32LE(16, optional + 92);
image.writeUInt32LE(0x2000, optional + 96 + 14 * 8);
image.writeUInt32LE(72, optional + 96 + 14 * 8 + 4);
const section = optional + 0x00e0;
image.write(".text\0\0\0", section, "ascii");
image.writeUInt32LE(0x1000, section + 8);
image.writeUInt32LE(0x2000, section + 12);
image.writeUInt32LE(0x0e00, section + 16);
image.writeUInt32LE(0x0200, section + 20);
image.writeUInt32LE(0x6000_0020, section + 36);
const cli = 0x0200;
image.writeUInt32LE(72, cli);
image.writeUInt16LE(2, cli + 4);
image.writeUInt16LE(5, cli + 6);
image.writeUInt32LE(0x2100, cli + 8);
image.writeUInt32LE(metadata.length, cli + 12);
image.writeUInt32LE(options.cliFlags ?? 1, cli + 16);
image.writeUInt32LE(0x0600_0001, cli + 20);
image.writeUInt32LE(0x2600, cli + 24);
image.writeUInt32LE(resourceDirectory.length, cli + 28);
if (options.readyToRun === true) {
image.writeUInt32LE(0x2700, cli + 64);
image.writeUInt32LE(4, cli + 68);
image.write("RTR\0", 0x0900, "ascii");
}
(
options.ilBody ??
Buffer.from([
0x32, 0x02, 0x7b, 0x01, 0x00, 0x00, 0x04, 0x28, 0x01, 0x00, 0x00, 0x0a,
0x2a,
])
).copy(image, 0x0a00);
metadata.copy(image, 0x0300);
resourceDirectory.copy(image, 0x0800);
return image;
};
/** Build a syntactically valid PE32 fixture with no CLI directory. */
export const buildNativePeFixture = (): Buffer => {
const image = buildManagedPeFixture();
const cliDirectory = 0x84 + 20 + 96 + 14 * 8;
image.writeUInt32LE(0, cliDirectory);
image.writeUInt32LE(0, cliDirectory + 4);
return image;
};