mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 16:26:36 +00:00
* feat(elf): inspect Linux kernel module metadata Adds inspect-kernel-module and inspect_kernel_module for ELF32/ELF64 ET_REL Linux kernel modules, read statically without executing them. Reports ordered .modinfo entries with duplicates preserved, __versions symbol CRCs, the presence of .gnu.linkonce.this_module and __ksymtab, .init.text and .exit.text sizes, and an appended module signature trailer with its PKCS#7 byte range and SHA-256. gzip, xz and zstd containers are reported unsupported with external decompression as the next probe. Malformed headers and exhausted entry budgets return no partial success. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe * fix(elf): bound kernel module ksymtab listing and tighten contracts Charge listed __ksymtab* sections against max_entries, omit section names longer than 256 bytes with ksymtab_name_exceeds_limit, and aggregate the non-file-backed limitation so repeated huge names cannot inflate Evidence. Use fixedTupleSchema for the signature pad bytes with projection parity tests, and assert the late-cancellation reason and retained observation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe * fix: resolve merge integration errors --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
255 lines
7.9 KiB
TypeScript
255 lines
7.9 KiB
TypeScript
import { MODULE_SIGNATURE_MAGIC } from "../../domain/native/kernelModule.js";
|
|
|
|
const concat = (...parts: readonly Buffer[]): Buffer => Buffer.concat(parts);
|
|
|
|
const write16 = (
|
|
bytes: Buffer,
|
|
offset: number,
|
|
value: number,
|
|
little: boolean,
|
|
): void => {
|
|
if (little) bytes.writeUInt16LE(value, offset);
|
|
else bytes.writeUInt16BE(value, offset);
|
|
};
|
|
|
|
const write32 = (
|
|
bytes: Buffer,
|
|
offset: number,
|
|
value: number,
|
|
little: boolean,
|
|
): void => {
|
|
if (little) bytes.writeUInt32LE(value, offset);
|
|
else bytes.writeUInt32BE(value, offset);
|
|
};
|
|
|
|
const writeWord = (
|
|
bytes: Buffer,
|
|
offset: number,
|
|
value: number,
|
|
bits: 32 | 64,
|
|
little: boolean,
|
|
): void => {
|
|
if (bits === 64) {
|
|
const wide = BigInt(value);
|
|
if (little) bytes.writeBigUInt64LE(wide, offset);
|
|
else bytes.writeBigUInt64BE(wide, offset);
|
|
return;
|
|
}
|
|
write32(bytes, offset, value, little);
|
|
};
|
|
|
|
/** One `__versions` record: unsigned long CRC plus a fixed NUL-padded name. */
|
|
export const modversionEntry = (
|
|
crc: number | bigint,
|
|
name: string,
|
|
bits: 32 | 64,
|
|
little: boolean,
|
|
): Buffer => {
|
|
const bytes = Buffer.alloc(64);
|
|
const encoded = Buffer.from(name, "latin1");
|
|
if (bits === 64) {
|
|
const value = BigInt(crc);
|
|
if (little) bytes.writeBigUInt64LE(value, 0);
|
|
else bytes.writeBigUInt64BE(value, 0);
|
|
if (encoded.length >= 56)
|
|
throw new Error("ELF64 modversion name must fit in 56 bytes");
|
|
encoded.copy(bytes, 8);
|
|
return bytes;
|
|
}
|
|
if (little) bytes.writeUInt32LE(Number(crc), 0);
|
|
else bytes.writeUInt32BE(Number(crc), 0);
|
|
if (encoded.length >= 60)
|
|
throw new Error("ELF32 modversion name must fit in 60 bytes");
|
|
encoded.copy(bytes, 4);
|
|
return bytes;
|
|
};
|
|
|
|
/**
|
|
* Appended module signature trailer. `sigLen` defaults to the PKCS#7 length;
|
|
* a larger value builds an inconsistent trailer.
|
|
*/
|
|
export const moduleSignatureTrailer = (
|
|
pkcs7: Buffer,
|
|
sigLen = pkcs7.length,
|
|
): Buffer => {
|
|
const info = Buffer.alloc(12);
|
|
info[2] = 2;
|
|
info.writeUInt32BE(sigLen >>> 0, 8);
|
|
return concat(pkcs7, info, Buffer.from(MODULE_SIGNATURE_MAGIC, "latin1"));
|
|
};
|
|
|
|
export interface KernelModuleFixtureOptions {
|
|
readonly bits?: 32 | 64;
|
|
readonly little?: boolean;
|
|
readonly type?: number;
|
|
readonly machine?: number;
|
|
/** Null omits the section. Undefined uses the synthetic default. */
|
|
readonly modinfo?: Buffer | null;
|
|
readonly versions?: Buffer | null;
|
|
readonly thisModule?: Buffer | null;
|
|
readonly ksymtab?: Buffer | null;
|
|
readonly initText?: Buffer | null;
|
|
readonly exitText?: Buffer | null;
|
|
/** Null omits the trailer. Undefined appends a consistent PKCS#7 trailer. */
|
|
readonly signature?: Buffer | null;
|
|
/** Declare a second `.modinfo` section with the same bytes. */
|
|
readonly repeatModinfo?: boolean;
|
|
/** Extra empty file-backed sections that share one name-table string. */
|
|
readonly sharedNameSections?: {
|
|
readonly name: string;
|
|
readonly count: number;
|
|
};
|
|
}
|
|
|
|
const defaultModinfo = (): Buffer =>
|
|
Buffer.from(
|
|
"license=GPL\0vermagic=5.15.0 SMP mod_unload\0alias=pci:v1\0alias=pci:v2\0name=demo\0depends=\0",
|
|
"latin1",
|
|
);
|
|
|
|
const defaultVersions = (bits: 32 | 64, little: boolean): Buffer =>
|
|
concat(
|
|
modversionEntry(0x12345678, "printk", bits, little),
|
|
modversionEntry(0x90abcdef, "module_layout", bits, little),
|
|
);
|
|
|
|
const defaultPkcs7 = (): Buffer => Buffer.from("fake-pkcs7-bytes", "latin1");
|
|
|
|
interface SectionBody {
|
|
readonly name: string;
|
|
readonly type: number;
|
|
readonly body: Buffer;
|
|
}
|
|
|
|
/** Synthetic ET_REL image with kernel-module sections and an optional trailer. */
|
|
export const kernelModuleFixture = (
|
|
options: KernelModuleFixtureOptions = {},
|
|
): Buffer => {
|
|
const bits = options.bits ?? 64;
|
|
const little = options.little ?? true;
|
|
const chosen = (
|
|
value: Buffer | null | undefined,
|
|
fallback: () => Buffer,
|
|
): Buffer | null =>
|
|
value === null ? null : value === undefined ? fallback() : value;
|
|
const bodies: SectionBody[] = [];
|
|
const add = (name: string, body: Buffer | null): void => {
|
|
if (body !== null) bodies.push({ name, type: 1, body });
|
|
};
|
|
add(".modinfo", chosen(options.modinfo, defaultModinfo));
|
|
if (options.repeatModinfo === true) {
|
|
const first = bodies.find((section) => section.name === ".modinfo");
|
|
if (first !== undefined)
|
|
bodies.push({ ...first, body: Buffer.from(first.body) });
|
|
}
|
|
add(
|
|
"__versions",
|
|
chosen(options.versions, () => defaultVersions(bits, little)),
|
|
);
|
|
add(
|
|
".gnu.linkonce.this_module",
|
|
chosen(options.thisModule, () => Buffer.alloc(8, 0x11)),
|
|
);
|
|
add(
|
|
"__ksymtab",
|
|
chosen(options.ksymtab, () => Buffer.alloc(8, 0x22)),
|
|
);
|
|
add(
|
|
".init.text",
|
|
chosen(options.initText, () => Buffer.from([0x90, 0x90, 0x90, 0x90])),
|
|
);
|
|
add(
|
|
".exit.text",
|
|
chosen(options.exitText, () => Buffer.from([0xc3])),
|
|
);
|
|
const shared = options.sharedNameSections;
|
|
if (shared !== undefined)
|
|
for (let index = 0; index < shared.count; index++)
|
|
bodies.push({ name: shared.name, type: 1, body: Buffer.alloc(0) });
|
|
const labels = [
|
|
...new Set([...bodies.map((section) => section.name), ".shstrtab"]),
|
|
];
|
|
const nameOffsets = new Map<string, number>();
|
|
const nameParts: Buffer[] = [Buffer.from([0])];
|
|
for (const label of labels) {
|
|
nameOffsets.set(
|
|
label,
|
|
nameParts.reduce((sum, part) => sum + part.length, 0),
|
|
);
|
|
nameParts.push(Buffer.from(`${label}\0`, "latin1"));
|
|
}
|
|
const names = concat(...nameParts);
|
|
const headerSize = bits === 64 ? 64 : 52;
|
|
const shentsize = bits === 64 ? 64 : 40;
|
|
const shnum = bodies.length + 2;
|
|
const shoff = headerSize;
|
|
let cursor = shoff + shnum * shentsize;
|
|
const placed = bodies.map((section) => {
|
|
const offset = cursor;
|
|
cursor += section.body.length;
|
|
return { ...section, offset };
|
|
});
|
|
const namesOffset = cursor;
|
|
cursor += names.length;
|
|
const signature =
|
|
options.signature === null
|
|
? Buffer.alloc(0)
|
|
: (options.signature ?? moduleSignatureTrailer(defaultPkcs7()));
|
|
const bytes = Buffer.alloc(cursor + signature.length);
|
|
bytes.writeUInt32LE(0x464c457f, 0);
|
|
bytes[4] = bits === 64 ? 2 : 1;
|
|
bytes[5] = little ? 1 : 2;
|
|
bytes[6] = 1;
|
|
write16(bytes, 16, options.type ?? 1, little);
|
|
write16(bytes, 18, options.machine ?? (bits === 64 ? 62 : 20), little);
|
|
write32(bytes, 20, 1, little);
|
|
write16(bytes, bits === 64 ? 52 : 40, headerSize, little);
|
|
write16(bytes, bits === 64 ? 54 : 42, bits === 64 ? 56 : 32, little);
|
|
writeWord(bytes, bits === 64 ? 40 : 32, shoff, bits, little);
|
|
write16(bytes, bits === 64 ? 58 : 46, shentsize, little);
|
|
write16(bytes, bits === 64 ? 60 : 48, shnum, little);
|
|
write16(bytes, bits === 64 ? 62 : 50, shnum - 1, little);
|
|
const writeSection = (
|
|
index: number,
|
|
name: string,
|
|
type: number,
|
|
offset: number,
|
|
size: number,
|
|
): void => {
|
|
const at = shoff + index * shentsize;
|
|
write32(bytes, at, nameOffsets.get(name) ?? 0, little);
|
|
write32(bytes, at + 4, type, little);
|
|
writeWord(bytes, at + (bits === 64 ? 24 : 16), offset, bits, little);
|
|
writeWord(bytes, at + (bits === 64 ? 32 : 20), size, bits, little);
|
|
writeWord(bytes, at + (bits === 64 ? 48 : 32), 1, bits, little);
|
|
};
|
|
placed.forEach((section, index) => {
|
|
writeSection(
|
|
index + 1,
|
|
section.name,
|
|
section.type,
|
|
section.offset,
|
|
section.body.length,
|
|
);
|
|
section.body.copy(bytes, section.offset);
|
|
});
|
|
writeSection(shnum - 1, ".shstrtab", 3, namesOffset, names.length);
|
|
names.copy(bytes, namesOffset);
|
|
signature.copy(bytes, cursor);
|
|
return bytes;
|
|
};
|
|
|
|
/** Whole-file compression magics this operation recognizes and does not decode. */
|
|
export const compressedKernelModuleContainer = (
|
|
codec: "gzip" | "xz" | "zstd",
|
|
): Buffer => {
|
|
switch (codec) {
|
|
case "gzip":
|
|
return Buffer.from([0x1f, 0x8b, 0x08, 0x00]);
|
|
case "xz":
|
|
return Buffer.from([0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00, 0x00]);
|
|
case "zstd":
|
|
return Buffer.from([0x28, 0xb5, 0x2f, 0xfd, 0x00]);
|
|
}
|
|
};
|