mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 14:56:32 +00:00
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:
1 parent
e50a271c72
commit
52d1a72af0
43 files changed
+5060
-368
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { createEmptyIl2cppMetadataFixture } from "../il2cpp/il2cppMetadata.fixture.js";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import {
|
||||
IL2CPP_METADATA_SECTION_NAMES,
|
||||
il2cppMetadataParentSchema,
|
||||
type Il2cppMetadata,
|
||||
} from "../il2cpp/il2cppMetadata.js";
|
||||
@@ -14,26 +14,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
const report: Il2cppMetadata = {
|
||||
artifact: {
|
||||
path: "/artifacts/global-metadata.dat",
|
||||
sha256: "a".repeat(64),
|
||||
bytes: 1024,
|
||||
},
|
||||
magic: 0xfab11baf,
|
||||
metadata_version: 39,
|
||||
index_widths: {
|
||||
type_definition: 1,
|
||||
generic_container: 1,
|
||||
parameter: 1,
|
||||
type: 1,
|
||||
type_width_source: "interface-offsets-record",
|
||||
},
|
||||
sections: IL2CPP_METADATA_SECTION_NAMES.map((name) => ({
|
||||
name,
|
||||
offset: 380,
|
||||
size: 0,
|
||||
count: 0,
|
||||
})),
|
||||
...createEmptyIl2cppMetadataFixture(),
|
||||
images: [{ name: "A.dll", type_start: 0, type_count: 2 }],
|
||||
types: [
|
||||
{
|
||||
@@ -61,6 +42,11 @@ const report: Il2cppMetadata = {
|
||||
events: [],
|
||||
parameters: [{ name: "times", method: 0 }],
|
||||
string_literals: [{ value: "Go", value_base64: null }],
|
||||
binary: null,
|
||||
method_addresses: [],
|
||||
return_types: [],
|
||||
field_types: [],
|
||||
parameter_types: [],
|
||||
limitations: [],
|
||||
};
|
||||
|
||||
@@ -72,6 +58,12 @@ const parent = (normalizedResult: unknown, sealedResult: boolean) => ({
|
||||
limitations: [],
|
||||
});
|
||||
|
||||
const summarize = (source: ReturnType<typeof parent>) => {
|
||||
const result = projectIl2cppMetadataView(source, { kind: "summary" });
|
||||
if (!result.ok) throw result.error;
|
||||
return result.value;
|
||||
};
|
||||
|
||||
const page = {
|
||||
kind: "il2cpp-metadata",
|
||||
collection: "types",
|
||||
@@ -98,6 +90,10 @@ it("validates a sealed parent once across pages and keeps counts exact", () => {
|
||||
events: 0,
|
||||
parameters: 1,
|
||||
string_literals: 1,
|
||||
method_addresses: null,
|
||||
return_types: null,
|
||||
field_types: null,
|
||||
parameter_types: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -123,9 +119,8 @@ it("preserves historical omission as unknown while keeping existing metadata que
|
||||
])
|
||||
delete historical[key];
|
||||
const source = parent(freezeJsonSnapshot(historical), true);
|
||||
const summary = projectIl2cppMetadataView(source, { kind: "summary" });
|
||||
if (!summary.ok) throw summary.error;
|
||||
expect(summary.value).toMatchObject({
|
||||
const summary = summarize(source);
|
||||
expect(summary).toMatchObject({
|
||||
summary: {
|
||||
counts: { types: 2, methods: 1, fields: null, parameters: null },
|
||||
},
|
||||
@@ -147,6 +142,141 @@ it("preserves historical omission as unknown while keeping existing metadata que
|
||||
expect(unavailable.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("treats Evidence retained before binary linking as unknown, not unlinked", () => {
|
||||
const historical = jsonObjectSchema.parse(report);
|
||||
for (const key of [
|
||||
"binary",
|
||||
"method_addresses",
|
||||
"return_types",
|
||||
"field_types",
|
||||
"parameter_types",
|
||||
])
|
||||
delete historical[key];
|
||||
const source = parent(freezeJsonSnapshot(historical), true);
|
||||
const summary = summarize(source);
|
||||
expect(summary).not.toHaveProperty("summary.binary");
|
||||
expect(summary).toMatchObject({
|
||||
summary: {
|
||||
counts: {
|
||||
fields: 1,
|
||||
method_addresses: null,
|
||||
method_address_states: null,
|
||||
return_types: null,
|
||||
},
|
||||
},
|
||||
unknowns: expect.arrayContaining([
|
||||
expect.stringContaining("did not collect method_addresses"),
|
||||
]),
|
||||
});
|
||||
expect(projectIl2cppMetadataView(source, page).ok).toBe(true);
|
||||
const unavailable = projectIl2cppMetadataView(source, {
|
||||
kind: "il2cpp-metadata",
|
||||
collection: "method_addresses",
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
});
|
||||
expect(unavailable.ok).toBe(false);
|
||||
const current = projectIl2cppMetadataView(parent(report, false), {
|
||||
kind: "summary",
|
||||
});
|
||||
if (!current.ok) throw current.error;
|
||||
expect(current.value).toMatchObject({
|
||||
summary: {
|
||||
binary: null,
|
||||
counts: {
|
||||
method_addresses: null,
|
||||
method_address_states: null,
|
||||
return_types: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
projectIl2cppMetadataView(parent(report, false), {
|
||||
kind: "il2cpp-metadata",
|
||||
collection: "method_addresses",
|
||||
offset: 0,
|
||||
limit: 64,
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
const linkedBinary = {
|
||||
artifact: {
|
||||
path: "/fixture/libil2cpp.so",
|
||||
sha256: "c".repeat(64),
|
||||
bytes: 64,
|
||||
},
|
||||
format: "elf" as const,
|
||||
architecture: "arm64",
|
||||
image_base: "0x0",
|
||||
code_gen_modules_field: "0x10",
|
||||
code_gen_modules_array: "0x20",
|
||||
code_gen_modules: [],
|
||||
types_field: "0x30",
|
||||
types_count: 4,
|
||||
};
|
||||
|
||||
it("selects parameter types by owning method through the parameters collection", () => {
|
||||
const linked = {
|
||||
...report,
|
||||
parameters: [
|
||||
{ name: "a", method: 0 },
|
||||
{ name: "b", method: 0 },
|
||||
],
|
||||
binary: { ...linkedBinary },
|
||||
method_addresses: report.methods.map(() => ({
|
||||
state: "no-body" as const,
|
||||
address: null,
|
||||
rva: null,
|
||||
})),
|
||||
return_types: report.methods.map(() => ({ type: -1, name: null })),
|
||||
field_types: report.fields.map(() => ({ type: 1, name: "int" })),
|
||||
parameter_types: [
|
||||
{ type: 2, name: "string" },
|
||||
{ type: 3, name: "bool" },
|
||||
],
|
||||
};
|
||||
const select = (
|
||||
source: ReturnType<typeof parent>,
|
||||
method_indices: number[],
|
||||
) =>
|
||||
projectIl2cppMetadataView(source, {
|
||||
kind: "il2cpp-metadata",
|
||||
collection: "parameter_types",
|
||||
method_indices,
|
||||
offset: 0,
|
||||
limit: 8,
|
||||
});
|
||||
const owned = select(parent(linked, false), [0]);
|
||||
if (!owned.ok) throw owned.error;
|
||||
expect(owned.value).toMatchObject({
|
||||
items: [
|
||||
{ index: 0, type: 2, name: "string" },
|
||||
{ index: 1, type: 3, name: "bool" },
|
||||
],
|
||||
});
|
||||
const none = select(parent(linked, false), [1]);
|
||||
if (!none.ok) throw none.error;
|
||||
expect(none.value).toMatchObject({ items: [] });
|
||||
// Without retained parameters the owners are unknown, so the filter is refused.
|
||||
const historical = jsonObjectSchema.parse(linked);
|
||||
delete historical.parameters;
|
||||
expect(select(parent(freezeJsonSnapshot(historical), true), [0]).ok).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it.each([{ method_addresses: undefined }, { binary: undefined }])(
|
||||
"rejects a binary link field retained without its collections: %j",
|
||||
(changed) => {
|
||||
const result = projectIl2cppMetadataView(
|
||||
parent({ ...report, ...changed }, false),
|
||||
{ kind: "summary" },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ fields: [{ name: "bad", declaring_type: 2 }] },
|
||||
{ parameters: [{ name: "bad", method: 1 }] },
|
||||
@@ -288,3 +418,137 @@ it("refuses a parent whose subject contradicts the recorded artifact identity",
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
|
||||
const linkedReport = (): Il2cppMetadata => ({
|
||||
...structuredClone(report),
|
||||
types: report.types.map((type, index) => ({
|
||||
...type,
|
||||
method_start: index * 2,
|
||||
method_count: 2,
|
||||
})),
|
||||
methods: [
|
||||
{ name: "A", declaring_type: 0 },
|
||||
{ name: "B", declaring_type: 0 },
|
||||
{ name: "Abstract", declaring_type: 1 },
|
||||
{ name: "Unresolved", declaring_type: 1 },
|
||||
],
|
||||
binary: {
|
||||
artifact: {
|
||||
path: "/artifacts/GameAssembly.dll",
|
||||
sha256: "c".repeat(64),
|
||||
bytes: 4096,
|
||||
},
|
||||
format: "pe",
|
||||
architecture: "amd64",
|
||||
image_base: "0x1000",
|
||||
code_gen_modules_field: "0x1200",
|
||||
code_gen_modules_array: "0x1300",
|
||||
code_gen_modules: [
|
||||
{ image: 0, address: "0x1400", method_pointer_count: 4 },
|
||||
],
|
||||
types_field: "0x1500",
|
||||
types_count: 1,
|
||||
},
|
||||
method_addresses: [
|
||||
{ state: "code", address: "0x1100", rva: "0x100" },
|
||||
{ state: "code", address: "0x1100", rva: "0x100" },
|
||||
{ state: "no-body", address: null, rva: null },
|
||||
{ state: "unknown", address: null, rva: null },
|
||||
],
|
||||
return_types: Array.from({ length: 4 }, () => ({ type: 0, name: "void" })),
|
||||
field_types: [{ type: 0, name: "int" }],
|
||||
parameter_types: [{ type: 0, name: "int" }],
|
||||
});
|
||||
|
||||
it("selects every folded method by numeric address with stable indices and both source identities", () => {
|
||||
const source = parent(freezeJsonSnapshot(linkedReport()), true);
|
||||
const view = projectIl2cppMetadataView(source, {
|
||||
kind: "il2cpp-metadata",
|
||||
collection: "method_addresses",
|
||||
addresses: ["0x00001100", "0x1100"],
|
||||
states: ["code"],
|
||||
fields: [],
|
||||
offset: 1,
|
||||
limit: 1,
|
||||
});
|
||||
if (!view.ok) throw view.error;
|
||||
expect(view.value).toMatchObject({
|
||||
items: [{ index: 1 }],
|
||||
artifact: { sha256: report.artifact.sha256 },
|
||||
binary_source: {
|
||||
artifact: { path: "/artifacts/GameAssembly.dll", sha256: "c".repeat(64) },
|
||||
image_base: "0x1000",
|
||||
},
|
||||
coverage: { total: 2, exhausted: true },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ addresses: [] },
|
||||
{ states: [] },
|
||||
{ indices: [] },
|
||||
{ addresses: ["0x1100"], indices: [2] },
|
||||
{ states: ["no-body" as const], addresses: ["0x1100"] },
|
||||
])(
|
||||
"intersects native selectors and preserves empty selection: %j",
|
||||
(selection) => {
|
||||
const view = projectIl2cppMetadataView(parent(linkedReport(), false), {
|
||||
kind: "il2cpp-metadata",
|
||||
collection: "method_addresses",
|
||||
...selection,
|
||||
offset: 0,
|
||||
limit: 64,
|
||||
});
|
||||
if (!view.ok) throw view.error;
|
||||
expect(view.value).toMatchObject({
|
||||
items: [],
|
||||
coverage: { total: 0, exhausted: true },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ state: "no-body", address: "0x1100", rva: null },
|
||||
{ state: "unknown", address: null, rva: "0x100" },
|
||||
{ state: "code", address: "0x1100", rva: "0x101" },
|
||||
{ state: "code", address: "0x10000000000000000", rva: "0x100" },
|
||||
{ state: "code", address: "malformed", rva: "0x100" },
|
||||
])(
|
||||
"rejects malformed native facts without throwing during boundary validation: %j",
|
||||
(row) => {
|
||||
const value = jsonObjectSchema.parse(linkedReport());
|
||||
value.method_addresses = [row, ...linkedReport().method_addresses.slice(1)];
|
||||
const view = projectIl2cppMetadataView(parent(value, false), {
|
||||
kind: "summary",
|
||||
});
|
||||
expect(view.ok).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps native summaries bounded while paging selected module facts", () => {
|
||||
const value = linkedReport(),
|
||||
source = parent(value, false);
|
||||
const summary = summarize(source);
|
||||
expect(summary).toMatchObject({
|
||||
summary: {
|
||||
counts: { code_gen_modules: 1 },
|
||||
binary: { artifact: value.binary!.artifact },
|
||||
},
|
||||
});
|
||||
expect("summary" in summary && summary.summary).not.toHaveProperty(
|
||||
"binary.code_gen_modules",
|
||||
);
|
||||
const page = projectIl2cppMetadataView(source, {
|
||||
kind: "il2cpp-metadata",
|
||||
collection: "code_gen_modules",
|
||||
image_indices: [0],
|
||||
fields: ["image", "address"],
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
});
|
||||
if (!page.ok) throw page.error;
|
||||
expect(page.value).toMatchObject({
|
||||
items: [{ index: 0, image: 0, address: "0x1400" }],
|
||||
coverage: { total: 1, exhausted: true },
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user