From dba4178465819aaf4de9ed5bd5ac0e7f754d15ab Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:25:30 +0800 Subject: [PATCH] fix: skip unrelated kernel scans during ELF layout inspection --- bridge/pwntools/layout_impl.py | 22 +++++++++- docs/binary-diagnostics.md | 10 +++++ scripts/verify-binary-layout.mjs | 75 ++++++++++++++++++++++++++------ 3 files changed, 92 insertions(+), 15 deletions(-) diff --git a/bridge/pwntools/layout_impl.py b/bridge/pwntools/layout_impl.py index 57ba9880ee..99cc30d440 100644 --- a/bridge/pwntools/layout_impl.py +++ b/bridge/pwntools/layout_impl.py @@ -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 diff --git a/docs/binary-diagnostics.md b/docs/binary-diagnostics.md index c4779846e1..b974ca32c9 100644 --- a/docs/binary-diagnostics.md +++ b/docs/binary-diagnostics.md @@ -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 diff --git a/scripts/verify-binary-layout.mjs b/scripts/verify-binary-layout.mjs index e4718e1df6..a6463d66c5 100644 --- a/scripts/verify-binary-layout.mjs +++ b/scripts/verify-binary-layout.mjs @@ -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);