rea/scripts/verify-real-ghidra.mjs

269 lines
8.3 KiB
JavaScript

#!/usr/bin/env node
import { execFile } from "node:child_process";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { isAbsolute, join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { parseBinaryTarget } from "../dist/domain/binaryTarget.js";
import { resolveGhidraAnalysisProfile } from "../dist/ghidra/GhidraAnalysisProfile.js";
import { GhidraClient } from "../dist/ghidra/GhidraClient.js";
import { GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS } from "../dist/ghidra/GhidraDefaults.js";
import {
inspectGhidraInstallation,
SUPPORTED_GHIDRA_VERSION,
} from "../dist/ghidra/GhidraInstallation.js";
import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js";
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProvider.js";
import {
assertCleanup,
assertCrossFixture,
assertDebugFixture,
assertMalformedFixture,
assertRuntimeCoordinates,
assertSession,
assertStrippedFixture,
summary,
} from "./verify-real-ghidra-assertions.mjs";
import {
allItems,
call,
verifyInventoryOperations,
} from "./verify-real-ghidra-inventory.mjs";
const exec = promisify(execFile);
const installDir = process.env.GHIDRA_INSTALL_DIR;
if (installDir === undefined || !isAbsolute(installDir))
throw new Error(
"Set GHIDRA_INSTALL_DIR to the absolute root of an extracted Ghidra 12.1.2 release.",
);
const installation = inspectGhidraInstallation({
installDir,
...(process.env.JAVA_HOME === undefined
? {}
: { javaHome: process.env.JAVA_HOME }),
});
if (!installation.available || installation.analyzeHeadlessPath === null)
throw new Error(
`Ghidra installation is unavailable: ${JSON.stringify(installation)}`,
);
if (installation.providerVersion !== SUPPORTED_GHIDRA_VERSION)
throw new Error("Ghidra provider version commitment drifted");
const fixtureRoot = await mkdtemp(join(tmpdir(), "rea-ghidra-fixtures-"));
const sourcePath = fileURLToPath(
new URL("../tests/conformance/ghidra/inventory.c", import.meta.url),
);
const crossFormatSourcePath = fileURLToPath(
new URL("../tests/conformance/ghidra/cross-format.c", import.meta.url),
);
const debugPath = join(fixtureRoot, "rea-ghidra-inventory-debug");
const strippedPath = join(fixtureRoot, "rea-ghidra-inventory-stripped");
const arm64ElfPath = join(fixtureRoot, "rea-ghidra-cross-arm64");
const peObjectPath = join(fixtureRoot, "rea-ghidra-cross-x86_64.obj");
const pePath = join(fixtureRoot, "rea-ghidra-cross-x86_64.exe");
const peImportLibraryPath = join(fixtureRoot, "rea-ghidra-cross-x86_64.lib");
const machObjectPath = join(fixtureRoot, "rea-ghidra-cross-x86_64.o");
const malformedPath = join(fixtureRoot, "rea-ghidra-malformed");
const compiler = process.env.REA_CC ?? "cc";
const clang = process.env.REA_CLANG ?? "clang";
const lldLink = process.env.REA_LLD_LINK ?? "lld-link";
try {
const common = ["-O0", "-g", "-fno-inline", "-fno-pie", "-no-pie"];
await exec(compiler, [...common, sourcePath, "-o", debugPath]);
await exec(compiler, [...common, "-s", sourcePath, "-o", strippedPath]);
await exec(clang, [
"--target=aarch64-linux-gnu",
"-O0",
"-g",
"-fno-inline",
"-fno-pie",
"-nostdlib",
"-static",
"-fuse-ld=lld",
crossFormatSourcePath,
"-Wl,-e,rea_cross_start",
"-o",
arm64ElfPath,
]);
await exec(clang, [
"--target=x86_64-pc-windows-msvc",
"-O0",
"-gcodeview",
"-fno-inline",
"-c",
crossFormatSourcePath,
"-o",
peObjectPath,
]);
await exec(lldLink, [
"/entry:rea_cross_start",
"/subsystem:console",
"/nodefaultlib",
"/export:rea_cross_entry",
`/implib:${peImportLibraryPath}`,
`/out:${pePath}`,
peObjectPath,
]);
await exec(clang, [
"--target=x86_64-apple-darwin",
"-O0",
"-g",
"-fno-inline",
"-c",
crossFormatSourcePath,
"-o",
machObjectPath,
]);
await writeFile(malformedPath, Buffer.from("not-a-binary\n", "utf8"));
const debug = await verifyTarget(debugPath, "debug", {
format: "elf",
architecture: "x86_64",
});
const stripped = await verifyTarget(strippedPath, "stripped", {
format: "elf",
architecture: "x86_64",
});
const arm64Elf = await verifyTarget(arm64ElfPath, "cross-arm64-elf", {
format: "elf",
architecture: "arm64",
});
const pe = await verifyTarget(pePath, "cross-x86_64-pe", {
format: "pe",
architecture: "x86_64",
});
const machObject = await verifyTarget(machObjectPath, "cross-x86_64-mach-o", {
format: "mach-o",
architecture: "x86_64",
});
assertDebugFixture(debug);
assertStrippedFixture(stripped);
for (const fixture of [arm64Elf, pe, machObject]) assertCrossFixture(fixture);
await assertMalformedFixture(malformedPath);
const customPath = process.env.GHIDRA_TARGET_PATH;
const custom =
customPath === undefined ? null : await verifyTarget(customPath, "custom");
process.stdout.write(
`${JSON.stringify({
ok: true,
provider: { id: "ghidra", version: SUPPORTED_GHIDRA_VERSION },
fixture_sources: [sourcePath, crossFormatSourcePath],
fixtures: [
summary(debug),
summary(stripped),
summary(arm64Elf),
summary(pe),
summary(machObject),
],
malformed_target: "rejected-before-provider-start",
custom_target: custom === null ? null : summary(custom),
cleanup: "complete",
})}\n`,
);
} finally {
await rm(fixtureRoot, { recursive: true, force: true });
}
async function verifyTarget(targetPath, variant, expectedTarget = null) {
const parsedTarget = await parseBinaryTarget(targetPath);
if (!parsedTarget.ok) throw parsedTarget.error;
if (
expectedTarget !== null &&
(parsedTarget.value.format !== expectedTarget.format ||
parsedTarget.value.architecture !== expectedTarget.architecture)
)
throw new Error(
`Fixture header classification drifted for ${variant}: ${JSON.stringify(parsedTarget.value)}`,
);
const profile = await resolveGhidraAnalysisProfile(
parsedTarget.value,
GHIDRA_PROVIDER_IDENTITY,
installation,
);
if (!profile.ok || profile.value.profile === null)
throw new Error("Ghidra analysis profile could not be committed");
const client = new GhidraClient({
launcher: new GhidraHeadlessLauncher({
analyzeHeadlessPath: installation.analyzeHeadlessPath,
...(process.env.JAVA_HOME === undefined
? {}
: { javaHome: process.env.JAVA_HOME }),
bridgeScriptPath: fileURLToPath(
new URL("../bridge/ghidra/ReaGhidraBridge.java", import.meta.url),
),
platform: installation.platform,
}),
targetPath: parsedTarget.value.path,
targetSha256: parsedTarget.value.sha256,
providerVersion: SUPPORTED_GHIDRA_VERSION,
profileDigest: profile.value.profile.digest,
requestTimeoutMs: GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS,
});
let runtimeCoordinates;
try {
const started = await client.start();
if (!started.ok) throw started.error;
assertSession(
started.value,
profile.value.profile.digest,
parsedTarget.value.sha256,
);
const pinged = await client.ping();
if (!pinged.ok) throw pinged.error;
assertSession(
pinged.value,
profile.value.profile.digest,
parsedTarget.value.sha256,
);
runtimeCoordinates = client.diagnostics();
assertRuntimeCoordinates(runtimeCoordinates);
const documents = await call(client, "list_documents", {});
const segments = await call(client, "list_segments", { document: null });
const procedures = await allItems(client, "list_procedures", {
document: null,
});
const names = await allItems(client, "list_names", {
document: null,
address: null,
});
const strings = await allItems(client, "list_strings", {
document: null,
address: null,
});
const probes =
variant === "custom"
? null
: await verifyInventoryOperations({
client,
variant,
procedures,
names,
strings,
});
return {
variant,
target: parsedTarget.value,
profile: profile.value.profile,
session: pinged.value,
documents,
segments,
procedures,
names,
strings,
probes,
};
} finally {
await client.close();
if (runtimeCoordinates !== undefined)
await assertCleanup(runtimeCoordinates);
}
}