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:
authored and GitHub committed 2026-10-25 14:18:44 +08:00
1 parent 0a2b7af905
commit 2c38becc4e
10 files changed
+291 -25

No files matched your search

+175 -2
View File
@@ -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")));