mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
* feat(pe): inspect import and delay-load tables Add inspect_pe_imports / inspect-pe-imports to read PE32/PE32+ import and delay-load directories without executing the image. The report keeps DLL names, named and ordinal functions, ILT/IAT RVAs, and a pefile import hash of the standard import table. A missing directory is observed absence, not proof the image never imported APIs. * fix(pe): default PE import fixture options Allow peImportImage() without an argument so CLI and filesystem tests can use the KERNEL32 sample without repeating an empty options object. * test(pe): keep import path cases on the dedicated MCP contract inspect_pe_imports already rejects relative MCP paths in its own boundary test. Leave the shared absolute-path matrix to tools whose handler issue projection matches that suite's expected shape. * fix(native): index virtual-to-file mapping lookups mappedFileOffset scans every mapping twice per span, so a parser that translates one span per table entry costs sections x entries. Add fileMappingLookup, which builds an O(n log n) interval index once and answers each span in O(log n) with the same accept, reject and translation results. A differential test checks it against mappedFileOffset over randomized overlapping mappings and zero fills. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g * fix(pe): report partial import evidence and exact pefile hashes - Withhold imphash with an explicit reason whenever the standard import table is partial (unreadable names or thunks, missing tables), or when pefile 2024.8.26 would hash a different row sequence: invalid or truncated names, ordinal 0, descriptors without functions, tables its bounds or repeat/spread heuristics discard, and its 8192-thunk import symbol cap. Delay-load issues do not withhold it. - Hash ordinal imports with pefile's complete ordlookup tables and its ord<decimal> fallback instead of declining unmapped ordinals. - Report a nonterminal descriptor whose FirstThunk is zero, and a delay-load descriptor whose IAT or INT RVA is zero, as malformed_entry partial coverage instead of a complete empty inventory. - Directory evidence lists the file spans actually consumed for the descriptor array and terminator; discontiguous mappings produce separate spans and separate Evidence ranges. - Carry exhausted limits as PeImportLimitFailure.limit and derive recovery from it; the fixed 16 MiB retained-text ceiling reports a fixed allowance instead of max_file_bytes advice. - Translate RVAs through the indexed mapping lookup and yield with a cancellation check every 1024 table entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g * fix(pe): read only own ordinal tables for DLL names DLL names such as constructor and __proto__ pass pefile's name rule but resolved inherited object members, throwing before the ord<n> fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g * fix(native): preserve PE import facts and prove hash compatibility * test(native): verify PE import workflows and measured costs * refactor(native): narrow PE facts across the container union * docs(native): record integrated PE import validation * fix(native): bound every PE import RVA span * docs(native): measure RVA guard completed workflows * fix(pe): reconcile import coverage with retained rows * fix(pe): retain partial imports at descriptor mapping gaps --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
315 lines
11 KiB
TypeScript
315 lines
11 KiB
TypeScript
const SECTION_RVA = 0x1000;
|
|
const SECTION_OFFSET = 0x400;
|
|
const SECTION_BYTES = 0x800;
|
|
const HEADER_BYTES = 0x400;
|
|
const SPLIT_GAP_BYTES = 0x200;
|
|
|
|
/** File offset of an RVA in an unsplit fixture image. */
|
|
export const peImportFixtureOffset = (rva: number): number =>
|
|
rva - SECTION_RVA + SECTION_OFFSET;
|
|
|
|
export type PeImportFixtureFunction =
|
|
| { readonly name: string; readonly hint?: number }
|
|
| { readonly ordinal: number };
|
|
|
|
export interface PeImportFixtureDll {
|
|
readonly name: string;
|
|
readonly functions: readonly PeImportFixtureFunction[];
|
|
}
|
|
|
|
/**
|
|
* Minimal PE32/PE32+ image whose `.idata` section holds IMAGE_IMPORT_DESCRIPTOR
|
|
* and optional IMAGE_DELAYLOAD_DESCRIPTOR tables at RVA 0x1000.
|
|
*
|
|
* `splitAt` keeps the RVA layout but moves section bytes from that section
|
|
* offset onward into a second section at a later, discontiguous file offset,
|
|
* filling the vacated file bytes with 0xcc.
|
|
*/
|
|
export const peImportImage = (
|
|
options: {
|
|
readonly bits?: 32 | 64;
|
|
readonly dlls?: readonly PeImportFixtureDll[];
|
|
readonly delayDlls?: readonly PeImportFixtureDll[];
|
|
readonly delayAttributes?: number;
|
|
readonly importDirectory?: boolean;
|
|
readonly delayDirectory?: boolean;
|
|
readonly sectionBytes?: number;
|
|
readonly splitAt?: number;
|
|
} = {},
|
|
): Buffer => {
|
|
const sectionBytes = options.sectionBytes ?? SECTION_BYTES;
|
|
const bits = options.bits ?? 64;
|
|
const dlls = options.dlls ?? [
|
|
{
|
|
name: "KERNEL32.dll",
|
|
functions: [
|
|
{ name: "GetProcAddress", hint: 1 },
|
|
{ name: "ExitProcess", hint: 2 },
|
|
],
|
|
},
|
|
];
|
|
const delayDlls = options.delayDlls ?? [];
|
|
const bytes = Buffer.alloc(SECTION_OFFSET + sectionBytes);
|
|
const wide = bits === 64;
|
|
const thunkSize = wide ? 8 : 4;
|
|
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(".idata", section, "ascii");
|
|
bytes.writeUInt32LE(sectionBytes, section + 8);
|
|
bytes.writeUInt32LE(SECTION_RVA, section + 12);
|
|
bytes.writeUInt32LE(sectionBytes, section + 16);
|
|
bytes.writeUInt32LE(SECTION_OFFSET, section + 20);
|
|
bytes.writeUInt32LE(0x40000040, section + 36);
|
|
|
|
const at = (rva: number) => rva - SECTION_RVA + SECTION_OFFSET;
|
|
const writeThunk = (rva: number, value: bigint): void => {
|
|
if (wide) bytes.writeBigUInt64LE(value, at(rva));
|
|
else bytes.writeUInt32LE(Number(value), at(rva));
|
|
};
|
|
let cursor = SECTION_RVA;
|
|
const place = (size: number): number => {
|
|
const rva = cursor;
|
|
cursor += size;
|
|
return rva;
|
|
};
|
|
|
|
const importCount = options.importDirectory === false ? 0 : dlls.length;
|
|
const delayCount =
|
|
options.delayDirectory === false || delayDlls.length === 0
|
|
? 0
|
|
: delayDlls.length;
|
|
const importTable = importCount === 0 ? 0 : place((importCount + 1) * 20);
|
|
const delayTable = delayCount === 0 ? 0 : place((delayCount + 1) * 32);
|
|
|
|
if (options.importDirectory !== false) {
|
|
bytes.writeUInt32LE(importTable, optional + directoriesAt + 8);
|
|
bytes.writeUInt32LE((importCount + 1) * 20, optional + directoriesAt + 12);
|
|
}
|
|
if (delayCount > 0) {
|
|
bytes.writeUInt32LE(delayTable, optional + directoriesAt + 13 * 8);
|
|
bytes.writeUInt32LE(
|
|
(delayCount + 1) * 32,
|
|
optional + directoriesAt + 13 * 8 + 4,
|
|
);
|
|
}
|
|
|
|
const writeFunctions = (
|
|
functions: readonly PeImportFixtureFunction[],
|
|
): { readonly ilt: number; readonly iat: number } => {
|
|
const ilt = place((functions.length + 1) * thunkSize);
|
|
const iat = place((functions.length + 1) * thunkSize);
|
|
functions.forEach((entry, index) => {
|
|
let thunk: bigint;
|
|
if ("ordinal" in entry) {
|
|
thunk =
|
|
(wide ? 0x8000000000000000n : 0x80000000n) | BigInt(entry.ordinal);
|
|
} else {
|
|
const hintName = place(2 + entry.name.length + 1);
|
|
bytes.writeUInt16LE(entry.hint ?? 0, at(hintName));
|
|
bytes.write(`${entry.name}\0`, at(hintName) + 2, "latin1");
|
|
thunk = BigInt(hintName);
|
|
}
|
|
writeThunk(ilt + index * thunkSize, thunk);
|
|
writeThunk(iat + index * thunkSize, thunk);
|
|
});
|
|
return { ilt, iat };
|
|
};
|
|
|
|
dlls.slice(0, importCount).forEach((dll, index) => {
|
|
const { ilt, iat } = writeFunctions(dll.functions);
|
|
const nameRva = place(dll.name.length + 1);
|
|
bytes.write(`${dll.name}\0`, at(nameRva), "latin1");
|
|
const descriptor = at(importTable + index * 20);
|
|
bytes.writeUInt32LE(ilt, descriptor);
|
|
bytes.writeUInt32LE(nameRva, descriptor + 12);
|
|
bytes.writeUInt32LE(iat, descriptor + 16);
|
|
});
|
|
|
|
delayDlls.slice(0, delayCount).forEach((dll, index) => {
|
|
const { ilt, iat } = writeFunctions(dll.functions);
|
|
const nameRva = place(dll.name.length + 1);
|
|
bytes.write(`${dll.name}\0`, at(nameRva), "latin1");
|
|
const descriptor = at(delayTable + index * 32);
|
|
bytes.writeUInt32LE(options.delayAttributes ?? 1, descriptor);
|
|
bytes.writeUInt32LE(nameRva, descriptor + 4);
|
|
bytes.writeUInt32LE(iat, descriptor + 12);
|
|
bytes.writeUInt32LE(ilt, descriptor + 16);
|
|
});
|
|
|
|
if (options.splitAt === undefined) return bytes;
|
|
const head = options.splitAt;
|
|
const tail = sectionBytes - head;
|
|
const moved = SECTION_OFFSET + sectionBytes + SPLIT_GAP_BYTES;
|
|
const split = Buffer.alloc(moved + tail);
|
|
bytes.copy(split, 0, 0, SECTION_OFFSET + head);
|
|
split.fill(0xcc, SECTION_OFFSET + head, moved);
|
|
bytes.copy(split, moved, SECTION_OFFSET + head);
|
|
split.writeUInt16LE(2, 134);
|
|
split.writeUInt32LE(head, section + 8);
|
|
split.writeUInt32LE(head, section + 16);
|
|
const second = section + 40;
|
|
split.write(".idata2", second, "ascii");
|
|
split.writeUInt32LE(tail, second + 8);
|
|
split.writeUInt32LE(SECTION_RVA + head, second + 12);
|
|
split.writeUInt32LE(tail, second + 16);
|
|
split.writeUInt32LE(moved, second + 20);
|
|
split.writeUInt32LE(0x40000040, second + 36);
|
|
return split;
|
|
};
|
|
|
|
/** PE image whose optional header admits no import data directory. */
|
|
export const peWithoutImportTableFixture = (bits: 32 | 64 = 64): Buffer =>
|
|
peImportImage({ bits, importDirectory: false, delayDlls: [] });
|
|
|
|
/** Add a file-backed PE32+ range while preserving the ordinary low-RVA payload. */
|
|
const withHighImportRange = (
|
|
source: Buffer,
|
|
rva: number,
|
|
data: Buffer,
|
|
): Buffer => {
|
|
const offset = 0x1000;
|
|
const bytes = Buffer.alloc(offset + data.length);
|
|
source.copy(bytes);
|
|
data.copy(bytes, offset);
|
|
bytes.writeUInt16LE(2, 134);
|
|
const section = 152 + 240 + 40;
|
|
bytes.write(".high", section, "ascii");
|
|
bytes.writeUInt32LE(data.length, section + 8);
|
|
bytes.writeUInt32LE(rva, section + 12);
|
|
bytes.writeUInt32LE(data.length, section + 16);
|
|
bytes.writeUInt32LE(offset, section + 20);
|
|
bytes.writeUInt32LE(0x40000040, section + 36);
|
|
return bytes;
|
|
};
|
|
|
|
/** Directory descriptor/terminator whose mapped range crosses the uint32 RVA end. */
|
|
export const peImportDirectoryAtRva = (
|
|
kind: "standard" | "delay",
|
|
rva: number,
|
|
): Buffer => {
|
|
const source = peImportImage({
|
|
delayDlls:
|
|
kind === "delay"
|
|
? [{ name: "ADVAPI32.dll", functions: [{ name: "RegOpenKeyExW" }] }]
|
|
: [],
|
|
});
|
|
const width = kind === "standard" ? 20 : 32;
|
|
const directory = 152 + 112 + (kind === "standard" ? 1 : 13) * 8;
|
|
const oldOffset = peImportFixtureOffset(source.readUInt32LE(directory));
|
|
const bytes = withHighImportRange(
|
|
source,
|
|
rva,
|
|
source.subarray(oldOffset, oldOffset + 2 * width),
|
|
);
|
|
bytes.writeUInt32LE(rva, directory);
|
|
return bytes;
|
|
};
|
|
|
|
/** A directory starts in a gap, or its first descriptor is followed by a gap. */
|
|
export const peImportDirectoryMappingStops = (
|
|
kind: "standard" | "delay",
|
|
retained: 0 | 1,
|
|
): Buffer => {
|
|
const bytes = peImportDirectoryAtRva(kind, 0x40000000);
|
|
const section = 152 + 240 + 40;
|
|
const size = retained * (kind === "standard" ? 20 : 32);
|
|
bytes.writeUInt32LE(size, section + 8);
|
|
bytes.writeUInt32LE(size, section + 16);
|
|
return bytes;
|
|
};
|
|
|
|
/** A DLL name whose NUL lies outside the uint32 RVA space. */
|
|
export const peImportNameAtRva = (rva: number): Buffer => {
|
|
const bytes = withHighImportRange(
|
|
peImportImage(),
|
|
rva,
|
|
Buffer.from("KERNEL32.dll\0", "latin1"),
|
|
);
|
|
bytes.writeUInt32LE(rva, peImportFixtureOffset(0x1000) + 12);
|
|
return bytes;
|
|
};
|
|
|
|
/** Later import section behind an empty or suspicious first section header. */
|
|
export const peImportLeadingSectionImage = (suspicious: boolean): Buffer => {
|
|
const bytes = peImportImage();
|
|
const section = 152 + 240;
|
|
bytes.copy(bytes, section + 40, section, section + 40);
|
|
bytes.fill(0, section, section + 40);
|
|
bytes.writeUInt16LE(2, 134);
|
|
if (suspicious) {
|
|
bytes.writeUInt32LE(0x20000000, section + 8);
|
|
bytes.writeUInt32LE(0x20000000, section + 12);
|
|
bytes.writeUInt32LE(0xfffffe00, section + 20);
|
|
}
|
|
return bytes;
|
|
};
|
|
|
|
/** Independent PE32+ mappings with imports in the last section, for capacity checks. */
|
|
export const peImportManySectionsImage = (
|
|
sectionCount: number,
|
|
functionCount: number,
|
|
): Buffer => {
|
|
const rawSize = 0x20000;
|
|
const original = peImportImage({
|
|
sectionBytes: rawSize,
|
|
dlls: [
|
|
{
|
|
name: "KERNEL32.dll",
|
|
functions: Array.from({ length: functionCount }, (_, i) => ({
|
|
name: `Function${String(i).padStart(4, "0")}`,
|
|
})),
|
|
},
|
|
],
|
|
});
|
|
const optional = 152;
|
|
const sectionStart = optional + 240;
|
|
const headerBytes = Math.ceil((sectionStart + sectionCount * 40) / 512) * 512;
|
|
const finalRaw = headerBytes + (sectionCount - 1) * 512;
|
|
const firstRva = Math.ceil(headerBytes / 0x1000) * 0x1000;
|
|
const finalRva = firstRva + (sectionCount - 1) * 0x1000;
|
|
const delta = finalRva - SECTION_RVA;
|
|
const bytes = Buffer.alloc(finalRaw + rawSize);
|
|
original.copy(bytes, 0, 0, sectionStart);
|
|
bytes.writeUInt16LE(sectionCount, 134);
|
|
bytes.writeUInt32LE(0x1000, optional + 32);
|
|
bytes.writeUInt32LE(512, optional + 36);
|
|
bytes.writeUInt32LE(headerBytes, optional + 60);
|
|
bytes.writeUInt32LE(finalRva, optional + 112 + 8);
|
|
bytes.writeUInt32LE(finalRva + rawSize, optional + 56);
|
|
for (let i = 0; i < sectionCount; i++) {
|
|
const at = sectionStart + i * 40;
|
|
const size = i === sectionCount - 1 ? rawSize : 512;
|
|
bytes.write(i === sectionCount - 1 ? ".idata" : ".dummy", at, "ascii");
|
|
bytes.writeUInt32LE(size, at + 8);
|
|
bytes.writeUInt32LE(firstRva + i * 0x1000, at + 12);
|
|
bytes.writeUInt32LE(size, at + 16);
|
|
bytes.writeUInt32LE(headerBytes + i * 512, at + 20);
|
|
bytes.writeUInt32LE(0x40000040, at + 36);
|
|
}
|
|
const data = Buffer.from(original.subarray(SECTION_OFFSET));
|
|
const ilt = data.readUInt32LE(0);
|
|
const iat = data.readUInt32LE(16);
|
|
for (const field of [0, 12, 16])
|
|
data.writeUInt32LE(data.readUInt32LE(field) + delta, field);
|
|
for (const table of [ilt, iat])
|
|
for (let i = 0; i < functionCount; i++) {
|
|
const at = table - SECTION_RVA + i * 8;
|
|
data.writeBigUInt64LE(data.readBigUInt64LE(at) + BigInt(delta), at);
|
|
}
|
|
data.copy(bytes, finalRaw);
|
|
return bytes;
|
|
};
|