From be6863e9a16bcc3a09d87c50981e69292795a512 Mon Sep 17 00:00:00 2001 From: kvalls <95490470+kvalls@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:11:06 +0100 Subject: [PATCH] feat(ghidra): support native PE DLLs on Windows --- .github/workflows/real-ghidra-windows.yml | 20 +++ AGENTS.md | 2 +- docs/installation.md | 2 +- docs/managed-code-analysis.md | 5 +- docs/provider-evaluation.md | 9 +- docs/roadmap.md | 2 +- docs/windows-ghidra-p0.md | 15 +- scripts/create-ghidra-windows-fixture.mjs | 155 +++++++++++++-------- scripts/verify-packaged-ghidra-windows.mjs | 114 ++++++++++++--- scripts/verify-real-ghidra-windows.mjs | 27 +++- src/ghidra/GhidraAnalysisProfile.ts | 6 + src/ghidra/GhidraProvider.behavior.test.ts | 84 ++++++----- src/ghidra/GhidraProvider.ts | 9 +- src/ghidra/GhidraProviderCapabilities.ts | 2 +- 14 files changed, 316 insertions(+), 136 deletions(-) diff --git a/.github/workflows/real-ghidra-windows.yml b/.github/workflows/real-ghidra-windows.yml index 42dc38fa4..d6de7469f 100644 --- a/.github/workflows/real-ghidra-windows.yml +++ b/.github/workflows/real-ghidra-windows.yml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 1acb82552..01ecf3d91 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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: diff --git a/docs/installation.md b/docs/installation.md index a0be32920..dfa3bc84b 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -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 diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index 92bc23060..bb87a7a63 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -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 diff --git a/docs/provider-evaluation.md b/docs/provider-evaluation.md index 8aa655e52..a18303a92 100644 --- a/docs/provider-evaluation.md +++ b/docs/provider-evaluation.md @@ -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 diff --git a/docs/roadmap.md b/docs/roadmap.md index b4ac0ba54..077efa632 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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. diff --git a/docs/windows-ghidra-p0.md b/docs/windows-ghidra-p0.md index ec54f5908..5f50d832c 100644 --- a/docs/windows-ghidra-p0.md +++ b/docs/windows-ghidra-p0.md @@ -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 ``` diff --git a/scripts/create-ghidra-windows-fixture.mjs b/scripts/create-ghidra-windows-fixture.mjs index 6538ec57f..e610b4688 100644 --- a/scripts/create-ghidra-windows-fixture.mjs +++ b/scripts/create-ghidra-windows-fixture.mjs @@ -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`); } diff --git a/scripts/verify-packaged-ghidra-windows.mjs b/scripts/verify-packaged-ghidra-windows.mjs index 17d29c4ad..268d949d9 100644 --- a/scripts/verify-packaged-ghidra-windows.mjs +++ b/scripts/verify-packaged-ghidra-windows.mjs @@ -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, "") .replaceAll(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 }); } diff --git a/scripts/verify-real-ghidra-windows.mjs b/scripts/verify-real-ghidra-windows.mjs index 1c5b3b829..3332ea4e2 100644 --- a/scripts/verify-real-ghidra-windows.mjs +++ b/scripts/verify-real-ghidra-windows.mjs @@ -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", ], }; diff --git a/src/ghidra/GhidraAnalysisProfile.ts b/src/ghidra/GhidraAnalysisProfile.ts index 1af7469fa..eeb8646a4 100644 --- a/src/ghidra/GhidraAnalysisProfile.ts +++ b/src/ghidra/GhidraAnalysisProfile.ts @@ -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 ?? []), diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index 7bbbc8c89..88e1eb07f 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -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, + }); + } + } + } }); }); diff --git a/src/ghidra/GhidraProvider.ts b/src/ghidra/GhidraProvider.ts index d657a156e..d782827d9 100644 --- a/src/ghidra/GhidraProvider.ts +++ b/src/ghidra/GhidraProvider.ts @@ -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 { diff --git a/src/ghidra/GhidraProviderCapabilities.ts b/src/ghidra/GhidraProviderCapabilities.ts index edbbb11c3..5b9a6ca8b 100644 --- a/src/ghidra/GhidraProviderCapabilities.ts +++ b/src/ghidra/GhidraProviderCapabilities.ts @@ -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.", ]);