diff --git a/src/il2cpp/Il2cppBinaryImage.ts b/src/il2cpp/Il2cppBinaryImage.ts index 5ce825f31a..6ad4bedb9f 100644 --- a/src/il2cpp/Il2cppBinaryImage.ts +++ b/src/il2cpp/Il2cppBinaryImage.ts @@ -76,7 +76,7 @@ export interface NativeImage { readonly imageBase: bigint; readonly unresolvedPointerSlots: ReadonlyMap; readonly zeroFillPointers: ReadonlyMap; - /** Mach-O loader fixups, used when interpreting method pointer slots. */ + /** Mach-O loader fixups, owned by every pointer interpretation. */ readonly machoPointerFixups?: PointerFixups; } @@ -191,10 +191,7 @@ export function* openImageSteps( }; } -export const readPointer = ( - image: NativeImage, - address: bigint, -): bigint | null => { +const readRawPointer = (image: NativeImage, address: bigint): bigint | null => { if (hasUnresolvedPointer(image, address)) return null; const virtual = image.zeroFillPointers.get(address); if (virtual !== undefined) return virtual; @@ -347,7 +344,7 @@ export const readSlot = ( let raw: bigint | null; if (virtual !== undefined) raw = virtual; else if (isZeroFilled(image, address, 8)) raw = 0n; - else raw = readPointer(image, address); + else raw = readRawPointer(image, address); if (raw === null) return null; if (image.machoPointerFixups !== undefined) { const resolution = image.machoPointerFixups.decode(address, raw); @@ -367,3 +364,12 @@ export const readSlot = ( throw cause; } }; + +/** Resolve a pointer through the same loader interpretation as method slots. */ +export const readPointer = ( + image: NativeImage, + address: bigint, +): bigint | null => { + const slot = readSlot(image, address); + return typeof slot === "bigint" ? slot : null; +}; diff --git a/src/il2cpp/Il2cppBinaryRegistration.ts b/src/il2cpp/Il2cppBinaryRegistration.ts index 0452477ec5..0090beea75 100644 --- a/src/il2cpp/Il2cppBinaryRegistration.ts +++ b/src/il2cpp/Il2cppBinaryRegistration.ts @@ -207,7 +207,7 @@ export function* findCodeRegistration( modules: readonly Il2cppCodeGenModule[], ): Generator { const expected = new Set(modules.map((module) => module.address)); - const total = BigInt(modules.length); + const total = modules.length; const matches: { field: bigint; array: bigint }[] = []; const matchedFields = new Set(); for (const mapping of image.container.mappings) { @@ -215,8 +215,9 @@ export function* findCodeRegistration( for (let at = start; at + 16 <= mapping.size; at += 8) { if ((at & 0xffff) < 8) yield; const offset = mapping.offset + at; - if (image.bytes.readBigUInt64LE(offset) !== total) continue; - const array = image.bytes.readBigUInt64LE(offset + 8); + if (image.bytes.readUInt32LE(offset) !== total) continue; + const array = readPointer(image, mapping.address + BigInt(at + 8)); + if (array === null) continue; if (!isMapped(image, array, modules.length * 8)) continue; const seen = new Set(); for (let slot = 0; slot < modules.length; slot++) { @@ -333,21 +334,27 @@ export function* findMetadataRegistration( const matches: { field: bigint; count: number; types: bigint }[] = []; const matchedFields = new Set(); const low = (offset: number) => image.bytes.readUInt32LE(offset); - const word = (offset: number) => image.bytes.readBigUInt64LE(offset); for (const mapping of image.container.mappings) { const start = Number((8n - (mapping.address % 8n)) % 8n); - for (let at = start; at + 128 <= mapping.size; at += 8) { + for (let at = start; at + 112 <= mapping.size; at += 8) { if ((at & 0xffff) < 8) yield; const base = mapping.offset + at; if (low(base + 80) !== typeDefinitions) continue; if (low(base + 96) !== typeDefinitions) continue; const count = low(base + 48); - const types = word(base + 56); + const address = mapping.address + BigInt(at); + if (!hasPointerArrayCoverage(image, address, 16)) continue; + const types = readPointer(image, address + 56n); + const fieldOffsets = readPointer(image, address + 88n); + const typeSizes = readPointer(image, address + 104n); if ( count === 0 || count > 0x7fffffff || - !isMapped(image, word(base + 88), 8) || - !isMapped(image, word(base + 104), 8) || + types === null || + fieldOffsets === null || + typeSizes === null || + !isMapped(image, fieldOffsets, 8) || + !isMapped(image, typeSizes, 8) || !isMapped(image, types, count * 8) ) continue; diff --git a/tests/boundary/il2cpp/il2cppBinaryFile.test.ts b/tests/boundary/il2cpp/il2cppBinaryFile.test.ts index c8c656191b..efb3739c7a 100644 --- a/tests/boundary/il2cpp/il2cppBinaryFile.test.ts +++ b/tests/boundary/il2cpp/il2cppBinaryFile.test.ts @@ -670,3 +670,40 @@ it("reads type attributes without requiring file-backed alignment padding", () = Array.from({ length: 5 }, () => "int"), ); }); + +it("ignores CodeRegistration count alignment padding", () => { + const bytes = createIl2cppBinaryFixture(); + bytes.fill(0xff, 0x604, 0x608); + expect(link(bytes).method_addresses).toEqual(expectedAddresses); +}); + +it.each([0, 8, 16])( + "finds registration with %s file-backed trailing zero bytes", + (tail) => { + const original = createIl2cppBinaryFixture(); + const bytes = Buffer.alloc(0x1000 + tail); + original.copy(bytes); + original.copy(bytes, 0xf90, 0x800, 0x880); + bytes.fill(0, 0x800, 0x880); + bytes.writeBigUInt64LE(0x2000n, 64); + bytes.writeBigUInt64LE(BigInt(bytes.length), 80); + const linked = link(bytes); + expect(linked.types_field).toBe(FIXTURE_BINARY_BASE + 0xfc0n); + expect(linked.method_addresses).toEqual(expectedAddresses); + expect(linked.return_types).toEqual(link(original).return_types); + }, +); + +it.each([0n, FIXTURE_BINARY_BASE + 0x980n, FIXTURE_BINARY_BASE + 0x9b0n])( + "refuses dyld-bound type placeholders regardless of raw word %s", + (raw) => { + const bytes = createIl2cppDyldBindFixture(0n); + bytes.writeBigUInt64LE(raw, 0x900); + bytes.writeUInt32LE(7, 88); + bytes.writeUInt32LE(7, 92); + const offset = 0xf00 + 2 + Buffer.byteLength("_FixtureMethod\0") + 2; + bytes[offset] = 0x80; + bytes[offset + 1] = 0x12; // ULEB128 0x900: type-table slot, not method slot. + expect(failure(bytes).reason).toBe("invalid-type"); + }, +);