Files
rea/src/il2cpp/Il2cppGhidraSeeds.ts
T
JANG BYUNGKUN 52d1a72af0 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.
2026-10-20 21:56:54 +08:00

200 lines
6.9 KiB
TypeScript

import {
MAX_GHIDRA_SEED_BYTES,
MAX_GHIDRA_SEED_ENTRIES,
MAX_GHIDRA_SEED_NAME_LENGTH,
} from "../ghidra/GhidraAnalysisSeeds.js";
import {
AnalysisInputError,
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
const OPERATION = "inspect_il2cpp_metadata";
const capacity = (message: string) =>
new AnalysisResourceConstraintError(OPERATION, "capacity", message, {
maximum_seed_bytes: MAX_GHIDRA_SEED_BYTES,
maximum_seed_name_characters: MAX_GHIDRA_SEED_NAME_LENGTH,
});
import type { Il2cppMetadata } from "../domain/il2cpp/il2cppMetadata.js";
/** A linked metadata report reduced to Ghidra function seeds. */
export interface Il2cppGhidraSeeds {
/** `address<TAB>function<TAB>name` lines in the REA_GHIDRA_SEED_FILE format. */
readonly text: string;
readonly functions: number;
/** Methods whose code address another method already seeded (identical code folding). */
readonly folded: number;
/** Each folded address with the name its function seed kept and the names it shares. */
readonly folded_groups: readonly {
readonly address: string;
readonly kept: string;
readonly also: readonly string[];
}[];
/** Methods without native code: abstract, interface, or generic definitions. */
readonly without_address: number;
readonly no_body: number;
readonly unknown: number;
}
/**
* Where Ghidra's default loaders place the binary: an ELF linked at 0 goes to
* 0x100000, while Mach-O and PE images keep their link base.
*/
export const ghidraDefaultBase = (
binary: NonNullable<Il2cppMetadata["binary"]>,
): bigint => {
const linked = BigInt(binary.image_base);
return binary.format === "elf" && linked === 0n ? 0x100000n : linked;
};
/** `Namespace.Outer.Inner` for a type, following declaring_type to the outermost type. */
const typeName = (report: Il2cppMetadata, index: number): string => {
const parts: string[] = [];
let namespace = "";
const seen = new Set<number>();
let length = 0;
for (let at = index; at !== -1;) {
if (seen.has(at))
throw new AnalysisInputError(OPERATION, undefined, [
{
path: ["types", at],
reason: "invalid_format",
message: "Cyclic type ancestry cannot name a seed.",
},
]);
seen.add(at);
const type = report.types[at];
if (type === undefined)
throw new AnalysisInputError(OPERATION, undefined, [
{
path: ["types", at],
reason: "invalid_format",
message: "Seed type owner is unavailable.",
},
]);
length += type.name.length + 1;
if (length > MAX_GHIDRA_SEED_NAME_LENGTH)
throw capacity("Qualified seed type name exceeds the Ghidra name limit.");
parts.unshift(type.name);
namespace = type.namespace;
if (length + namespace.length > MAX_GHIDRA_SEED_NAME_LENGTH)
throw capacity("Qualified seed type name exceeds the Ghidra name limit.");
at = type.declaring_type;
}
return namespace === "" ? parts.join(".") : `${namespace}.${parts.join(".")}`;
};
/**
* Name every linked method `Type$$Method`, the Il2CppDumper convention, at
* `base + rva`. Whitespace becomes `_` because seed names cannot contain it.
* The first method at an address names its function, since a seed file holds
* one function seed per address. The other methods at that address are listed
* in `#` comment lines after the seeds, so a reader of the file or the
* decompilation can see that the kept name is one of several.
*/
export const il2cppGhidraSeeds = (
report: Il2cppMetadata,
base: bigint,
): Il2cppGhidraSeeds => {
if (report.binary === null)
throw new AnalysisInputError(OPERATION, undefined, [
{
path: ["binary_path"],
reason: "invalid_value",
message: "Ghidra seeds need a report linked with binary_path.",
},
]);
const seeded = new Map<bigint, { kept: string; also: string[] }>();
const lines: string[] = [];
let folded = 0;
let withoutAddress = 0,
noBody = 0,
unknown = 0,
bytes = 0;
const typeNames = new Map<number, string>();
const reserve = (length: number) => {
if (length > MAX_GHIDRA_SEED_BYTES - bytes)
throw capacity("Seed file exceeds Ghidra byte admission.");
bytes += length;
};
const emit = (text: string) => {
reserve(Buffer.byteLength(text));
lines.push(text);
};
report.methods.forEach((method, index) => {
const rva = report.method_addresses[index]?.rva ?? null;
if (rva === null) {
withoutAddress++;
if (report.method_addresses[index]?.state === "no-body") noBody++;
else unknown++;
return;
}
const address = base + BigInt(rva);
if (address < 0n || address > 0xffffffffffffffffn)
throw new AnalysisInputError(OPERATION, undefined, [
{
path: ["seed_base"],
reason: "invalid_value",
message: "Rebased seed address exceeds 64 bits.",
},
]);
let ownerName = typeNames.get(method.declaring_type);
if (ownerName === undefined) {
ownerName = typeName(report, method.declaring_type);
typeNames.set(method.declaring_type, ownerName);
}
if (ownerName.length + 2 + method.name.length > MAX_GHIDRA_SEED_NAME_LENGTH)
throw capacity(
"Qualified method seed name exceeds Ghidra character admission.",
);
const name = `${ownerName}$$${method.name}`.replace(/\s/gu, "_");
if (name.includes("\0"))
throw new AnalysisInputError(OPERATION, undefined, [
{
path: ["methods", index, "name"],
reason: "invalid_format",
message: "Seed names cannot contain NUL.",
},
]);
const group = seeded.get(address);
if (group !== undefined) {
folded++;
if (group.also.length === 0)
reserve(
Buffer.byteLength(
`# folded 0x${address.toString(16)} ${group.kept} also\n`,
),
);
reserve(Buffer.byteLength(` ${name}`));
group.also.push(name);
return;
}
seeded.set(address, { kept: name, also: [] });
if (seeded.size > MAX_GHIDRA_SEED_ENTRIES)
throw capacity("Seed function count exceeds Ghidra entry admission.");
emit(`0x${address.toString(16)}\tfunction\t${name}\n`);
});
const foldedGroups = [...seeded]
.filter(([, group]) => group.also.length > 0)
.map(([address, group]) => ({
address: `0x${address.toString(16)}`,
kept: group.kept,
also: group.also,
}));
for (const group of foldedGroups) {
lines.push(`# folded ${group.address} ${group.kept} also`);
for (const name of group.also) lines.push(` ${name}`);
lines.push("\n");
}
emit(
`# metadata_sha256 ${report.artifact.sha256}\n# binary_sha256 ${report.binary.artifact.sha256}\n# link_base ${report.binary.image_base}\n# seed_base 0x${base.toString(16)}\n`,
);
return {
text: lines.join(""),
functions: seeded.size,
folded,
folded_groups: foldedGroups,
without_address: withoutAddress,
no_body: noBody,
unknown,
};
};