fix: skip unrelated kernel scans during ELF layout inspection

This commit is contained in:
morluto committed 2026-10-24 02:29:08 +08:00
1 parent 06fe7eecbc
commit dba4178465
3 files changed
+92 -15

No files matched your search

+20 -2
View File
@@ -132,6 +132,11 @@ def inspect_elf(path, cache):
try:
headers = ELFFile(io.BytesIO(content))
section_count = headers.num_sections()
if section_count:
location(headers.header.e_shoff, section_count * headers.header.e_shentsize, len(content))
segment_count = headers.num_segments()
if segment_count:
location(headers.header.e_phoff, segment_count * headers.header.e_phentsize, len(content))
if headers.header.e_shstrndx == 0xffff and section_count == 0:
raise LayoutFailure("format", "ELF extended section-name index requires a declared section-zero header.")
raw_name_index = headers.header.e_shstrndx
@@ -147,9 +152,21 @@ def inspect_elf(path, cache):
if names_header.sh_type != "SHT_STRTAB":
raise LayoutFailure("format", f"ELF section-name table {section_name_index} is not SHT_STRTAB.")
location(names_header.sh_offset, names_header.sh_size, len(content))
except ELFError as error:
except (ELFError, ValueError, OverflowError, AssertionError, IndexError) as error:
raise LayoutFailure("format", str(error)) from error
class LayoutELF(ELF):
def search(self, needle, writable=False, executable=False):
# This adapter exposes structural facts, GOT/PLT and mitigation
# indicators, not kernel config/version discovery. Upstream's
# constructor searches virtual memory for those unrelated
# heuristics, materializing NOBITS tails and interpreting
# ordinary IKCFG_ST data as a compressed kernel config.
return iter(())
def _populate_kernel_version(self):
# Kernel banner decoding is outside this report too.
return None
def _get_section_name(self, header):
# SHN_UNDEF declares no names; upstream would otherwise read
# strings at the inactive section-zero offset.
@@ -294,6 +311,7 @@ def inspect_elf(path, cache):
"Relocatable relocation targets with SHN_UNDEF or inactive SHT_NULL headers retain their reported section index/offset as unknown; they do not establish an associated target section.",
"GOT/PLT are derived upstream convenience maps; PLT inference can emulate selected instructions in Unicorn. They do not establish target runtime behavior; aliases may collapse and warnings may indicate incomplete coverage. Their completeness is unknown.",
"Mitigations are upstream static heuristics, not runtime protection. ET_DYN does not prove an executable; absent canary symbols do not prove every function unprotected.",
"Kernel configuration/version discovery is not performed; these memory-content scans do not supply the reported structural, linkage or mitigation facts.",
"This profile inspects ELF EXEC/DYN/REL layout only. It does not analyze recorded cores, resolve loaded libraries, launch the target as a host process or start a debugger.",
"Symbol/relocation inventories reflect original section tables. A sectionless image can still report dynamic dependency names; missing tables do not prove absence of dynamic symbols or relocations.",
*(["Absent section-name table: section-dependent RELRO/canary heuristics may be incomplete; protection coverage is unknown."] if all_sections and section_name_index == 0 else []),
@@ -302,7 +320,7 @@ def inspect_elf(path, cache):
}
except LayoutFailure:
raise
except (ELFError, ValueError, AssertionError, IndexError) as error:
except (ELFError, ValueError, OverflowError, AssertionError, IndexError) as error:
raise LayoutFailure("format", "Selected ELF failed unchanged upstream structural parsing: " + str(error)) from error
+10
View File
@@ -29,6 +29,16 @@ the selected object as a host process, or requests runtime library resolution. T
uses isolated mode and an owned cache. Exact upstream profiles are recorded in
[upstream provenance](https://github.com/morluto/rea/blob/main/third_party/pwntools/README.md).
The adapter skips pwntools kernel configuration and version discovery: those
memory-content scans supply none of this report's fields. Ordinary `IKCFG_ST`
data is not interpreted as a kernel-config request, and virtual zero-filled
LOAD tails are reported from their headers without materializing them for that
scan. GOT/PLT inference and the advertised static mitigation indicators still
use the pinned upstream implementations. Header-table file ranges are checked
before dependency seeks. Structural inventory and the convenience-map engine
still parse the snapshot separately; eliminating that remaining duplication
requires preserving the upstream GOT/PLT and mitigation facts.
## Interpreting results
- Artifact path, SHA-256 and size identify original selected bytes. All parsing
+62 -13
View File
@@ -329,6 +329,48 @@ try {
}
}
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")));
const markerOffset = markerBytes.indexOf(Buffer.from("source-owned"));
assert.ok(markerOffset >= 0);
markerBytes.write("IKCFG_ST", markerOffset, "ascii");
const markerPath = join(root.path, "ordinary-kernel-marker");
await writeFile(markerPath, markerBytes);
for (const mode of ["cli", "mcp"]) {
const value = await inspect(mode, markerPath);
for (const field of [
"sections",
"segments",
"symbols",
"relocations",
"linkage",
"mitigations",
])
assert.deepEqual(value[field], protectedReport[field]);
assert.equal(
value.artifact.sha256,
createHash("sha256").update(markerBytes).digest("hex"),
);
cases++;
}
assert.deepEqual(await readFile(markerPath), markerBytes);
// File-table offsets are validated before narrower dependency seek APIs.
const overflowingTable = Buffer.from(markerBytes);
overflowingTable.writeBigUInt64LE(0x8000000000000040n, 32);
const overflowingTablePath = join(root.path, "overflowing-program-table");
await writeFile(overflowingTablePath, overflowingTable);
for (const mode of ["cli", "mcp"]) {
const error = await inspect(mode, overflowingTablePath, "invalid_input");
assert.equal(error.code, "invalid_request");
assert.ok(
error.details.issues.some(
({ path, reason }) => path[0] === "path" && reason === "invalid_format",
),
);
cases++;
}
assert.deepEqual(await readFile(overflowingTablePath), overflowingTable);
const fileSymbols = protectedReport.symbols.filter(
(symbol) => symbol.type === "STT_FILE",
);
@@ -927,9 +969,8 @@ try {
sectionHeavy.writeUInt16LE(sectionCount, 60);
const sectionHeavyPath = join(root.path, "section-heavy.o");
await writeFile(sectionHeavyPath, sectionHeavy);
// pwntools materializes each PT_LOAD zero-filled tail during inspection.
// One allocation larger than the whole address-space budget fails promptly;
// a large section count instead races CPU work against the command deadline.
// A structural report must retain a large virtual zero tail without allocating it.
// Bootstrap controls above retain genuine MemoryError/ENOMEM classification.
const memoryHeavy = Buffer.from(await readFile(join(root.path, "protected")));
const memoryPhOffset = Number(memoryHeavy.readBigUInt64LE(32));
const memoryPhSize = memoryHeavy.readUInt16LE(54);
@@ -944,7 +985,9 @@ try {
assert.notEqual(lastLoadIndex, undefined);
const memorySizeOffset = memoryPhOffset + lastLoadIndex * memoryPhSize + 40;
memoryHeavy.writeBigUInt64LE(
memoryHeavy.readBigUInt64LE(memorySizeOffset) + 128n * 1024n * 1024n,
memoryHeavy.readBigUInt64LE(memorySizeOffset) +
3n * 1024n * 1024n * 1024n +
1n,
memorySizeOffset,
);
const memoryHeavyPath = join(root.path, "memory-zero-tail");
@@ -963,21 +1006,27 @@ try {
try {
await memoryClient.connect(memoryTransport);
for (const mode of ["cli", "mcp"]) {
const error = await inspect(
const value = await inspect(
mode,
memoryHeavyPath,
"resource_constraint",
undefined,
memoryEnvironment,
memoryClient,
);
assert.equal(error.code, "resource_constraint");
assert.equal(error.details.resource, "memory");
assert.ok(
error.details.reported_limits === null ||
error.details.reported_limits.address_space_bytes === 100663296,
const original = protectedReport.segments[lastLoadIndex];
assert.deepEqual(value.segments[lastLoadIndex], {
...original,
memory_size: `0x${memoryHeavy.readBigUInt64LE(memorySizeOffset).toString(16)}`,
});
assert.deepEqual(value.sections, protectedReport.sections);
assert.deepEqual(value.symbols, protectedReport.symbols);
assert.deepEqual(value.relocations, protectedReport.relocations);
assert.deepEqual(value.linkage, protectedReport.linkage);
assert.deepEqual(value.mitigations, protectedReport.mitigations);
assert.equal(
value.artifact.sha256,
createHash("sha256").update(memoryHeavy).digest("hex"),
);
assert.equal(error.details.captured_output.truncated, false);
assert.ok(error.remediation.action.includes("memory"));
cases++;
}
assert.deepEqual(await readFile(memoryHeavyPath), memoryHeavy);