rea/scripts/verify-real-ghidra-function...

423 lines
12 KiB
JavaScript

import {
parseGhidraFunctionInput,
parseGhidraFunctionResult,
} from "../dist/ghidra/GhidraFunctionValues.js";
import {
assertDossier,
assertLocalProcedureInfo,
assertProviderText,
assertReferenceMetadata,
dossierSummary,
matchesSymbol,
requireProcedure,
} from "./verify-real-ghidra-assertions.mjs";
async function functionCall(client, operation, parameters) {
const input = parseGhidraFunctionInput(operation, parameters);
if (!input.ok) throw input.error;
const called = await client.callTool(operation, input.value);
if (!called.ok)
throw new Error(
`Ghidra ${operation} failed for ${JSON.stringify(input.value)}: ${called.error.message}`,
{ cause: called.error },
);
const parsed = parseGhidraFunctionResult(operation, called.value);
if (!parsed.ok) throw parsed.error;
return parsed.value;
}
export async function verifyDebugFunctionOperations(
client,
{ procedures, strings, entry },
) {
const leaf = requireProcedure(procedures, "rea_ghidra_inventory_leaf");
const branch = requireProcedure(procedures, "rea_ghidra_inventory_branch");
const indirect = requireProcedure(
procedures,
"rea_ghidra_inventory_indirect",
);
const main = requireProcedure(procedures, "main");
const entryString = strings.find(
(item) => item.value === "REA_GHIDRA_INVENTORY_ENTRY",
);
if (entryString === undefined)
throw new Error("Ghidra debug string function probe is unavailable");
const [info, assembly, pseudocode, callees, callers, outgoing, entryXrefs] =
await collectEntryFunctionData(client, entry);
assertLocalProcedureInfo(info, entry.address);
assertProviderText(assembly, pseudocode, entry.address);
assertEntryCallGraph({ branch, indirect, main, callees, callers });
const { xrefOwners, xrefOwnerFailures } = await resolveXrefOwners(
client,
entryXrefs,
);
assertEntryXrefAttribution({
entryXrefs,
callers,
xrefOwners,
xrefOwnerFailures,
main,
});
assertReferenceMetadata(outgoing);
await assertStringXrefs(client, entryString.address);
const entryDossier = await analyzeEntryDossier(
client,
entry,
branch,
indirect,
);
const branchDossier = await analyzeBranchDossier(client, branch);
const indirectDossier = await analyzeIndirectDossier(client, indirect);
assertIndirectDossier(indirectDossier, leaf.address);
const { cancellation, timeout } = await verifyRequestCancellationAndTimeout(
client,
entry,
);
const concurrent = await verifyConcurrentRequests(client, entry, leaf);
return {
entry: dossierSummary(entryDossier),
branch: dossierSummary(branchDossier),
indirect: dossierSummary(indirectDossier),
cancellation,
timeout,
concurrency: concurrent,
};
}
async function collectEntryFunctionData(client, entry) {
return Promise.all([
functionCall(client, "procedure_info", {
document: null,
procedure: entry.value,
}),
functionCall(client, "procedure_assembly", {
document: null,
procedure: entry.value,
}),
functionCall(client, "procedure_pseudo_code", {
document: null,
procedure: entry.value,
}),
functionCall(client, "procedure_callees", {
document: null,
procedure: entry.value,
}),
functionCall(client, "procedure_callers", {
document: null,
procedure: entry.value,
}),
functionCall(client, "procedure_references", {
document: null,
procedure: entry.value,
direction: "outgoing",
}),
functionCall(client, "xrefs", {
document: null,
address: entry.address,
}),
]);
}
function assertEntryCallGraph({ branch, indirect, main, callees, callers }) {
for (const expected of [branch.address, indirect.address])
if (!callees.includes(expected))
throw new Error(`Ghidra call graph missed ${expected}`);
if (!callers.includes(main.address))
throw new Error("Ghidra caller or xref relation missed main");
}
async function resolveXrefOwners(client, entryXrefs) {
const xrefOwners = [];
const xrefOwnerFailures = [];
for (const address of entryXrefs) {
try {
xrefOwners.push(
await functionCall(client, "resolve_containing_procedure", {
document: null,
address,
}),
);
} catch (cause) {
xrefOwnerFailures.push({
address,
message: cause instanceof Error ? cause.message : String(cause),
});
}
}
return { xrefOwners, xrefOwnerFailures };
}
function assertEntryXrefAttribution({
entryXrefs,
callers,
xrefOwners,
xrefOwnerFailures,
main,
}) {
if (
!xrefOwners.some(
(owner) => owner.found && owner.procedure.address === main.address,
)
)
throw new Error(
`Ghidra entry xrefs were not attributable to main: ${JSON.stringify({ entryXrefs, callers, xrefOwners, xrefOwnerFailures })}`,
);
}
async function assertStringXrefs(client, entryStringAddress) {
const stringXrefs = await functionCall(client, "xrefs", {
document: null,
address: entryStringAddress,
});
if (stringXrefs.length === 0)
throw new Error("Ghidra xrefs missed the entry string");
}
async function analyzeEntryDossier(client, entry, branch, indirect) {
const entryDossier = await functionCall(client, "analyze_function", {
procedure: entry.address,
include_assembly: true,
limit: 500,
max_pseudocode_chars: 100_000,
max_instructions: 5_000,
});
assertDossier(entryDossier, {
address: entry.address,
expectedCallees: [branch.address, indirect.address],
expectedString: "REA_GHIDRA_INVENTORY_ENTRY",
requireAssembly: true,
});
return entryDossier;
}
async function analyzeBranchDossier(client, branch) {
const branchDossier = await functionCall(client, "analyze_function", {
procedure: branch.address,
limit: 500,
max_pseudocode_chars: 100_000,
max_instructions: 5_000,
});
assertDossier(branchDossier, {
address: branch.address,
requireMultiBlock: true,
});
return branchDossier;
}
async function analyzeIndirectDossier(client, indirect) {
const indirectDossier = await functionCall(client, "analyze_function", {
procedure: indirect.address,
include_assembly: true,
limit: 500,
max_pseudocode_chars: 100_000,
max_instructions: 5_000,
});
assertDossier(indirectDossier, {
address: indirect.address,
requireAssembly: true,
});
return indirectDossier;
}
function assertIndirectDossier(indirectDossier, leafAddress) {
if (
indirectDossier.callees.items.some(({ address }) => address === leafAddress)
)
throw new Error(
"Ghidra dossier falsely resolved a targetless callback as the fixture leaf",
);
if (!/\bcall\b/iu.test(indirectDossier.assembly.items.join("\n")))
throw new Error("Ghidra indirect-call fixture lost its computed call site");
}
async function verifyRequestCancellationAndTimeout(client, entry) {
const aborted = new AbortController();
aborted.abort();
const cancelled = await client.callTool(
"procedure_info",
{ document: null, procedure: entry.address },
{ signal: aborted.signal },
);
if (cancelled.ok || cancelled.error.kind !== "cancelled")
throw new Error("Ghidra established-request cancellation drifted");
const timedOut = await client.callTool(
"procedure_info",
{ document: null, procedure: entry.address },
{ timeoutMs: 0 },
);
if (timedOut.ok || timedOut.error.kind !== "timeout")
throw new Error("Ghidra established-request deadline drifted");
return {
cancellation: "cancelled-before-wire",
timeout: "expired-before-wire",
};
}
async function verifyConcurrentRequests(client, entry, leaf) {
const concurrent = await Promise.all([
functionCall(client, "procedure_info", {
document: null,
procedure: entry.address,
}),
functionCall(client, "xrefs", {
document: null,
address: leaf.address,
}),
]);
if (concurrent.length !== 2)
throw new Error("Ghidra serial request queue lost a concurrent result");
return "serialized-two-results";
}
export async function verifyStrippedFunctionOperations(client, entryString) {
const stringXrefs = await functionCall(client, "xrefs", {
document: null,
address: entryString.address,
});
if (stringXrefs.length === 0)
throw new Error("Ghidra stripped string xref probe is unavailable");
const containing = await functionCall(
client,
"resolve_containing_procedure",
{
document: null,
address: stringXrefs[0],
},
);
if (!containing.found)
throw new Error("Ghidra could not recover a stripped containing function");
const procedure = containing.procedure;
const pseudocode = await functionCall(client, "procedure_pseudo_code", {
document: null,
procedure: procedure.address,
});
if (typeof pseudocode !== "string" || pseudocode.trim().length === 0)
throw new Error("Ghidra did not decompile the stripped function");
const dossier = await functionCall(client, "analyze_function", {
procedure: procedure.address,
include_assembly: true,
limit: 500,
max_pseudocode_chars: 100_000,
max_instructions: 5_000,
});
assertDossier(dossier, {
address: procedure.address,
expectedString: "REA_GHIDRA_INVENTORY_ENTRY",
requireAssembly: true,
});
if (symbolTail(dossier.procedure.name) === "rea_ghidra_inventory_entry")
throw new Error("Ghidra stripped dossier invented a source symbol");
return dossierSummary(dossier);
}
export async function verifyCrossFormatOperations({
client,
variant,
procedures,
names,
strings,
}) {
const message = strings.find(
(item) => item.value === "REA_GHIDRA_CROSS_FORMAT",
);
if (message === undefined)
throw new Error(`${variant} string inventory missed the source oracle`);
const entry =
procedures.find((item) => matchesSymbol(item.value, "rea_cross_entry")) ??
names.find((item) => matchesSymbol(item.value, "rea_cross_entry"));
if (entry === undefined)
throw new Error(`${variant} did not retain the exported entry symbol`);
const entryAddress = entry.address;
const info = await functionCall(client, "procedure_info", {
document: null,
procedure: entryAddress,
});
assertLocalProcedureInfo(info, entryAddress);
const [pseudocode, assembly, callees, messageXrefs] = await Promise.all([
functionCall(client, "procedure_pseudo_code", {
document: null,
procedure: entryAddress,
}),
functionCall(client, "procedure_assembly", {
document: null,
procedure: entryAddress,
}),
functionCall(client, "procedure_callees", {
document: null,
procedure: entryAddress,
}),
functionCall(client, "xrefs", {
document: null,
address: message.address,
}),
]);
assertProviderText(assembly, pseudocode, entryAddress);
if (callees.length < 2)
throw new Error(`${variant} call graph missed direct fixture calls`);
if (messageXrefs.length === 0)
throw new Error(`${variant} xrefs missed the fixture string`);
const messageOwner = await findCrossStringOwner(client, messageXrefs);
const stringDossier = await functionCall(client, "analyze_function", {
procedure: messageOwner.procedure.address,
limit: 500,
max_pseudocode_chars: 100_000,
max_instructions: 5_000,
});
assertDossier(stringDossier, {
address: messageOwner.procedure.address,
expectedString: "REA_GHIDRA_CROSS_FORMAT",
});
const branchAddress = await findCrossBranchAddress(client, variant, callees);
const branchDossier = await functionCall(client, "analyze_function", {
procedure: branchAddress,
limit: 500,
max_pseudocode_chars: 100_000,
max_instructions: 5_000,
});
assertDossier(branchDossier, {
address: branchAddress,
requireMultiBlock: true,
});
return {
entry: entryAddress,
exported_symbol: entry.value,
string_owner: dossierSummary(stringDossier),
branch: dossierSummary(branchDossier),
};
}
async function findCrossStringOwner(client, messageXrefs) {
for (const address of messageXrefs) {
const candidate = await functionCall(
client,
"resolve_containing_procedure",
{
document: null,
address,
},
);
if (candidate.found) return candidate;
}
throw new Error("Cross-format did not resolve the string-owning function");
}
async function findCrossBranchAddress(client, variant, callees) {
for (const address of callees) {
const candidate = await functionCall(client, "procedure_info", {
document: null,
procedure: address,
});
if (candidate.basicblock_count > 1) return address;
}
throw new Error(`${variant} did not recover the multi-block callee`);
}