Files
reverse-engineering-anything/tests/ghidraLauncher.test.ts
N0zoM1z0 bc0fb45f16 feat(ghidra): launch bounded Windows headless sessions
Invoke analyzeHeadless.bat through an absolute cmd.exe with fixed interpreter switches, quoted arguments, delayed expansion disabled, and fail-closed metacharacter and length checks. Isolate Windows home and temporary coordinates, hide provider windows, and terminate the P0 launcher tree through bounded taskkill cleanup. Advertise explicit limitations that this preview does not provide Job Object ownership, DACL enforcement, reparse-point authority, or hostile-target safety.
2026-07-17 03:05:19 +09:00

200 lines
6.4 KiB
TypeScript

import { access, chmod, mkdtemp, readFile, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
ghidraHeadlessArguments,
ghidraHeadlessCommand,
GhidraHeadlessLauncher,
} from "../src/ghidra/GhidraLauncher.js";
const fixturePath = fileURLToPath(
new URL(
process.platform === "win32"
? "./fixtures/captureGhidraLaunch.cmd"
: "./fixtures/captureGhidraLaunch.mjs",
import.meta.url,
),
);
const roots: string[] = [];
beforeAll(async () => {
if (process.platform !== "win32") await chmod(fixturePath, 0o755);
});
afterEach(async () => {
vi.unstubAllEnvs();
await Promise.all(
roots.splice(0).map((path) => rm(path, { recursive: true, force: true })),
);
});
describe("Ghidra headless launcher", () => {
it("builds a bounded read-only import in deterministic order", () => {
expect(
ghidraHeadlessArguments({
projectRoot: "/tmp/project",
targetPath: "/tmp/target",
bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java",
descriptorPath: "/tmp/session.json",
ghidraLogPath: "/tmp/ghidra.log",
scriptLogPath: "/tmp/script.log",
}),
).toEqual([
"/tmp/project",
"rea-project",
"-import",
"/tmp/target",
"-readOnly",
"-deleteProject",
"-analysisTimeoutPerFile",
"300",
"-max-cpu",
"2",
"-log",
"/tmp/ghidra.log",
"-scriptlog",
"/tmp/script.log",
"-scriptPath",
"/package/bridge",
"-postScript",
"ReaGhidraBridge.java",
"/tmp/session.json",
]);
});
it("wraps the Windows batch launcher without enabling a Node shell", () => {
const command = ghidraHeadlessCommand({
platform: "win32",
analyzeHeadlessPath:
"C:\\Program Files\\Ghidra 12.1.2\\support\\analyzeHeadless.bat",
arguments: [
"C:\\REA Runtime\\project",
"rea-project",
"-import",
"C:\\REA Runtime\\target.exe",
],
comSpec: "C:\\Windows\\System32\\cmd.exe",
});
expect(command).toEqual({
command: "C:\\Windows\\System32\\cmd.exe",
arguments: [
"/d",
"/e:on",
"/v:off",
"/s",
"/c",
'""C:\\Program Files\\Ghidra 12.1.2\\support\\analyzeHeadless.bat" "C:\\REA Runtime\\project" "rea-project" "-import" "C:\\REA Runtime\\target.exe""',
],
});
});
it.each([
"C:\\targets\\%TEMP%\\fixture.exe",
"C:\\targets\\fixture & calc.exe",
'C:\\targets\\fixture".exe',
])("rejects a Windows command-interpreter path: %s", (targetPath) => {
expect(() =>
ghidraHeadlessCommand({
platform: "win32",
analyzeHeadlessPath: "C:\\Ghidra\\support\\analyzeHeadless.bat",
arguments: ["-import", targetPath],
comSpec: "C:\\Windows\\System32\\cmd.exe",
}),
).toThrow(/metacharacters/u);
});
it("keeps authority in a private descriptor and isolates Ghidra state", async () => {
vi.stubEnv("GHIDRA_JAVA_OPTIONS", "-javaagent:/unapproved/agent.jar");
vi.stubEnv("JAVA_TOOL_OPTIONS", "-Duser.home=/unapproved/home");
vi.stubEnv("JDK_JAVA_OPTIONS", "-XX:MaxRAMPercentage=99");
vi.stubEnv("_JAVA_OPTIONS", "-Xmx99G");
const runtimeRoot = await mkdtemp(join(tmpdir(), "rea-launcher-test-"));
roots.push(runtimeRoot);
const token = "secret-token-that-must-not-leak";
const javaHome =
process.platform === "win32" ? "C:\\Java\\jdk-21" : "/opt/jdk-21";
const launcher = new GhidraHeadlessLauncher({
analyzeHeadlessPath: fixturePath,
javaHome,
bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java",
});
const launched = await launcher.launch({
runtimeRoot,
transport: "unix-socket",
endpointPath: join(runtimeRoot, "bridge.sock"),
token,
runId: "d6fcbb66-e829-4ff6-a535-0035aec63139",
targetPath: "/tmp/fixture",
targetSha256: "b".repeat(64),
providerVersion: "12.1.2",
profileDigest: "a".repeat(64),
});
expect(launched.ok).toBe(true);
if (!launched.ok) return;
const capturePath = join(runtimeRoot, "launch-capture.json");
await waitFor(capturePath);
const capture = JSON.parse(await readFile(capturePath, "utf8"));
const encodedArguments = JSON.stringify(capture.arguments);
const encodedEnvironment = JSON.stringify(capture.environment);
expect(encodedArguments).not.toContain(token);
expect(encodedEnvironment).not.toContain(token);
expect(capture).toMatchObject({
...(process.platform === "win32" ? {} : { descriptor_mode: 0o600 }),
descriptor_has_token: true,
environment: {
HOME: join(runtimeRoot, "home"),
...(process.platform === "win32"
? {
USERPROFILE: join(runtimeRoot, "home"),
TEMP: join(runtimeRoot, "tmp"),
TMP: join(runtimeRoot, "tmp"),
}
: {}),
TMPDIR: join(runtimeRoot, "tmp"),
XDG_CACHE_HOME: join(runtimeRoot, "cache"),
XDG_CONFIG_HOME: join(runtimeRoot, "config"),
XDG_DATA_HOME: join(runtimeRoot, "data"),
GHIDRA_HEADLESS_MAXMEM: "2G",
GHIDRA_JAVA_OPTIONS: "",
JAVA_TOOL_OPTIONS: "",
JDK_JAVA_OPTIONS: "",
_JAVA_OPTIONS: "",
JAVA_HOME: javaHome,
REA_PROCESS_RUN_ID: "d6fcbb66-e829-4ff6-a535-0035aec63139",
},
});
expect(capture.environment.PATH).toMatch(
process.platform === "win32"
? /^C:\\Java\\jdk-21\\bin;/u
: /^\/opt\/jdk-21\/bin:/u,
);
expect(capture.environment.GHIDRA_HEADLESS_JAVA_OPTIONS).toContain(
`-Duser.home=${join(runtimeRoot, "home")}`,
);
if (process.platform !== "win32")
expect(
(await stat(join(runtimeRoot, "ownership.json"))).mode & 0o777,
).toBe(0o600);
const cleaned = await launched.value.cleanup?.();
expect(cleaned).toMatchObject({ cleaned: true });
await expect(access(join(runtimeRoot, "project"))).resolves.toBeUndefined();
});
});
const waitFor = async (path: string): Promise<void> => {
for (let attempt = 0; attempt < 100; attempt += 1) {
try {
await access(path);
return;
} catch {
await new Promise((resolve) => setTimeout(resolve, 10));
}
}
throw new Error(`Timed out waiting for ${path}`);
};