mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
228 lines
7.6 KiB
JavaScript
228 lines
7.6 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { promisify } from "node:util";
|
|
|
|
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
|
import { parseConfig } from "../dist/config/parseConfig.js";
|
|
import { GhidraClient } from "../dist/ghidra/GhidraClient.js";
|
|
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
|
|
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallationPolicy.js";
|
|
import { GhidraProvider } from "../dist/ghidra/GhidraProvider.js";
|
|
import { silentLogger } from "../dist/logger.js";
|
|
import { cleanupOwnedProcessGroup } from "../dist/process/ProcessOwnership.js";
|
|
import {
|
|
ProviderProcessSupervisor,
|
|
spawnOwnedProviderProcess,
|
|
} from "../dist/process/ProviderProcess.js";
|
|
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
|
|
|
const exec = promisify(execFile);
|
|
const run = createVerifierRun();
|
|
const procedureName = (name) =>
|
|
process.platform === "darwin" ? `_${name}` : name;
|
|
assert.ok(
|
|
process.platform === "linux" || process.platform === "darwin",
|
|
"This regression lane requires Linux or macOS",
|
|
);
|
|
assert.ok(
|
|
process.arch === "x64" || process.arch === "arm64",
|
|
"This regression lane requires x86-64 or arm64",
|
|
);
|
|
const config = parseConfig(process.env);
|
|
if (!config.ok) throw config.error;
|
|
const installation = inspectGhidraInstallation({
|
|
environment: process.env,
|
|
installDir: config.value.ghidraInstallDir,
|
|
javaHome: config.value.ghidraJavaHome,
|
|
});
|
|
assert.equal(installation.status, "available", JSON.stringify(installation));
|
|
assert.equal(installation.providerVersion, SUPPORTED_GHIDRA_VERSION);
|
|
const fixtures = fileURLToPath(
|
|
new URL("../tests/conformance/ghidra", import.meta.url),
|
|
);
|
|
const bridge = fileURLToPath(new URL("../bridge/ghidra", import.meta.url));
|
|
const workspace = await mkdtemp(join(tmpdir(), "rea-ghidra-noreturn-"));
|
|
let cleaned = true;
|
|
let report;
|
|
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
try {
|
|
const targetPath = join(
|
|
workspace,
|
|
process.platform === "darwin" ? "noreturn.dylib" : "noreturn.so",
|
|
);
|
|
const targetOptions =
|
|
process.platform === "darwin"
|
|
? [
|
|
"-dynamiclib",
|
|
"-undefined",
|
|
"dynamic_lookup",
|
|
"-U_FORTIFY_SOURCE",
|
|
"-D_FORTIFY_SOURCE=0",
|
|
]
|
|
: ["-shared"];
|
|
await exec(process.env.REA_CC ?? "cc", [
|
|
...targetOptions,
|
|
"-fPIC",
|
|
"-O0",
|
|
"-fno-builtin",
|
|
join(fixtures, "no-return.c"),
|
|
"-o",
|
|
targetPath,
|
|
]);
|
|
const targetDigest = digest(await readFile(targetPath));
|
|
const sourceDigests = Object.fromEntries(
|
|
await Promise.all(
|
|
["ReaGhidraBridge.java", "ReaGhidraNoReturnFix.java"].map(
|
|
async (name) => [name, digest(await readFile(join(bridge, name)))],
|
|
),
|
|
),
|
|
);
|
|
const project = join(workspace, "project");
|
|
await mkdir(project);
|
|
const launch = await spawnOwnedProviderProcess({
|
|
command: installation.analyzeHeadlessPath,
|
|
arguments: [
|
|
project,
|
|
"NoReturn",
|
|
"-import",
|
|
targetPath,
|
|
"-readOnly",
|
|
"-deleteProject",
|
|
"-scriptPath",
|
|
`${fixtures};${bridge}`,
|
|
"-postScript",
|
|
join(fixtures, "ReaNoReturnProbe.java"),
|
|
bridge,
|
|
],
|
|
runId: run.run_id,
|
|
expectedCommand: null,
|
|
});
|
|
const supervisor = new ProviderProcessSupervisor({
|
|
...launch,
|
|
ownsProcessLifetime: true,
|
|
cleanup: () => cleanupOwnedProcessGroup(launch.ownership),
|
|
});
|
|
let probe;
|
|
try {
|
|
assert.ok(
|
|
await supervisor.waitForExit(120000),
|
|
"No-return probe timed out",
|
|
);
|
|
assert.ok(
|
|
await supervisor.waitForOutputClose(1000),
|
|
"No-return probe output did not close",
|
|
);
|
|
const snapshot = supervisor.snapshot();
|
|
const output = `${snapshot.stdout.text}\n${snapshot.stderr.text}`;
|
|
assert.equal(snapshot.exitCode, 0, output);
|
|
const reports = [
|
|
...output.matchAll(/REA_NO_RETURN_PROBE_JSON (\{[^\r\n]*\})/gu),
|
|
];
|
|
assert.equal(reports.length, 1, output);
|
|
probe = JSON.parse(reports[0][1]);
|
|
assert.equal(probe.status, "passed");
|
|
assert.equal(probe.ghidra_version, SUPPORTED_GHIDRA_VERSION);
|
|
assert.equal(probe.target_sha256, targetDigest);
|
|
for (const [name, expected] of Object.entries(sourceDigests))
|
|
assert.equal(probe[name], expected);
|
|
assert.ok(probe.checks.includes("independent_entry_preserved"));
|
|
assert.ok(probe.checks.includes("propagated_flag_repaired_rea_tls_outer"));
|
|
assert.ok(probe.checks.includes("recovered_pseudocode"));
|
|
} finally {
|
|
const stopped = await supervisor.stop();
|
|
cleaned = stopped.status === "verified-cleanup";
|
|
assert.ok(
|
|
cleaned,
|
|
`Owned cleanup incomplete; retained ${workspace}: ${JSON.stringify(stopped)}`,
|
|
);
|
|
}
|
|
|
|
// Exercise the actual Java server, authenticated transport, request queue and Evidence
|
|
// projection. This read-only TCP lane also runs where AF_UNIX is unavailable; it does
|
|
// not claim Windows authority or validation of the default Linux Unix-socket transport.
|
|
const provider = new GhidraProvider(
|
|
config.value,
|
|
silentLogger,
|
|
process.env,
|
|
undefined,
|
|
(options) =>
|
|
new GhidraClient({ ...options, transport: "authenticated-loopback-tcp" }),
|
|
);
|
|
const target = await parseBinaryTarget(targetPath);
|
|
if (!target.ok) throw target.error;
|
|
const profile = await provider.resolveAnalysisProfile(target.value);
|
|
if (!profile.ok) throw profile.error;
|
|
assert.ok(profile.value.profile);
|
|
assert.equal(
|
|
profile.value.profile.parameters.no_return_repair,
|
|
"returning-imports-decoded-return-v2",
|
|
);
|
|
const client = provider.createClient(target.value, profile.value.profile);
|
|
cleaned = false;
|
|
const observations = [];
|
|
const failures = [];
|
|
try {
|
|
for (const operation of [
|
|
"procedure_pseudo_code",
|
|
"procedure_assembly",
|
|
"procedure_info",
|
|
"read_function_instructions",
|
|
"analyze_function",
|
|
]) {
|
|
const result = await client.execute(operation, {
|
|
procedure: procedureName("rea_abort"),
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
const warnings = result.value.limitations.filter((value) =>
|
|
/terminal call/u.test(value),
|
|
);
|
|
assert.ok(
|
|
warnings.some(
|
|
(value) => /abort/u.test(value) && /fallthrough/u.test(value),
|
|
),
|
|
operation,
|
|
);
|
|
observations.push({ operation, warnings });
|
|
}
|
|
const healthy = await client.execute("procedure_pseudo_code", {
|
|
procedure: procedureName("rea_tls"),
|
|
});
|
|
if (!healthy.ok) throw healthy.error;
|
|
assert.ok(
|
|
!healthy.value.limitations.some((value) => /terminal call/u.test(value)),
|
|
);
|
|
} catch (cause) {
|
|
failures.push(cause);
|
|
}
|
|
const closed = await client.close();
|
|
cleaned = closed.ok;
|
|
if (!closed.ok) failures.push(closed.error);
|
|
if (failures.length > 0)
|
|
throw new AggregateError(
|
|
failures,
|
|
"No-return Evidence verification failed",
|
|
);
|
|
assert.equal(digest(await readFile(targetPath)), targetDigest);
|
|
report = {
|
|
status: "passed",
|
|
probe,
|
|
observations,
|
|
scope:
|
|
"Real Ghidra ELF or Mach-O analysis with injected flags; real read-only TCP Evidence projection. No target execution; exact Amethyst artifact and Windows are unverified.",
|
|
};
|
|
} finally {
|
|
if (cleaned) await rm(workspace, { recursive: true, force: true });
|
|
}
|
|
console.log(
|
|
JSON.stringify({
|
|
...report,
|
|
cleanup: "complete",
|
|
verifier_run: await completeVerifierRun(run),
|
|
}),
|
|
);
|