Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30473dbcac | ||
|
|
edb4c63adf | ||
|
|
ff46705dfd |
@@ -841,14 +841,13 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|||||||
|
|
||||||
private JsonObject instructionReference(Reference reference) {
|
private JsonObject instructionReference(Reference reference) {
|
||||||
JsonObject item = new JsonObject();
|
JsonObject item = new JsonObject();
|
||||||
RefType type = reference.getReferenceType();
|
JsonObject kind = referenceKind(reference);
|
||||||
|
item.addProperty("source_address", canonicalAddress(reference.getFromAddress()));
|
||||||
item.addProperty("target_address", canonicalAddress(reference.getToAddress()));
|
item.addProperty("target_address", canonicalAddress(reference.getToAddress()));
|
||||||
item.addProperty("type", type.getName());
|
for (String field : List.of("type", "call", "jump", "indirect", "computed",
|
||||||
item.addProperty("call", type.isCall());
|
"operand_index", "data", "read", "write", "primary", "provenance"))
|
||||||
item.addProperty("jump", type.isJump());
|
item.add(field, kind.get(field));
|
||||||
item.addProperty("indirect", type.isIndirect());
|
item.addProperty("source", normalizedSource(reference.getSource()));
|
||||||
item.addProperty("computed", type.isComputed());
|
|
||||||
item.addProperty("operand_index", reference.getOperandIndex());
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -195,6 +195,14 @@ outside-memory and undecodable addresses have separate outcomes. Effective
|
|||||||
memory base/index/displacement roles and per-instruction context mode remain
|
memory base/index/displacement roles and per-instruction context mode remain
|
||||||
unavailable; `mode` is the program language variant.
|
unavailable; `mode` is the program language variant.
|
||||||
|
|
||||||
|
Compact instruction references and call-target references preserve the source
|
||||||
|
address, operand index, DATA/READ/WRITE classification, primary status, provider
|
||||||
|
`provenance`, and reference `source` (for Ghidra: `analysis`, `default`, `imported`
|
||||||
|
or `user_defined`). Primary status reports the provider's chosen reference; it
|
||||||
|
does not establish literal addressing. Analyzer-inferred indexed targets remain
|
||||||
|
static references, not observed runtime reads or writes. Older evidence may omit
|
||||||
|
the additive fields, and null or omitted facts mean unavailable rather than false.
|
||||||
|
|
||||||
Ghidra distinguishes a return using decoded instruction p-code's `RETURN`
|
Ghidra distinguishes a return using decoded instruction p-code's `RETURN`
|
||||||
operation. `flow.classification_evidence` preserves the Listing flow type and
|
operation. `flow.classification_evidence` preserves the Listing flow type and
|
||||||
the observed return evidence; mnemonic spelling alone is not the classifier.
|
the observed return evidence; mnemonic spelling alone is not the classifier.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ for (const architecture of ["x86_64", "x86"]) {
|
|||||||
"fixtures",
|
"fixtures",
|
||||||
`rea-ghidra-windows${x86 ? "-x86" : ""}.${dll ? "dll" : "exe"}`,
|
`rea-ghidra-windows${x86 ? "-x86" : ""}.${dll ? "dll" : "exe"}`,
|
||||||
);
|
);
|
||||||
const bytes = Buffer.alloc(1024);
|
const bytes = Buffer.alloc(dll ? 1024 : 1536);
|
||||||
const peOffset = 0x80;
|
const peOffset = 0x80;
|
||||||
const optionalHeader = peOffset + 24;
|
const optionalHeader = peOffset + 24;
|
||||||
const optionalHeaderSize = x86 ? 0xe0 : 0xf0;
|
const optionalHeaderSize = x86 ? 0xe0 : 0xf0;
|
||||||
@@ -24,7 +24,7 @@ for (const architecture of ["x86_64", "x86"]) {
|
|||||||
bytes.writeUInt32LE(peOffset, 0x3c);
|
bytes.writeUInt32LE(peOffset, 0x3c);
|
||||||
bytes.write("PE\0\0", peOffset, "binary");
|
bytes.write("PE\0\0", peOffset, "binary");
|
||||||
bytes.writeUInt16LE(x86 ? 0x14c : 0x8664, peOffset + 4);
|
bytes.writeUInt16LE(x86 ? 0x14c : 0x8664, peOffset + 4);
|
||||||
bytes.writeUInt16LE(1, peOffset + 6);
|
bytes.writeUInt16LE(dll ? 1 : 2, peOffset + 6);
|
||||||
bytes.writeUInt16LE(optionalHeaderSize, peOffset + 20);
|
bytes.writeUInt16LE(optionalHeaderSize, peOffset + 20);
|
||||||
bytes.writeUInt16LE(
|
bytes.writeUInt16LE(
|
||||||
(x86 ? 0x102 : 0x22) | (dll ? 0x2000 : 0),
|
(x86 ? 0x102 : 0x22) | (dll ? 0x2000 : 0),
|
||||||
@@ -33,6 +33,7 @@ for (const architecture of ["x86_64", "x86"]) {
|
|||||||
|
|
||||||
bytes.writeUInt16LE(x86 ? 0x10b : 0x20b, optionalHeader);
|
bytes.writeUInt16LE(x86 ? 0x10b : 0x20b, optionalHeader);
|
||||||
bytes.writeUInt32LE(0x200, optionalHeader + 4);
|
bytes.writeUInt32LE(0x200, optionalHeader + 4);
|
||||||
|
if (!dll) bytes.writeUInt32LE(0x200, optionalHeader + 8);
|
||||||
bytes.writeUInt32LE(dll ? 0 : 0x1000, optionalHeader + 16);
|
bytes.writeUInt32LE(dll ? 0 : 0x1000, optionalHeader + 16);
|
||||||
bytes.writeUInt32LE(0x1000, optionalHeader + 20);
|
bytes.writeUInt32LE(0x1000, optionalHeader + 20);
|
||||||
if (x86) bytes.writeUInt32LE(0x400000, optionalHeader + 28);
|
if (x86) bytes.writeUInt32LE(0x400000, optionalHeader + 28);
|
||||||
@@ -41,7 +42,7 @@ for (const architecture of ["x86_64", "x86"]) {
|
|||||||
bytes.writeUInt32LE(0x200, optionalHeader + 36);
|
bytes.writeUInt32LE(0x200, optionalHeader + 36);
|
||||||
bytes.writeUInt16LE(6, optionalHeader + 40);
|
bytes.writeUInt16LE(6, optionalHeader + 40);
|
||||||
bytes.writeUInt16LE(6, optionalHeader + 48);
|
bytes.writeUInt16LE(6, optionalHeader + 48);
|
||||||
bytes.writeUInt32LE(0x2000, optionalHeader + 56);
|
bytes.writeUInt32LE(dll ? 0x2000 : 0x3000, optionalHeader + 56);
|
||||||
bytes.writeUInt32LE(0x200, optionalHeader + 60);
|
bytes.writeUInt32LE(0x200, optionalHeader + 60);
|
||||||
bytes.writeUInt16LE(3, optionalHeader + 68);
|
bytes.writeUInt16LE(3, optionalHeader + 68);
|
||||||
bytes.writeUInt16LE(x86 ? 0x8140 : 0x8160, optionalHeader + 70);
|
bytes.writeUInt16LE(x86 ? 0x8140 : 0x8160, optionalHeader + 70);
|
||||||
@@ -60,7 +61,7 @@ for (const architecture of ["x86_64", "x86"]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bytes.write(".text", sectionTable, "ascii");
|
bytes.write(".text", sectionTable, "ascii");
|
||||||
bytes.writeUInt32LE(dll ? 0x180 : 0x16, sectionTable + 8);
|
bytes.writeUInt32LE(dll ? 0x180 : 0x50, sectionTable + 8);
|
||||||
bytes.writeUInt32LE(0x1000, sectionTable + 12);
|
bytes.writeUInt32LE(0x1000, sectionTable + 12);
|
||||||
bytes.writeUInt32LE(0x200, sectionTable + 16);
|
bytes.writeUInt32LE(0x200, sectionTable + 16);
|
||||||
bytes.writeUInt32LE(0x200, sectionTable + 20);
|
bytes.writeUInt32LE(0x200, sectionTable + 20);
|
||||||
@@ -70,11 +71,48 @@ for (const architecture of ["x86_64", "x86"]) {
|
|||||||
bytes.set(
|
bytes.set(
|
||||||
dll
|
dll
|
||||||
? [0xe8, 0x0b, 0, 0, 0, 0x83, 0xc0, 1, 0xc3]
|
? [0xe8, 0x0b, 0, 0, 0, 0x83, 0xc0, 1, 0xc3]
|
||||||
: [0xe8, 0x0b, 0, 0, 0, 0x31, 0xc0, 0xc3],
|
: [0xe8, 0x0b, 0, 0, 0, 0xe8, 0x16, 0, 0, 0, 0x31, 0xc0, 0xc3],
|
||||||
0x200,
|
0x200,
|
||||||
);
|
);
|
||||||
bytes.set([0xb8, 0x2a, 0, 0, 0, 0xc3], 0x210);
|
bytes.set([0xb8, 0x2a, 0, 0, 0, 0xc3], 0x210);
|
||||||
|
|
||||||
|
if (!dll) {
|
||||||
|
const dataSection = sectionTable + 40;
|
||||||
|
bytes.write(".data", dataSection, "ascii");
|
||||||
|
bytes.writeUInt32LE(0x20, dataSection + 8);
|
||||||
|
bytes.writeUInt32LE(0x2000, dataSection + 12);
|
||||||
|
bytes.writeUInt32LE(0x200, dataSection + 16);
|
||||||
|
bytes.writeUInt32LE(0x400, dataSection + 20);
|
||||||
|
bytes.writeUInt32LE(0xc000_0040, dataSection + 36);
|
||||||
|
bytes.writeUInt32LE(0x1122_3344, 0x400);
|
||||||
|
const referenceInstructions = x86
|
||||||
|
? [
|
||||||
|
"ba00204000", // MOV EDX, address: DATA only.
|
||||||
|
"a100204000", // MOV EAX, [address]: READ.
|
||||||
|
"a304204000", // MOV [address], EAX: WRITE.
|
||||||
|
"ba10204000", // Indexed base, unknown ECX.
|
||||||
|
"85db", // TEST EBX, EBX: two candidate bases.
|
||||||
|
"7405", // JZ over the alternate base.
|
||||||
|
"ba18204000", // MOV EDX, alternate base.
|
||||||
|
"c6040a01", // MOV byte [EDX + ECX], 1.
|
||||||
|
"ffd7", // CALL EDI: unknown target.
|
||||||
|
"c3",
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
"488d15d90f0000", // LEA RDX, [RIP + displacement]: DATA.
|
||||||
|
"8b05d30f0000", // MOV EAX, [RIP + displacement]: READ.
|
||||||
|
"8905d10f0000", // MOV [RIP + displacement], EAX: WRITE.
|
||||||
|
"488d15d60f0000", // Indexed base, unknown RCX.
|
||||||
|
"85db", // TEST EBX, EBX: two candidate bases.
|
||||||
|
"7407", // JZ over the alternate base.
|
||||||
|
"488d15d30f0000", // LEA RDX, alternate base.
|
||||||
|
"c6040a01", // MOV byte [RDX + RCX], 1.
|
||||||
|
"41ffd0", // CALL R8: unknown target.
|
||||||
|
"c3",
|
||||||
|
];
|
||||||
|
bytes.set(Buffer.from(referenceInstructions.join(""), "hex"), 0x220);
|
||||||
|
}
|
||||||
|
|
||||||
if (dll) {
|
if (dll) {
|
||||||
// Export both functions; an entry-point-free library is discovered by exports.
|
// Export both functions; an entry-point-free library is discovered by exports.
|
||||||
bytes.writeUInt32LE(0x1040, directories);
|
bytes.writeUInt32LE(0x1040, directories);
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
/** Verify actual reference facts on the source-owned Windows PE fixture. */
|
||||||
|
export async function verifyGhidraCompactReferences(call, x86, cli) {
|
||||||
|
const base = x86 ? 0x400000n : 0x1_4000_0000n;
|
||||||
|
const address = (offset) => `0x${(base + BigInt(offset)).toString(16)}`;
|
||||||
|
const cases = [
|
||||||
|
["data", 0x1020, 0x2000],
|
||||||
|
["read", x86 ? 0x1025 : 0x1027, 0x2000],
|
||||||
|
["write", x86 ? 0x102a : 0x102d, 0x2004],
|
||||||
|
];
|
||||||
|
const full = await call("procedure_references", {
|
||||||
|
procedure: address(0x1020),
|
||||||
|
});
|
||||||
|
const inspected = {};
|
||||||
|
for (const [kind, offset, target] of cases) {
|
||||||
|
const instruction = await call("inspect_native_instruction", {
|
||||||
|
address: address(offset),
|
||||||
|
});
|
||||||
|
inspected[kind] = instruction;
|
||||||
|
const reference = instruction.references.find(
|
||||||
|
(item) => item.target_address === address(target),
|
||||||
|
);
|
||||||
|
assert.ok(reference, `Missing ${kind} fixture reference`);
|
||||||
|
assertReference(reference, instruction.address, full.references);
|
||||||
|
assert.equal(reference.data, true);
|
||||||
|
assert.equal(reference.read, kind === "read");
|
||||||
|
assert.equal(reference.write, kind === "write");
|
||||||
|
}
|
||||||
|
const indexed = await call("inspect_native_instruction", {
|
||||||
|
address: address(x86 ? 0x103d : 0x1045),
|
||||||
|
});
|
||||||
|
for (const reference of indexed.references)
|
||||||
|
assertReference(reference, indexed.address, full.references);
|
||||||
|
assert.ok(
|
||||||
|
indexed.references.some(
|
||||||
|
(reference) =>
|
||||||
|
reference.primary === false && reference.source === "analysis",
|
||||||
|
),
|
||||||
|
"Indexed fixture lacks a non-primary analyzer reference",
|
||||||
|
);
|
||||||
|
|
||||||
|
const { direct, targets } = await verifyDirectCall(
|
||||||
|
call,
|
||||||
|
address(0x1000),
|
||||||
|
address(0x1010),
|
||||||
|
);
|
||||||
|
|
||||||
|
const indirectAddress = address(x86 ? 0x1041 : 0x1049);
|
||||||
|
const indirect = await call("inspect_native_instruction", {
|
||||||
|
address: indirectAddress,
|
||||||
|
});
|
||||||
|
const unresolved = await call("resolve_native_call_targets", {
|
||||||
|
address: indirectAddress,
|
||||||
|
});
|
||||||
|
assert.equal(indirect.flow.kind, "call");
|
||||||
|
assert.deepEqual(indirect.references, []);
|
||||||
|
assert.equal(unresolved.status, "unresolved");
|
||||||
|
assert.deepEqual(unresolved.targets, []);
|
||||||
|
|
||||||
|
const returned = await verifyEmptyReferences(
|
||||||
|
call,
|
||||||
|
address(x86 ? 0x1043 : 0x104c),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cli !== undefined) {
|
||||||
|
for (const instruction of [inspected.write, indexed, direct, returned])
|
||||||
|
assert.deepEqual(
|
||||||
|
await cli("inspect-native-instruction", instruction.address),
|
||||||
|
instruction,
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
await cli("resolve-native-call-targets", direct.address),
|
||||||
|
targets,
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
await cli("resolve-native-call-targets", indirectAddress),
|
||||||
|
unresolved,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data_read_write: true,
|
||||||
|
source_primary_provenance: true,
|
||||||
|
full_reference_parity: true,
|
||||||
|
direct_call_reference_parity: true,
|
||||||
|
indexed_reference_count: indexed.references.length,
|
||||||
|
indexed_non_primary_count: indexed.references.filter(
|
||||||
|
(reference) => reference.primary === false,
|
||||||
|
).length,
|
||||||
|
indexed_non_primary_analyzer_reference: true,
|
||||||
|
indexed_reference_kinds: indexed.references.map(
|
||||||
|
({ type, read, write, primary, source }) => ({
|
||||||
|
type,
|
||||||
|
read,
|
||||||
|
write,
|
||||||
|
primary,
|
||||||
|
source,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
targetless_indirect: true,
|
||||||
|
empty_and_unavailable: true,
|
||||||
|
cli_mcp_parity: cli !== undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyDirectCall(call, entry, callee) {
|
||||||
|
const direct = await call("inspect_native_instruction", { address: entry });
|
||||||
|
const targets = await call("resolve_native_call_targets", { address: entry });
|
||||||
|
assert.equal(targets.status, "direct");
|
||||||
|
assert.equal(targets.targets[0].address, callee);
|
||||||
|
assert.deepEqual(
|
||||||
|
targets.targets[0].references,
|
||||||
|
direct.references.filter((item) => item.call),
|
||||||
|
);
|
||||||
|
const full = await call("procedure_references", { procedure: entry });
|
||||||
|
for (const reference of direct.references)
|
||||||
|
assertReference(reference, entry, full.references);
|
||||||
|
return { direct, targets };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyEmptyReferences(call, address) {
|
||||||
|
const returned = await call("inspect_native_instruction", { address });
|
||||||
|
assert.equal(returned.flow.kind, "return");
|
||||||
|
assert.deepEqual(returned.references, []);
|
||||||
|
assert.equal(
|
||||||
|
(await call("resolve_native_call_targets", { address })).status,
|
||||||
|
"not-call",
|
||||||
|
);
|
||||||
|
const outside = await call("inspect_native_instruction", { address: "0x0" });
|
||||||
|
assert.equal(outside.status, "outside-memory");
|
||||||
|
assert.deepEqual(outside.references, []);
|
||||||
|
assert.equal(
|
||||||
|
(await call("resolve_native_call_targets", { address: "0x0" })).status,
|
||||||
|
"unavailable",
|
||||||
|
);
|
||||||
|
return returned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertReference(reference, source, full) {
|
||||||
|
assert.equal(reference.source_address, source);
|
||||||
|
assert.equal(reference.provenance, "ghidra-reference-manager");
|
||||||
|
assert.equal(typeof reference.primary, "boolean");
|
||||||
|
assert.equal(typeof reference.source, "string");
|
||||||
|
assert.ok(reference.source.length > 0);
|
||||||
|
const edge = full.find(
|
||||||
|
(item) =>
|
||||||
|
item.source_address === source &&
|
||||||
|
item.target_address === reference.target_address &&
|
||||||
|
item.kind.operand_index === reference.operand_index,
|
||||||
|
);
|
||||||
|
assert.ok(edge, "Compact reference lacks its matching full reference");
|
||||||
|
for (const field of [
|
||||||
|
"type",
|
||||||
|
"call",
|
||||||
|
"jump",
|
||||||
|
"indirect",
|
||||||
|
"computed",
|
||||||
|
"operand_index",
|
||||||
|
"data",
|
||||||
|
"read",
|
||||||
|
"write",
|
||||||
|
"primary",
|
||||||
|
"provenance",
|
||||||
|
])
|
||||||
|
assert.equal(
|
||||||
|
reference[field],
|
||||||
|
edge.kind[field],
|
||||||
|
`Reference ${field} differs from its full representation`,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
|
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
|
||||||
|
import { verifyGhidraCompactReferences } from "./lib/real-ghidra-compact-references.mjs";
|
||||||
import { execFile } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import {
|
import {
|
||||||
@@ -358,6 +359,36 @@ try {
|
|||||||
await call("inspect_native_data_type", { type: "/undefined8" });
|
await call("inspect_native_data_type", { type: "/undefined8" });
|
||||||
await call("inspect_native_instruction", { address: procedure.address });
|
await call("inspect_native_instruction", { address: procedure.address });
|
||||||
await call("resolve_native_call_targets", { address: procedure.address });
|
await call("resolve_native_call_targets", { address: procedure.address });
|
||||||
|
// Explicit caller-selected targets need not contain our fixture instructions.
|
||||||
|
const controlledTargets = [
|
||||||
|
"rea-ghidra-windows.exe",
|
||||||
|
"rea-ghidra-windows-x86.exe",
|
||||||
|
].map((name) => resolve("build", "fixtures", name));
|
||||||
|
if (controlledTargets.includes(target))
|
||||||
|
report.compactReferences = await verifyGhidraCompactReferences(
|
||||||
|
call,
|
||||||
|
target === controlledTargets[1],
|
||||||
|
async (command, address) => {
|
||||||
|
const result = await exec(
|
||||||
|
process.execPath,
|
||||||
|
[entry, command, target, address, "--provider", "ghidra", "--json"],
|
||||||
|
{
|
||||||
|
env: environment,
|
||||||
|
cwd: callerDirectory,
|
||||||
|
timeout: 360_000,
|
||||||
|
maxBuffer: 8 * 1024 * 1024,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const evidence = JSON.parse(result.stdout);
|
||||||
|
assert.ok(evidence.error === undefined, `CLI ${command} failed`);
|
||||||
|
const contract = TOOL_CONTRACTS.find(
|
||||||
|
({ name }) => name === command.replaceAll("-", "_"),
|
||||||
|
);
|
||||||
|
assert.ok(contract, `Missing CLI ${command} contract`);
|
||||||
|
contract.outputSchema.parse(evidence);
|
||||||
|
return evidence.normalized_result;
|
||||||
|
},
|
||||||
|
);
|
||||||
const expected = (
|
const expected = (
|
||||||
await import(
|
await import(
|
||||||
pathToFileURL(join(packageRoot, "dist/ghidra/GhidraSessionValues.js"))
|
pathToFileURL(join(packageRoot, "dist/ghidra/GhidraSessionValues.js"))
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { windowsP0Capabilities } from "../dist/ghidra/GhidraProviderCapabilities
|
|||||||
import { GHIDRA_SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
|
import { GHIDRA_SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
|
||||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||||
|
import { verifyGhidraCompactReferences } from "./lib/real-ghidra-compact-references.mjs";
|
||||||
|
|
||||||
const verifierRun = createVerifierRun();
|
const verifierRun = createVerifierRun();
|
||||||
const SESSION_CAPABILITIES = GHIDRA_SESSION_CAPABILITIES.filter(
|
const SESSION_CAPABILITIES = GHIDRA_SESSION_CAPABILITIES.filter(
|
||||||
@@ -247,6 +248,14 @@ try {
|
|||||||
`Windows Ghidra operation proof was incomplete: ${JSON.stringify([...observed])}`,
|
`Windows Ghidra operation proof was incomplete: ${JSON.stringify([...observed])}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// The reference controls occupy the EXE fixture's additional function.
|
||||||
|
// DLL fixtures retain their independent exported-caller/return controls.
|
||||||
|
const compactReferences = dllFixture
|
||||||
|
? null
|
||||||
|
: await verifyGhidraCompactReferences(
|
||||||
|
functionOperation,
|
||||||
|
architecture === "x86",
|
||||||
|
);
|
||||||
report = {
|
report = {
|
||||||
ok: true,
|
ok: true,
|
||||||
provider: { id: "ghidra", version: SUPPORTED_GHIDRA_VERSION },
|
provider: { id: "ghidra", version: SUPPORTED_GHIDRA_VERSION },
|
||||||
@@ -261,6 +270,7 @@ try {
|
|||||||
transport: "authenticated-loopback-tcp",
|
transport: "authenticated-loopback-tcp",
|
||||||
operations: [...observed].sort((left, right) => left.localeCompare(right)),
|
operations: [...observed].sort((left, right) => left.localeCompare(right)),
|
||||||
cleanup: "complete",
|
cleanup: "complete",
|
||||||
|
compact_references: compactReferences,
|
||||||
native_controls: {
|
native_controls: {
|
||||||
job_object: true,
|
job_object: true,
|
||||||
protected_dacl: true,
|
protected_dacl: true,
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import type { JsonValue } from "../jsonValue.js";
|
||||||
|
|
||||||
|
/** Wire-boundary controls; these do not establish real Ghidra behavior. */
|
||||||
|
export const compactReference = () => ({
|
||||||
|
source_address: "0x401000",
|
||||||
|
target_address: "0x402000",
|
||||||
|
type: "COMPUTED_CALL",
|
||||||
|
call: true,
|
||||||
|
jump: false,
|
||||||
|
indirect: false,
|
||||||
|
computed: true,
|
||||||
|
operand_index: 1,
|
||||||
|
data: false,
|
||||||
|
read: false,
|
||||||
|
write: false,
|
||||||
|
primary: false,
|
||||||
|
provenance: "ghidra-reference-manager",
|
||||||
|
source: "analysis",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const compactInstruction = (references: JsonValue[] = []) => ({
|
||||||
|
address: "0x401000",
|
||||||
|
status: "decoded",
|
||||||
|
procedure: "0x401000",
|
||||||
|
architecture: "x86:LE:32:default",
|
||||||
|
mode: "default",
|
||||||
|
bytes: "ffd0",
|
||||||
|
length: 2,
|
||||||
|
mnemonic: "CALL",
|
||||||
|
raw_disassembly: "CALL EAX",
|
||||||
|
operands: [],
|
||||||
|
flow: {
|
||||||
|
kind: "call",
|
||||||
|
conditional: false,
|
||||||
|
computed: true,
|
||||||
|
direct_destinations: [],
|
||||||
|
},
|
||||||
|
references,
|
||||||
|
limitations: ["Static references do not establish runtime targets."],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const compactCallTargets = (references: JsonValue[] = []) => ({
|
||||||
|
call_site: "0x401000",
|
||||||
|
procedure: "0x401000",
|
||||||
|
status: references.length === 0 ? "unresolved" : "resolved-indirect",
|
||||||
|
mechanism: "computed",
|
||||||
|
targets:
|
||||||
|
references.length === 0
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
address: "0x402000",
|
||||||
|
procedure: null,
|
||||||
|
status: "resolved-indirect",
|
||||||
|
basis: "provider-reference",
|
||||||
|
references,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
limitations: ["Static references do not establish runtime targets."],
|
||||||
|
});
|
||||||
@@ -13,6 +13,33 @@ const reference = z.strictObject({
|
|||||||
indirect: z.boolean(),
|
indirect: z.boolean(),
|
||||||
computed: z.boolean(),
|
computed: z.boolean(),
|
||||||
operand_index: z.number().int(),
|
operand_index: z.number().int(),
|
||||||
|
source_address: z.string().min(1).nullable().optional(),
|
||||||
|
data: z.boolean().nullable().optional(),
|
||||||
|
read: z.boolean().nullable().optional(),
|
||||||
|
write: z.boolean().nullable().optional(),
|
||||||
|
primary: z
|
||||||
|
.boolean()
|
||||||
|
.nullable()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"Provider primary-reference status, not proof of literal addressing or a runtime access. Null or omission means unavailable.",
|
||||||
|
),
|
||||||
|
provenance: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.nullable()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"Provider subsystem reporting this reference; null or omission means unavailable.",
|
||||||
|
),
|
||||||
|
source: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.nullable()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"Provider reference origin, such as analysis, default, imported or user_defined; null or omission means unavailable.",
|
||||||
|
),
|
||||||
});
|
});
|
||||||
/** Provider-decoded facts for one instruction; operand tokens preserve their native ordering. */
|
/** Provider-decoded facts for one instruction; operand tokens preserve their native ordering. */
|
||||||
export const nativeInstructionSchema = z.strictObject({
|
export const nativeInstructionSchema = z.strictObject({
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import { describe, expect, it } from "vitest";
|
|||||||
import type { JsonValue } from "../domain/jsonValue.js";
|
import type { JsonValue } from "../domain/jsonValue.js";
|
||||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||||
import { functionDossierSchema } from "../domain/hopperValues.js";
|
import { functionDossierSchema } from "../domain/hopperValues.js";
|
||||||
|
import {
|
||||||
|
compactReference,
|
||||||
|
compactInstruction,
|
||||||
|
compactCallTargets,
|
||||||
|
} from "../domain/native/nativeInstruction.fixture.js";
|
||||||
import {
|
import {
|
||||||
parseGhidraFunctionResult,
|
parseGhidraFunctionResult,
|
||||||
type GhidraFunctionOperation,
|
type GhidraFunctionOperation,
|
||||||
@@ -118,6 +123,75 @@ describe("Ghidra function-analysis result values", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Ghidra compact reference provenance", () => {
|
||||||
|
const operations = [
|
||||||
|
["inspect_native_instruction", compactInstruction],
|
||||||
|
["resolve_native_call_targets", compactCallTargets],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
it.each(operations)(
|
||||||
|
"preserves complete, legacy and unknown facts in %s",
|
||||||
|
(operation, output) => {
|
||||||
|
const complete = compactReference();
|
||||||
|
const legacy = Object.fromEntries(
|
||||||
|
Object.entries(complete).filter(
|
||||||
|
([key]) =>
|
||||||
|
![
|
||||||
|
"source_address",
|
||||||
|
"data",
|
||||||
|
"read",
|
||||||
|
"write",
|
||||||
|
"primary",
|
||||||
|
"provenance",
|
||||||
|
"source",
|
||||||
|
].includes(key),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const unknown = {
|
||||||
|
...legacy,
|
||||||
|
source_address: null,
|
||||||
|
data: null,
|
||||||
|
read: null,
|
||||||
|
write: null,
|
||||||
|
primary: null,
|
||||||
|
provenance: null,
|
||||||
|
source: null,
|
||||||
|
};
|
||||||
|
for (const reference of [complete, legacy, unknown]) {
|
||||||
|
const expected = output([reference]);
|
||||||
|
const parsed = parseGhidraFunctionResult(operation, expected);
|
||||||
|
if (!parsed.ok) throw parsed.error;
|
||||||
|
expect(parsed.value).toEqual(expected);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(operations)(
|
||||||
|
"rejects malformed present provenance in %s",
|
||||||
|
(operation, output) => {
|
||||||
|
for (const [field, value] of [
|
||||||
|
["source_address", 1],
|
||||||
|
["data", "true"],
|
||||||
|
["read", 1],
|
||||||
|
["write", 0],
|
||||||
|
["primary", "false"],
|
||||||
|
["provenance", ""],
|
||||||
|
["source", false],
|
||||||
|
] as const) {
|
||||||
|
expect(
|
||||||
|
parseGhidraFunctionResult(
|
||||||
|
operation,
|
||||||
|
output([{ ...compactReference(), [field]: value }]),
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: { _tag: "AnalysisOutputError" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe("Ghidra jump-table mapping contract", () => {
|
describe("Ghidra jump-table mapping contract", () => {
|
||||||
it("preserves evidenced defaults separately from cases and unresolved targets", () => {
|
it("preserves evidenced defaults separately from cases and unresolved targets", () => {
|
||||||
const dossier = functionDossierSchema.parse(ghidraFunctionDossier());
|
const dossier = functionDossierSchema.parse(ghidraFunctionDossier());
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { ok } from "../../../src/domain/result.js";
|
||||||
|
import {
|
||||||
|
compactReference,
|
||||||
|
compactInstruction,
|
||||||
|
compactCallTargets,
|
||||||
|
} from "../../../src/domain/native/nativeInstruction.fixture.js";
|
||||||
|
import { connectGhidraMcp, sessionEvidence } from "./ghidraMcpHarness.js";
|
||||||
|
|
||||||
|
describe("compact Ghidra reference MCP evidence", () => {
|
||||||
|
it("retains access kind, non-primary analyzer references and call-target provenance", async () => {
|
||||||
|
const base = {
|
||||||
|
...compactReference(),
|
||||||
|
call: false,
|
||||||
|
computed: false,
|
||||||
|
data: true,
|
||||||
|
};
|
||||||
|
const references = [
|
||||||
|
{ ...base, type: "DATA", primary: true, operand_index: 1 },
|
||||||
|
{ ...base, type: "READ", primary: true, read: true, operand_index: 1 },
|
||||||
|
{ ...base, type: "WRITE", primary: false, write: true, operand_index: 0 },
|
||||||
|
];
|
||||||
|
const outputs = {
|
||||||
|
inspect_native_instruction: compactInstruction(references),
|
||||||
|
resolve_native_call_targets: compactCallTargets([compactReference()]),
|
||||||
|
};
|
||||||
|
const harness = await connectGhidraMcp(
|
||||||
|
"ghidra-compact-provenance",
|
||||||
|
(operation) => {
|
||||||
|
if (
|
||||||
|
operation !== "inspect_native_instruction" &&
|
||||||
|
operation !== "resolve_native_call_targets"
|
||||||
|
)
|
||||||
|
throw new TypeError(`Unexpected operation: ${operation}`);
|
||||||
|
return Promise.resolve(ok(outputs[operation]));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
for (const name of [
|
||||||
|
"inspect_native_instruction",
|
||||||
|
"resolve_native_call_targets",
|
||||||
|
] as const) {
|
||||||
|
const result = await harness.mcp.callTool({
|
||||||
|
name,
|
||||||
|
arguments: { address: "0x401000" },
|
||||||
|
});
|
||||||
|
expect(result.isError).not.toBe(true);
|
||||||
|
const evidence = sessionEvidence(
|
||||||
|
harness.session,
|
||||||
|
result.structuredContent,
|
||||||
|
);
|
||||||
|
expect(evidence.normalized_result).toEqual(outputs[name]);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await harness.close();
|
||||||
|
}
|
||||||
|
}, 30_000);
|
||||||
|
|
||||||
|
it("preserves empty references and unresolved targets without manufacturing provenance", async () => {
|
||||||
|
const outputs = {
|
||||||
|
inspect_native_instruction: compactInstruction(),
|
||||||
|
resolve_native_call_targets: compactCallTargets(),
|
||||||
|
};
|
||||||
|
const harness = await connectGhidraMcp(
|
||||||
|
"ghidra-compact-unknown",
|
||||||
|
(operation) => {
|
||||||
|
if (
|
||||||
|
operation !== "inspect_native_instruction" &&
|
||||||
|
operation !== "resolve_native_call_targets"
|
||||||
|
)
|
||||||
|
throw new TypeError(`Unexpected operation: ${operation}`);
|
||||||
|
return Promise.resolve(ok(outputs[operation]));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
for (const name of [
|
||||||
|
"inspect_native_instruction",
|
||||||
|
"resolve_native_call_targets",
|
||||||
|
] as const) {
|
||||||
|
const result = await harness.mcp.callTool({
|
||||||
|
name,
|
||||||
|
arguments: { address: "0x401000" },
|
||||||
|
});
|
||||||
|
expect(result.isError).not.toBe(true);
|
||||||
|
expect(
|
||||||
|
sessionEvidence(harness.session, result.structuredContent)
|
||||||
|
.normalized_result,
|
||||||
|
).toEqual(outputs[name]);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await harness.close();
|
||||||
|
}
|
||||||
|
}, 30_000);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user