mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
feat(binary-layout): admit ELF32 i386, ARM and MIPS layout profiles (#7052)
* feat(binary-layout): admit ELF32 i386, ARM and MIPS layout profiles inspect_binary_layout refused every ELF32 object, so router and IoT firmware (ARM32, MIPS32) got no layout facts at all. The pwntools adapter now admits EXEC/DYN/REL objects in these profiles: ELF64 x86-64, ELF32 i386, ELF32 little-endian ARM and ELF32 little- or big-endian MIPS. Header fields are read in the declared byte order, the ELF32 header is bounds-checked at 52 bytes, and pwntools' own reported architecture, class and byte order must match the admitted profile. Other machines, classes and byte orders keep an unsupported_target refusal that names the admitted profiles. The output schema admits exactly those architecture combinations, and the Evidence subject now names the reported machine instead of always claiming x86_64. verify:binary:layout cross-compiles i386, ARM, MIPS and MIPSel executables and relocatable objects (never executed) and checks every section name, address, offset and size against GNU readelf through CLI and MCP. The real-binary-layout workflow installs those toolchains. Refs #6934 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci(binary-layout): install target libc headers for ELF32 fixtures --no-install-recommends skips the libc6-dev-*-cross packages the cross compilers only recommend, so the i686 fixture fell back to the host's /usr/include and failed to find bits/libc-header-start.h. Install the four target libc development packages explicitly. Refs #6934 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(binary-layout): size the minimum symbol entry by ELF class The relocation reference check required a 24-byte Elf64_Sym entry, so every positive symbol reference in an ELF32 report was refused as unreadable output. Elf32_Sym is 16 bytes; take the minimum from the reported class. A 64-bit table with 16-byte entries is still refused. Refs #6934 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(binary-layout): compare ELF32 symbols and relocations with readelf --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
@@ -35,11 +35,27 @@ if (
|
||||
!isAbsolute(strace ?? "")
|
||||
)
|
||||
throw new Error(
|
||||
"verify:binary:layout requires Linux x64, absolute REA_PWNTOOLS_PYTHON (pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2) and REA_VERIFY_STRACE_COMMAND. gcc, ld and strip must be available. No target is executed.",
|
||||
"verify:binary:layout requires Linux x64, absolute REA_PWNTOOLS_PYTHON (pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2) and REA_VERIFY_STRACE_COMMAND. gcc, ld, strip and the i686/ARM/MIPS/MIPSel linux-gnu cross gcc and readelf must be available. No target is executed.",
|
||||
);
|
||||
const run = createVerifierRun();
|
||||
const execute = promisify(execFile);
|
||||
for (const command of ["gcc", "ld", "strip", "/usr/bin/prlimit", strace]) {
|
||||
const ELF32_TOOLCHAINS = [
|
||||
["i686-linux-gnu", "EM_386", "little"],
|
||||
["arm-linux-gnueabi", "EM_ARM", "little"],
|
||||
["mips-linux-gnu", "EM_MIPS", "big"],
|
||||
["mipsel-linux-gnu", "EM_MIPS", "little"],
|
||||
];
|
||||
for (const command of [
|
||||
"gcc",
|
||||
"ld",
|
||||
"strip",
|
||||
"/usr/bin/prlimit",
|
||||
strace,
|
||||
...ELF32_TOOLCHAINS.flatMap(([prefix]) => [
|
||||
`${prefix}-gcc`,
|
||||
`${prefix}-readelf`,
|
||||
]),
|
||||
]) {
|
||||
try {
|
||||
await execute(command, [command === strace ? "-V" : "--version"], {
|
||||
timeout: 10_000,
|
||||
@@ -328,6 +344,163 @@ try {
|
||||
cases++;
|
||||
}
|
||||
}
|
||||
// ELF32 profiles: cross-compiled, never executed, checked against GNU readelf.
|
||||
for (const [
|
||||
prefix,
|
||||
machine,
|
||||
byteOrder,
|
||||
kind,
|
||||
flags,
|
||||
] of ELF32_TOOLCHAINS.flatMap((toolchain) => [
|
||||
[...toolchain, "executable", ["-fPIE", "-pie"]],
|
||||
[...toolchain, "relocatable", ["-c", "-fPIC"]],
|
||||
])) {
|
||||
const path = join(root.path, `${prefix}-${kind}`);
|
||||
await execute(`${prefix}-gcc`, ["-O1", ...flags, source, "-o", path], {
|
||||
timeout: 30_000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
const { stdout } = await execute(`${prefix}-readelf`, ["-SWsr", path], {
|
||||
timeout: 10_000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
env: { ...process.env, LC_ALL: "C" },
|
||||
});
|
||||
const expectedSections = [
|
||||
...stdout.matchAll(
|
||||
/^\s*\[\s*(\d+)\]\s+(\S+)\s+\S+\s+([0-9a-f]+)\s+([0-9a-f]+)\s+([0-9a-f]+)\s+[0-9a-f]+\s+[A-Za-z]*\s+(\d+)\s+\d+\s+\d+\s*$/gm,
|
||||
),
|
||||
]
|
||||
.filter(([, index]) => index !== "0")
|
||||
.map(([, index, name, address, offset, size, link]) => ({
|
||||
index: Number(index),
|
||||
name,
|
||||
address: BigInt(`0x${address}`),
|
||||
offset: BigInt(`0x${offset}`),
|
||||
size: BigInt(`0x${size}`),
|
||||
link: Number(link),
|
||||
}));
|
||||
assert.ok(
|
||||
expectedSections.length > 0,
|
||||
`readelf listed no sections for ${path}`,
|
||||
);
|
||||
const expectedSymbols = [];
|
||||
for (const [, name, count, body] of stdout.matchAll(
|
||||
/^Symbol table '([^']+)' contains (\d+) entr(?:y|ies):\n((?:[^\n]+\n?)*)/gm,
|
||||
)) {
|
||||
const table = expectedSections.find((section) => section.name === name);
|
||||
assert.notEqual(table, undefined, `readelf symbol table absent: ${name}`);
|
||||
const rows = [...body.matchAll(/^\s*(\d+):\s+([0-9a-f]+)\s+(\d+)\s/gm)];
|
||||
assert.equal(rows.length, Number(count), `readelf symbol count: ${name}`);
|
||||
for (const [, index, value, size] of rows)
|
||||
expectedSymbols.push({
|
||||
table_index: table.index,
|
||||
entry_index: Number(index),
|
||||
value: BigInt(`0x${value}`),
|
||||
size: BigInt(size),
|
||||
});
|
||||
}
|
||||
assert.ok(
|
||||
expectedSymbols.length > 0,
|
||||
`readelf listed no symbols for ${path}`,
|
||||
);
|
||||
const expectedRelocations = [];
|
||||
for (const [, name, count, body] of stdout.matchAll(
|
||||
/^Relocation section '([^']+)' at offset 0x[0-9a-f]+ contains (\d+) entr(?:y|ies):\n((?:[^\n]+\n?)*)/gm,
|
||||
)) {
|
||||
const section = expectedSections.find((section) => section.name === name);
|
||||
assert.notEqual(
|
||||
section,
|
||||
undefined,
|
||||
`readelf relocation section absent: ${name}`,
|
||||
);
|
||||
const rows = [...body.matchAll(/^\s*([0-9a-f]+)\s+([0-9a-f]+)\s+\S+/gm)];
|
||||
assert.equal(
|
||||
rows.length,
|
||||
Number(count),
|
||||
`readelf relocation count: ${name}`,
|
||||
);
|
||||
for (const [entryIndex, [, offset, info]] of rows.entries()) {
|
||||
// ELF32 r_info uses the low byte for type and the upper 24 bits for
|
||||
// the symbol index. readelf has already decoded the file's byte order.
|
||||
const decodedInfo = BigInt(`0x${info}`);
|
||||
expectedRelocations.push({
|
||||
section_index: section.index,
|
||||
entry_index: entryIndex,
|
||||
reported_offset: BigInt(`0x${offset}`),
|
||||
type: Number(decodedInfo & 0xffn),
|
||||
symbol_table_index: section.link,
|
||||
symbol_index: Number(decodedInfo >> 8n),
|
||||
});
|
||||
}
|
||||
}
|
||||
assert.ok(
|
||||
expectedRelocations.length > 0,
|
||||
`readelf listed no relocations for ${path}`,
|
||||
);
|
||||
const bytes = await readFile(path);
|
||||
for (const mode of ["cli", "mcp"]) {
|
||||
const value = await inspect(mode, path);
|
||||
assert.deepEqual(value.architecture, {
|
||||
machine,
|
||||
bits: 32,
|
||||
byte_order: byteOrder,
|
||||
});
|
||||
assert.equal(
|
||||
value.image_type,
|
||||
kind === "executable" ? "ET_DYN" : "ET_REL",
|
||||
);
|
||||
for (const section of expectedSections) {
|
||||
const reported = value.sections[section.index];
|
||||
assert.equal(reported.name.display, section.name);
|
||||
assert.equal(BigInt(reported.address), section.address);
|
||||
assert.equal(BigInt(reported.offset), section.offset);
|
||||
assert.equal(BigInt(reported.size), section.size);
|
||||
}
|
||||
assert.equal(value.sections.length, expectedSections.length + 1);
|
||||
assert.deepEqual(
|
||||
value.symbols.map(({ table_index, entry_index, value, size }) => ({
|
||||
table_index,
|
||||
entry_index,
|
||||
value: BigInt(value),
|
||||
size: BigInt(size),
|
||||
})),
|
||||
expectedSymbols,
|
||||
`${mode} symbol values differ from readelf for ${path}`,
|
||||
);
|
||||
assert.deepEqual(
|
||||
value.relocations.map(
|
||||
({
|
||||
section_index,
|
||||
entry_index,
|
||||
reported_offset,
|
||||
type,
|
||||
symbol_table_index,
|
||||
symbol_index,
|
||||
}) => ({
|
||||
section_index,
|
||||
entry_index,
|
||||
reported_offset: BigInt(reported_offset),
|
||||
type,
|
||||
symbol_table_index,
|
||||
symbol_index,
|
||||
}),
|
||||
),
|
||||
expectedRelocations,
|
||||
`${mode} relocations differ from readelf for ${path}`,
|
||||
);
|
||||
if (kind === "executable") {
|
||||
assert.ok(value.segments.length > 0);
|
||||
assert.equal(value.linkage.interpreters.length, 1);
|
||||
assert.ok(
|
||||
value.linkage.needed_libraries.some(
|
||||
(library) => library.display === "libc.so.6",
|
||||
),
|
||||
);
|
||||
}
|
||||
assert.deepEqual(await readFile(path), bytes);
|
||||
cases++;
|
||||
}
|
||||
}
|
||||
const protectedReport = reports.get("protected");
|
||||
// Ordinary data markers are not a request to discover/decompress a kernel config.
|
||||
const markerBytes = Buffer.from(await readFile(join(root.path, "protected")));
|
||||
|
||||
Reference in New Issue
Block a user