Files
reverse-engineering-anything/src/native/elf/KernelModule.fixture.ts
T
dba8a31978 feat(elf): inspect Linux kernel module metadata (#7010)
* 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>
2026-10-24 22:35:20 +08:00

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]);
}
};