Files
reverse-engineering-anything/src/domain/binaryTarget.fixture.ts
T
729ca2ab6a fix(ghidra): report unapplied PE CodeView PDB limitation (#2283)
* fix(ghidra): report unapplied PE CodeView PDB limitation (#2259)

When Ghidra analyzes a PE target that references companion debug symbols
in its CodeView (IMAGE_DEBUG_TYPE_CODEVIEW / RSDS or NB10) debug directory,
Ghidra's headless analyzer cannot automatically locate or apply the companion
PDB from isolated runtime directories without companion admission. Procedure
names remain analysis-generated (FUN_<address>).

Parse CodeView debug metadata (RSDS GUID/age and NB10 age) during PE target
resolution, and report an Evidence limitation informing callers and agents
that referenced debug symbols were not applied and procedure names are
analysis-generated.

Fixes #2259

* test(pe): group CodeView header coverage separately

* fix(ghidra): avoid contradictory PDB limitations

* fix(android): make DEX descriptor pattern portable

* fix(ci): isolate Linux Hopper process fixture inventory

* fix(comparison): recheck historical calls-only changes (#2223)

* fix(comparison): preserve incomplete function call evidence

Keep missing, unknown, partial and unresolved call relationships unknown in function comparisons and reconstruction claims. Aggregate IDA caller and callee coverage without overwriting uncertainty.

Validate with 144 focused tests, typecheck/lint, docs checks and test discovery. Real providers were not verified; the full headless IDA session suite requires unavailable Windows native controls.

* fix(comparison): recheck historical calls-only changes

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>

* style: format identity comparison and IDA runner

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: Lucien <164857476+dakjdakd@users.noreply.github.com>
2026-10-14 02:56:26 +08:00

174 lines
6.2 KiB
TypeScript

/** Build a minimal ELF header for binary-target parser tests. */
export const elf = (
class_: number,
endian: number,
machine: number,
): Buffer => {
const bytes = Buffer.alloc(class_ === 1 ? 52 : 64);
bytes.set([0x7f, 0x45, 0x4c, 0x46, class_, endian, 1]);
if (endian === 1) {
bytes.writeUInt16LE(machine, 18);
bytes.writeUInt32LE(1, 20);
bytes.writeUInt16LE(class_ === 1 ? 52 : 64, class_ === 1 ? 40 : 52);
} else {
bytes.writeUInt16BE(machine, 18);
bytes.writeUInt32BE(1, 20);
bytes.writeUInt16BE(class_ === 1 ? 52 : 64, class_ === 1 ? 40 : 52);
}
return bytes;
};
/** Build a minimal thin Mach-O header for binary-target parser tests. */
export const thinMach = (magic: number, cpu: number): Buffer => {
const bytes = Buffer.alloc(
magic === 0xfeedfacf || magic === 0xcffaedfe ? 32 : 28,
);
bytes.writeUInt32BE(magic, 0);
if (magic === 0xcefaedfe || magic === 0xcffaedfe) bytes.writeUInt32LE(cpu, 4);
else bytes.writeUInt32BE(cpu, 4);
return bytes;
};
/** Build a FAT Mach-O with in-bounds thin slices, or an explicitly truncated table. */
export const fat = (
cpus: readonly number[],
declared = cpus.length,
): Buffer => {
const tableSize = 8 + cpus.length * 20;
const slices = cpus.map((cpu) => {
const bytes = thinMach(0xfeedfacf, cpu);
bytes.writeUInt32BE(2, 12);
return bytes;
});
const sliceStart = Math.ceil(tableSize / 16) * 16;
const bytes = Buffer.alloc(
declared === cpus.length ? sliceStart + cpus.length * 32 : tableSize,
);
bytes.writeUInt32BE(0xcafebabe, 0);
bytes.writeUInt32BE(declared, 4);
cpus.forEach((cpu, index) => {
const entry = 8 + index * 20;
const offset = sliceStart + index * 32;
bytes.writeUInt32BE(cpu, entry);
bytes.writeUInt32BE(offset, entry + 8);
bytes.writeUInt32BE(32, entry + 12);
bytes.writeUInt32BE(4, entry + 16);
if (declared === cpus.length) slices[index]?.copy(bytes, offset);
});
return bytes;
};
export interface PeCodeViewFixture {
readonly type?: "RSDS" | "NB10";
readonly guid?: string;
readonly age?: number;
readonly pdbPath?: string;
}
/** Build a bounded PE image with optional managed metadata or CodeView debug data. */
export const pe = (
machine: number,
offset = 64,
characteristics = 0x0002,
managed = false,
codeView?: PeCodeViewFixture,
): Buffer => {
const optionalHeaderSize =
machine === 0x014c || machine === 0x01c0 ? 224 : 240;
const sectionTableOffset = offset + 24 + optionalHeaderSize;
const sectionCount = codeView !== undefined ? 1 : 0;
const rawDataOffset = 512;
let codeViewData: Buffer | undefined;
if (codeView !== undefined) {
const type = codeView.type ?? "RSDS";
const pdbPath = codeView.pdbPath ?? "app.pdb";
const age = codeView.age ?? 1;
const pathBuf = Buffer.from(`${pdbPath}\0`, "utf8");
if (type === "RSDS") {
codeViewData = Buffer.alloc(24 + pathBuf.length);
codeViewData.write("RSDS", 0, "ascii");
const guid = codeView.guid ?? "01234567-89ab-cdef-0123-456789abcdef";
const parts = guid.split("-");
if (parts.length === 5) {
codeViewData.writeUInt32LE(parseInt(parts[0]!, 16), 4);
codeViewData.writeUInt16LE(parseInt(parts[1]!, 16), 8);
codeViewData.writeUInt16LE(parseInt(parts[2]!, 16), 10);
Buffer.from(parts[3]!, "hex").copy(codeViewData, 12);
Buffer.from(parts[4]!, "hex").copy(codeViewData, 14);
}
codeViewData.writeUInt32LE(age, 20);
pathBuf.copy(codeViewData, 24);
} else {
codeViewData = Buffer.alloc(16 + pathBuf.length);
codeViewData.write("NB10", 0, "ascii");
codeViewData.writeUInt32LE(0, 4);
codeViewData.writeUInt32LE(0, 8);
codeViewData.writeUInt32LE(age, 12);
pathBuf.copy(codeViewData, 16);
}
}
const minSize =
codeViewData !== undefined
? rawDataOffset + 28 + codeViewData.length + 32
: Math.max(512, offset + 24 + optionalHeaderSize);
const bytes = Buffer.alloc(minSize);
bytes.write("MZ", 0, "ascii");
bytes.writeUInt32LE(offset, 0x3c);
bytes.write("PE\0\0", offset, "binary");
bytes.writeUInt16LE(machine, offset + 4);
bytes.writeUInt16LE(sectionCount, offset + 6);
bytes.writeUInt16LE(optionalHeaderSize, offset + 20);
bytes.writeUInt16LE(characteristics, offset + 22);
const optionalHeader = offset + 24;
const pe32 = machine === 0x014c || machine === 0x01c0;
bytes.writeUInt16LE(pe32 ? 0x10b : 0x20b, optionalHeader);
const directoryOffset = pe32 ? 96 : 112;
bytes.writeUInt32LE(16, optionalHeader + directoryOffset - 4);
if (managed) {
bytes.writeUInt32LE(0x2000, optionalHeader + directoryOffset + 14 * 8);
bytes.writeUInt32LE(72, optionalHeader + directoryOffset + 14 * 8 + 4);
}
if (codeViewData !== undefined) {
const debugDirRva = 0x1000;
const debugDirSize = 28;
bytes.writeUInt32LE(debugDirRva, optionalHeader + directoryOffset + 6 * 8);
bytes.writeUInt32LE(
debugDirSize,
optionalHeader + directoryOffset + 6 * 8 + 4,
);
bytes.write(".rdata\0\0", sectionTableOffset, "ascii");
bytes.writeUInt32LE(0x1000, sectionTableOffset + 8);
bytes.writeUInt32LE(0x1000, sectionTableOffset + 12);
bytes.writeUInt32LE(512, sectionTableOffset + 16);
bytes.writeUInt32LE(rawDataOffset, sectionTableOffset + 20);
bytes.writeUInt32LE(0x40000040, sectionTableOffset + 36);
bytes.writeUInt32LE(2, rawDataOffset + 12);
bytes.writeUInt32LE(codeViewData.length, rawDataOffset + 16);
bytes.writeUInt32LE(debugDirRva + 28, rawDataOffset + 20);
bytes.writeUInt32LE(rawDataOffset + 28, rawDataOffset + 24);
codeViewData.copy(bytes, rawDataOffset + 28);
}
return bytes;
};
/** Build a DOS MZ image with a 32-byte header and a 16-bit entry point. */
export const dosMz = (moduleSize = 128): Buffer => {
const size = 32 + moduleSize;
const bytes = Buffer.alloc(size);
bytes.write("MZ", 0, "ascii");
bytes.writeUInt16LE(size % 512, 2);
bytes.writeUInt16LE(Math.ceil(size / 512), 4);
bytes.writeUInt16LE(2, 8);
bytes.writeUInt16LE(0xffff, 12);
bytes.writeUInt16LE(28, 24);
bytes.set([0xb8, 0x00, 0x4c, 0xcd, 0x21], 32);
// This is module data, not an e_lfanew field in a Windows DOS stub.
if (size >= 64) bytes.writeUInt32LE(0xfedcba98, 60);
return bytes;
};