117 lines
3.5 KiB
JavaScript
117 lines
3.5 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { createReadStream } from "node:fs";
|
|
import { mkdir, open, readFile, writeFile } from "node:fs/promises";
|
|
import { resolve } from "node:path";
|
|
import { parseArgs } from "node:util";
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
|
|
const { values } = parseArgs({
|
|
options: {
|
|
"runtime-root": { type: "string" },
|
|
path: { type: "string" },
|
|
providers: { type: "string" },
|
|
output: { type: "string" },
|
|
},
|
|
});
|
|
if (
|
|
!values["runtime-root"] ||
|
|
!values.path ||
|
|
!values.providers ||
|
|
!values.output
|
|
)
|
|
throw new Error(
|
|
"Use --runtime-root ABS --path ABS --providers JSON --output NEW_ABS_DIR",
|
|
);
|
|
const selected = JSON.parse(await readFile(values.providers, "utf8"));
|
|
const expectedName = selected.REA_GHIDRA_DISABLED_ANALYZERS;
|
|
if (!expectedName)
|
|
throw new Error(
|
|
"The probe requires an explicitly selected invalid analyzer name",
|
|
);
|
|
const digest = async () => {
|
|
const hash = createHash("sha256");
|
|
for await (const chunk of createReadStream(values.path)) hash.update(chunk);
|
|
return hash.digest("hex");
|
|
};
|
|
const before = await digest();
|
|
await mkdir(values.output, { mode: 0o700 });
|
|
const file = await open(values.path, "r");
|
|
let address;
|
|
try {
|
|
const header = Buffer.alloc(32);
|
|
const { bytesRead } = await file.read(header, 0, header.length, 0);
|
|
if (
|
|
bytesRead !== 32 ||
|
|
header.subarray(0, 6).toString("hex") !== "7f454c460201"
|
|
)
|
|
throw new Error(
|
|
"This counterexample uses a real ELF64 little-endian application",
|
|
);
|
|
address = `0x${header.readBigUInt64LE(24).toString(16)}`;
|
|
} finally {
|
|
await file.close();
|
|
}
|
|
const client = new Client({ name: "rea-analyzer-verification", version: "1" });
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [resolve(values["runtime-root"], "scripts/rea.mjs"), "mcp"],
|
|
env: { ...process.env, ...selected },
|
|
stderr: "pipe",
|
|
});
|
|
const calls = [];
|
|
const call = async (name, arguments_) => {
|
|
const reply = await client.callTool(
|
|
{ name, arguments: arguments_ },
|
|
{ timeout: 600_000 },
|
|
);
|
|
await writeFile(
|
|
resolve(values.output, `call-${calls.length + 1}.json`),
|
|
JSON.stringify(reply),
|
|
);
|
|
calls.push({ name, is_error: reply.isError === true });
|
|
return reply;
|
|
};
|
|
let rejected = false;
|
|
let named = false;
|
|
let failure = null;
|
|
try {
|
|
await client.connect(transport);
|
|
const opened = await call("open_binary", {
|
|
path: values.path,
|
|
provider_id: "ghidra",
|
|
});
|
|
const reply = opened.isError
|
|
? opened
|
|
: await call("resolve_containing_procedure", { address });
|
|
rejected = reply.isError === true;
|
|
named = rejected && JSON.stringify(reply).includes(expectedName);
|
|
} catch (error) {
|
|
failure = String(error);
|
|
} finally {
|
|
try {
|
|
await call("close_binary", {});
|
|
} catch (error) {
|
|
failure = `${failure ?? ""} close: ${String(error)}`;
|
|
}
|
|
await client.close();
|
|
}
|
|
const after = await digest();
|
|
const report = {
|
|
target_sha256: before,
|
|
target_sha256_after: after,
|
|
expected_analyzer: expectedName,
|
|
rejected,
|
|
rejection_names_requested_analyzer: named,
|
|
failure,
|
|
calls,
|
|
oracle:
|
|
"A syntactically valid but nonexistent analyzer must produce a meaningful public refusal, not successful Evidence claiming that preset. A not-found procedure still means Ghidra reached readiness.",
|
|
};
|
|
await writeFile(
|
|
resolve(values.output, "summary.json"),
|
|
JSON.stringify(report, null, 2),
|
|
);
|
|
process.stdout.write(JSON.stringify(report) + "\n");
|
|
if (!rejected || !named || failure || before !== after) process.exitCode = 1;
|