Link IL2CPP metadata names to native addresses and types through shared CLI/MCP workflows, with bounded queries and atomic Ghidra seed export. Preserve the original implementation by 8rulerstar and follow-up correctness, resource, provenance, and CI dependency repairs. Verified against real Windows/Android artifacts, independent references, actual Ghidra, and focused CLI/MCP tests. Fixes #5112.
332 lines
13 KiB
TypeScript
332 lines
13 KiB
TypeScript
/**
|
|
* Synthetic thin arm64 Mach-O holding the IL2CPP registration tables for the
|
|
* metadata fixture: image names, code-generation modules, their method
|
|
* pointer arrays and the CodeRegistration module count and array pointer.
|
|
*/
|
|
export const FIXTURE_BINARY_BASE = 0x1_0000_0000n;
|
|
|
|
/** Method addresses by metadata method index; ToString has no body. */
|
|
export const FIXTURE_METHOD_ADDRESSES = [
|
|
FIXTURE_BINARY_BASE + 0x200n,
|
|
FIXTURE_BINARY_BASE + 0x210n,
|
|
FIXTURE_BINARY_BASE + 0x220n,
|
|
FIXTURE_BINARY_BASE + 0x230n,
|
|
"no-body",
|
|
] as const;
|
|
|
|
export interface Il2cppBinaryFixtureOptions {
|
|
/** methodPointerCount written for Assembly-CSharp.dll; 3 matches the metadata. */
|
|
readonly firstModuleCount?: number;
|
|
/** Exact module names, including names shared by different metadata images. */
|
|
readonly imageNames?: readonly [string, string];
|
|
/** Write a second record that also matches Assembly-CSharp.dll. */
|
|
readonly duplicateModule?: boolean;
|
|
/** Add an LC_DYLD_CHAINED_FIXUPS load command. */
|
|
readonly chainedFixups?: boolean;
|
|
/** Omit the CodeRegistration module count and array pointer. */
|
|
readonly codeRegistration?: boolean;
|
|
/** Write a RIP-relative `lea rax, [module array]`, as an inlined build loads it. */
|
|
readonly codeReference?: boolean;
|
|
/** Omit the MetadataRegistration type table. */
|
|
readonly metadataRegistration?: boolean;
|
|
/** Byte written before each image name instead of NUL (MSVC pads with 0xFF). */
|
|
readonly namePadding?: number;
|
|
/** Text written directly before mscorlib.dll, making it the tail of a longer name. */
|
|
readonly namePrefix?: string;
|
|
}
|
|
|
|
/** Every fixture method returns type 0, an array of a generic instance. */
|
|
export const FIXTURE_RETURN_TYPE_NAME = "System.Größe<int>[]";
|
|
|
|
const SIZE = 0x1000;
|
|
const METADATA_REGISTRATION = 0x800;
|
|
const TYPE_POINTERS = 0x900;
|
|
const TYPE_RECORDS = 0x980;
|
|
const GENERIC_CLASS = 0xa00;
|
|
const GENERIC_INSTANCE = 0xa40;
|
|
const GENERIC_ARGUMENTS = 0xa60;
|
|
const STRINGS = 0x100;
|
|
const POINTERS = 0x300;
|
|
const MODULES = 0x400;
|
|
const MODULE_ARRAY = 0x500;
|
|
const REGISTRATION = 0x600;
|
|
const DUPLICATE = 0x700;
|
|
|
|
export const createIl2cppBinaryFixture = (
|
|
options: Il2cppBinaryFixtureOptions = {},
|
|
): Buffer => {
|
|
const bytes = Buffer.alloc(SIZE);
|
|
const address = (offset: number) => FIXTURE_BINARY_BASE + BigInt(offset);
|
|
const chained = options.chainedFixups === true;
|
|
// Mach-O 64-bit header: arm64 dylib with one or two load commands.
|
|
bytes.writeUInt32LE(0xfeedfacf, 0);
|
|
bytes.writeUInt32LE(0x0100000c, 4);
|
|
bytes.writeUInt32LE(6, 12);
|
|
bytes.writeUInt32LE(chained ? 2 : 1, 16);
|
|
bytes.writeUInt32LE(chained ? 72 + 16 : 72, 20);
|
|
// LC_SEGMENT_64 __TEXT mapping the whole file read-execute.
|
|
bytes.writeUInt32LE(0x19, 32);
|
|
bytes.writeUInt32LE(72, 36);
|
|
bytes.write("__TEXT", 40, "latin1");
|
|
bytes.writeBigUInt64LE(FIXTURE_BINARY_BASE, 56);
|
|
bytes.writeBigUInt64LE(BigInt(SIZE), 64);
|
|
bytes.writeBigUInt64LE(0n, 72);
|
|
bytes.writeBigUInt64LE(BigInt(SIZE), 80);
|
|
bytes.writeUInt32LE(5, 88);
|
|
bytes.writeUInt32LE(5, 92);
|
|
if (chained) {
|
|
// linkedit_data_command pointing at an empty range.
|
|
bytes.writeUInt32LE(0x80000034, 104);
|
|
bytes.writeUInt32LE(16, 108);
|
|
}
|
|
const names = options.imageNames ?? ["Assembly-CSharp.dll", "mscorlib.dll"];
|
|
const nameAt: number[] = [];
|
|
let cursor = STRINGS;
|
|
for (const name of names) {
|
|
bytes[cursor++] = options.namePadding ?? 0;
|
|
if (name === "mscorlib.dll" && options.namePrefix !== undefined)
|
|
cursor += bytes.write(options.namePrefix, cursor, "utf8");
|
|
nameAt.push(cursor);
|
|
cursor += bytes.write(`${name}\0`, cursor, "utf8");
|
|
}
|
|
// Slots are indexed by token row id - 1 (see FIXTURE_METHOD_TOKENS).
|
|
const tables = [
|
|
[0x210n, 0x200n, 0x220n],
|
|
[0x230n, null],
|
|
];
|
|
const tableAt: number[] = [];
|
|
let pointer = POINTERS;
|
|
for (const table of tables) {
|
|
tableAt.push(pointer);
|
|
for (const slot of table) {
|
|
bytes.writeBigUInt64LE(
|
|
slot === null ? 0n : FIXTURE_BINARY_BASE + slot,
|
|
pointer,
|
|
);
|
|
pointer += 8;
|
|
}
|
|
}
|
|
const writeModule = (at: number, image: number, count: number) => {
|
|
bytes.writeBigUInt64LE(address(nameAt[image]!), at);
|
|
bytes.writeUInt32LE(count, at + 8);
|
|
bytes.writeBigUInt64LE(address(tableAt[image]!), at + 16);
|
|
};
|
|
writeModule(MODULES, 0, options.firstModuleCount ?? 3);
|
|
writeModule(MODULES + 0x40, 1, 2);
|
|
if (options.duplicateModule === true) writeModule(DUPLICATE, 0, 3);
|
|
bytes.writeBigUInt64LE(address(MODULES), MODULE_ARRAY);
|
|
bytes.writeBigUInt64LE(address(MODULES + 0x40), MODULE_ARRAY + 8);
|
|
if (options.codeReference === true) {
|
|
// lea rax, [rip + disp32]; disp is relative to the next instruction.
|
|
const at = 0xc00;
|
|
bytes.set([0x48, 0x8d, 0x05], at);
|
|
bytes.writeInt32LE(MODULE_ARRAY - (at + 7), at + 3);
|
|
}
|
|
if (options.codeRegistration !== false) {
|
|
bytes.writeBigUInt64LE(2n, REGISTRATION);
|
|
bytes.writeBigUInt64LE(address(MODULE_ARRAY), REGISTRATION + 8);
|
|
}
|
|
// Il2CppType records: 8-byte data, then attrs:16 and type:8 bits.
|
|
const types: readonly [kind: number, data: bigint][] = [
|
|
[0x1d, address(TYPE_RECORDS + 16)],
|
|
[0x15, address(GENERIC_CLASS)],
|
|
[0x12, 3n],
|
|
[0x08, 0n],
|
|
];
|
|
types.forEach(([kind, data], index) => {
|
|
const at = TYPE_RECORDS + index * 16;
|
|
bytes.writeBigUInt64LE(data, at);
|
|
bytes.writeUInt32LE(kind << 16, at + 8);
|
|
bytes.writeBigUInt64LE(address(at), TYPE_POINTERS + index * 8);
|
|
});
|
|
// Version 29+ Il2CppGenericClass: definition type, then class_inst.
|
|
bytes.writeBigUInt64LE(address(TYPE_RECORDS + 32), GENERIC_CLASS);
|
|
bytes.writeBigUInt64LE(address(GENERIC_INSTANCE), GENERIC_CLASS + 8);
|
|
bytes.writeUInt32LE(1, GENERIC_INSTANCE);
|
|
bytes.writeBigUInt64LE(address(GENERIC_ARGUMENTS), GENERIC_INSTANCE + 8);
|
|
bytes.writeBigUInt64LE(address(TYPE_RECORDS + 48), GENERIC_ARGUMENTS);
|
|
if (options.metadataRegistration !== false) {
|
|
// typesCount and types, then fieldOffsets and typeDefinitionsSizes,
|
|
// whose counts equal the 4 metadata type definitions.
|
|
const word = (slot: number, value: bigint) =>
|
|
bytes.writeBigUInt64LE(value, METADATA_REGISTRATION + slot * 8);
|
|
word(6, BigInt(types.length));
|
|
word(7, address(TYPE_POINTERS));
|
|
word(10, 4n);
|
|
word(11, address(TYPE_POINTERS));
|
|
word(12, 4n);
|
|
word(13, address(TYPE_POINTERS));
|
|
}
|
|
return bytes;
|
|
};
|
|
|
|
/** Wrap thin Mach-O slices in a 32-bit universal header, page aligned. */
|
|
export const createUniversalFixture = (
|
|
slices: readonly { readonly cpu: number; readonly bytes: Buffer }[],
|
|
): Buffer => {
|
|
const align = 0x1000;
|
|
const header = Buffer.alloc(align);
|
|
header.writeUInt32BE(0xcafebabe, 0);
|
|
header.writeUInt32BE(slices.length, 4);
|
|
const parts: Buffer[] = [header];
|
|
let offset = align;
|
|
slices.forEach((slice, index) => {
|
|
const at = 8 + index * 20;
|
|
header.writeUInt32BE(slice.cpu, at);
|
|
header.writeUInt32BE(0, at + 4);
|
|
header.writeUInt32BE(offset, at + 8);
|
|
header.writeUInt32BE(slice.bytes.length, at + 12);
|
|
header.writeUInt32BE(12, at + 16);
|
|
const padded = Buffer.alloc(Math.ceil(slice.bytes.length / align) * align);
|
|
slice.bytes.copy(padded);
|
|
parts.push(padded);
|
|
offset += padded.length;
|
|
});
|
|
return Buffer.concat(parts);
|
|
};
|
|
|
|
/** PE section with a method array ending across its file/zero-fill boundary. */
|
|
export const createSplitPointerPeFixture = (zeroTail = true): Buffer => {
|
|
const payload = createIl2cppBinaryFixture();
|
|
payload.writeBigUInt64LE(FIXTURE_BINARY_BASE + 0xff8n, 0x450);
|
|
payload.writeBigUInt64LE(FIXTURE_BINARY_BASE + 0x230n, 0xff8);
|
|
const header = Buffer.alloc(0x200);
|
|
header.writeUInt16LE(0x5a4d);
|
|
header.writeUInt32LE(0x40, 60);
|
|
header.writeUInt32LE(0x4550, 0x40);
|
|
header.writeUInt16LE(0x8664, 0x44);
|
|
header.writeUInt16LE(1, 0x46);
|
|
header.writeUInt16LE(240, 0x54);
|
|
const optional = 0x58;
|
|
header.writeUInt16LE(0x20b, optional);
|
|
header.writeBigUInt64LE(FIXTURE_BINARY_BASE, optional + 24);
|
|
header.writeUInt32LE(0x1000, optional + 32);
|
|
header.writeUInt32LE(0x200, optional + 36);
|
|
header.writeUInt32LE(0x2000, optional + 56);
|
|
header.writeUInt32LE(0x200, optional + 60);
|
|
const section = optional + 240;
|
|
header.write(".text", section);
|
|
header.writeUInt32LE(zeroTail ? 0x1008 : 0x1000, section + 8);
|
|
header.writeUInt32LE(0x1000, section + 16);
|
|
header.writeUInt32LE(0x200, section + 20);
|
|
header.writeUInt32LE(0x60000020, section + 36);
|
|
return Buffer.concat([header, payload]);
|
|
};
|
|
|
|
/** Android ELF relocation tables appended after the fixture image. */
|
|
export interface ElfFixtureOptions {
|
|
/** e_machine; 183 is AArch64. */
|
|
readonly machine?: number;
|
|
/** Add DT_ANDROID_RELA, the packed APS2 encoding. */
|
|
readonly androidPacked?: boolean;
|
|
/** Omit PT_DYNAMIC. */
|
|
readonly dynamic?: boolean;
|
|
/** Give every relocation a symbol index, so none is RELATIVE-only. */
|
|
readonly symbolRelocations?: boolean;
|
|
/** Write DT_RELA twice. */
|
|
readonly duplicateRela?: boolean;
|
|
/** Override DT_RELASZ or DT_RELAENT. */
|
|
readonly relaSize?: bigint;
|
|
readonly relaEntry?: bigint;
|
|
/** Move mscorlib.dll's method pointers into zero-fill past the file end. */
|
|
readonly zeroFilledTable?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Rewrap a fixture image as a 64-bit ELF shared object loaded at the same
|
|
* base: every pointer slot into the image is zeroed on disk and restored by
|
|
* an R_AARCH64_RELATIVE entry, as a position-independent libil2cpp.so stores it.
|
|
*/
|
|
export const createElfFixture = (
|
|
image: Buffer,
|
|
options: ElfFixtureOptions = {},
|
|
): Buffer => {
|
|
const body = Buffer.from(image);
|
|
body.fill(0, 0, 0x100);
|
|
const relocations: [slot: bigint, value: bigint][] = [];
|
|
for (let at = 0x100; at + 8 <= SIZE; at += 8) {
|
|
const value = body.readBigUInt64LE(at);
|
|
if (
|
|
value < FIXTURE_BINARY_BASE ||
|
|
value >= FIXTURE_BINARY_BASE + BigInt(SIZE)
|
|
)
|
|
continue;
|
|
relocations.push([FIXTURE_BINARY_BASE + BigInt(at), value]);
|
|
body.writeBigUInt64LE(0n, at);
|
|
}
|
|
const tagCount =
|
|
4 +
|
|
(options.duplicateRela === true ? 1 : 0) +
|
|
(options.androidPacked === true ? 1 : 0);
|
|
const fileSize = SIZE + relocations.length * 24 + tagCount * 16;
|
|
if (options.zeroFilledTable === true) {
|
|
// The second module record's methodPointers field (MODULES + 0x40 + 16).
|
|
const slot = FIXTURE_BINARY_BASE + BigInt(0x400 + 0x40 + 16);
|
|
const entry = relocations.find(([at]) => at === slot)!;
|
|
entry[1] = FIXTURE_BINARY_BASE + BigInt(fileSize);
|
|
}
|
|
const rela = Buffer.alloc(relocations.length * 24);
|
|
relocations.forEach(([slot, value], index) => {
|
|
rela.writeBigUInt64LE(slot, index * 24);
|
|
rela.writeBigUInt64LE(
|
|
(options.symbolRelocations === true ? 1n << 32n : 0n) |
|
|
(options.machine === 62 ? 8n : 1027n),
|
|
index * 24 + 8,
|
|
);
|
|
rela.writeBigInt64LE(value, index * 24 + 16);
|
|
});
|
|
const tags: [bigint, bigint][] = [
|
|
[7n, FIXTURE_BINARY_BASE + BigInt(SIZE)],
|
|
...(options.duplicateRela === true
|
|
? [[7n, FIXTURE_BINARY_BASE + BigInt(SIZE)] as [bigint, bigint]]
|
|
: []),
|
|
[8n, options.relaSize ?? BigInt(rela.length)],
|
|
[9n, options.relaEntry ?? 24n],
|
|
...(options.androidPacked === true
|
|
? [[0x60000011n, 0n] as [bigint, bigint]]
|
|
: []),
|
|
[0n, 0n],
|
|
];
|
|
const dynamic = Buffer.alloc(tags.length * 16);
|
|
tags.forEach(([tag, value], index) => {
|
|
dynamic.writeBigInt64LE(tag, index * 16);
|
|
dynamic.writeBigUInt64LE(value, index * 16 + 8);
|
|
});
|
|
const file = Buffer.concat([body, rela, dynamic]);
|
|
if (file.length !== fileSize) throw new Error("ELF fixture size drifted");
|
|
const withDynamic = options.dynamic !== false;
|
|
// ELF64 header: little-endian shared object, no section headers.
|
|
file.write("\x7fELF", 0, "latin1");
|
|
file.writeUInt8(2, 4);
|
|
file.writeUInt8(1, 5);
|
|
file.writeUInt8(1, 6);
|
|
file.writeUInt16LE(3, 16);
|
|
file.writeUInt16LE(options.machine ?? 183, 18);
|
|
file.writeUInt32LE(1, 20);
|
|
file.writeBigUInt64LE(64n, 32);
|
|
file.writeUInt16LE(64, 52);
|
|
file.writeUInt16LE(56, 54);
|
|
file.writeUInt16LE(withDynamic ? 2 : 1, 56);
|
|
// Synthetic PT_LOAD maps code and relocation data read-write-execute.
|
|
file.writeUInt32LE(1, 64);
|
|
file.writeUInt32LE(7, 68);
|
|
file.writeBigUInt64LE(FIXTURE_BINARY_BASE, 80);
|
|
file.writeBigUInt64LE(FIXTURE_BINARY_BASE, 88);
|
|
file.writeBigUInt64LE(BigInt(file.length), 96);
|
|
file.writeBigUInt64LE(
|
|
BigInt(file.length) + (options.zeroFilledTable === true ? 0x100n : 0n),
|
|
104,
|
|
);
|
|
if (withDynamic) {
|
|
const at = SIZE + rela.length;
|
|
file.writeUInt32LE(2, 120);
|
|
file.writeUInt32LE(6, 124);
|
|
file.writeBigUInt64LE(BigInt(at), 128);
|
|
file.writeBigUInt64LE(FIXTURE_BINARY_BASE + BigInt(at), 136);
|
|
file.writeBigUInt64LE(FIXTURE_BINARY_BASE + BigInt(at), 144);
|
|
file.writeBigUInt64LE(BigInt(dynamic.length), 152);
|
|
file.writeBigUInt64LE(BigInt(dynamic.length), 160);
|
|
}
|
|
return file;
|
|
};
|