442 lines
13 KiB
TypeScript
442 lines
13 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)]);
|
|
|
|
interface MethodDefRowInput {
|
|
readonly name: number;
|
|
readonly signature: number;
|
|
readonly rva?: number;
|
|
readonly flags?: number;
|
|
readonly implFlags?: number;
|
|
}
|
|
|
|
const methodDefRow = ({
|
|
name,
|
|
signature,
|
|
rva = 0,
|
|
flags = 0x0016,
|
|
implFlags = 0,
|
|
}: MethodDefRowInput): 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]);
|
|
};
|
|
|
|
const buildManagedFixtureMetadata = (
|
|
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({
|
|
name: methodName,
|
|
signature: methodSignature,
|
|
rva: 0x2800,
|
|
flags: 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);
|
|
return metadata;
|
|
};
|
|
|
|
const buildManagedFixtureImage = (
|
|
options: ManagedPeFixtureOptions,
|
|
metadata: Buffer,
|
|
): Buffer => {
|
|
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 source-owned PE32/CLI fixture without committing compiled binaries. */
|
|
export const buildManagedPeFixture = (
|
|
options: ManagedPeFixtureOptions = {},
|
|
): Buffer =>
|
|
buildManagedFixtureImage(options, buildManagedFixtureMetadata(options));
|
|
|
|
/** 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;
|
|
};
|