feat(il2cpp): link native methods and export Ghidra seeds

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.
This commit is contained in:
JANG BYUNGKUN authored and GitHub committed 2026-10-20 21:56:54 +08:00
1 parent e50a271c72
commit 52d1a72af0
43 files changed
+5060 -368

No files matched your search

+122 -29
View File
@@ -1,5 +1,6 @@
import {
checkRange,
readIndex,
Il2cppMetadataFormatFailure,
} from "./Il2cppMetadataFormat.js";
import {
@@ -24,7 +25,13 @@ export type Il2cppMetadataVersion = (typeof IL2CPP_METADATA_VERSIONS)[number];
type RecordSection = "typeDefinitions" | "images" | "methods";
export type Il2cppMetadataDecoded = Omit<
Il2cppMetadata,
"artifact" | "limitations"
| "artifact"
| "limitations"
| "binary"
| "method_addresses"
| "return_types"
| "field_types"
| "parameter_types"
>;
/**
@@ -121,19 +128,6 @@ export const il2cppMetadataRecordBytes = (
const isIndexWidth = (value: number): value is IndexWidth =>
value === 1 || value === 2 || value === 4;
/** Read a serialized index; the all-ones value of narrow widths means -1. */
export const readIndex = (bytes: Buffer, offset: number, width: IndexWidth) => {
if (width === 1) {
const value = bytes.readUInt8(offset);
return value === 0xff ? -1 : value;
}
if (width === 2) {
const value = bytes.readUInt16LE(offset);
return value === 0xffff ? -1 : value;
}
return bytes.readInt32LE(offset);
};
const readHeader = (bytes: Buffer) => {
const magic = bytes.length < 4 ? null : bytes.readUInt32LE(0);
if (bytes.length < 8 || magic !== IL2CPP_METADATA_MAGIC)
@@ -390,6 +384,7 @@ export interface MetadataTables {
readonly members: ReturnType<typeof readIl2cppMemberTables>;
readonly beforeText: Il2cppMetadataReadOptions["beforeText"];
readonly onRow: Il2cppMetadataReadOptions["onRow"];
readonly linkFacts?: Il2cppMetadataLinkFacts;
}
function* readImages({
@@ -572,6 +567,7 @@ function* readMethods({
counts,
readString,
methodReturnParameterToken,
linkFacts,
onRow,
}: MetadataTables): Generator<
void,
@@ -596,6 +592,17 @@ function* readMethods({
"index-out-of-range",
`method ${index} declares type ${method.declaring_type} outside the ${typeTotal} type definitions`,
);
if (linkFacts !== undefined) {
const returnAt = at + 4 + widths.type_definition;
const tokenAt =
returnAt +
widths.type +
(methodReturnParameterToken ? 4 : 0) +
widths.parameter +
widths.generic_container;
linkFacts.returnTypes[index] = readIndex(bytes, returnAt, widths.type);
linkFacts.methodTokens[index] = bytes.readUInt32LE(tokenAt);
}
parameterRanges.push({
start: readIndex(
bytes,
@@ -700,11 +707,75 @@ export interface Il2cppMetadataReadOptions {
) => void;
}
/** Per-row typed fields the public inventory omits but native linking needs. */
export interface Il2cppMetadataLinkFacts {
/** Method definition token per method index. */
readonly methodTokens: Uint32Array;
/** Il2CppType table index of each method's return type. */
readonly returnTypes: Int32Array;
readonly fieldTypes: Int32Array;
readonly parameterTypes: Int32Array;
/** Demand-read names from this stable metadata snapshot, by GenericParameterIndex. */
readonly genericParameterName: (index: number) => string | undefined;
}
/** Own the row layout once; unused generic names allocate no arrays or strings. */
const createGenericParameterReader = (tables: MetadataTables) => {
const parameters = tables.section("genericParameters"),
width = tables.widths.generic_container + 12;
const count = entriesIn(parameters, width);
if (parameters.count !== null && count !== parameters.count)
throw new Il2cppMetadataFormatFailure(
"record-size-mismatch",
"generic parameter count disagrees with its extent",
);
return (index: number): string | undefined => {
if (!Number.isInteger(index) || index < 0 || index >= count)
return undefined;
return tables.readString(
tables.bytes.readInt32LE(
parameters.offset + index * width + tables.widths.generic_container,
),
`generic parameter ${index} name`,
);
};
};
/** Decoded metadata plus the typed row facts that link it to a native binary. */
export interface Il2cppMetadataForLinking {
readonly metadata: Il2cppMetadataDecoded;
readonly link: Il2cppMetadataLinkFacts;
}
/** Decode image, type, method and member names and string literals without native binaries or decryption. */
export function* readIl2cppMetadataSteps(
bytes: Buffer,
options: Il2cppMetadataReadOptions = {},
): Generator<void, Il2cppMetadataDecoded> {
return (yield* readMetadataCoreSteps(bytes, options, false)).metadata;
}
/** Decode metadata and retain the typed row facts used for native linking. */
export function* readIl2cppMetadataForLinkingSteps(
bytes: Buffer,
options: Il2cppMetadataReadOptions = {},
): Generator<void, Il2cppMetadataForLinking> {
const parsed = yield* readMetadataCoreSteps(bytes, options, true);
if (parsed.link === undefined)
throw new Error(
"Native metadata reader did not retain requested linking facts",
);
return { metadata: parsed.metadata, link: parsed.link };
}
function* readMetadataCoreSteps(
bytes: Buffer,
options: Il2cppMetadataReadOptions,
linking: boolean,
): Generator<
void,
{ metadata: Il2cppMetadataDecoded; link: Il2cppMetadataLinkFacts | undefined }
> {
const { magic, version, headerBytes, sections } = readHeader(bytes);
checkSections(bytes, headerBytes, sections);
const byName = new Map(sections.map((entry) => [entry.name, entry]));
@@ -713,7 +784,7 @@ export function* readIl2cppMetadataSteps(
const widths =
layout.indexing === "counted" ? resolveWidths(section) : FIXED_WIDTHS;
const expected = il2cppMetadataRecordBytes(widths, version);
const tables: MetadataTables = {
let tables: MetadataTables = {
bytes,
section,
widths,
@@ -741,6 +812,17 @@ export function* readIl2cppMetadataSteps(
methods: tables.counts.methods,
...tables.members.counts,
});
if (linking)
tables = {
...tables,
linkFacts: {
methodTokens: new Uint32Array(tables.counts.methods),
returnTypes: new Int32Array(tables.counts.methods),
fieldTypes: new Int32Array(tables.members.counts.fields),
parameterTypes: new Int32Array(tables.members.counts.parameters),
genericParameterName: createGenericParameterReader(tables),
},
};
yield;
const images = yield* readImages(tables);
yield* checkImageRanges(images);
@@ -752,25 +834,36 @@ export function* readIl2cppMetadataSteps(
parameters: parameterRanges,
});
return {
magic,
metadata_version: version,
index_widths: widths,
sections,
images,
types,
methods,
...members,
metadata: {
magic,
metadata_version: version,
index_widths: widths,
sections,
images,
types,
methods,
...members,
},
link: tables.linkFacts,
};
}
/** Synchronous consumption of the same decoder for byte-level callers. */
export const readIl2cppMetadata = (
bytes: Buffer,
options: Il2cppMetadataReadOptions = {},
): Il2cppMetadataDecoded => {
const steps = readIl2cppMetadataSteps(bytes, options);
const runSteps = <T>(steps: Generator<void, T>): T => {
for (;;) {
const step = steps.next();
if (step.done) return step.value;
}
};
/** Synchronous consumption of the same decoder for byte-level callers. */
export const readIl2cppMetadata = (
bytes: Buffer,
options: Il2cppMetadataReadOptions = {},
): Il2cppMetadataDecoded => runSteps(readIl2cppMetadataSteps(bytes, options));
/** Synchronous decoding that also retains the typed row facts used for native linking. */
export const readIl2cppMetadataForLinking = (
bytes: Buffer,
options: Il2cppMetadataReadOptions = {},
): Il2cppMetadataForLinking =>
runSteps(readIl2cppMetadataForLinkingSteps(bytes, options));