diff --git a/src/application/MipsElfMetadata.ts b/src/application/MipsElfMetadata.ts index cc4e3cc35f..33ed40fb63 100644 --- a/src/application/MipsElfMetadata.ts +++ b/src/application/MipsElfMetadata.ts @@ -1,3 +1,4 @@ +import { readElfRawTableDeclarations } from "../domain/elfRawTableDeclarations.js"; import type { FileHandle } from "node:fs/promises"; import type { MipsAbiFlags, @@ -91,12 +92,15 @@ export const readMipsElfMetadata = async ( if (fixedHeader.value.headerSize !== 52) return err("invalid MIPS ELF32 version or header size"); + const raw = readElfRawTableDeclarations(bytes, fixedHeader.value); + if (!raw.ok) return err(raw.error.message); + const { programs, sections } = raw.value; const tables = [ { label: "program header table", - offset: u32(bytes, 28), - count: u16(bytes, 44), - stride: u16(bytes, 42), + offset: Number(programs.offset), + count: programs.count, + stride: programs.width, entrySize: 32, typeOffset: 0, dataOffset: 4, @@ -106,9 +110,9 @@ export const readMipsElfMetadata = async ( }, { label: "section header table", - offset: u32(bytes, 32), - count: u16(bytes, 48), - stride: u16(bytes, 46), + offset: Number(sections.offset), + count: sections.count, + stride: sections.width, entrySize: 40, typeOffset: 4, dataOffset: 16, @@ -119,7 +123,7 @@ export const readMipsElfMetadata = async ( ]; // Extended numbering needs a separate section-zero reader. Refuse it rather // than silently treating an unexamined table as having no ABI declarations. - if (u16(bytes, 44) === 0xffff || u16(bytes, 48) >= 0xff00) + if (programs.count === 0xffff || sections.count >= 0xff00) return err("extended or reserved MIPS ELF table numbering is unsupported"); let record: Buffer | undefined; let sharedLoad: MipsElfMetadata["sharedLoad"]; diff --git a/src/artifacts/nodeSea/NodeSeaLocation.ts b/src/artifacts/nodeSea/NodeSeaLocation.ts index 9f90deb25c..432c333cb8 100644 --- a/src/artifacts/nodeSea/NodeSeaLocation.ts +++ b/src/artifacts/nodeSea/NodeSeaLocation.ts @@ -1,3 +1,4 @@ +import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js"; import { createHash } from "node:crypto"; import { parsePeResources } from "../../native/pe/PeResourceParser.js"; import { @@ -323,16 +324,22 @@ const locateElfNotes = async ( Math.min(carrier.fileSize, header.value.headerSize), ); if (fields === undefined) return []; - const phoff = elf64 - ? readU64(fields, 32, little) - : readU32(fields, 28, little); - const shoff = elf64 - ? readU64(fields, 40, little) - : readU32(fields, 32, little); - const phentsize = readU16(fields, elf64 ? 54 : 42, little); - const phnum = readU16(fields, elf64 ? 56 : 44, little); - const shentsize = readU16(fields, elf64 ? 58 : 46, little); - const shnum = readU16(fields, elf64 ? 60 : 48, little); + const raw = readElfRawTableDeclarations(fields, header.value); + if (!raw.ok) return []; + const { programs: programDeclarations, sections: sectionDeclarations } = + raw.value; + const phoff = + programDeclarations.offset <= BigInt(Number.MAX_SAFE_INTEGER) + ? Number(programDeclarations.offset) + : undefined; + const shoff = + sectionDeclarations.offset <= BigInt(Number.MAX_SAFE_INTEGER) + ? Number(sectionDeclarations.offset) + : undefined; + const phentsize = programDeclarations.width; + const phnum = programDeclarations.count; + const shentsize = sectionDeclarations.width; + const shnum = sectionDeclarations.count; const minimumProgram = elf64 ? 56 : 32; const minimumSection = elf64 ? 64 : 40; const found: BlobRange[] = []; @@ -645,9 +652,6 @@ const cString = (bytes: Buffer, offset: number, length: number): string => { return bytes.toString("latin1", offset, end); }; -const readU16 = (bytes: Buffer, offset: number, little: boolean): number => - little ? bytes.readUInt16LE(offset) : bytes.readUInt16BE(offset); - const readU32 = (bytes: Buffer, offset: number, little: boolean): number => little ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset); diff --git a/src/domain/elfDeclaredRanges.ts b/src/domain/elfDeclaredRanges.ts index 06bc4e63b1..13425525b1 100644 --- a/src/domain/elfDeclaredRanges.ts +++ b/src/domain/elfDeclaredRanges.ts @@ -1,3 +1,4 @@ +import { readElfRawTableDeclarations } from "./elfRawTableDeclarations.js"; import { readElfFixedHeader, type ElfFixedHeader } from "./elfFixedHeader.js"; import { err, ok, type Result } from "./result.js"; @@ -21,8 +22,6 @@ export interface ElfTableDeclarations { const reader = (bytes: Buffer, header: ElfFixedHeader) => { const little = header.byteOrder === "little"; - const u16 = (at: number) => - little ? bytes.readUInt16LE(at) : bytes.readUInt16BE(at); const u32 = (at: number) => little ? bytes.readUInt32LE(at) : bytes.readUInt32BE(at); const word = (at: number) => @@ -31,7 +30,7 @@ const reader = (bytes: Buffer, header: ElfFixedHeader) => { : little ? bytes.readBigUInt64LE(at) : bytes.readBigUInt64BE(at); - return { u16, u32, word }; + return { u32, word }; }; /** Zero-sized/inactive declarations own no file bytes. Arithmetic stays exact. */ @@ -110,29 +109,6 @@ const extendedCounts = ( return ok(counts); }; -const TABLE_FIELDS = { - 32: { - programOffset: 28, - sectionOffset: 32, - programWidth: 42, - programCount: 44, - sectionWidth: 46, - sectionCount: 48, - namesIndex: 50, - sectionHeaderSize: 40, - }, - 64: { - programOffset: 32, - sectionOffset: 40, - programWidth: 54, - programCount: 56, - sectionWidth: 58, - sectionCount: 60, - namesIndex: 62, - sectionHeaderSize: 64, - }, -} as const; - /** Canonical table declarations, including extended counts in inactive section zero. */ export function* readElfTableDeclarations( bytes: Buffer, @@ -141,15 +117,16 @@ export function* readElfTableDeclarations( const fixed = readElfFixedHeader(bytes, fileSize); if (!fixed.ok) return fixed; const header = fixed.value; - const r = reader(bytes, header); - const fields = TABLE_FIELDS[header.elfClass]; - const programOffset = r.word(fields.programOffset); - const sectionOffset = r.word(fields.sectionOffset); - const programWidth = r.u16(fields.programWidth); - const sectionWidth = r.u16(fields.sectionWidth); - let programCount = BigInt(r.u16(fields.programCount)); - let sectionCount = BigInt(r.u16(fields.sectionCount)); - let namesIndex = r.u16(fields.namesIndex); + const raw = readElfRawTableDeclarations(bytes, header); + if (!raw.ok) return err(raw.error.message); + const { programs, sections } = raw.value; + const programOffset = programs.offset; + const sectionOffset = sections.offset; + const programWidth = programs.width; + const sectionWidth = sections.width; + let programCount = BigInt(programs.count); + let sectionCount = BigInt(sections.count); + let namesIndex = sections.namesIndex; if ( sectionOffset === 0n && (sectionCount !== 0n || namesIndex !== 0 || programCount === 0xffffn) @@ -168,7 +145,7 @@ export function* readElfTableDeclarations( if (failure !== undefined) return err(failure); const first = yield { offset: Number(sectionOffset), - size: fields.sectionHeaderSize, + size: header.elfClass === 64 ? 64 : 40, }; const counts = extendedCounts(first, header, { programCount, diff --git a/src/domain/elfRawTableDeclarations.test.ts b/src/domain/elfRawTableDeclarations.test.ts new file mode 100644 index 0000000000..f1775340de --- /dev/null +++ b/src/domain/elfRawTableDeclarations.test.ts @@ -0,0 +1,68 @@ +import { expect, it } from "vitest"; +import { readElfRawTableDeclarations } from "./elfRawTableDeclarations.js"; + +const formats = [ + { elfClass: 32, byteOrder: "little" }, + { elfClass: 32, byteOrder: "big" }, + { elfClass: 64, byteOrder: "little" }, + { elfClass: 64, byteOrder: "big" }, +] as const; + +it.each(formats)( + "preserves unresolved raw ELF$elfClass $byteOrder declarations", + (format) => { + const wide = format.elfClass === 64; + const little = format.byteOrder === "little"; + const length = wide ? 64 : 52; + const backing = Buffer.alloc(length + 17, 0xa5); + const bytes = backing.subarray(7, 7 + length); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const programOffset = wide ? 0xfedcba9876543210n : 0xfedcba98n; + const sectionOffset = wide ? 0xffffffffffffffffn : 0xffffffffn; + if (wide) { + view.setBigUint64(32, programOffset, little); + view.setBigUint64(40, sectionOffset, little); + } else { + view.setUint32(28, Number(programOffset), little); + view.setUint32(32, Number(sectionOffset), little); + } + [1, 0xffff, 2, 0, 0xffff].forEach((value, index) => + view.setUint16((wide ? 54 : 42) + index * 2, value, little), + ); + const before = Buffer.from(backing); + const parsed = readElfRawTableDeclarations(backing.subarray(7), format); + expect(backing.equals(before)).toBe(true); + backing.fill(0); + expect(parsed).toEqual({ + ok: true, + value: { + programs: { offset: programOffset, width: 1, count: 0xffff }, + sections: { + offset: sectionOffset, + width: 2, + count: 0, + namesIndex: 0xffff, + }, + }, + }); + // Format identity and whether these unresolved declarations are admissible belong to callers. + }, +); + +it.each(formats)( + "refuses truncated ELF$elfClass $byteOrder declaration records", + (format) => { + const length = format.elfClass === 64 ? 64 : 52; + const backing = Buffer.alloc(length + 7); + for (let observed = 0; observed < length; observed++) + expect( + readElfRawTableDeclarations(backing.subarray(7, 7 + observed), format), + ).toEqual({ + ok: false, + error: { + kind: "malformed", + message: "ELF table declarations are truncated", + }, + }); + }, +); diff --git a/src/domain/elfRawTableDeclarations.ts b/src/domain/elfRawTableDeclarations.ts new file mode 100644 index 0000000000..14e12dd063 --- /dev/null +++ b/src/domain/elfRawTableDeclarations.ts @@ -0,0 +1,33 @@ +import { err, ok } from "./result.js"; + +/** Fixed-header table words, before extended numbering or file-range admission. */ +export const readElfRawTableDeclarations = ( + bytes: Uint8Array, + format: { readonly elfClass: 32 | 64; readonly byteOrder: "little" | "big" }, +) => { + const wide = format.elfClass === 64; + if (bytes.byteLength < (wide ? 64 : 52)) + return err({ + kind: "malformed" as const, + message: "ELF table declarations are truncated", + }); + const little = format.byteOrder === "little"; + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const word = (offset: number): bigint => + wide + ? view.getBigUint64(offset, little) + : BigInt(view.getUint32(offset, little)); + return ok({ + programs: { + offset: word(wide ? 32 : 28), + width: view.getUint16(wide ? 54 : 42, little), + count: view.getUint16(wide ? 56 : 44, little), + }, + sections: { + offset: word(wide ? 40 : 32), + width: view.getUint16(wide ? 58 : 46, little), + count: view.getUint16(wide ? 60 : 48, little), + namesIndex: view.getUint16(wide ? 62 : 50, little), + }, + }); +}; diff --git a/src/flutter/DartElf.test.ts b/src/flutter/DartElf.test.ts index c726225078..48f3e7be0c 100644 --- a/src/flutter/DartElf.test.ts +++ b/src/flutter/DartElf.test.ts @@ -502,3 +502,22 @@ describe("readSnapshotHeader", () => { }); }); }); + +it("keeps section admission ahead of program admission for unrepresentable raw offsets", async () => { + const bytes = buildStrippedElf64Image([]); + bytes.writeBigUInt64LE(0xffffffffffffffffn, 0x28); + bytes.writeBigUInt64LE(0xffffffffffffffffn, 0x20); + expect(await readElfSymbols(bytes, ["_kDartVmSnapshotData"])).toEqual({ + symbols: [], + failures: [ + { reason: "ELF section header table is missing or out of bounds" }, + ], + }); + bytes.writeBigUInt64LE(0n, 0x28); + expect(await readElfSymbols(bytes, ["_kDartVmSnapshotData"])).toEqual({ + symbols: [], + failures: [ + { reason: "ELF program header table is missing or out of bounds" }, + ], + }); +}); diff --git a/src/flutter/DartElf.ts b/src/flutter/DartElf.ts index 59759eaff2..1ea20fdcaa 100644 --- a/src/flutter/DartElf.ts +++ b/src/flutter/DartElf.ts @@ -1,3 +1,4 @@ +import { readElfRawTableDeclarations } from "../domain/elfRawTableDeclarations.js"; import { Buffer } from "node:buffer"; import { setImmediate as yieldToEventLoop } from "node:timers/promises"; import { createElfLoadMap } from "./ElfLoadMap.js"; @@ -84,7 +85,6 @@ export const readElfHeader = ( bytes: Buffer, totalBytes: number = bytes.length, ) => { - const reader = new BoundedReader(bytes); const fixedHeader = readElfFixedHeader(bytes, totalBytes); if (!fixedHeader.ok) return { failure: { reason: fixedHeader.error } }; const is64 = fixedHeader.value.elfClass === 64; @@ -96,21 +96,28 @@ export const readElfHeader = ( const e_type = fixedHeader.value.type; if (e_type !== 1 && e_type !== 2 && e_type !== ET_DYN) return { failure: { reason: "unsupported ELF type" } }; - const e_shoff = is64 ? reader.u64(0x28) : reader.u32(0x20); - const e_shentsize = reader.u16(is64 ? 0x3a : 0x2e); - const e_shnum = reader.u16(is64 ? 0x3c : 0x30); - const e_phoff = is64 ? reader.u64(0x20) : reader.u32(0x1c); - const e_phentsize = reader.u16(is64 ? 0x36 : 0x2a); - const e_phnum = reader.u16(is64 ? 0x38 : 0x2c); + const raw = readElfRawTableDeclarations(bytes, fixedHeader.value); + if (!raw.ok) return { failure: { reason: raw.error.message } }; + const { programs, sections } = raw.value; + const e_shoff = + sections.offset <= BigInt(Number.MAX_SAFE_INTEGER) + ? Number(sections.offset) + : null; + const e_shentsize = sections.width; + const e_shnum = sections.count; + const e_phoff = + programs.offset <= BigInt(Number.MAX_SAFE_INTEGER) + ? Number(programs.offset) + : null; + const e_phentsize = programs.width; + const e_phnum = programs.count; const tableFits = ( offset: number | null, - width: number | null, - count: number | null, + width: number, + count: number, minimumWidth: number, ): boolean => offset !== null && - width !== null && - count !== null && offset >= headerBytes && offset <= totalBytes && width >= minimumWidth && @@ -125,8 +132,6 @@ export const readElfHeader = ( e_type === ET_DYN && e_shoff === 0 && e_shnum === 0; if ( e_shoff === null || - e_shentsize === null || - e_shnum === null || (!sectionTableAbsent && !tableFits(e_shoff, e_shentsize, e_shnum, MIN_SHENT)) ) @@ -138,8 +143,6 @@ export const readElfHeader = ( const MIN_PHENT = is64 ? 56 : 32; if ( e_phoff === null || - e_phentsize === null || - e_phnum === null || e_phnum === 0xffff || !tableFits(e_phoff, e_phentsize, e_phnum, MIN_PHENT) ) diff --git a/src/native/elf/ElfNoteParser.test.ts b/src/native/elf/ElfNoteParser.test.ts index f87312b1d6..5401b8c8bf 100644 --- a/src/native/elf/ElfNoteParser.test.ts +++ b/src/native/elf/ElfNoteParser.test.ts @@ -617,3 +617,11 @@ it("fails when the entry budget is exhausted", () => { ), ).toThrow(ArtifactReaderFailure); }); + +it("keeps unsafe table offsets ahead of unsupported extended numbering", () => { + const bytes = elfExtendedSectionCountFixture(); + bytes.writeBigUInt64LE(0xffffffffffffffffn, 32); + expect(() => parse(bytes)).toThrow( + "ELF offset or size exceeds a safe integer.", + ); +}); diff --git a/src/native/elf/ElfNoteParser.ts b/src/native/elf/ElfNoteParser.ts index e795c10fc2..9fe1876b3f 100644 --- a/src/native/elf/ElfNoteParser.ts +++ b/src/native/elf/ElfNoteParser.ts @@ -1,3 +1,4 @@ +import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js"; import { createHash } from "node:crypto"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { readElfFixedHeader } from "../../domain/elfFixedHeader.js"; @@ -98,13 +99,6 @@ class ElfReader { readonly bits: 32 | 64, ) {} - u16(offset: number): number { - this.require(offset, 2); - return this.little - ? this.bytes.readUInt16LE(offset) - : this.bytes.readUInt16BE(offset); - } - u32(offset: number): number { this.require(offset, 4); return this.little @@ -328,13 +322,21 @@ export const parseElfNotes = ( ); const wide = header.elfClass === 64; const type = header.type; - const phoff = reader.word(wide ? 32 : 28); - const shoff = reader.word(wide ? 40 : 32); - const phentsize = reader.u16(wide ? 54 : 42); - const phnum = reader.u16(wide ? 56 : 44); - const shentsize = reader.u16(wide ? 58 : 46); - const shnum = reader.u16(wide ? 60 : 48); - const shstrndx = reader.u16(wide ? 62 : 50); + const raw = readElfRawTableDeclarations(bytes, header); + if (!raw.ok) return fail(raw.error.message); + const { programs, sections } = raw.value; + if ( + programs.offset > BigInt(Number.MAX_SAFE_INTEGER) || + sections.offset > BigInt(Number.MAX_SAFE_INTEGER) + ) + return fail("ELF offset or size exceeds a safe integer."); + const phoff = Number(programs.offset); + const shoff = Number(sections.offset); + const phentsize = programs.width; + const phnum = programs.count; + const shentsize = sections.width; + const shnum = sections.count; + const shstrndx = sections.namesIndex; if ( phnum === 0xffff || shnum === 0xffff || diff --git a/src/native/pwntools/RecordedCrashStructureBindings.test.ts b/src/native/pwntools/RecordedCrashStructureBindings.test.ts index bd7cceae00..dd7665278a 100644 --- a/src/native/pwntools/RecordedCrashStructureBindings.test.ts +++ b/src/native/pwntools/RecordedCrashStructureBindings.test.ts @@ -50,3 +50,14 @@ it.each([3, 0x46494c45])( expect(recordedCrashSchema.safeParse(report).success).toBe(false); }, ); + +it("keeps snapshot rebinding independent of unused ELF section and header-size admission", () => { + const report = recordedCrashSchema.parse(recordedCrashFixture()); + const bytes = recordedCrashFixtureBytes(report); + bytes.writeUInt16LE(0, 52); + bytes.writeBigUInt64LE(0xffffffffffffffffn, 40); + bytes.writeUInt16LE(0xffff, 60); + expect(() => + validateRecordedCrashStructure(report, bytes, report.decoder_diagnostics), + ).not.toThrow(); +}); diff --git a/src/native/pwntools/RecordedCrashStructureBindings.ts b/src/native/pwntools/RecordedCrashStructureBindings.ts index 09aa582665..9da9a64caf 100644 --- a/src/native/pwntools/RecordedCrashStructureBindings.ts +++ b/src/native/pwntools/RecordedCrashStructureBindings.ts @@ -1,3 +1,4 @@ +import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js"; import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; import { RECORDED_LINUX_CORE_PROFILES, @@ -103,12 +104,18 @@ export function validateRecordedCrashStructure( header.readUInt16LE(18) !== expectedMachine ) fail(); - const table = header.readBigUInt64LE(32); - const entryBytes = header.readUInt16LE(54); - let count = header.readUInt16LE(56); + const rawTables = readElfRawTableDeclarations(header, { + elfClass: 64, + byteOrder: "little", + }); + if (!rawTables.ok) return fail(); + const { programs, sections } = rawTables.value; + const table = programs.offset; + const entryBytes = programs.width; + let count = programs.count; if (count === 0xffff) { - if (header.readUInt16LE(58) < 64) return fail(); - count = bytesAt(header.readBigUInt64LE(40), 64).readUInt32LE(44); + if (sections.width < 64) return fail(); + count = bytesAt(sections.offset, 64).readUInt32LE(44); } if (count !== report.segments.length || (count > 0 && entryBytes < 56)) fail();