Files
reverse-engineering-anything/tests/fixtures/binaryDiagnostics/nativeExports.ts
T
Kaoru0822andClaude Opus 5.5 933af60eba feat(native): inspect PE and ELF export tables without an engine (#6278)
* feat(native): read PE and ELF export tables

Add a host-independent export-table reader on the shared native
container layer. PE images report each used export-address-table slot
with its ordinal, one row per name, its address or forwarder text, and
the export directory identity; the PE image reader now exposes its data
directories. ELF images report defined default/protected global, weak
and unique .dynsym symbols with type, binding and .gnu.version_d
version (hidden versions kept distinct, unreadable version tables
"unknown"). Unreadable entries are counted in issues and make coverage
partial; an ELF without section headers is unsupported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(native): inspect native export tables through CLI and MCP

Expose the reader as inspect_native_exports / inspect-native-exports:
one stable, digest-bound read of an explicit file with a caller-selected
input budget, typed failures for malformed, unsupported, oversized and
cancelled reads, and observed Evidence bound to the file path and
SHA-256. Mach-O inputs point to inspect_macho.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(native): describe portable PE and ELF export inventories

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(native): bound export inventories and read header-backed PE tables

Address review of the export-table reader:

- Charge every retained row and its name, forwarder and version text
  against shared 262144-row and 16 MiB allowances before storing it, so
  repeated long names or PE aliases cannot exhaust memory.
- Map PE RVAs through the headers as well as sections; the PE image
  reader now exposes its SizeOfHeaders mapping.
- Keep ELF exports with version "unknown" when .gnu.version lies outside
  the file, and report an empty version-definition name as unresolved.
- Reject a .dynsym without its mandatory null entry as malformed.
- Decode the ELF section directory once.
- Correct the docs on which gaps drop rows and which keep them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:30:05 -07:00

337 lines
11 KiB
TypeScript

/** One PE export-address-table slot: code RVA, forwarder text, or unused. */
export type PeFixtureFunction =
| { readonly rva: number }
| { readonly forwarder: string }
| null;
const SECTION_RVA = 0x1000;
const SECTION_OFFSET = 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
* RVA 0x1000. Name entries point at `functions` by index.
*/
export const peExportImage = (options: {
readonly bits?: 32 | 64;
readonly ordinalBase?: number;
readonly functions?: readonly PeFixtureFunction[];
readonly names?: readonly { readonly name: string; readonly index: number }[];
/** Replace one name's RVA, e.g. with an unmapped one. */
readonly nameRvaOverride?: {
readonly position: number;
readonly rva: number;
};
readonly exportDirectory?: boolean;
readonly declaredFunctions?: number;
/** Store the export directory and its tables in the PE headers. */
readonly inHeaders?: boolean;
}): Buffer => {
const bits = options.bits ?? 64;
const bytes = Buffer.alloc(SECTION_OFFSET + SECTION_BYTES);
const wide = bits === 64;
bytes.write("MZ", 0, "ascii");
bytes.writeUInt32LE(128, 60);
bytes.write("PE\0\0", 128, "ascii");
bytes.writeUInt16LE(wide ? 0x8664 : 0x14c, 132);
bytes.writeUInt16LE(1, 134);
const directoriesAt = wide ? 112 : 96;
const optionalSize = directoriesAt + 16 * 8;
bytes.writeUInt16LE(optionalSize, 148);
const optional = 152;
bytes.writeUInt16LE(wide ? 0x20b : 0x10b, optional);
if (wide) bytes.writeBigUInt64LE(0x180000000n, optional + 24);
else bytes.writeUInt32LE(0x10000000, optional + 28);
bytes.writeUInt32LE(16, optional + directoriesAt - 4);
bytes.writeUInt32LE(HEADER_BYTES, optional + 60);
const section = optional + optionalSize;
bytes.write(".edata", section, "ascii");
bytes.writeUInt32LE(SECTION_BYTES, section + 8);
bytes.writeUInt32LE(SECTION_RVA, section + 12);
bytes.writeUInt32LE(SECTION_BYTES, section + 16);
bytes.writeUInt32LE(SECTION_OFFSET, section + 20);
bytes.writeUInt32LE(0x40000040, section + 36);
if (options.exportDirectory === false) return bytes;
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 names = options.names ?? [];
// Directory, address table, name table, ordinals, then strings; forwarders
// stay inside the 0x200-byte declared directory range.
const eat = base + 0x40;
const nameTable = base + 0x80;
const ordinalTable = base + 0xc0;
let forwarderText = base + 0x100;
let text = base + (options.inHeaders === true ? 0x180 : 0x200);
const string = (value: string, inside: boolean): number => {
const rva = inside ? forwarderText : text;
bytes.write(`${value}\0`, at(rva), "latin1");
if (inside) forwarderText += value.length + 1;
else text += value.length + 1;
return rva;
};
bytes.writeUInt32LE(base, optional + directoriesAt);
bytes.writeUInt32LE(0x200, optional + directoriesAt + 4);
const directory = at(base);
bytes.writeUInt32LE(0x5f5e0ff, directory + 4);
bytes.writeUInt32LE(string("fixture.dll", false), directory + 12);
bytes.writeUInt32LE(options.ordinalBase ?? 1, directory + 16);
bytes.writeUInt32LE(
options.declaredFunctions ?? functions.length,
directory + 20,
);
bytes.writeUInt32LE(names.length, directory + 24);
bytes.writeUInt32LE(eat, directory + 28);
bytes.writeUInt32LE(nameTable, directory + 32);
bytes.writeUInt32LE(ordinalTable, directory + 36);
functions.forEach((entry, index) => {
const value =
entry === null
? 0
: "rva" in entry
? entry.rva
: string(entry.forwarder, true);
bytes.writeUInt32LE(value, at(eat) + index * 4);
});
names.forEach(({ name, index }, position) => {
const rva =
options.nameRvaOverride?.position === position
? options.nameRvaOverride.rva
: string(name, false);
bytes.writeUInt32LE(rva, at(nameTable) + position * 4);
bytes.writeUInt16LE(index, at(ordinalTable) + position * 2);
});
return bytes;
};
/** One `.dynsym` entry; `version` is its raw `.gnu.version` half-word. */
export interface ElfFixtureSymbol {
readonly name: string;
readonly binding?: number;
readonly type?: number;
readonly visibility?: number;
readonly section?: number;
readonly value?: bigint;
readonly size?: bigint;
readonly version?: number;
/** Replace the symbol's string-table offset. */
readonly nameOffset?: number;
}
/**
* Minimal ELF shared object with `.dynsym`, `.dynstr`, `.gnu.version` and
* `.gnu.version_d`, laid out in order. Version definitions are indexed from 1.
*/
export const elfExportImage = (options: {
readonly bits?: 32 | 64;
readonly little?: boolean;
readonly machine?: number;
readonly symbols: readonly ElfFixtureSymbol[];
readonly versionDefinitions?: readonly string[];
/** Declare a `.gnu.version` table that is one entry short. */
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 dynamicSymbols?: boolean;
}): Buffer => {
const bits = options.bits ?? 64;
const little = options.little ?? true;
const wide = bits === 64;
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) =>
little
? bytes.writeUInt16LE(value, offset)
: bytes.writeUInt16BE(value, offset);
const u32 = (offset: number, value: number) =>
little
? bytes.writeUInt32LE(value, offset)
: bytes.writeUInt32BE(value, offset);
const word = (offset: number, value: bigint) => {
if (!wide) u32(offset, Number(value));
else if (little) bytes.writeBigUInt64LE(value, offset);
else bytes.writeBigUInt64BE(value, offset);
};
bytes.set([0x7f, 0x45, 0x4c, 0x46, wide ? 2 : 1, little ? 1 : 2, 1]);
u16(16, 3);
u16(18, options.machine ?? (wide ? 62 : 40));
u32(20, 1);
u16(wide ? 52 : 40, wide ? 64 : 52);
u16(wide ? 54 : 42, wide ? 56 : 32);
u16(wide ? 58 : 46, sectionWidth);
if (options.sectionHeaders === false) return bytes;
symbols.forEach((symbol, index) => {
if (symbol === null) return;
const at = dynsymAt + index * symbolWidth;
const info = ((symbol.binding ?? 1) << 4) | (symbol.type ?? 2);
const other = symbol.visibility ?? 0;
const value = symbol.value ?? BigInt(0x1000 + index * 16);
u32(at, symbolNames[index]!);
if (wide) {
bytes[at + 4] = info;
bytes[at + 5] = other;
u16(at + 6, symbol.section ?? 1);
word(at + 8, value);
word(at + 16, symbol.size ?? 16n);
} else {
word(at + 4, value);
word(at + 8, symbol.size ?? 16n);
bytes[at + 12] = info;
bytes[at + 13] = other;
u16(at + 14, symbol.section ?? 1);
}
});
Buffer.from(dynstr.join(""), "latin1").copy(bytes, dynstrAt);
symbols.forEach((symbol, index) =>
u16(versymAt + index * 2, symbol?.version ?? (index === 0 ? 0 : 1)),
);
definitions.forEach((_, index) => {
const at = verdefAt + index * 28;
u16(at, 1);
u16(at + 2, index === 0 ? 1 : 0);
u16(at + 4, index + 1);
u16(at + 6, 1);
u32(at + 12, 20);
u32(at + 16, index === definitions.length - 1 ? 0 : 28);
u32(at + 20, definitionNames[index]!);
});
bytes.write(shstrtab, shstrtabAt, "latin1");
const sections = elfSectionRows(options, {
dynsymAt,
dynsym: symbols.length * symbolWidth,
symbolWidth,
dynstrAt,
dynstrBytes,
versymAt,
versym: symbols.length * 2,
verdefAt,
definitions: definitions.length,
shstrtabAt,
shstrtab: shstrtab.length,
end: cursor,
});
sections.forEach(([name, type, offset, size, link, info, entsize], index) => {
const at = sectionAt + index * sectionWidth;
u32(at, name);
u32(at + 4, type);
word(at + (wide ? 24 : 16), BigInt(offset));
word(at + (wide ? 32 : 20), BigInt(size));
u32(at + (wide ? 40 : 24), link);
u32(at + (wide ? 44 : 28), info);
word(at + (wide ? 56 : 36), BigInt(entsize));
});
word(wide ? 40 : 32, BigInt(sectionAt));
u16(wide ? 60 : 48, sections.length);
u16(wide ? 62 : 50, sections.length - 1);
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],
];