Files
reverse-engineering-anything/src/artifacts/MachoImage.fixture.ts
T
Kaoru0822andClaude Opus 5.5 a0ec3e2016 fix(artifacts): follow dyld slice grading and report unparsed images
Address review findings in trace_dylib_resolution:
- Choose child slices by dyld's graded architectures, so an x86_64h
  process loads a generic x86_64 slice instead of reporting
  architecture-missing.
- Read FAT containers with the byte-swapped FAT_CIGAM and FAT_CIGAM_64
  magics, which target opening already accepts.
- Report string-bearing load commands too short to hold their string
  offset as malformed instead of failing with a RangeError.
- Keep recognized but unsupported images as an `unsupported` candidate
  outcome. It is undeterminable like an outside-root path, not
  malformed.
- List Mach-O files that do not parse during root enumeration as
  unparsed images, so coverage is partial rather than silently complete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:07:31 +00:00

153 lines
5.0 KiB
TypeScript

/** Little-endian Mach-O images and FAT containers built field by field for tests. */
export const LC = {
LOAD_DYLIB: 0xc,
ID_DYLIB: 0xd,
LOAD_WEAK_DYLIB: 0x80000018,
REEXPORT_DYLIB: 0x8000001f,
LAZY_LOAD_DYLIB: 0x20,
LOAD_UPWARD_DYLIB: 0x80000023,
RPATH: 0x8000001c,
DYLD_ENVIRONMENT: 0x27,
CODE_SIGNATURE: 0x1d,
} as const;
export const CPU = {
arm64: { type: 0x0100000c, subtype: 0 },
arm64e: { type: 0x0100000c, subtype: 0x80000002 },
x86_64: { type: 0x01000007, subtype: 3 },
} as const;
export const FILE_TYPE = { execute: 2, dylib: 6, bundle: 8 } as const;
const encoder = new TextEncoder();
/** One load command with a NUL-terminated string at `nameOffset`, padded to 8 bytes. */
const commandWithString = (
command: number,
fields: readonly number[],
text: string,
): Uint8Array => {
const nameOffset = 8 + fields.length * 4 + 4;
const name = encoder.encode(text);
const size = Math.ceil((nameOffset + name.length + 1) / 8) * 8;
const bytes = new Uint8Array(size);
const view = new DataView(bytes.buffer);
view.setUint32(0, command, true);
view.setUint32(4, size, true);
view.setUint32(8, nameOffset, true);
fields.forEach((value, index) => view.setUint32(12 + index * 4, value, true));
bytes.set(name, nameOffset);
return bytes;
};
/** Classic `dylib_command`: timestamp, current and compatibility versions. */
export const dylibCommand = (
command: number,
name: string,
versions: { readonly current?: number; readonly compatibility?: number } = {},
): Uint8Array =>
commandWithString(
command,
[2, versions.current ?? 0x10000, versions.compatibility ?? 0x10000],
name,
);
/** `dylib_use_command`: marker, versions and `DYLIB_USE_*` flags. */
export const dylibUseCommand = (
command: number,
name: string,
flags: number,
): Uint8Array =>
commandWithString(command, [0x1a741800, 0x10000, 0x10000, flags], name);
export const rpathCommand = (path: string): Uint8Array =>
commandWithString(LC.RPATH, [], path);
export const dyldEnvironmentCommand = (value: string): Uint8Array =>
commandWithString(LC.DYLD_ENVIRONMENT, [], value);
export const codeSignatureCommand = (): Uint8Array => {
const bytes = new Uint8Array(16);
const view = new DataView(bytes.buffer);
view.setUint32(0, LC.CODE_SIGNATURE, true);
view.setUint32(4, 16, true);
return bytes;
};
/** A 64-bit (or 32-bit) little-endian Mach-O image made of the given commands. */
export const machoImage = (options: {
readonly cpu?: { readonly type: number; readonly subtype: number };
readonly fileType?: number;
readonly commands?: readonly Uint8Array[];
readonly wide?: boolean;
}): Uint8Array => {
const wide = options.wide ?? true;
const commands = options.commands ?? [];
const headerSize = wide ? 32 : 28;
const commandBytes = commands.reduce((total, item) => total + item.length, 0);
const bytes = new Uint8Array(headerSize + commandBytes + 16);
const view = new DataView(bytes.buffer);
const cpu = options.cpu ?? CPU.arm64;
view.setUint32(0, wide ? 0xfeedfacf : 0xfeedface, true);
view.setUint32(4, cpu.type, true);
view.setUint32(8, cpu.subtype, true);
view.setUint32(12, options.fileType ?? FILE_TYPE.execute, true);
view.setUint32(16, commands.length, true);
view.setUint32(20, commandBytes, true);
let offset = headerSize;
for (const command of commands) {
bytes.set(command, offset);
offset += command.length;
}
return bytes;
};
/**
* A FAT container whose slices start on 4 KiB boundaries. Headers are
* big-endian unless `littleEndian` selects the swapped FAT_CIGAM magics.
*/
export const fatImage = (
slices: readonly {
readonly cpu: { readonly type: number; readonly subtype: number };
readonly bytes: Uint8Array;
}[],
wide = false,
littleEndian = false,
): Uint8Array => {
const entrySize = wide ? 32 : 20;
const offsets: number[] = [];
let end = 4096;
for (const slice of slices) {
offsets.push(end);
end += Math.ceil(slice.bytes.length / 4096) * 4096;
}
const bytes = new Uint8Array(end);
const view = new DataView(bytes.buffer);
view.setUint32(0, wide ? 0xcafebabf : 0xcafebabe, littleEndian);
view.setUint32(4, slices.length, littleEndian);
slices.forEach((slice, index) => {
const base = 8 + index * entrySize;
const offset = offsets[index] ?? 0;
view.setUint32(base, slice.cpu.type, littleEndian);
view.setUint32(base + 4, slice.cpu.subtype, littleEndian);
if (wide) {
view.setBigUint64(base + 8, BigInt(offset), littleEndian);
view.setBigUint64(base + 16, BigInt(slice.bytes.length), littleEndian);
view.setUint32(base + 24, 12, littleEndian);
} else {
view.setUint32(base + 8, offset, littleEndian);
view.setUint32(base + 12, slice.bytes.length, littleEndian);
view.setUint32(base + 16, 12, littleEndian);
}
bytes.set(slice.bytes, offset);
});
return bytes;
};
/** In-memory `ReadAt` over one buffer. */
export const readerOf =
(bytes: Uint8Array) =>
(offset: number, length: number): Promise<Uint8Array> =>
Promise.resolve(bytes.subarray(offset, offset + length));