mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
Merge branch 'feat/4057-native-export-inventory' into feat/4057-verify-native-exports
This commit is contained in:
commit
198aa6e4aa
7 files changed
+295
-102
No files matched your search
@@ -334,13 +334,17 @@ count and SHA-256.
|
|||||||
`"unknown"` when the version table cannot be read. A hidden version is not
|
`"unknown"` when the version table cannot be read. A hidden version is not
|
||||||
the default version that a by-name `dlsym` lookup selects.
|
the default version that a by-name `dlsym` lookup selects.
|
||||||
|
|
||||||
Unreadable names, out-of-range name ordinals, unreadable forwarders and
|
Unreadable names, out-of-range name ordinals and unreadable forwarders are
|
||||||
unresolved version indexes are listed in `issues`, the affected entries are
|
listed in `issues`, their entries are not reported, and coverage is `partial`.
|
||||||
not reported, and coverage is `partial`. An ELF without section headers is
|
Unresolved symbol versions keep their rows: the version name is null for an
|
||||||
`unsupported`: enumerating dynamic symbols from the dynamic segment is not
|
index no definition names, and `version` is `"unknown"` when the version table
|
||||||
|
cannot be read; coverage is likewise `partial`. A dynamic symbol table without
|
||||||
|
its null entry is malformed. An ELF without section headers is `unsupported`:
|
||||||
|
enumerating dynamic symbols from the dynamic segment is not
|
||||||
implemented. Mach-O exports are reported by `inspect_macho`.
|
implemented. Mach-O exports are reported by `inspect_macho`.
|
||||||
|
|
||||||
`max_file_bytes` defaults to 256 MiB (maximum 512 MiB; CLI `--max-file-bytes`).
|
`max_file_bytes` defaults to 256 MiB (maximum 512 MiB; CLI `--max-file-bytes`).
|
||||||
An export or dynamic symbol table above 262144 entries fails without partial
|
Export directories in PE headers are read like those in sections. An export or
|
||||||
success. Export rows do not establish that a symbol is reachable, safe to call
|
dynamic symbol table above 262144 entries, more than 262144 rows, or more than
|
||||||
|
16 MiB of retained names fails without partial success. Export rows do not establish that a symbol is reachable, safe to call
|
||||||
or loaded at runtime.
|
or loaded at runtime.
|
||||||
@@ -282,6 +282,82 @@ describe("ELF dynamic symbol exports", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("export inventory review regressions", () => {
|
||||||
|
it("reads an export directory stored in the PE headers", () => {
|
||||||
|
const result = parse(
|
||||||
|
peExportImage({
|
||||||
|
inHeaders: true,
|
||||||
|
functions: [{ rva: 0x2000 }, { forwarder: "OTHER.Target" }],
|
||||||
|
names: [
|
||||||
|
{ name: "HeaderExport", index: 0 },
|
||||||
|
{ name: "HeaderForward", index: 1 },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
export_directory: { rva: 0x200, dll_name: "fixture.dll" },
|
||||||
|
coverage: { status: "complete", exports: 2 },
|
||||||
|
});
|
||||||
|
expect(result.exports.map(({ name }) => name)).toEqual([
|
||||||
|
"HeaderExport",
|
||||||
|
"HeaderForward",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("bounds retained name text before storing rows", () => {
|
||||||
|
// 300 rows naming one 60000-byte string retain about 18 MB of text.
|
||||||
|
const long = "n".repeat(60000);
|
||||||
|
const bytes = elfExportImage({
|
||||||
|
symbols: Array.from({ length: 300 }, () => ({ name: long })),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(bytes.length).toBeLessThan(100_000);
|
||||||
|
expect(() => parseNativeExports(bytes, artifact)).toThrow(
|
||||||
|
BinaryResourceFailure,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps exports when the version table lies outside the file", () => {
|
||||||
|
const result = parse(
|
||||||
|
elfExportImage({
|
||||||
|
symbols: [{ name: "a" }, { name: "b", version: 2 }],
|
||||||
|
versionDefinitions: ["lib.so", "V1"],
|
||||||
|
versionsOutsideFile: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
result.format === "elf" && result.exports.map(({ version }) => version),
|
||||||
|
).toEqual(["unknown", "unknown"]);
|
||||||
|
expect(result.coverage.status).toBe("partial");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports an empty version name as unresolved", () => {
|
||||||
|
const result = parse(
|
||||||
|
elfExportImage({
|
||||||
|
symbols: [{ name: "a", version: 2 }],
|
||||||
|
versionDefinitions: ["lib.so", ""],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
result.format === "elf" && result.exports.map(({ version }) => version),
|
||||||
|
).toEqual([{ index: 2, name: null, hidden: false }]);
|
||||||
|
expect(result.issues).toMatchObject([{ code: "version_unresolved" }]);
|
||||||
|
expect(result.coverage.status).toBe("partial");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a dynamic symbol table without its null entry", () => {
|
||||||
|
expect(() =>
|
||||||
|
parseNativeExports(
|
||||||
|
elfExportImage({ symbols: [], emptyDynamicSymbols: true }),
|
||||||
|
artifact,
|
||||||
|
),
|
||||||
|
).toThrow(BinaryFormatFailure);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("refuses formats other than PE and ELF", () => {
|
it("refuses formats other than PE and ELF", () => {
|
||||||
const macho = Buffer.alloc(64);
|
const macho = Buffer.alloc(64);
|
||||||
macho.writeUInt32LE(0xfeedfacf, 0);
|
macho.writeUInt32LE(0xfeedfacf, 0);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
BinaryResourceFailure,
|
BinaryResourceFailure,
|
||||||
mappedFileOffset,
|
mappedFileOffset,
|
||||||
} from "../metadata/BinaryContainer.js";
|
} from "../metadata/BinaryContainer.js";
|
||||||
import { readElfImage } from "../metadata/ElfImage.js";
|
import { architectureForElfMachine } from "../metadata/ElfImage.js";
|
||||||
import { readElfString, readElfSymbol } from "../metadata/ElfSymbol.js";
|
import { readElfString, readElfSymbol } from "../metadata/ElfSymbol.js";
|
||||||
import {
|
import {
|
||||||
readNativeDirectoryFromBuffer,
|
readNativeDirectoryFromBuffer,
|
||||||
@@ -28,6 +28,25 @@ const SHT_GNU_VERSYM = 0x6fffffff;
|
|||||||
const PE_EXPORT_DIRECTORY = 0;
|
const PE_EXPORT_DIRECTORY = 0;
|
||||||
const PE_EXPORT_DIRECTORY_BYTES = 40;
|
const PE_EXPORT_DIRECTORY_BYTES = 40;
|
||||||
const MAXIMUM_NAME_BYTES = 4096;
|
const MAXIMUM_NAME_BYTES = 4096;
|
||||||
|
/** Text retained across all rows; names can repeat one long string table entry. */
|
||||||
|
const MAXIMUM_RETAINED_TEXT_BYTES = 16 * 1024 * 1024;
|
||||||
|
|
||||||
|
/** Charge every retained row and its text before it is stored. */
|
||||||
|
const createRetention = () => {
|
||||||
|
let rows = 0;
|
||||||
|
let text = 0;
|
||||||
|
return (...strings: readonly (string | null)[]): void => {
|
||||||
|
rows++;
|
||||||
|
rowBudget(rows, "Export inventory");
|
||||||
|
for (const value of strings) text += value?.length ?? 0;
|
||||||
|
if (text > MAXIMUM_RETAINED_TEXT_BYTES)
|
||||||
|
throw new BinaryResourceFailure(
|
||||||
|
"structure",
|
||||||
|
MAXIMUM_RETAINED_TEXT_BYTES,
|
||||||
|
`Export names exceed the ${String(MAXIMUM_RETAINED_TEXT_BYTES)}-byte retained-text allowance`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const hex = (value: bigint | number) => `0x${value.toString(16)}`;
|
const hex = (value: bigint | number) => `0x${value.toString(16)}`;
|
||||||
|
|
||||||
@@ -88,13 +107,12 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
const reader = new BinaryReader(bytes, true);
|
const reader = new BinaryReader(bytes, true);
|
||||||
|
const mappings =
|
||||||
|
image.pe_header_mapping === undefined
|
||||||
|
? image.mappings
|
||||||
|
: [image.pe_header_mapping, ...image.mappings];
|
||||||
const map = (rva: number, size: number): number =>
|
const map = (rva: number, size: number): number =>
|
||||||
mappedFileOffset(
|
mappedFileOffset(mappings, base + BigInt(rva), size, image.zero_fills);
|
||||||
image.mappings,
|
|
||||||
base + BigInt(rva),
|
|
||||||
size,
|
|
||||||
image.zero_fills,
|
|
||||||
);
|
|
||||||
const cString = (rva: number): string => {
|
const cString = (rva: number): string => {
|
||||||
const start = map(rva, 1);
|
const start = map(rva, 1);
|
||||||
const end = bytes
|
const end = bytes
|
||||||
@@ -167,6 +185,7 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const forwarderEnd = directory.virtual_address + directory.size;
|
const forwarderEnd = directory.virtual_address + directory.size;
|
||||||
|
const retain = createRetention();
|
||||||
const exports: PeExport[] = [];
|
const exports: PeExport[] = [];
|
||||||
for (let index = 0; index < functionCount; index++) {
|
for (let index = 0; index < functionCount; index++) {
|
||||||
const rva = reader.u32(functions + index * 4);
|
const rva = reader.u32(functions + index * 4);
|
||||||
@@ -188,11 +207,15 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
|||||||
} else target = { address: hex(base + BigInt(rva)), forwarder: null };
|
} else target = { address: hex(base + BigInt(rva)), forwarder: null };
|
||||||
const exportNames = namesByFunction.get(index);
|
const exportNames = namesByFunction.get(index);
|
||||||
if (exportNames !== undefined)
|
if (exportNames !== undefined)
|
||||||
for (const name of exportNames)
|
for (const name of exportNames) {
|
||||||
|
retain(name, target.forwarder);
|
||||||
exports.push({ name, ordinal, ...target });
|
exports.push({ name, ordinal, ...target });
|
||||||
|
}
|
||||||
// A slot whose only name was unreadable is reported through its issue.
|
// A slot whose only name was unreadable is reported through its issue.
|
||||||
else if (!namedFunctions.has(index))
|
else if (!namedFunctions.has(index)) {
|
||||||
|
retain(target.forwarder);
|
||||||
exports.push({ name: null, ordinal, ...target });
|
exports.push({ name: null, ordinal, ...target });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
@@ -293,15 +316,14 @@ const readVersionDefinitions = (
|
|||||||
|
|
||||||
/** Read defined exported entries of `.dynsym` with their symbol versions. */
|
/** Read defined exported entries of `.dynsym` with their symbol versions. */
|
||||||
const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
||||||
const image = readElfImage(bytes);
|
|
||||||
const directory = readNativeDirectoryFromBuffer(bytes, "elf");
|
const directory = readNativeDirectoryFromBuffer(bytes, "elf");
|
||||||
const reader = new BinaryReader(bytes, directory.little);
|
const reader = new BinaryReader(bytes, directory.little);
|
||||||
const result = {
|
const result = {
|
||||||
artifact,
|
artifact,
|
||||||
format: "elf" as const,
|
format: "elf" as const,
|
||||||
architecture: image.architecture,
|
architecture: architectureForElfMachine(directory.machine),
|
||||||
bits: image.bits,
|
bits: directory.bits,
|
||||||
byte_order: image.byte_order,
|
byte_order: directory.little ? ("little" as const) : ("big" as const),
|
||||||
};
|
};
|
||||||
const { sections } = directory;
|
const { sections } = directory;
|
||||||
if (sections.length === 0)
|
if (sections.length === 0)
|
||||||
@@ -335,11 +357,13 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
|||||||
if (
|
if (
|
||||||
dynsym.entry_size !== BigInt(width) ||
|
dynsym.entry_size !== BigInt(width) ||
|
||||||
dynsym.size % BigInt(width) !== 0n ||
|
dynsym.size % BigInt(width) !== 0n ||
|
||||||
|
// The table must hold at least its mandatory null symbol.
|
||||||
|
dynsym.size < BigInt(width) ||
|
||||||
strings?.type !== SHT_STRTAB
|
strings?.type !== SHT_STRTAB
|
||||||
)
|
)
|
||||||
throw new BinaryFormatFailure(
|
throw new BinaryFormatFailure(
|
||||||
"malformed",
|
"malformed",
|
||||||
"ELF dynamic symbol table entry size, length or string-table link is invalid",
|
"ELF dynamic symbol table entry size, length, null entry or string-table link is invalid",
|
||||||
);
|
);
|
||||||
const count = Number(dynsym.size / BigInt(width));
|
const count = Number(dynsym.size / BigInt(width));
|
||||||
rowBudget(count, "ELF dynamic symbol table");
|
rowBudget(count, "ELF dynamic symbol table");
|
||||||
@@ -364,17 +388,25 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
|||||||
);
|
);
|
||||||
let versions: number | null = null;
|
let versions: number | null = null;
|
||||||
if (versym !== undefined) {
|
if (versym !== undefined) {
|
||||||
if (versym.size === BigInt(count * 2))
|
try {
|
||||||
|
if (versym.size !== BigInt(count * 2))
|
||||||
|
throw new BinaryFormatFailure(
|
||||||
|
"malformed",
|
||||||
|
`Symbol version table has ${String(versym.size)} bytes for ${String(count)} symbols`,
|
||||||
|
);
|
||||||
versions = reader.table(versym.offset, count, 2, "ELF symbol versions");
|
versions = reader.table(versym.offset, count, 2, "ELF symbol versions");
|
||||||
else
|
} catch (cause) {
|
||||||
|
// Keep every export; their versions become unknown.
|
||||||
issues.push({
|
issues.push({
|
||||||
code: "version_unresolved",
|
code: "version_unresolved",
|
||||||
location: hex(versym.offset),
|
location: hex(versym.offset),
|
||||||
message: `Symbol version table has ${String(versym.size)} bytes for ${String(count)} symbols`,
|
message: failureMessage(cause),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const definitions = readVersionDefinitions(reader, sections, issues);
|
const definitions = readVersionDefinitions(reader, sections, issues);
|
||||||
const unresolvedVersions = new Set<number>();
|
const unresolvedVersions = new Set<number>();
|
||||||
|
const retain = createRetention();
|
||||||
const exports: ElfExport[] = [];
|
const exports: ElfExport[] = [];
|
||||||
for (let index = 1; index < count; index++) {
|
for (let index = 1; index < count; index++) {
|
||||||
const at = table + index * width;
|
const at = table + index * width;
|
||||||
@@ -399,18 +431,19 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
|||||||
const versionIndex = raw & 0x7fff;
|
const versionIndex = raw & 0x7fff;
|
||||||
if (versionIndex === 0) continue;
|
if (versionIndex === 0) continue;
|
||||||
if (versionIndex >= 2) {
|
if (versionIndex >= 2) {
|
||||||
const name = definitions.get(versionIndex) ?? null;
|
const defined = definitions.get(versionIndex);
|
||||||
|
const name = defined === undefined || defined === "" ? null : defined;
|
||||||
if (name === null && !unresolvedVersions.has(versionIndex)) {
|
if (name === null && !unresolvedVersions.has(versionIndex)) {
|
||||||
unresolvedVersions.add(versionIndex);
|
unresolvedVersions.add(versionIndex);
|
||||||
issues.push({
|
issues.push({
|
||||||
code: "version_unresolved",
|
code: "version_unresolved",
|
||||||
location: hex(symbol.value),
|
location: hex(symbol.value),
|
||||||
message: `Version index ${String(versionIndex)}, first used by symbol ${String(index)}, is named by no version definition`,
|
message: `Version index ${String(versionIndex)}, first used by symbol ${String(index)}, has no named version definition`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
version = {
|
version = {
|
||||||
index: versionIndex,
|
index: versionIndex,
|
||||||
name: name === "" ? null : name,
|
name,
|
||||||
hidden: (raw & 0x8000) !== 0,
|
hidden: (raw & 0x8000) !== 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -434,6 +467,7 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
|||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
retain(name, typeof version === "object" ? (version?.name ?? null) : null);
|
||||||
exports.push({
|
exports.push({
|
||||||
name,
|
name,
|
||||||
value: hex(symbol.value),
|
value: hex(symbol.value),
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ export interface BinaryContainer {
|
|||||||
readonly byte_order: "little" | "big";
|
readonly byte_order: "little" | "big";
|
||||||
/** Declared PE preferred base, when available. */
|
/** Declared PE preferred base, when available. */
|
||||||
readonly image_base?: bigint;
|
readonly image_base?: bigint;
|
||||||
|
/**
|
||||||
|
* File-backed PE headers (`SizeOfHeaders`, clipped to the file), which the
|
||||||
|
* loader maps at the image base. Kept apart from section `mappings`.
|
||||||
|
*/
|
||||||
|
readonly pe_header_mapping?: FileMapping;
|
||||||
/** PE optional-header data directories, indexed as declared. */
|
/** PE optional-header data directories, indexed as declared. */
|
||||||
readonly pe_data_directories?: readonly {
|
readonly pe_data_directories?: readonly {
|
||||||
readonly virtual_address: number;
|
readonly virtual_address: number;
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const readElfImage = (bytes: Buffer): BinaryContainer => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Map ELF machine identifiers to stable architecture names. */
|
/** Map ELF machine identifiers to stable architecture names. */
|
||||||
const architectureForElfMachine = (machine: number): string => {
|
export const architectureForElfMachine = (machine: number): string => {
|
||||||
const names = new Map([
|
const names = new Map([
|
||||||
[3, "386"],
|
[3, "386"],
|
||||||
[8, "mips"],
|
[8, "mips"],
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => {
|
|||||||
return { virtual_address: reader.u32(entry), size: reader.u32(entry + 4) };
|
return { virtual_address: reader.u32(entry), size: reader.u32(entry + 4) };
|
||||||
});
|
});
|
||||||
const imageBase = reader.word(optional + (bits === 64 ? 24 : 28), bits);
|
const imageBase = reader.word(optional + (bits === 64 ? 24 : 28), bits);
|
||||||
|
const headerMapping: FileMapping = {
|
||||||
|
address: imageBase,
|
||||||
|
offset: 0,
|
||||||
|
size: Math.min(reader.u32(optional + 60), bytes.length),
|
||||||
|
};
|
||||||
const count = reader.u16(pe + 6);
|
const count = reader.u16(pe + 6);
|
||||||
const start = reader.table(
|
const start = reader.table(
|
||||||
BigInt(optional + optionalSize),
|
BigInt(optional + optionalSize),
|
||||||
@@ -157,6 +162,7 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => {
|
|||||||
format: "pe",
|
format: "pe",
|
||||||
image_base: imageBase,
|
image_base: imageBase,
|
||||||
pe_data_directories: dataDirectories,
|
pe_data_directories: dataDirectories,
|
||||||
|
pe_header_mapping: headerMapping,
|
||||||
architecture: names.get(machine) ?? `pe-machine-${machine}`,
|
architecture: names.get(machine) ?? `pe-machine-${machine}`,
|
||||||
bits,
|
bits,
|
||||||
byte_order: "little",
|
byte_order: "little",
|
||||||
|
|||||||
+143
-75
@@ -7,6 +7,8 @@ export type PeFixtureFunction =
|
|||||||
const SECTION_RVA = 0x1000;
|
const SECTION_RVA = 0x1000;
|
||||||
const SECTION_OFFSET = 0x400;
|
const SECTION_OFFSET = 0x400;
|
||||||
const SECTION_BYTES = 0x400;
|
const SECTION_BYTES = 0x400;
|
||||||
|
/** SizeOfHeaders; header RVAs map to the same file offsets. */
|
||||||
|
const HEADER_BYTES = 0x400;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimal PE32/PE32+ image whose only section holds an export directory at
|
* Minimal PE32/PE32+ image whose only section holds an export directory at
|
||||||
@@ -24,6 +26,8 @@ export const peExportImage = (options: {
|
|||||||
};
|
};
|
||||||
readonly exportDirectory?: boolean;
|
readonly exportDirectory?: boolean;
|
||||||
readonly declaredFunctions?: number;
|
readonly declaredFunctions?: number;
|
||||||
|
/** Store the export directory and its tables in the PE headers. */
|
||||||
|
readonly inHeaders?: boolean;
|
||||||
}): Buffer => {
|
}): Buffer => {
|
||||||
const bits = options.bits ?? 64;
|
const bits = options.bits ?? 64;
|
||||||
const bytes = Buffer.alloc(SECTION_OFFSET + SECTION_BYTES);
|
const bytes = Buffer.alloc(SECTION_OFFSET + SECTION_BYTES);
|
||||||
@@ -41,6 +45,7 @@ export const peExportImage = (options: {
|
|||||||
if (wide) bytes.writeBigUInt64LE(0x180000000n, optional + 24);
|
if (wide) bytes.writeBigUInt64LE(0x180000000n, optional + 24);
|
||||||
else bytes.writeUInt32LE(0x10000000, optional + 28);
|
else bytes.writeUInt32LE(0x10000000, optional + 28);
|
||||||
bytes.writeUInt32LE(16, optional + directoriesAt - 4);
|
bytes.writeUInt32LE(16, optional + directoriesAt - 4);
|
||||||
|
bytes.writeUInt32LE(HEADER_BYTES, optional + 60);
|
||||||
const section = optional + optionalSize;
|
const section = optional + optionalSize;
|
||||||
bytes.write(".edata", section, "ascii");
|
bytes.write(".edata", section, "ascii");
|
||||||
bytes.writeUInt32LE(SECTION_BYTES, section + 8);
|
bytes.writeUInt32LE(SECTION_BYTES, section + 8);
|
||||||
@@ -50,16 +55,18 @@ export const peExportImage = (options: {
|
|||||||
bytes.writeUInt32LE(0x40000040, section + 36);
|
bytes.writeUInt32LE(0x40000040, section + 36);
|
||||||
if (options.exportDirectory === false) return bytes;
|
if (options.exportDirectory === false) return bytes;
|
||||||
|
|
||||||
const at = (rva: number) => rva - SECTION_RVA + SECTION_OFFSET;
|
const at = (rva: number) =>
|
||||||
|
rva < HEADER_BYTES ? rva : rva - SECTION_RVA + SECTION_OFFSET;
|
||||||
|
const base = options.inHeaders === true ? 0x200 : SECTION_RVA;
|
||||||
const functions = options.functions ?? [];
|
const functions = options.functions ?? [];
|
||||||
const names = options.names ?? [];
|
const names = options.names ?? [];
|
||||||
// Directory, address table, name table, ordinals, then strings; forwarders
|
// Directory, address table, name table, ordinals, then strings; forwarders
|
||||||
// stay inside the 0x200-byte declared directory range.
|
// stay inside the 0x200-byte declared directory range.
|
||||||
const eat = SECTION_RVA + 0x40;
|
const eat = base + 0x40;
|
||||||
const nameTable = SECTION_RVA + 0x80;
|
const nameTable = base + 0x80;
|
||||||
const ordinalTable = SECTION_RVA + 0xc0;
|
const ordinalTable = base + 0xc0;
|
||||||
let forwarderText = SECTION_RVA + 0x100;
|
let forwarderText = base + 0x100;
|
||||||
let text = SECTION_RVA + 0x200;
|
let text = base + (options.inHeaders === true ? 0x180 : 0x200);
|
||||||
const string = (value: string, inside: boolean): number => {
|
const string = (value: string, inside: boolean): number => {
|
||||||
const rva = inside ? forwarderText : text;
|
const rva = inside ? forwarderText : text;
|
||||||
bytes.write(`${value}\0`, at(rva), "latin1");
|
bytes.write(`${value}\0`, at(rva), "latin1");
|
||||||
@@ -67,9 +74,9 @@ export const peExportImage = (options: {
|
|||||||
else text += value.length + 1;
|
else text += value.length + 1;
|
||||||
return rva;
|
return rva;
|
||||||
};
|
};
|
||||||
bytes.writeUInt32LE(SECTION_RVA, optional + directoriesAt);
|
bytes.writeUInt32LE(base, optional + directoriesAt);
|
||||||
bytes.writeUInt32LE(0x200, optional + directoriesAt + 4);
|
bytes.writeUInt32LE(0x200, optional + directoriesAt + 4);
|
||||||
const directory = at(SECTION_RVA);
|
const directory = at(base);
|
||||||
bytes.writeUInt32LE(0x5f5e0ff, directory + 4);
|
bytes.writeUInt32LE(0x5f5e0ff, directory + 4);
|
||||||
bytes.writeUInt32LE(string("fixture.dll", false), directory + 12);
|
bytes.writeUInt32LE(string("fixture.dll", false), directory + 12);
|
||||||
bytes.writeUInt32LE(options.ordinalBase ?? 1, directory + 16);
|
bytes.writeUInt32LE(options.ordinalBase ?? 1, directory + 16);
|
||||||
@@ -117,7 +124,7 @@ export interface ElfFixtureSymbol {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimal ELF shared object with `.dynsym`, `.dynstr`, `.gnu.version` and
|
* Minimal ELF shared object with `.dynsym`, `.dynstr`, `.gnu.version` and
|
||||||
* `.gnu.version_d`. Version definitions are indexed from 1 in order.
|
* `.gnu.version_d`, laid out in order. Version definitions are indexed from 1.
|
||||||
*/
|
*/
|
||||||
export const elfExportImage = (options: {
|
export const elfExportImage = (options: {
|
||||||
readonly bits?: 32 | 64;
|
readonly bits?: 32 | 64;
|
||||||
@@ -127,13 +134,53 @@ export const elfExportImage = (options: {
|
|||||||
readonly versionDefinitions?: readonly string[];
|
readonly versionDefinitions?: readonly string[];
|
||||||
/** Declare a `.gnu.version` table that is one entry short. */
|
/** Declare a `.gnu.version` table that is one entry short. */
|
||||||
readonly truncatedVersions?: boolean;
|
readonly truncatedVersions?: boolean;
|
||||||
|
/** Point `.gnu.version` past the end of the file. */
|
||||||
|
readonly versionsOutsideFile?: boolean;
|
||||||
|
/** Declare a zero-byte `.dynsym`, without its null entry. */
|
||||||
|
readonly emptyDynamicSymbols?: boolean;
|
||||||
readonly sectionHeaders?: boolean;
|
readonly sectionHeaders?: boolean;
|
||||||
readonly dynamicSymbols?: boolean;
|
readonly dynamicSymbols?: boolean;
|
||||||
}): Buffer => {
|
}): Buffer => {
|
||||||
const bits = options.bits ?? 64;
|
const bits = options.bits ?? 64;
|
||||||
const little = options.little ?? true;
|
const little = options.little ?? true;
|
||||||
const wide = bits === 64;
|
const wide = bits === 64;
|
||||||
const bytes = Buffer.alloc(0x1000);
|
const symbolWidth = wide ? 24 : 16;
|
||||||
|
const sectionWidth = wide ? 64 : 40;
|
||||||
|
const symbols = [null, ...options.symbols];
|
||||||
|
const definitions = options.versionDefinitions ?? [];
|
||||||
|
|
||||||
|
const dynstr: string[] = ["\0"];
|
||||||
|
let dynstrBytes = 1;
|
||||||
|
const strings = new Map<string, number>();
|
||||||
|
const intern = (value: string): number => {
|
||||||
|
const known = strings.get(value);
|
||||||
|
if (known !== undefined) return known;
|
||||||
|
const offset = dynstrBytes;
|
||||||
|
strings.set(value, offset);
|
||||||
|
dynstr.push(`${value}\0`);
|
||||||
|
dynstrBytes += value.length + 1;
|
||||||
|
return offset;
|
||||||
|
};
|
||||||
|
const definitionNames = definitions.map(intern);
|
||||||
|
const symbolNames = symbols.map((symbol) =>
|
||||||
|
symbol === null ? 0 : (symbol.nameOffset ?? intern(symbol.name)),
|
||||||
|
);
|
||||||
|
const shstrtab =
|
||||||
|
"\0.dynsym\0.dynstr\0.gnu.version\0.gnu.version_d\0.shstrtab\0";
|
||||||
|
let cursor = 0x100;
|
||||||
|
const place = (size: number): number => {
|
||||||
|
const offset = cursor;
|
||||||
|
cursor = Math.ceil((cursor + size) / 16) * 16;
|
||||||
|
return offset;
|
||||||
|
};
|
||||||
|
const dynsymAt = place(symbols.length * symbolWidth);
|
||||||
|
const dynstrAt = place(dynstrBytes);
|
||||||
|
const versymAt = place(symbols.length * 2);
|
||||||
|
const verdefAt = place(definitions.length * 28);
|
||||||
|
const shstrtabAt = place(shstrtab.length);
|
||||||
|
const sectionAt = place(6 * sectionWidth);
|
||||||
|
const bytes = Buffer.alloc(cursor);
|
||||||
|
|
||||||
const u16 = (offset: number, value: number) =>
|
const u16 = (offset: number, value: number) =>
|
||||||
little
|
little
|
||||||
? bytes.writeUInt16LE(value, offset)
|
? bytes.writeUInt16LE(value, offset)
|
||||||
@@ -151,53 +198,36 @@ export const elfExportImage = (options: {
|
|||||||
u16(16, 3);
|
u16(16, 3);
|
||||||
u16(18, options.machine ?? (wide ? 62 : 40));
|
u16(18, options.machine ?? (wide ? 62 : 40));
|
||||||
u32(20, 1);
|
u32(20, 1);
|
||||||
const headerSize = wide ? 64 : 52;
|
u16(wide ? 52 : 40, wide ? 64 : 52);
|
||||||
u16(wide ? 52 : 40, headerSize);
|
|
||||||
u16(wide ? 54 : 42, wide ? 56 : 32);
|
u16(wide ? 54 : 42, wide ? 56 : 32);
|
||||||
u16(wide ? 58 : 46, wide ? 64 : 40);
|
u16(wide ? 58 : 46, sectionWidth);
|
||||||
if (options.sectionHeaders === false) return bytes;
|
if (options.sectionHeaders === false) return bytes;
|
||||||
|
|
||||||
const dynstr = ["\0"];
|
|
||||||
let dynstrBytes = 1;
|
|
||||||
const intern = (value: string): number => {
|
|
||||||
const offset = dynstrBytes;
|
|
||||||
dynstr.push(`${value}\0`);
|
|
||||||
dynstrBytes += value.length + 1;
|
|
||||||
return offset;
|
|
||||||
};
|
|
||||||
const definitions = options.versionDefinitions ?? [];
|
|
||||||
const definitionNames = definitions.map(intern);
|
|
||||||
const symbolWidth = wide ? 24 : 16;
|
|
||||||
const symbols = [null, ...options.symbols];
|
|
||||||
const dynsymAt = 0x200;
|
|
||||||
symbols.forEach((symbol, index) => {
|
symbols.forEach((symbol, index) => {
|
||||||
if (symbol === null) return;
|
if (symbol === null) return;
|
||||||
const at = dynsymAt + index * symbolWidth;
|
const at = dynsymAt + index * symbolWidth;
|
||||||
const info = ((symbol.binding ?? 1) << 4) | (symbol.type ?? 2);
|
const info = ((symbol.binding ?? 1) << 4) | (symbol.type ?? 2);
|
||||||
const other = symbol.visibility ?? 0;
|
const other = symbol.visibility ?? 0;
|
||||||
const name = symbol.nameOffset ?? intern(symbol.name);
|
const value = symbol.value ?? BigInt(0x1000 + index * 16);
|
||||||
u32(at, name);
|
u32(at, symbolNames[index]!);
|
||||||
if (wide) {
|
if (wide) {
|
||||||
bytes[at + 4] = info;
|
bytes[at + 4] = info;
|
||||||
bytes[at + 5] = other;
|
bytes[at + 5] = other;
|
||||||
u16(at + 6, symbol.section ?? 1);
|
u16(at + 6, symbol.section ?? 1);
|
||||||
word(at + 8, symbol.value ?? BigInt(0x1000 + index * 16));
|
word(at + 8, value);
|
||||||
word(at + 16, symbol.size ?? 16n);
|
word(at + 16, symbol.size ?? 16n);
|
||||||
} else {
|
} else {
|
||||||
word(at + 4, symbol.value ?? BigInt(0x1000 + index * 16));
|
word(at + 4, value);
|
||||||
word(at + 8, symbol.size ?? 16n);
|
word(at + 8, symbol.size ?? 16n);
|
||||||
bytes[at + 12] = info;
|
bytes[at + 12] = info;
|
||||||
bytes[at + 13] = other;
|
bytes[at + 13] = other;
|
||||||
u16(at + 14, symbol.section ?? 1);
|
u16(at + 14, symbol.section ?? 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const dynstrAt = 0x600;
|
|
||||||
Buffer.from(dynstr.join(""), "latin1").copy(bytes, dynstrAt);
|
Buffer.from(dynstr.join(""), "latin1").copy(bytes, dynstrAt);
|
||||||
const versymAt = 0x900;
|
|
||||||
symbols.forEach((symbol, index) =>
|
symbols.forEach((symbol, index) =>
|
||||||
u16(versymAt + index * 2, symbol?.version ?? (index === 0 ? 0 : 1)),
|
u16(versymAt + index * 2, symbol?.version ?? (index === 0 ? 0 : 1)),
|
||||||
);
|
);
|
||||||
const verdefAt = 0xa00;
|
|
||||||
definitions.forEach((_, index) => {
|
definitions.forEach((_, index) => {
|
||||||
const at = verdefAt + index * 28;
|
const at = verdefAt + index * 28;
|
||||||
u16(at, 1);
|
u16(at, 1);
|
||||||
@@ -208,49 +238,21 @@ export const elfExportImage = (options: {
|
|||||||
u32(at + 16, index === definitions.length - 1 ? 0 : 28);
|
u32(at + 16, index === definitions.length - 1 ? 0 : 28);
|
||||||
u32(at + 20, definitionNames[index]!);
|
u32(at + 20, definitionNames[index]!);
|
||||||
});
|
});
|
||||||
const shstrtab =
|
|
||||||
"\0.dynsym\0.dynstr\0.gnu.version\0.gnu.version_d\0.shstrtab\0";
|
|
||||||
const shstrtabAt = 0xc00;
|
|
||||||
bytes.write(shstrtab, shstrtabAt, "latin1");
|
bytes.write(shstrtab, shstrtabAt, "latin1");
|
||||||
const sections: (readonly [
|
const sections = elfSectionRows(options, {
|
||||||
number,
|
dynsymAt,
|
||||||
number,
|
dynsym: symbols.length * symbolWidth,
|
||||||
number,
|
symbolWidth,
|
||||||
number,
|
dynstrAt,
|
||||||
number,
|
dynstrBytes,
|
||||||
number,
|
versymAt,
|
||||||
number,
|
versym: symbols.length * 2,
|
||||||
])[] = [
|
verdefAt,
|
||||||
// name, type, offset, size, link, info, entsize
|
definitions: definitions.length,
|
||||||
[0, 0, 0, 0, 0, 0, 0],
|
shstrtabAt,
|
||||||
...(options.dynamicSymbols === false
|
shstrtab: shstrtab.length,
|
||||||
? []
|
end: cursor,
|
||||||
: ([
|
});
|
||||||
[1, 11, dynsymAt, symbols.length * symbolWidth, 2, 1, symbolWidth],
|
|
||||||
[9, 3, dynstrAt, dynstrBytes, 0, 0, 0],
|
|
||||||
[
|
|
||||||
17,
|
|
||||||
0x6fffffff,
|
|
||||||
versymAt,
|
|
||||||
(symbols.length - (options.truncatedVersions === true ? 1 : 0)) * 2,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
2,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30,
|
|
||||||
0x6ffffffd,
|
|
||||||
verdefAt,
|
|
||||||
definitions.length * 28,
|
|
||||||
2,
|
|
||||||
definitions.length,
|
|
||||||
0,
|
|
||||||
],
|
|
||||||
] as const)),
|
|
||||||
[45, 3, shstrtabAt, shstrtab.length, 0, 0, 0],
|
|
||||||
];
|
|
||||||
const sectionAt = 0xd00;
|
|
||||||
const sectionWidth = wide ? 64 : 40;
|
|
||||||
sections.forEach(([name, type, offset, size, link, info, entsize], index) => {
|
sections.forEach(([name, type, offset, size, link, info, entsize], index) => {
|
||||||
const at = sectionAt + index * sectionWidth;
|
const at = sectionAt + index * sectionWidth;
|
||||||
u32(at, name);
|
u32(at, name);
|
||||||
@@ -266,3 +268,69 @@ export const elfExportImage = (options: {
|
|||||||
u16(wide ? 62 : 50, sections.length - 1);
|
u16(wide ? 62 : 50, sections.length - 1);
|
||||||
return bytes;
|
return bytes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ElfSectionRow = readonly [
|
||||||
|
name: number,
|
||||||
|
type: number,
|
||||||
|
offset: number,
|
||||||
|
size: number,
|
||||||
|
link: number,
|
||||||
|
info: number,
|
||||||
|
entsize: number,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Section header rows for the fixture layout, with the requested defects. */
|
||||||
|
const elfSectionRows = (
|
||||||
|
options: Parameters<typeof elfExportImage>[0],
|
||||||
|
layout: {
|
||||||
|
readonly dynsymAt: number;
|
||||||
|
readonly dynsym: number;
|
||||||
|
readonly symbolWidth: number;
|
||||||
|
readonly dynstrAt: number;
|
||||||
|
readonly dynstrBytes: number;
|
||||||
|
readonly versymAt: number;
|
||||||
|
readonly versym: number;
|
||||||
|
readonly verdefAt: number;
|
||||||
|
readonly definitions: number;
|
||||||
|
readonly shstrtabAt: number;
|
||||||
|
readonly shstrtab: number;
|
||||||
|
readonly end: number;
|
||||||
|
},
|
||||||
|
): readonly ElfSectionRow[] => [
|
||||||
|
[0, 0, 0, 0, 0, 0, 0],
|
||||||
|
...(options.dynamicSymbols === false
|
||||||
|
? []
|
||||||
|
: ([
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
11,
|
||||||
|
layout.dynsymAt,
|
||||||
|
options.emptyDynamicSymbols === true ? 0 : layout.dynsym,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
layout.symbolWidth,
|
||||||
|
],
|
||||||
|
[9, 3, layout.dynstrAt, layout.dynstrBytes, 0, 0, 0],
|
||||||
|
[
|
||||||
|
17,
|
||||||
|
0x6fffffff,
|
||||||
|
options.versionsOutsideFile === true
|
||||||
|
? layout.end + 64
|
||||||
|
: layout.versymAt,
|
||||||
|
layout.versym - (options.truncatedVersions === true ? 2 : 0),
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30,
|
||||||
|
0x6ffffffd,
|
||||||
|
layout.verdefAt,
|
||||||
|
layout.definitions * 28,
|
||||||
|
2,
|
||||||
|
layout.definitions,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
] satisfies ElfSectionRow[])),
|
||||||
|
[45, 3, layout.shstrtabAt, layout.shstrtab, 0, 0, 0],
|
||||||
|
];
|
||||||
Reference in new issue
Block a user