mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
feat(ghidra): apply validated analyzer overrides before analysis
Apply caller-selected analyzer opt-outs before Ghidra analysis, reject unknown or ineffective names, and commit raw/header imports to the same analyzer profile. Use supported x86 raw compiler specs and cover the real analyzer/readback and raw workflows. Adopts the original implementation from #1800 with validated corrections. Refs #1783, #1836. Co-authored-by: akushonkamen <74654766+akushonkamen@users.noreply.github.com> Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
@@ -172,11 +172,13 @@ const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
const runtimeParent = join(workspace, "runtime with spaces");
|
||||
await mkdir(runtimeParent);
|
||||
const callerDirectory = join(workspace, "caller cwd with spaces");
|
||||
// Keep sorted for comparison with the final caller-directory listing.
|
||||
const scriptCollisions = [
|
||||
"ReaGhidraBridge.java",
|
||||
"ReaGhidraPrepareCom.java",
|
||||
"ReaGhidraAnalysisOptions.java",
|
||||
"ReaGhidraApplySeeds.java",
|
||||
"ReaGhidraBridge.java",
|
||||
"ReaGhidraNoReturnFix.java",
|
||||
"ReaGhidraPrepareCom.java",
|
||||
"ReaGhidraPrepareRaw.java",
|
||||
];
|
||||
await mkdir(callerDirectory);
|
||||
|
||||
@@ -53,6 +53,15 @@ const env = {
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { JAVA_HOME: process.env.JAVA_HOME }),
|
||||
...(process.env.GHIDRA_HEADLESS_MAXMEM === undefined
|
||||
? {}
|
||||
: { GHIDRA_HEADLESS_MAXMEM: process.env.GHIDRA_HEADLESS_MAXMEM }),
|
||||
...(process.env.REA_GHIDRA_DISABLED_ANALYZERS === undefined
|
||||
? {}
|
||||
: {
|
||||
REA_GHIDRA_DISABLED_ANALYZERS:
|
||||
process.env.REA_GHIDRA_DISABLED_ANALYZERS,
|
||||
}),
|
||||
};
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
@@ -160,6 +169,48 @@ try {
|
||||
assert.equal(cli.normalized_result.base_address, "0x8000000");
|
||||
assert.equal(cli.analysis_profile.digest, profiles[0]);
|
||||
assert.deepEqual(await readFile(path), bytes);
|
||||
const x86Targets = [];
|
||||
for (const architecture of ["x86", "x86_64"]) {
|
||||
const x86Bytes = Buffer.from("b82a000000c3", "hex"); // mov eax, 42; ret.
|
||||
const x86Sha256 = createHash("sha256").update(x86Bytes).digest("hex");
|
||||
const x86Path = join(workspace, `${architecture}.bin`);
|
||||
await writeFile(x86Path, x86Bytes);
|
||||
await call("open_binary", {
|
||||
path: x86Path,
|
||||
format: "raw-binary",
|
||||
architecture,
|
||||
entry_point: "0x1000",
|
||||
});
|
||||
const image = await call("inspect_native_load_image", {}, x86Sha256);
|
||||
assert.equal(
|
||||
image.normalized_result.status,
|
||||
"verified",
|
||||
JSON.stringify(image),
|
||||
);
|
||||
assert.equal(image.normalized_result.base_address, "0x1000");
|
||||
assert.equal(image.normalized_result.observations.compiler_spec_id, "gcc");
|
||||
assert.equal(image.analysis_profile.parameters.compiler_spec_id, "gcc");
|
||||
const memory = await call(
|
||||
"read_bytes",
|
||||
{ address: "0x1000", length: x86Bytes.length },
|
||||
x86Sha256,
|
||||
);
|
||||
assert.equal(memory.normalized_result.bytes_hex, x86Bytes.toString("hex"));
|
||||
const pseudo = await call(
|
||||
"procedure_pseudo_code",
|
||||
{ procedure: "0x1000" },
|
||||
x86Sha256,
|
||||
);
|
||||
assert.match(pseudo.normalized_result, /\b(?:42|0x2a)\b/iu);
|
||||
await call("close_binary");
|
||||
assert.deepEqual(await readFile(x86Path), x86Bytes);
|
||||
x86Targets.push({
|
||||
architecture,
|
||||
sha256: x86Sha256,
|
||||
compiler_spec_id: "gcc",
|
||||
return_42: true,
|
||||
});
|
||||
}
|
||||
report = {
|
||||
ok: true,
|
||||
mocked: false,
|
||||
@@ -179,6 +230,7 @@ try {
|
||||
invalid_entry_preserves_active_session: true,
|
||||
load_images_verified: true,
|
||||
original_unchanged: true,
|
||||
x86_targets: x86Targets,
|
||||
};
|
||||
} catch (error) {
|
||||
if (stderr.trim()) process.stderr.write(stderr);
|
||||
@@ -199,7 +251,7 @@ process.stdout.write(
|
||||
}) + "\n",
|
||||
);
|
||||
|
||||
async function call(name, args = {}) {
|
||||
async function call(name, args = {}, targetSha256 = sha256) {
|
||||
const result = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
{ timeout: 240000 },
|
||||
@@ -213,7 +265,7 @@ async function call(name, args = {}) {
|
||||
)
|
||||
return projected;
|
||||
const evidence = parseEvidence(result.structuredContent);
|
||||
assert.equal(evidence.subject.digest.sha256, sha256);
|
||||
assert.equal(evidence.subject.digest.sha256, targetSha256);
|
||||
assert.equal(evidence.subject.format, "raw-binary");
|
||||
return evidence;
|
||||
}
|
||||
|
||||
@@ -42,11 +42,66 @@ try {
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
const output = await runHeadless(target, "seed-regression", [
|
||||
"-postScript",
|
||||
"ReaSeedRegressionProbe.java",
|
||||
seeds,
|
||||
]);
|
||||
// Headless can exit zero after a script fails, so require the probe's marker.
|
||||
assert.ok(output.includes("REA_SEED_REGRESSION_OK "), output);
|
||||
|
||||
const analyzer = "Decompiler Switch Analysis";
|
||||
const disabled = await runHeadless(target, "analyzer-regression-valid", [
|
||||
"-preScript",
|
||||
"ReaGhidraAnalysisOptions.java",
|
||||
analyzer,
|
||||
"-postScript",
|
||||
"ReaAnalyzerOptionsRegressionProbe.java",
|
||||
analyzer,
|
||||
]);
|
||||
assert.ok(
|
||||
disabled.includes(`REA_ANALYZER_OPTIONS_DISABLED_OK ${analyzer}`),
|
||||
disabled,
|
||||
);
|
||||
|
||||
const unknownAnalyzer = "REA Unknown Analyzer";
|
||||
const rejected = await runHeadless(target, "analyzer-regression-unknown", [
|
||||
"-preScript",
|
||||
"ReaGhidraAnalysisOptions.java",
|
||||
`${analyzer},${unknownAnalyzer}`,
|
||||
"-postScript",
|
||||
"ReaAnalyzerOptionsRegressionProbe.java",
|
||||
analyzer,
|
||||
]);
|
||||
assert.ok(
|
||||
rejected.includes(
|
||||
`Unknown Ghidra analyzer for this program: ${unknownAnalyzer}`,
|
||||
),
|
||||
rejected,
|
||||
);
|
||||
assert.ok(
|
||||
rejected.includes("Processing aborted as a result of pre-script."),
|
||||
rejected,
|
||||
);
|
||||
assert.ok(!rejected.includes("REA_ANALYZER_OPTIONS_DISABLED_OK"), rejected);
|
||||
} finally {
|
||||
if (supervisor !== undefined) {
|
||||
const stopped = await supervisor.stop();
|
||||
assert.notEqual(stopped.status, "incomplete", JSON.stringify(stopped));
|
||||
supervisor.dispose();
|
||||
}
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
console.log(
|
||||
"Real Ghidra pre-script regression passed: descending function order, out-of-range seed, BSS label, BSS code rejection, analyzer option readback, unknown-analyzer abort before post-scripts, and owned-process cleanup.",
|
||||
);
|
||||
|
||||
async function runHeadless(target, project, scriptArguments) {
|
||||
const launch = await spawnOwnedProviderProcess({
|
||||
command: join(installation, "support", "analyzeHeadless"),
|
||||
arguments: [
|
||||
workspace,
|
||||
"seed-regression",
|
||||
project,
|
||||
"-import",
|
||||
target,
|
||||
"-loader",
|
||||
@@ -63,9 +118,7 @@ try {
|
||||
new URL("../../../tests/conformance/ghidra", import.meta.url),
|
||||
),
|
||||
].join(";"),
|
||||
"-postScript",
|
||||
"ReaSeedRegressionProbe.java",
|
||||
seeds,
|
||||
...scriptArguments,
|
||||
],
|
||||
runId: run.run_id,
|
||||
expectedCommand: null,
|
||||
@@ -82,16 +135,9 @@ try {
|
||||
const snapshot = supervisor.snapshot();
|
||||
const output = `${snapshot.stdout.text}\n${snapshot.stderr.text}`;
|
||||
assert.equal(snapshot.exitCode, 0, output);
|
||||
// Headless can exit zero after a script fails, so require the probe's marker.
|
||||
assert.ok(output.includes("REA_SEED_REGRESSION_OK "), output);
|
||||
} finally {
|
||||
if (supervisor !== undefined) {
|
||||
const stopped = await supervisor.stop();
|
||||
assert.notEqual(stopped.status, "incomplete", JSON.stringify(stopped));
|
||||
supervisor.dispose();
|
||||
}
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
const stopped = await supervisor.stop();
|
||||
assert.notEqual(stopped.status, "incomplete", JSON.stringify(stopped));
|
||||
supervisor.dispose();
|
||||
supervisor = undefined;
|
||||
return output;
|
||||
}
|
||||
console.log(
|
||||
"Real Ghidra seed regression passed: descending function order, out-of-range seed, BSS label, BSS code rejection, and owned-process cleanup.",
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user