Files
reverse-engineering-anything/src/go/GoElfImage.ts
T
morlutoandKaoru0822 bf54c46264 fix(electron): share executable architecture facts for fuse inspection
Own ELF machine and canonical family in the fixed-header parser. Fuse inspection consumes validated ELF facts and complete COFF machine values while retaining null for unknown or incomplete headers. Binary target, MIPS and Go consumers reuse the parsed facts.

Adapt public CLI controls and fixture builders from #4788; extend x86, ELF byte order, malformed and unknown headers, MCP projection and byte preservation coverage. 430 focused tests passed, one Windows-only skip. Five authored inert fixtures have full actual CLI/stdio MCP Evidence parity; real Linux/Windows Electron apps remain unverified.

Fixes #4786

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-17 21:10:07 +08:00

342 lines
11 KiB
TypeScript

import {
declaredFileRange,
GoBinaryFormatFailure,
GoBinaryReader,
mappedFileOffset,
type GoBinaryContainer,
type GoFileMapping,
type GoImageSection,
type GoZeroFillMapping,
} from "./GoBinaryContainer.js";
import { readElfFixedHeader } from "../domain/elfFixedHeader.js";
/** Read ELF build-info sections and file-backed program mappings. */
export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
const fixedHeader = readElfFixedHeader(bytes);
if (!fixedHeader.ok)
throw new GoBinaryFormatFailure("malformed", fixedHeader.error);
const { elfClass: bits, headerSize } = fixedHeader.value;
const reader = new GoBinaryReader(
bytes,
fixedHeader.value.byteOrder === "little",
);
const wide = bits === 64;
const phOffset = reader.word(wide ? 32 : 28, bits);
const shOffset = reader.word(wide ? 40 : 32, bits);
const phWidth = reader.u16(wide ? 54 : 42);
const shWidth = reader.u16(wide ? 58 : 46);
let phCount = reader.u16(wide ? 56 : 44);
let shCount = reader.u16(wide ? 60 : 48);
let namesIndex = reader.u16(wide ? 62 : 50);
if (
(phCount !== 0 && phOffset < BigInt(headerSize)) ||
(shOffset !== 0n && shOffset < BigInt(headerSize))
)
throw new GoBinaryFormatFailure(
"malformed",
"ELF header table overlaps the native header",
);
if (
shOffset !== 0n &&
(shCount === 0 || phCount === 0xffff || namesIndex === 0xffff)
) {
if (shWidth < (wide ? 64 : 40))
throw new GoBinaryFormatFailure(
"malformed",
"ELF extended section header width is invalid",
);
const first = reader.fileRange(
shOffset,
BigInt(shWidth),
"ELF section zero",
).offset;
if (reader.u32(first + 4) !== 0)
throw new GoBinaryFormatFailure(
"malformed",
"ELF initial section for extended numbering must have SHT_NULL type",
);
if (shCount === 0) {
const count = reader.word(first + (wide ? 32 : 20), bits);
if (count < 0xff00n || count > BigInt(Number.MAX_SAFE_INTEGER))
throw new GoBinaryFormatFailure(
"malformed",
"ELF extended section count is invalid",
);
shCount = Number(count);
}
if (phCount === 0xffff) {
phCount = reader.u32(first + (wide ? 44 : 28));
if (phCount < 0xffff)
throw new GoBinaryFormatFailure(
"malformed",
"ELF extended program count is below its reserved range",
);
}
if (namesIndex === 0xffff) {
namesIndex = reader.u32(first + (wide ? 40 : 24));
if (namesIndex < 0xff00)
throw new GoBinaryFormatFailure(
"malformed",
"ELF extended section name index is below its reserved range",
);
}
} else if (
shOffset === 0n &&
(shCount !== 0 || namesIndex !== 0 || phCount === 0xffff)
) {
throw new GoBinaryFormatFailure(
"malformed",
"ELF section table is missing",
);
}
if (phCount !== 0 && phWidth < (wide ? 56 : 32))
throw new GoBinaryFormatFailure(
"malformed",
"ELF program header width is invalid",
);
const phStart = reader.table(
phOffset,
phCount,
phWidth,
"ELF program headers",
);
const mappings: GoFileMapping[] = [];
const zeroFills: GoZeroFillMapping[] = [];
const sections: GoImageSection[] = [];
const fallback: GoFileMapping[] = [];
for (let index = 0; index < phCount; index++) {
const offset = phStart + index * phWidth;
if (reader.u32(offset) !== 1) continue;
const flags = reader.u32(offset + (wide ? 4 : 24));
const fileOffset = reader.word(offset + (wide ? 8 : 4), bits);
const address = reader.word(offset + (wide ? 16 : 8), bits);
const size = reader.word(offset + (wide ? 32 : 16), bits);
const memorySize = reader.word(offset + (wide ? 40 : 20), bits);
if (size > memorySize || address + memorySize > 1n << BigInt(bits))
throw new GoBinaryFormatFailure(
"malformed",
"ELF load segment sizes or addresses are invalid",
);
const mapping = {
...(size === 0n
? { offset: 0, size: 0 }
: reader.fileRange(fileOffset, size, "ELF load segment")),
address,
};
if (mapping.size !== 0) mappings.push(mapping);
if (memorySize > size)
zeroFills.push({ address: address + size, size: memorySize - size });
if (mapping.size !== 0 && (flags & 3) === 2) fallback.push(mapping);
}
let search: GoFileMapping | null = null;
if (shCount !== 0) {
if (shWidth < (wide ? 64 : 40) || namesIndex >= shCount)
throw new GoBinaryFormatFailure(
"malformed",
"ELF section width or name-table index is invalid",
);
const shStart = reader.table(
shOffset,
shCount,
shWidth,
"ELF section headers",
);
if (namesIndex !== 0) {
const namesHeader = shStart + namesIndex * shWidth;
if (reader.u32(namesHeader + 4) !== 3)
throw new GoBinaryFormatFailure(
"malformed",
"ELF section name table is not a string table",
);
const names = reader.fileRange(
reader.word(namesHeader + (wide ? 24 : 16), bits),
reader.word(namesHeader + (wide ? 32 : 20), bits),
"ELF section names",
);
for (let index = 0; index < shCount; index++) {
const offset = shStart + index * shWidth;
const nameOffset = reader.u32(offset);
if (nameOffset >= names.size)
throw new GoBinaryFormatFailure(
"malformed",
"ELF section name offset is outside its string table",
);
recordElfSection(sections, {
bytes,
names,
nameOffset,
reader,
bits,
wide,
header: offset,
});
// Comparing the complete fixed name avoids quadratic scans through malicious string tables.
const expectedName = Buffer.from(".go.buildinfo\0");
if (
expectedName.length > names.size - nameOffset ||
!bytes
.subarray(
names.offset + nameOffset,
names.offset + nameOffset + expectedName.length,
)
.equals(expectedName)
)
continue;
if (search !== null)
throw new GoBinaryFormatFailure(
"malformed",
"ELF contains ambiguous build-info sections",
);
if (reader.u32(offset + 4) !== 1)
throw new GoBinaryFormatFailure(
"malformed",
"ELF build-info section is not file-backed data",
);
if ((reader.word(offset + 8, bits) & 0x800n) !== 0n)
throw new GoBinaryFormatFailure(
"unsupported",
"Compressed ELF build-info sections are unsupported",
);
const address = reader.word(offset + (wide ? 16 : 12), bits);
search = {
...reader.fileRange(
reader.word(offset + (wide ? 24 : 16), bits),
reader.word(offset + (wide ? 32 : 20), bits),
"ELF build-info section",
),
address,
};
if (
mappedFileOffset(mappings, address, search.size, zeroFills) !==
search.offset
)
throw new GoBinaryFormatFailure(
"malformed",
"ELF build-info section disagrees with its load mapping",
);
}
}
}
return {
format: "elf",
architecture: goArchitectureForElfMachine(fixedHeader.value.machine),
bits,
byte_order: reader.little ? "little" : "big",
mappings,
zero_fills: zeroFills,
sections,
search: search === null ? fallback : [search],
};
};
const recordElfSection = (
sections: GoImageSection[],
input: {
readonly bytes: Buffer;
readonly names: { readonly offset: number; readonly size: number };
readonly nameOffset: number;
readonly reader: GoBinaryReader;
readonly bits: 32 | 64;
readonly wide: boolean;
readonly header: number;
},
): void => {
const decoded = elfSectionName(input.bytes, input.names, input.nameOffset);
const header = {
reader: input.reader,
bits: input.bits,
wide: input.wide,
header: input.header,
};
switch (decoded.status) {
case "named":
sections.push(elfImageSection({ ...header, name: decoded.name }));
return;
case "truncated": {
// Names past this window stay out of .dep-v0 matching. Read-only
// bytes can still hold the only compiler path.
const section = elfImageSection({ ...header, name: "" });
if (section.read_only) sections.push(section);
return;
}
case "unnamed":
return;
default: {
const unreachable: never = decoded;
return unreachable;
}
}
};
const elfSectionName = (
bytes: Buffer,
names: { readonly offset: number; readonly size: number },
nameOffset: number,
):
| { readonly status: "named"; readonly name: string }
| { readonly status: "unnamed" }
| { readonly status: "truncated" } => {
const start = names.offset + nameOffset;
const limit = Math.min(start + 64, names.offset + names.size);
const nul = bytes.subarray(start, limit).indexOf(0);
if (nul === 0) return { status: "unnamed" };
if (nul < 0) return { status: "truncated" };
return {
status: "named",
name: bytes.toString("latin1", start, start + nul),
};
};
const elfImageSection = ({
reader,
bits,
wide,
header,
name,
}: {
readonly reader: GoBinaryReader;
readonly bits: 32 | 64;
readonly wide: boolean;
readonly header: number;
readonly name: string;
}): GoImageSection => {
const type = reader.u32(header + 4);
const flags = reader.word(header + 8, bits);
const declared = declaredFileRange(
reader.bytes.length,
reader.word(header + (wide ? 24 : 16), bits),
reader.word(header + (wide ? 32 : 20), bits),
);
// SHT_NOBITS offsets and sizes do not describe stored bytes.
const range =
type === SHT_NOBITS ? { ...declared, file_backed: false } : declared;
const compressed = (flags & 0x800n) !== 0n;
const readOnly =
type === 1 &&
range.file_backed &&
!compressed &&
(flags & 1n) === 0n &&
(flags & 4n) === 0n &&
(flags & 2n) !== 0n;
return { name, ...range, read_only: readOnly, compressed };
};
const SHT_NOBITS = 8;
/** Go build-info vocabulary, including families outside REA target admission. */
const goArchitectureForElfMachine = (machine: number): string => {
const names = new Map([
[3, "386"],
[8, "mips"],
[20, "ppc"],
[21, "ppc64"],
[22, "s390"],
[40, "arm"],
[62, "amd64"],
[183, "arm64"],
[243, "riscv"],
[258, "loongarch"],
]);
return names.get(machine) ?? `elf-machine-${machine}`;
};