feat(ghidra): support native PE DLLs on Windows
This commit is contained in:
parent
411498d45f
commit
be6863e9a1
|
|
@ -64,6 +64,14 @@ jobs:
|
|||
env:
|
||||
REA_ANALYSIS_PROVIDER: ghidra
|
||||
run: npm run verify:ghidra:windows -- --x86 | Tee-Object -FilePath windows-ghidra-x86-proof.log
|
||||
- name: Verify native x64 DLL Windows Ghidra operations
|
||||
env:
|
||||
REA_ANALYSIS_PROVIDER: ghidra
|
||||
run: npm run verify:ghidra:windows -- --dll | Tee-Object -FilePath windows-ghidra-dll-proof.log
|
||||
- name: Verify native x86 DLL Windows Ghidra operations
|
||||
env:
|
||||
REA_ANALYSIS_PROVIDER: ghidra
|
||||
run: npm run verify:ghidra:windows -- --x86 --dll | Tee-Object -FilePath windows-ghidra-x86-dll-proof.log
|
||||
- name: Verify ordinary-user CLI and MCP operations
|
||||
env:
|
||||
REA_ANALYSIS_PROVIDER: ghidra
|
||||
|
|
@ -72,6 +80,14 @@ jobs:
|
|||
env:
|
||||
REA_ANALYSIS_PROVIDER: ghidra
|
||||
run: npm run verify:ghidra:windows:package -- --x86 | Tee-Object -FilePath windows-ghidra-x86-package-proof.log
|
||||
- name: Verify ordinary-user x64 DLL CLI and MCP operations
|
||||
env:
|
||||
REA_ANALYSIS_PROVIDER: ghidra
|
||||
run: npm run verify:ghidra:windows:package -- --dll | Tee-Object -FilePath windows-ghidra-dll-package-proof.log
|
||||
- name: Verify ordinary-user x86 DLL CLI and MCP operations
|
||||
env:
|
||||
REA_ANALYSIS_PROVIDER: ghidra
|
||||
run: npm run verify:ghidra:windows:package -- --x86 --dll | Tee-Object -FilePath windows-ghidra-x86-dll-package-proof.log
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
|
|
@ -81,4 +97,8 @@ jobs:
|
|||
windows-ghidra-package-proof.log
|
||||
windows-ghidra-x86-proof.log
|
||||
windows-ghidra-x86-package-proof.log
|
||||
windows-ghidra-dll-proof.log
|
||||
windows-ghidra-x86-dll-proof.log
|
||||
windows-ghidra-dll-package-proof.log
|
||||
windows-ghidra-x86-dll-package-proof.log
|
||||
retention-days: 30
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Product Direction
|
||||
|
||||
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper, the bring-your-own Ghidra adapter, and the bring-your-own IDA MCP adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64/arm64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86 and x86-64 PE applications; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. IDA adapts upstream legacy attached GUI and modern headless database-supervisor profiles for live read-only analysis; initial real verification covers Windows, and an attached GUI database is never saved or closed. Keep provider-specific code out of the domain and application layers.
|
||||
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper, the bring-your-own Ghidra adapter, and the bring-your-own IDA MCP adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64/arm64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86 and x86-64 PE applications and DLLs; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. IDA adapts upstream legacy attached GUI and modern headless database-supervisor profiles for live read-only analysis; initial real verification covers Windows, and an attached GUI database is never saved or closed. Keep provider-specific code out of the domain and application layers.
|
||||
|
||||
Prioritize:
|
||||
|
||||
|
|
|
|||
|
|
@ -452,7 +452,7 @@ entry comments atomically and returns refreshed analysis. These session metadata
|
|||
edits leave executable bytes unchanged and are discarded on close. GUI controls
|
||||
require Hopper; Windows P0 remains read-only.
|
||||
|
||||
Windows P0 admits native x86 and x86-64 PE applications on fixed local NTFS volumes.
|
||||
Windows P0 admits native x86 and x86-64 PE applications and DLLs on fixed local NTFS volumes.
|
||||
The npm package bundles native Job Object ownership, protected private runtime
|
||||
DACLs, and handle-based path admission; no separate addon installation is needed.
|
||||
See the [Windows Ghidra P0 guide](windows-ghidra-p0.md) for verified scope and
|
||||
|
|
|
|||
|
|
@ -125,8 +125,9 @@ The upstream analyzer currently gates its own analysis to x86-64. NativeAOT's
|
|||
broader platform matrix does not establish REA coverage: PE/COFF, ELF, Mach-O,
|
||||
shared libraries, target architectures, and runtime metadata versions each need
|
||||
their own provider verification. REA's experimental Windows Ghidra boundary is
|
||||
limited to admitted native x86 and x86-64 PE applications, so it does not currently
|
||||
establish coverage for PE DLLs. Mobile NativeAOT targets should remain
|
||||
limited to admitted native x86 and x86-64 PE applications and DLLs for static
|
||||
analysis. This does not establish Windows NativeAOT metadata-recovery coverage.
|
||||
Mobile NativeAOT targets should remain
|
||||
experimental until REA has a matching provider and package-level verification.
|
||||
|
||||
For REA's native analysis, preserve the same artifact path and digest used by
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ comments atomically, verifies readback, and returns refreshed function analysis.
|
|||
Changes live only in the session database and leave executable bytes unchanged.
|
||||
Ghidra does not expose GUI controls; Windows P0 remains read-only.
|
||||
|
||||
Windows x64 P0 supports native x86 and x86-64 PE applications on local NTFS with
|
||||
Windows x64 P0 supports native x86 and x86-64 PE applications and DLLs on local NTFS with
|
||||
bundled Job Object ownership, protected runtime DACLs, and handle-based path
|
||||
admission. Real ordinary-user verification covers all 25 read-only operations
|
||||
through the packaged CLI and MCP, target integrity, and cleanup. This remains
|
||||
|
|
@ -62,7 +62,7 @@ maximum, when the release leaves those at the 12.1 defaults). Otherwise doctor
|
|||
probes `java`/`javac` or `java.exe`/`javac.exe` from `PATH`. Supported Linux and macOS
|
||||
hosts accept compatible ELF, PE, and Mach-O executable targets. Host admission
|
||||
and target compatibility are separate checks. Windows x64 P0 admits only native,
|
||||
non-managed, non-DLL x86 and x86-64 PE applications on fixed local NTFS, with the packaged
|
||||
non-managed x86 and x86-64 PE applications and DLLs on fixed local NTFS, with the packaged
|
||||
native authority available before launch.
|
||||
|
||||
The launcher creates one ephemeral runtime root with project,
|
||||
|
|
@ -136,8 +136,9 @@ Mach-O target coverage. It requires `clang`, LLD, and `lld-link`; set
|
|||
these tools before compilation. Keeping this matrix separate lets Linux
|
||||
host/provider acceptance run with only the host compiler.
|
||||
|
||||
`npm run verify:ghidra:windows` and its `-- --x86` variant check the source-owned native x86 and x86-64 PE
|
||||
fixture, all 25 read-only operations, target/snapshot/import SHA-256 linkage,
|
||||
`npm run verify:ghidra:windows` and its `-- --x86` variant check source-owned native x86 and x86-64 PE
|
||||
applications. Adding `-- --dll` or `-- --x86 --dll` selects entry-point-free DLLs
|
||||
with exported caller/callee functions. These lanes check all 25 read-only operations, target/snapshot/import SHA-256 linkage,
|
||||
and project, endpoint, process, and runtime cleanup. The independent native
|
||||
lane checks DACLs, handle admission, cancellation, and Job Object lifecycle.
|
||||
`npm run verify:ghidra:windows:package` packs and installs REA into an isolated
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ and exact commands.
|
|||
|
||||
Experimental Windows x64 P0 now supplies bundled Job Object ownership,
|
||||
protected runtime DACLs, and handle-based admission for native x86 and x86-64 PE
|
||||
applications on local NTFS. Real ordinary-user CLI/MCP verification covers
|
||||
applications and DLLs on local NTFS. Real ordinary-user CLI/MCP verification covers
|
||||
all 25 read-only operations and cleanup. The [Windows P0 guide](windows-ghidra-p0.md)
|
||||
describes this boundary and unverified broader coverage.
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ additional permission flag or degraded mode is required.
|
|||
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+;
|
||||
- an operator-installed Ghidra 12.1.x distribution (verified with 12.1.4);
|
||||
- a 64-bit full JDK inside that installation's declared Java range (JDK 21 or newer, with no maximum, for current 12.1 releases);
|
||||
- an explicit native, non-managed, non-DLL x86 or x86-64 PE application; and
|
||||
- an explicit native, non-managed x86 or x86-64 PE application or DLL; and
|
||||
- the 25 read-only Ghidra inventory, memory, and function-analysis operations.
|
||||
|
||||
Loaded memory reads and file offsets preserve Ghidra's observed source mappings.
|
||||
|
|
@ -38,6 +38,13 @@ REA selects its packaged bridge scripts by explicit path. Same-named files or
|
|||
directories in the caller's working directory do not select a different
|
||||
script or require users to clean that directory before analysis.
|
||||
|
||||
Native DLL analysis preserves the PE shared-library role in the analysis profile.
|
||||
REA imports an immutable copy for static analysis; it does not load the library
|
||||
into the analyst process or execute its entry routine. The source-owned DLL
|
||||
fixtures have no entry routine or imports and expose two exported functions.
|
||||
The same native snapshot, path-admission and cleanup requirements apply to EXEs
|
||||
and DLLs.
|
||||
|
||||
## Configuration
|
||||
|
||||
```powershell
|
||||
|
|
@ -100,7 +107,7 @@ and artifact SHA-256 before loading the package-owned addon. Native failure
|
|||
reasons remain distinct from a valid installation and an unsupported target.
|
||||
|
||||
PE header classification and SHA-256 come from the same open file. The Windows
|
||||
provider admits native x86 and x86-64 PE applications and rejects unsupported roles,
|
||||
provider admits native x86 and x86-64 PE applications and DLLs and rejects unsupported roles,
|
||||
architectures, managed images, and malformed headers. The original selected
|
||||
source coordinate is preserved for native admission.
|
||||
|
||||
|
|
@ -159,7 +166,11 @@ artifact and manifest.
|
|||
npm run verify:windows-native
|
||||
npm run verify:ghidra:windows
|
||||
npm run verify:ghidra:windows -- --x86
|
||||
npm run verify:ghidra:windows -- --dll
|
||||
npm run verify:ghidra:windows -- --x86 --dll
|
||||
npm run verify:ghidra:windows:package -- --x86
|
||||
npm run verify:ghidra:windows:package -- --dll
|
||||
npm run verify:ghidra:windows:package -- --x86 --dll
|
||||
npm run verify:ghidra:windows:package -- C:\fixtures\installed-rea C:\fixtures\sample.exe
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -3,68 +3,103 @@ import { dirname, resolve } from "node:path";
|
|||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
// Source-owned static fixtures: no compiler, target loading or execution.
|
||||
for (const architecture of ["x86_64", "x86"]) {
|
||||
const x86 = architecture === "x86";
|
||||
const output = resolve(
|
||||
root,
|
||||
"build",
|
||||
"fixtures",
|
||||
x86 ? "rea-ghidra-windows-x86.exe" : "rea-ghidra-windows.exe",
|
||||
);
|
||||
const bytes = Buffer.alloc(1024);
|
||||
const peOffset = 0x80;
|
||||
const optionalHeader = peOffset + 24;
|
||||
const optionalHeaderSize = x86 ? 0xe0 : 0xf0;
|
||||
const sectionTable = optionalHeader + optionalHeaderSize;
|
||||
for (const dll of [false, true]) {
|
||||
const x86 = architecture === "x86";
|
||||
const output = resolve(
|
||||
root,
|
||||
"build",
|
||||
"fixtures",
|
||||
`rea-ghidra-windows${x86 ? "-x86" : ""}.${dll ? "dll" : "exe"}`,
|
||||
);
|
||||
const bytes = Buffer.alloc(1024);
|
||||
const peOffset = 0x80;
|
||||
const optionalHeader = peOffset + 24;
|
||||
const optionalHeaderSize = x86 ? 0xe0 : 0xf0;
|
||||
const sectionTable = optionalHeader + optionalHeaderSize;
|
||||
const directories = optionalHeader + (x86 ? 96 : 112);
|
||||
|
||||
bytes.write("MZ", 0, "ascii");
|
||||
bytes.writeUInt32LE(peOffset, 0x3c);
|
||||
bytes.write("PE\0\0", peOffset, "binary");
|
||||
bytes.writeUInt16LE(x86 ? 0x14c : 0x8664, peOffset + 4);
|
||||
bytes.writeUInt16LE(1, peOffset + 6);
|
||||
bytes.writeUInt16LE(optionalHeaderSize, peOffset + 20);
|
||||
bytes.writeUInt16LE(x86 ? 0x102 : 0x22, peOffset + 22);
|
||||
bytes.write("MZ", 0, "ascii");
|
||||
bytes.writeUInt32LE(peOffset, 0x3c);
|
||||
bytes.write("PE\0\0", peOffset, "binary");
|
||||
bytes.writeUInt16LE(x86 ? 0x14c : 0x8664, peOffset + 4);
|
||||
bytes.writeUInt16LE(1, peOffset + 6);
|
||||
bytes.writeUInt16LE(optionalHeaderSize, peOffset + 20);
|
||||
bytes.writeUInt16LE(
|
||||
(x86 ? 0x102 : 0x22) | (dll ? 0x2000 : 0),
|
||||
peOffset + 22,
|
||||
);
|
||||
|
||||
bytes.writeUInt16LE(x86 ? 0x10b : 0x20b, optionalHeader);
|
||||
bytes.writeUInt32LE(0x200, optionalHeader + 4);
|
||||
bytes.writeUInt32LE(0x1000, optionalHeader + 16);
|
||||
bytes.writeUInt32LE(0x1000, optionalHeader + 20);
|
||||
if (x86) bytes.writeUInt32LE(0x400000, optionalHeader + 28);
|
||||
else bytes.writeBigUInt64LE(0x1_4000_0000n, optionalHeader + 24);
|
||||
bytes.writeUInt32LE(0x1000, optionalHeader + 32);
|
||||
bytes.writeUInt32LE(0x200, optionalHeader + 36);
|
||||
bytes.writeUInt16LE(6, optionalHeader + 40);
|
||||
bytes.writeUInt16LE(6, optionalHeader + 48);
|
||||
bytes.writeUInt32LE(0x2000, optionalHeader + 56);
|
||||
bytes.writeUInt32LE(0x200, optionalHeader + 60);
|
||||
bytes.writeUInt16LE(3, optionalHeader + 68);
|
||||
bytes.writeUInt16LE(x86 ? 0x8140 : 0x8160, optionalHeader + 70);
|
||||
if (x86) {
|
||||
bytes.writeUInt32LE(0x10_0000, optionalHeader + 72);
|
||||
bytes.writeUInt32LE(0x1000, optionalHeader + 76);
|
||||
bytes.writeUInt32LE(0x10_0000, optionalHeader + 80);
|
||||
bytes.writeUInt32LE(0x1000, optionalHeader + 84);
|
||||
bytes.writeUInt32LE(16, optionalHeader + 92);
|
||||
} else {
|
||||
bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 72);
|
||||
bytes.writeBigUInt64LE(0x1000n, optionalHeader + 80);
|
||||
bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 88);
|
||||
bytes.writeBigUInt64LE(0x1000n, optionalHeader + 96);
|
||||
bytes.writeUInt32LE(16, optionalHeader + 108);
|
||||
bytes.writeUInt16LE(x86 ? 0x10b : 0x20b, optionalHeader);
|
||||
bytes.writeUInt32LE(0x200, optionalHeader + 4);
|
||||
bytes.writeUInt32LE(dll ? 0 : 0x1000, optionalHeader + 16);
|
||||
bytes.writeUInt32LE(0x1000, optionalHeader + 20);
|
||||
if (x86) bytes.writeUInt32LE(0x400000, optionalHeader + 28);
|
||||
else bytes.writeBigUInt64LE(0x1_4000_0000n, optionalHeader + 24);
|
||||
bytes.writeUInt32LE(0x1000, optionalHeader + 32);
|
||||
bytes.writeUInt32LE(0x200, optionalHeader + 36);
|
||||
bytes.writeUInt16LE(6, optionalHeader + 40);
|
||||
bytes.writeUInt16LE(6, optionalHeader + 48);
|
||||
bytes.writeUInt32LE(0x2000, optionalHeader + 56);
|
||||
bytes.writeUInt32LE(0x200, optionalHeader + 60);
|
||||
bytes.writeUInt16LE(3, optionalHeader + 68);
|
||||
bytes.writeUInt16LE(x86 ? 0x8140 : 0x8160, optionalHeader + 70);
|
||||
if (x86) {
|
||||
bytes.writeUInt32LE(0x10_0000, optionalHeader + 72);
|
||||
bytes.writeUInt32LE(0x1000, optionalHeader + 76);
|
||||
bytes.writeUInt32LE(0x10_0000, optionalHeader + 80);
|
||||
bytes.writeUInt32LE(0x1000, optionalHeader + 84);
|
||||
bytes.writeUInt32LE(16, optionalHeader + 92);
|
||||
} else {
|
||||
bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 72);
|
||||
bytes.writeBigUInt64LE(0x1000n, optionalHeader + 80);
|
||||
bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 88);
|
||||
bytes.writeBigUInt64LE(0x1000n, optionalHeader + 96);
|
||||
bytes.writeUInt32LE(16, optionalHeader + 108);
|
||||
}
|
||||
|
||||
bytes.write(".text", sectionTable, "ascii");
|
||||
bytes.writeUInt32LE(dll ? 0x180 : 0x16, sectionTable + 8);
|
||||
bytes.writeUInt32LE(0x1000, sectionTable + 12);
|
||||
bytes.writeUInt32LE(0x200, sectionTable + 16);
|
||||
bytes.writeUInt32LE(0x200, sectionTable + 20);
|
||||
bytes.writeUInt32LE(0x6000_0020, sectionTable + 36);
|
||||
|
||||
// EXE entry returns zero. DLL caller adds one to the callee's 42.
|
||||
bytes.set(
|
||||
dll
|
||||
? [0xe8, 0x0b, 0, 0, 0, 0x83, 0xc0, 1, 0xc3]
|
||||
: [0xe8, 0x0b, 0, 0, 0, 0x31, 0xc0, 0xc3],
|
||||
0x200,
|
||||
);
|
||||
bytes.set([0xb8, 0x2a, 0, 0, 0, 0xc3], 0x210);
|
||||
|
||||
if (dll) {
|
||||
// Export both functions; an entry-point-free library is discovered by exports.
|
||||
bytes.writeUInt32LE(0x1040, directories);
|
||||
bytes.writeUInt32LE(0x130, directories + 4);
|
||||
const exports = 0x240;
|
||||
bytes.writeUInt32LE(0x1100, exports + 12);
|
||||
bytes.writeUInt32LE(1, exports + 16);
|
||||
bytes.writeUInt32LE(2, exports + 20);
|
||||
bytes.writeUInt32LE(2, exports + 24);
|
||||
bytes.writeUInt32LE(0x1080, exports + 28);
|
||||
bytes.writeUInt32LE(0x1088, exports + 32);
|
||||
bytes.writeUInt32LE(0x1090, exports + 36);
|
||||
bytes.writeUInt32LE(0x1000, 0x280);
|
||||
bytes.writeUInt32LE(0x1010, 0x284);
|
||||
bytes.writeUInt32LE(0x1120, 0x288);
|
||||
bytes.writeUInt32LE(0x1140, 0x28c);
|
||||
bytes.writeUInt16LE(0, 0x290);
|
||||
bytes.writeUInt16LE(1, 0x292);
|
||||
bytes.write("rea-fixture.dll\0", 0x300, "binary");
|
||||
bytes.write("fixture_caller\0", 0x320, "binary");
|
||||
bytes.write("fixture_return42\0", 0x340, "binary");
|
||||
}
|
||||
|
||||
await mkdir(dirname(output), { recursive: true });
|
||||
await writeFile(output, bytes);
|
||||
if (!x86 && !dll) process.stdout.write(`${output}\n`);
|
||||
}
|
||||
|
||||
bytes.write(".text", sectionTable, "ascii");
|
||||
bytes.writeUInt32LE(0x16, sectionTable + 8);
|
||||
bytes.writeUInt32LE(0x1000, sectionTable + 12);
|
||||
bytes.writeUInt32LE(0x200, sectionTable + 16);
|
||||
bytes.writeUInt32LE(0x200, sectionTable + 20);
|
||||
bytes.writeUInt32LE(0x6000_0020, sectionTable + 36);
|
||||
|
||||
// entry calls the second function, returns zero; second returns 42.
|
||||
bytes.set([0xe8, 0x0b, 0, 0, 0, 0x31, 0xc0, 0xc3], 0x200);
|
||||
bytes.set([0xb8, 0x2a, 0, 0, 0, 0xc3], 0x210);
|
||||
|
||||
await mkdir(dirname(output), { recursive: true });
|
||||
await writeFile(output, bytes);
|
||||
if (!x86) process.stdout.write(`${output}\n`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
|
@ -24,10 +24,43 @@ if (process.platform !== "win32" || process.arch !== "x64")
|
|||
throw new Error(
|
||||
"Packaged Windows Ghidra acceptance requires a Windows x64 host.",
|
||||
);
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-packaged-ghidra-"));
|
||||
const exec = promisify(execFile);
|
||||
const x86Fixture = process.argv[2] === "--x86";
|
||||
const packageRootArgument = x86Fixture ? undefined : process.argv[2];
|
||||
const arguments_ = process.argv.slice(2);
|
||||
// Windows locks the loaded package-owned addon until the verifier exits.
|
||||
// Keep installation cleanup in a parent that never loads that addon.
|
||||
if (arguments_[0] !== "--internal-workspace") {
|
||||
const ownedWorkspace = await mkdtemp(join(tmpdir(), "rea-packaged-ghidra-"));
|
||||
try {
|
||||
const result = await exec(
|
||||
process.execPath,
|
||||
[
|
||||
fileURLToPath(import.meta.url),
|
||||
"--internal-workspace",
|
||||
ownedWorkspace,
|
||||
...arguments_,
|
||||
],
|
||||
{ timeout: 900_000, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
process.stdout.write(result.stdout);
|
||||
process.stderr.write(result.stderr);
|
||||
} catch (cause) {
|
||||
if (typeof cause.stdout === "string") process.stdout.write(cause.stdout);
|
||||
if (typeof cause.stderr === "string") process.stderr.write(cause.stderr);
|
||||
throw cause;
|
||||
} finally {
|
||||
await rm(ownedWorkspace, { recursive: true, force: true });
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
assert.ok(arguments_[1] !== undefined, "Missing verifier workspace.");
|
||||
const workspace = resolve(arguments_[1]);
|
||||
const fixtureArguments = arguments_.slice(2);
|
||||
const x86Fixture = fixtureArguments.includes("--x86");
|
||||
const dllFixture = fixtureArguments.includes("--dll");
|
||||
const [packageRootArgument, targetArgument, reportArgument] =
|
||||
fixtureArguments.filter(
|
||||
(argument) => argument !== "--x86" && argument !== "--dll",
|
||||
);
|
||||
let packageRoot = resolve(packageRootArgument ?? ".");
|
||||
// The default lane verifies the npm artifact in an isolated prefix. Explicit
|
||||
// package roots support an already-installed artifact without a second install.
|
||||
|
|
@ -75,17 +108,17 @@ if (packageRootArgument === undefined) {
|
|||
);
|
||||
packageRoot = join(prefix, "node_modules", "rea-agents");
|
||||
} catch (cause) {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
throw cause;
|
||||
throw new Error("Packaged Windows verifier installation failed.", {
|
||||
cause,
|
||||
});
|
||||
}
|
||||
}
|
||||
const target = resolve(
|
||||
process.argv[3] ??
|
||||
(x86Fixture
|
||||
? "build/fixtures/rea-ghidra-windows-x86.exe"
|
||||
: "build/fixtures/rea-ghidra-windows.exe"),
|
||||
targetArgument ??
|
||||
`build/fixtures/rea-ghidra-windows${x86Fixture ? "-x86" : ""}.${dllFixture ? "dll" : "exe"}`,
|
||||
);
|
||||
let TOOL_CONTRACTS, native, token, sha256;
|
||||
let parsedTarget;
|
||||
try {
|
||||
({ TOOL_CONTRACTS } = await import(
|
||||
pathToFileURL(join(packageRoot, "dist/contracts/toolContracts.js"))
|
||||
|
|
@ -107,9 +140,25 @@ try {
|
|||
);
|
||||
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
sha256 = digest(await readFile(target));
|
||||
const { parseBinaryTarget } = await import(
|
||||
pathToFileURL(join(packageRoot, "dist/application/BinaryTargetResolver.js"))
|
||||
);
|
||||
parsedTarget = await parseBinaryTarget(target);
|
||||
assert.equal(parsedTarget.ok, true);
|
||||
assert.equal(parsedTarget.value.format, "pe");
|
||||
assert.equal(parsedTarget.value.managed, false);
|
||||
if (targetArgument === undefined) {
|
||||
assert.equal(
|
||||
parsedTarget.value.architecture,
|
||||
x86Fixture ? "x86" : "x86_64",
|
||||
);
|
||||
assert.equal(
|
||||
parsedTarget.value.executableRole,
|
||||
dllFixture ? "shared-library" : "application",
|
||||
);
|
||||
}
|
||||
} catch (cause) {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
throw cause;
|
||||
throw new Error("Packaged Windows verifier prerequisites failed.", { cause });
|
||||
}
|
||||
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
const runtimeParent = join(workspace, "runtime with spaces");
|
||||
|
|
@ -145,6 +194,8 @@ const report = {
|
|||
architecture: "x64",
|
||||
nonAdmin: true,
|
||||
targetSha256: sha256,
|
||||
targetArchitecture: parsedTarget.value.architecture,
|
||||
targetRole: parsedTarget.value.executableRole,
|
||||
cli: {},
|
||||
mcpOperations: [],
|
||||
nativeControls: "available",
|
||||
|
|
@ -208,8 +259,8 @@ try {
|
|||
});
|
||||
const call = async (name, arguments_) => {
|
||||
report.lastOperation = name;
|
||||
if (process.argv[4] !== undefined)
|
||||
await writeFile(process.argv[4], `${JSON.stringify(report)}\n`);
|
||||
if (reportArgument !== undefined)
|
||||
await writeFile(reportArgument, `${JSON.stringify(report)}\n`);
|
||||
const contract = TOOL_CONTRACTS.find((value) => value.name === name);
|
||||
assert.ok(contract !== undefined, `Missing canonical contract: ${name}`);
|
||||
const argumentsParsed = contract.inputSchema.parse(arguments_);
|
||||
|
|
@ -230,6 +281,25 @@ try {
|
|||
await call("list_names", {});
|
||||
const procedures = await call("list_procedures", {});
|
||||
assert.ok(procedures.length > 0);
|
||||
if (targetArgument === undefined) {
|
||||
const calleeAddress = x86Fixture ? "0x401010" : "0x140001010";
|
||||
const calleeCode = await call("procedure_pseudo_code", {
|
||||
procedure: calleeAddress,
|
||||
});
|
||||
assert.match(calleeCode, /\b(?:42|0x2a)\b/iu);
|
||||
if (dllFixture) {
|
||||
const callerAddress = x86Fixture ? "0x401000" : "0x140001000";
|
||||
assert.ok(
|
||||
(
|
||||
await call("procedure_callees", { procedure: callerAddress })
|
||||
).includes(calleeAddress),
|
||||
);
|
||||
assert.match(
|
||||
await call("procedure_pseudo_code", { procedure: callerAddress }),
|
||||
/\+\s*1\b/u,
|
||||
);
|
||||
}
|
||||
}
|
||||
assert.ok((await call("list_segments", {})).length > 0);
|
||||
await call("list_strings", {});
|
||||
const procedure =
|
||||
|
|
@ -296,6 +366,12 @@ try {
|
|||
"Packaged verification omitted an admitted operation.",
|
||||
);
|
||||
const session = await call("binary_session", {});
|
||||
assert.equal(
|
||||
session.analysis_provider_binding?.analysis_profile?.parameters
|
||||
?.executable_role,
|
||||
parsedTarget.value.executableRole,
|
||||
"MCP profile must preserve the selected PE role.",
|
||||
);
|
||||
assert.equal(
|
||||
session.analysis_provider_binding?.selection_source,
|
||||
"auto-single-candidate",
|
||||
|
|
@ -327,8 +403,8 @@ try {
|
|||
report.callerScriptCollisionsPreserved = true;
|
||||
report.runtimeCleanup = true;
|
||||
report.ok = true;
|
||||
if (process.argv[4] !== undefined)
|
||||
await writeFile(process.argv[4], `${JSON.stringify(report)}\n`);
|
||||
if (reportArgument !== undefined)
|
||||
await writeFile(reportArgument, `${JSON.stringify(report)}\n`);
|
||||
process.stdout.write(`${JSON.stringify(report)}\n`);
|
||||
} catch (cause) {
|
||||
report.failure = {
|
||||
|
|
@ -338,9 +414,7 @@ try {
|
|||
.replaceAll(target, "<target>")
|
||||
.replaceAll(workspace, "<workspace>"),
|
||||
};
|
||||
if (process.argv[4] !== undefined)
|
||||
await writeFile(process.argv[4], `${JSON.stringify(report)}\n`);
|
||||
if (reportArgument !== undefined)
|
||||
await writeFile(reportArgument, `${JSON.stringify(report)}\n`);
|
||||
throw cause;
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,21 +62,21 @@ if (
|
|||
);
|
||||
|
||||
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
||||
const architecture = process.argv[2] === "--x86" ? "x86" : "x86_64";
|
||||
const architecture = process.argv.includes("--x86") ? "x86" : "x86_64";
|
||||
const dllFixture = process.argv.includes("--dll");
|
||||
const role = dllFixture ? "shared-library" : "application";
|
||||
const targetPath = resolve(
|
||||
root,
|
||||
"build",
|
||||
"fixtures",
|
||||
architecture === "x86"
|
||||
? "rea-ghidra-windows-x86.exe"
|
||||
: "rea-ghidra-windows.exe",
|
||||
`rea-ghidra-windows${architecture === "x86" ? "-x86" : ""}.${dllFixture ? "dll" : "exe"}`,
|
||||
);
|
||||
const target = await parseBinaryTarget(targetPath);
|
||||
if (
|
||||
!target.ok ||
|
||||
target.value.format !== "pe" ||
|
||||
target.value.architecture !== architecture ||
|
||||
target.value.executableRole !== "application" ||
|
||||
target.value.executableRole !== role ||
|
||||
target.value.managed !== false
|
||||
)
|
||||
throw new Error(
|
||||
|
|
@ -89,6 +89,8 @@ const profile = await resolveGhidraAnalysisProfile(
|
|||
);
|
||||
if (!profile.ok || profile.value.profile === null)
|
||||
throw new Error("Windows Ghidra profile could not be committed");
|
||||
if (profile.value.profile.parameters.executable_role !== role)
|
||||
throw new Error("Windows Ghidra profile lost the observed PE role");
|
||||
|
||||
const client = new GhidraClient({
|
||||
platform: installation.platform,
|
||||
|
|
@ -162,6 +164,19 @@ try {
|
|||
throw new Error(
|
||||
"Windows fixture decompilation lost its known return value",
|
||||
);
|
||||
if (dllFixture) {
|
||||
const callerAddress = architecture === "x86" ? "0x401000" : "0x140001000";
|
||||
const callees = await functionOperation("procedure_callees", {
|
||||
procedure: callerAddress,
|
||||
});
|
||||
if (!callees.includes(callee.address))
|
||||
throw new Error("DLL export caller lost its controlled call reference");
|
||||
const callerCode = await functionOperation("procedure_pseudo_code", {
|
||||
procedure: callerAddress,
|
||||
});
|
||||
if (typeof callerCode !== "string" || !/\+\s*1\b/u.test(callerCode))
|
||||
throw new Error("DLL export caller lost its addition of one");
|
||||
}
|
||||
|
||||
await inventory("inspect_native_load_image", {});
|
||||
const memory = await inventory("read_bytes", {
|
||||
|
|
@ -254,7 +269,7 @@ try {
|
|||
limitations: [
|
||||
"approved-non-sensitive-fixtures-only",
|
||||
"windows-x64-local-ntfs-only",
|
||||
"native-x86-and-x86-64-pe-applications-only",
|
||||
"native-x86-and-x86-64-pe-applications-and-dlls-only",
|
||||
"no-gui-or-mutation-authority",
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ export const resolveGhidraAnalysisProfile = (
|
|||
profile: createAnalysisProfile(provider, {
|
||||
target_kind: target.kind,
|
||||
target_format: target.format,
|
||||
...(target.format === "pe"
|
||||
? {
|
||||
executable_role: target.executableRole ?? null,
|
||||
managed: target.managed ?? null,
|
||||
}
|
||||
: {}),
|
||||
architecture: target.architecture ?? null,
|
||||
available_architectures: [
|
||||
...(target.availableArchitectures ?? []),
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ describe("Ghidra platform support", () => {
|
|||
expect(
|
||||
ghidra.inspectTargetSupport({
|
||||
...nativeApplication,
|
||||
executableRole: "shared-library",
|
||||
executableRole: "non-executable",
|
||||
}),
|
||||
).toMatchObject({
|
||||
status: "unsupported",
|
||||
|
|
@ -295,45 +295,59 @@ describe("Ghidra platform support", () => {
|
|||
code: "architecture_unsupported",
|
||||
});
|
||||
});
|
||||
it("admits generated PE32 applications while rejecting unsupported x86 roles", async () => {
|
||||
});
|
||||
|
||||
describe("Ghidra Windows PE fixture admission", () => {
|
||||
it("admits generated PE32 and PE32+ DLLs and applications while preserving native-only Windows admission", async () => {
|
||||
const root = fileURLToPath(new URL("../../", import.meta.url));
|
||||
await promisify(execFile)(process.execPath, [
|
||||
join(root, "scripts/create-ghidra-windows-fixture.mjs"),
|
||||
]);
|
||||
const path = join(root, "build/fixtures/rea-ghidra-windows-x86.exe");
|
||||
const bytes = await readFile(path);
|
||||
const metadata = parseExecutableHeader(bytes, "x64");
|
||||
expect(metadata).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
format: "pe",
|
||||
architecture: "x86",
|
||||
executableRole: "application",
|
||||
managed: false,
|
||||
},
|
||||
});
|
||||
if (!metadata.ok) throw new Error(metadata.error);
|
||||
if (metadata.value.format !== "pe")
|
||||
throw new Error("Expected a PE fixture");
|
||||
const target: BinaryTarget = {
|
||||
path,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
kind: "executable",
|
||||
...metadata.value,
|
||||
};
|
||||
const ghidra = provider({ ...installationHost(), platform: "win32" });
|
||||
expect(ghidra.inspectTargetSupport(target).status).toBe("supported");
|
||||
for (const [candidate, code] of [
|
||||
[
|
||||
{ ...target, executableRole: "shared-library" },
|
||||
"target_role_unsupported",
|
||||
],
|
||||
[{ ...target, managed: true }, "managed_target_unsupported"],
|
||||
] as const)
|
||||
expect(ghidra.inspectTargetSupport(candidate)).toMatchObject({
|
||||
status: "unsupported",
|
||||
code,
|
||||
});
|
||||
for (const architecture of ["x86", "x86_64"] as const) {
|
||||
for (const dll of [false, true]) {
|
||||
const path = join(
|
||||
root,
|
||||
"build/fixtures",
|
||||
`rea-ghidra-windows${architecture === "x86" ? "-x86" : ""}.${dll ? "dll" : "exe"}`,
|
||||
);
|
||||
const bytes = await readFile(path);
|
||||
const metadata = parseExecutableHeader(bytes, "x64");
|
||||
const role = dll ? "shared-library" : "application";
|
||||
expect(metadata).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
format: "pe",
|
||||
architecture,
|
||||
executableRole: role,
|
||||
managed: false,
|
||||
},
|
||||
});
|
||||
if (!metadata.ok) throw new Error(metadata.error);
|
||||
if (metadata.value.format !== "pe")
|
||||
throw new Error("Expected a PE fixture");
|
||||
const target: BinaryTarget = {
|
||||
path,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
kind: "executable",
|
||||
...metadata.value,
|
||||
};
|
||||
expect(ghidra.inspectTargetSupport(target).status).toBe("supported");
|
||||
for (const [candidate, code] of [
|
||||
[
|
||||
{ ...target, executableRole: "non-executable" },
|
||||
"target_role_unsupported",
|
||||
],
|
||||
[{ ...target, managed: true }, "managed_target_unsupported"],
|
||||
[{ ...target, architecture: "arm64" }, "architecture_unsupported"],
|
||||
] as const) {
|
||||
expect(ghidra.inspectTargetSupport(candidate)).toMatchObject({
|
||||
status: "unsupported",
|
||||
code,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -247,12 +247,15 @@ const inspectWindowsP0TargetSupport = (
|
|||
reason: "Windows Ghidra P0 accepts x86 and x86-64 PE targets only.",
|
||||
diagnostics,
|
||||
};
|
||||
if (target.executableRole !== "application")
|
||||
if (
|
||||
target.executableRole !== "application" &&
|
||||
target.executableRole !== "shared-library"
|
||||
)
|
||||
return {
|
||||
status: "unsupported",
|
||||
code: "target_role_unsupported",
|
||||
reason:
|
||||
"Windows Ghidra P0 accepts PE applications, not DLL or non-executable images.",
|
||||
"Windows Ghidra P0 accepts PE applications and DLLs, not non-executable or unclassified images.",
|
||||
diagnostics,
|
||||
};
|
||||
if (target.managed !== false)
|
||||
|
|
@ -260,7 +263,7 @@ const inspectWindowsP0TargetSupport = (
|
|||
status: "unsupported",
|
||||
code: "managed_target_unsupported",
|
||||
reason:
|
||||
"Windows Ghidra P0 accepts native PE applications; managed or unclassified PE targets are unsupported.",
|
||||
"Windows Ghidra P0 accepts native PE applications and DLLs; managed or unclassified PE targets are unsupported.",
|
||||
diagnostics,
|
||||
};
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const healthLimitations = Object.freeze([
|
|||
|
||||
/** Additional limitations applied to the experimental Windows x64 P0 boundary. */
|
||||
export const windowsP0Limitations = Object.freeze([
|
||||
"Windows Ghidra P0 accepts approved native x86 and x86-64 PE applications only; DLL, managed, hostile, sensitive, and mutable-path targets are unsupported.",
|
||||
"Windows Ghidra P0 accepts approved native x86 and x86-64 PE applications and DLLs; managed, hostile, sensitive, and mutable-path targets are unsupported.",
|
||||
"The Windows bridge uses authenticated IPv4 loopback because Node path-based IPC does not expose Java AF_UNIX sockets; the endpoint file contains no bearer token.",
|
||||
"Windows sessions require the matching packaged Windows x64 native addon, local NTFS targets and runtimes, and Windows 10 or later. Native handles enforce path admission, private DACLs, and Job Object ownership automatically.",
|
||||
]);
|
||||
|
|
|
|||
Loading…
Reference in New Issue