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

+232 -19
View File
@@ -10,9 +10,14 @@ import {
il2cppMetadataSectionSchema,
il2cppMetadataParameterSchema,
il2cppMetadataStringLiteralSchema,
il2cppMethodAddressSchema,
il2cppBinaryLinkSchema,
il2cppCodeGenModuleSchema,
il2cppTypeReferenceSchema,
il2cppMetadataObjectSchema,
il2cppMetadataParentSchema,
il2cppMetadataMemberCollections,
il2cppMetadataBinaryCollections,
il2cppMetadataTypeSchema,
} from "../il2cpp/il2cppMetadata.js";
import { err, ok, type Result } from "../result.js";
@@ -38,6 +43,19 @@ const countsSchema = z.strictObject({
events: z.number().int().nonnegative().nullable(),
parameters: z.number().int().nonnegative().nullable(),
string_literals: z.number().int().nonnegative().nullable(),
code_gen_modules: z.number().int().nonnegative().nullable(),
method_addresses: z.number().int().nonnegative().nullable(),
/** method_addresses rows per state; null when native observations were not collected. */
method_address_states: z
.strictObject({
code: z.number().int().nonnegative(),
no_body: z.number().int().nonnegative(),
unknown: z.number().int().nonnegative(),
})
.nullable(),
return_types: z.number().int().nonnegative().nullable(),
field_types: z.number().int().nonnegative().nullable(),
parameter_types: z.number().int().nonnegative().nullable(),
});
export const il2cppMetadataSummarySchema = il2cppMetadataObjectSchema
@@ -51,8 +69,19 @@ export const il2cppMetadataSummarySchema = il2cppMetadataObjectSchema
events: true,
parameters: true,
string_literals: true,
method_addresses: true,
return_types: true,
field_types: true,
parameter_types: true,
binary: true,
})
.extend({ counts: countsSchema });
.extend({
binary: il2cppBinaryLinkSchema
.omit({ code_gen_modules: true })
.nullable()
.optional(),
counts: countsSchema,
});
const indexSchema = z.number().int().nonnegative();
const sectionRowSchema = il2cppMetadataSectionSchema.extend({
@@ -72,6 +101,15 @@ const parameterRowSchema = il2cppMetadataParameterSchema.extend({
const stringLiteralRowSchema = il2cppMetadataStringLiteralSchema.extend({
index: indexSchema,
});
const methodAddressRowSchema = il2cppMethodAddressSchema.extend({
index: indexSchema,
});
const typeReferenceRowSchema = il2cppTypeReferenceSchema.extend({
index: indexSchema,
});
const codeGenModuleRowSchema = il2cppCodeGenModuleSchema.extend({
index: indexSchema,
});
const indexPage = {
kind: z.literal("il2cpp-metadata"),
indices: z.array(indexSchema).optional(),
@@ -128,6 +166,33 @@ export const il2cppMetadataViewRequestSchema = z.discriminatedUnion(
collection: z.literal("string_literals"),
fields: z.array(stringLiteralRowSchema.keyof()).optional(),
}),
z.strictObject({
...indexPage,
collection: z.literal("method_addresses"),
addresses: z.array(z.string().regex(/^0x[0-9a-fA-F]{1,16}$/u)).optional(),
states: z.array(il2cppMethodAddressSchema.shape.state).optional(),
fields: z.array(methodAddressRowSchema.keyof()).optional(),
}),
z.strictObject({
...indexPage,
collection: z.literal("code_gen_modules"),
image_indices: z.array(indexSchema).optional(),
fields: z.array(codeGenModuleRowSchema.keyof()).optional(),
}),
...(["return_types", "field_types"] as const).map((collection) =>
z.strictObject({
...indexPage,
collection: z.literal(collection),
fields: z.array(typeReferenceRowSchema.keyof()).optional(),
}),
),
z.strictObject({
...indexPage,
collection: z.literal("parameter_types"),
// Owners come from the retained parameters collection.
method_indices: z.array(indexSchema).optional(),
fields: z.array(typeReferenceRowSchema.keyof()).optional(),
}),
],
);
@@ -139,16 +204,30 @@ export const il2cppMetadataViewItemsSchema = z.union([
z.array(memberRowSchema.partial().required({ index: true })),
z.array(parameterRowSchema.partial().required({ index: true })),
z.array(stringLiteralRowSchema.partial().required({ index: true })),
z.array(methodAddressRowSchema.partial().required({ index: true })),
z.array(typeReferenceRowSchema.partial().required({ index: true })),
z.array(codeGenModuleRowSchema.partial().required({ index: true })),
]);
export const il2cppMetadataViewContextSchema = z.strictObject({
metadata_version: il2cppMetadataObjectSchema.shape.metadata_version,
counts: countsSchema,
binary_source: il2cppBinaryLinkSchema
.pick({
artifact: true,
format: true,
architecture: true,
image_base: true,
})
.optional(),
});
type Report = z.infer<typeof il2cppMetadataParentSchema>;
type Request = z.infer<typeof il2cppMetadataViewRequestSchema>;
type Row = NonNullable<Report[Request["collection"]]>[number];
type CollectionRows = Omit<Report, "binary"> & {
code_gen_modules: NonNullable<Report["binary"]>["code_gen_modules"];
};
type Row = NonNullable<CollectionRows[Request["collection"]]>[number];
interface CheckedReport {
readonly report: Report;
readonly counts: z.infer<typeof countsSchema>;
@@ -190,6 +269,27 @@ const parseMetadataReport = (
events: report.events?.length ?? null,
parameters: report.parameters?.length ?? null,
string_literals: report.string_literals?.length ?? null,
code_gen_modules: report.binary?.code_gen_modules.length ?? null,
method_addresses:
report.binary == null
? null
: (report.method_addresses?.length ?? null),
method_address_states:
(report.binary == null ? undefined : report.method_addresses)?.reduce(
(states, row) => {
if (row.state === "code") states.code++;
else if (row.state === "no-body") states.no_body++;
else states.unknown++;
return states;
},
{ code: 0, no_body: 0, unknown: 0 },
) ?? null,
return_types:
report.binary == null ? null : (report.return_types?.length ?? null),
field_types:
report.binary == null ? null : (report.field_types?.length ?? null),
parameter_types:
report.binary == null ? null : (report.parameter_types?.length ?? null),
},
};
if (reusable) checkedReports.set(key, checked);
@@ -231,10 +331,27 @@ const selection = (view: Request, report: Report) => {
!("declaring_types" in view) || view.declaring_types === undefined
? undefined
: new Set(view.declaring_types);
const addresses =
view.collection === "method_addresses" && view.addresses !== undefined
? new Set(view.addresses.map((value) => BigInt(value).toString(16)))
: undefined;
const states =
view.collection === "method_addresses" && view.states !== undefined
? new Set<string>(view.states)
: undefined;
const methods =
view.collection === "parameters" && view.method_indices !== undefined
(view.collection === "parameters" ||
view.collection === "parameter_types") &&
view.method_indices !== undefined
? new Set(view.method_indices)
: undefined;
// A parameter type row is owned by the method of the parameter at its index.
const parameterOwner = (row: Row, index: number) =>
"method" in row
? row.method
: view.collection === "parameter_types"
? report.parameters?.[index]?.method
: undefined;
const namespaces =
view.collection !== "types" || view.namespaces === undefined
? undefined
@@ -263,7 +380,38 @@ const selection = (view: Request, report: Report) => {
else ranges.push(range);
}
}
const moduleImages =
view.collection === "code_gen_modules" && view.image_indices !== undefined
? new Set(view.image_indices)
: undefined;
if (
[
moduleImages,
addresses,
states,
names,
owners,
namespaces,
methods,
imageIndices,
].every((filter) => filter === undefined)
)
return undefined;
return (row: Row, index: number) => {
if (
moduleImages !== undefined &&
(!("image" in row) || !moduleImages.has(row.image))
)
return false;
if (
addresses !== undefined &&
(!("address" in row) ||
row.address === null ||
!addresses.has(BigInt(row.address).toString(16)))
)
return false;
if (states !== undefined && (!("state" in row) || !states.has(row.state)))
return false;
if (
names !== undefined &&
(!("name" in row) || row.name === null || !names.has(row.name))
@@ -279,10 +427,7 @@ const selection = (view: Request, report: Report) => {
(!("namespace" in row) || !namespaces.has(row.namespace))
)
return false;
if (
methods !== undefined &&
(!("method" in row) || !methods.has(row.method))
)
if (methods !== undefined && !methods.has(parameterOwner(row, index) ?? -1))
return false;
if (imageIndices === undefined) return true;
let lower = 0;
@@ -309,7 +454,7 @@ export const projectIl2cppMetadataView = (
path: ["view"],
reason: "invalid_value",
message:
"IL2CPP metadata Evidence supports summary and selected sections, images, types, methods, member, parameter and string literal pages.",
"IL2CPP metadata Evidence supports summary and selected sections, images, types, methods, member, parameter, string literal, method address and type reference pages.",
},
]),
);
@@ -342,9 +487,14 @@ export const projectIl2cppMetadataView = (
events,
parameters,
string_literals,
method_addresses,
return_types,
field_types,
parameter_types,
...identity
} = report;
const allRows = {
code_gen_modules: report.binary?.code_gen_modules,
sections,
images,
types,
@@ -354,6 +504,10 @@ export const projectIl2cppMetadataView = (
events,
parameters,
string_literals,
method_addresses,
return_types,
field_types,
parameter_types,
};
const shared = {
parent_evidence_id: parent.evidenceId,
@@ -362,13 +516,23 @@ export const projectIl2cppMetadataView = (
artifact: { path: report.artifact.path, sha256: report.artifact.sha256 },
limitations: [...parent.limitations],
unknowns: [
...il2cppMetadataMemberCollections
...(report.binary != null && report.binary.code_gen_modules_field === null
? [
"Module-array association is inferred from executable byte patterns; instruction boundaries and runtime use remain unverified.",
]
: []),
...[
...il2cppMetadataMemberCollections,
...il2cppMetadataBinaryCollections,
]
.filter((name) => report[name] === undefined)
.map(
(name) =>
`Retained Evidence did not collect ${name}; its count is unknown. Reinspect the metadata to acquire these facts.`,
),
"Names come from unencrypted metadata only; native code addresses and runtime registration remain unknown.",
report.binary == null
? "Names come from unencrypted metadata only; native code addresses and runtime registration remain unknown."
: "Addresses come from static binary tables; runtime behavior and generic instance code remain unknown.",
...(parent.subject == null
? [
"The parent has no recorded subject; its normalized artifact identity cannot be cross-checked against a subject digest.",
@@ -376,15 +540,32 @@ export const projectIl2cppMetadataView = (
: []),
],
};
if (view.kind === "summary")
if (view.kind === "summary") {
const binary =
report.binary == null
? report.binary
: (({ code_gen_modules: _modules, ...facts }) => facts)(report.binary);
return ok({
kind: "summary",
view,
...shared,
summary: { ...identity, counts },
summary: {
...identity,
...(binary === undefined ? {} : { binary }),
counts,
},
coverage: completeWithinViewCoverage(1, 1),
});
const rows = allRows[view.collection];
}
const nativeUnavailable =
report.binary == null &&
[
"method_addresses",
"return_types",
"field_types",
"parameter_types",
].includes(view.collection);
const rows = nativeUnavailable ? undefined : allRows[view.collection];
if (rows === undefined)
return err(
analysisViewInputError([
@@ -395,17 +576,39 @@ export const projectIl2cppMetadataView = (
},
]),
);
if (
view.collection === "parameter_types" &&
view.method_indices !== undefined &&
report.parameters === undefined
)
return err(
analysisViewInputError([
{
path: ["view", "method_indices"],
reason: "invalid_value",
message:
"Retained Evidence did not collect parameters, so parameter owners are unknown; inspect the metadata again to select parameter types by method.",
},
]),
);
const matches = selection(view, report);
const selectedFields =
view.fields === undefined ? undefined : new Set<string>(view.fields);
const items = [];
let total = 0;
for (const index of rowIndices(rows.length, view.indices)) {
const row = rows[index]!;
if (!matches(row, index)) continue;
if (total >= view.offset && items.length < view.limit)
items.push(selectFields(row, index, selectedFields));
total++;
if (matches === undefined && view.indices === undefined) {
total = rows.length;
const end = Math.min(rows.length, view.offset + view.limit);
for (let index = Math.min(view.offset, rows.length); index < end; index++)
items.push(selectFields(rows[index]!, index, selectedFields));
} else {
for (const index of rowIndices(rows.length, view.indices)) {
const row = rows[index]!;
if (matches !== undefined && !matches(row, index)) continue;
if (total >= view.offset && items.length < view.limit)
items.push(selectFields(row, index, selectedFields));
total++;
}
}
return ok({
kind: "il2cpp-metadata",
@@ -413,6 +616,16 @@ export const projectIl2cppMetadataView = (
...shared,
items,
metadata_version: report.metadata_version,
...(report.binary == null
? {}
: {
binary_source: {
artifact: report.binary.artifact,
format: report.binary.format,
architecture: report.binary.architecture,
image_base: report.binary.image_base,
},
}),
counts,
coverage: pageViewCoverage(view.offset, items.length, total),
});