From c6dad932084032bcb32acc61e8066a7784497710 Mon Sep 17 00:00:00 2001 From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com> Date: Thu, 8 Oct 2026 05:11:47 +0800 Subject: [PATCH] feat(ghidra): support native x86 PE applications on Windows (#1030) --- .github/workflows/real-ghidra-windows.yml | 19 +++- AGENTS.md | 2 +- README.md | 10 +- docs/installation.md | 2 +- docs/managed-code-analysis.md | 2 +- docs/provider-evaluation.md | 22 ++--- docs/roadmap.md | 2 +- docs/testing.md | 2 +- docs/windows-ghidra-p0.md | 8 +- scripts/create-ghidra-windows-fixture.mjs | 102 ++++++++++++--------- scripts/verify-packaged-ghidra-windows.mjs | 14 ++- scripts/verify-real-ghidra-windows.mjs | 31 ++++++- src/ghidra/GhidraProvider.behavior.test.ts | 66 +++++++++++-- src/ghidra/GhidraProvider.ts | 4 +- src/ghidra/GhidraProviderCapabilities.ts | 2 +- 15 files changed, 203 insertions(+), 85 deletions(-) diff --git a/.github/workflows/real-ghidra-windows.yml b/.github/workflows/real-ghidra-windows.yml index 6bebfa6237..42dc38fa46 100644 --- a/.github/workflows/real-ghidra-windows.yml +++ b/.github/workflows/real-ghidra-windows.yml @@ -21,7 +21,14 @@ jobs: with: node-version-file: .nvmrc - name: Prepare cross compiler - run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64 + run: | + for source in /etc/apt/apt-mirrors.txt /etc/apt/sources.list /etc/apt/sources.list.d/ubuntu.sources; do + if [ -f "$source" ]; then + sudo sed -i 's|azure.archive.ubuntu.com|archive.ubuntu.com|g; s|http://archive.ubuntu.com|https://archive.ubuntu.com|g' "$source" + fi + done + sudo apt-get update + sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64 - run: npm run build:windows-native - run: npm run verify:windows-native:artifact - uses: actions/upload-artifact@v4 @@ -53,10 +60,18 @@ jobs: env: REA_ANALYSIS_PROVIDER: ghidra run: npm run verify:ghidra:windows | Tee-Object -FilePath windows-ghidra-proof.log + - name: Verify native x86 Windows Ghidra operations + env: + REA_ANALYSIS_PROVIDER: ghidra + run: npm run verify:ghidra:windows -- --x86 | Tee-Object -FilePath windows-ghidra-x86-proof.log - name: Verify ordinary-user CLI and MCP operations env: REA_ANALYSIS_PROVIDER: ghidra run: npm run verify:ghidra:windows:package | Tee-Object -FilePath windows-ghidra-package-proof.log + - name: Verify ordinary-user x86 CLI and MCP operations + env: + REA_ANALYSIS_PROVIDER: ghidra + run: npm run verify:ghidra:windows:package -- --x86 | Tee-Object -FilePath windows-ghidra-x86-package-proof.log - uses: actions/upload-artifact@v4 if: always() with: @@ -64,4 +79,6 @@ jobs: path: | windows-ghidra-proof.log windows-ghidra-package-proof.log + windows-ghidra-x86-proof.log + windows-ghidra-x86-package-proof.log retention-days: 30 diff --git a/AGENTS.md b/AGENTS.md index 19c5ba16ff..c12152690e 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 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native 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 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. Prioritize: diff --git a/README.md b/README.md index 233209af1d..b130e63671 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ Firmware region inspection and explicit extraction use caller-supplied Binwalk a Static APK analysis uses a separately supplied headless JADX JAR and a full JDK, with no emulator or APK execution. See [Android analysis](docs/android-analysis.md) for setup, CLI/MCP operations, coverage and public test fixtures. Authenticated IPA and macOS `.app`, ZIP, or DMG inventory Evidence can be projected into bundle anatomy, such as XPC services, app extensions, login items, privileged helpers, and launchd plists, with [Apple application analysis](docs/apple-application-analysis.md). -Repository main and npm 4.1.0 include experimental Windows x64 Ghidra support for native x86-64 PE applications on local NTFS, with bundled Job Object, private-DACL, and path-admission controls. Check the [release boundary](docs/installation.md#released-package-and-main) before expecting this from an older npm package. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for prerequisites and verified scope. +Repository main includes experimental Windows x64 Ghidra support for native x86 and x86-64 PE applications on local NTFS, with bundled Job Object, private-DACL, and path-admission controls. Native x86 support requires the main implementation until its next npm release; published versions through 5.0.0 admit x86-64 targets. Check the [release boundary](docs/installation.md#released-package-and-main) before choosing a package version. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for prerequisites and verified scope. If something is not working, run: @@ -226,7 +226,7 @@ Ghidra also imports DOS MZ executables with an explicit 16-bit x86 real-mode pro See [optional NativeAOT metadata recovery](docs/ghidra-nativeaot.md) for the pinned headless adapter, supported layout and existing native-tool workflow. -Windows Ghidra P0 uses bundled native controls for its read-only native x86-64 PE boundary on local NTFS; see the [Windows Ghidra P0 guide](docs/windows-ghidra-p0.md). See [Ghidra installation](docs/installation.md#ghidra), [provider evaluation](docs/provider-evaluation.md), and [testing](docs/testing.md) for configuration details, coverage, and real-provider verification. +Windows Ghidra P0 uses bundled native controls for its read-only native x86 and x86-64 PE boundary on local NTFS; see the [Windows Ghidra P0 guide](docs/windows-ghidra-p0.md). See [Ghidra installation](docs/installation.md#ghidra), [provider evaluation](docs/provider-evaluation.md), and [testing](docs/testing.md) for configuration details, coverage, and real-provider verification. To remove only REA-owned MCP registrations and the managed skill: @@ -460,7 +460,7 @@ Static Android APK inspection supports Linux and macOS; the current metadata bri - **Open questions:** Track unresolved findings, contradictions, and follow-up probes. Reconstruction checks report pass, fail, or unknown rather than treating missing evidence as a pass. - **Guided workflows:** Start six [MCP investigation workflows](docs/mcp-prompts.md) with suggestions based on your current session. -Windows x64 Ghidra P0 supports native, non-managed, non-DLL x86-64 PE applications on fixed local NTFS with 25 read-only operations. Linux/macOS Ghidra additionally supports atomic session-scoped function names and entry comments. Ghidra has no GUI controls; Windows P0 has no mutation authority. +Windows x64 Ghidra P0 supports native, non-managed, non-DLL x86 and x86-64 PE applications on fixed local NTFS with 25 read-only operations. Linux/macOS Ghidra additionally supports atomic session-scoped function names and entry comments. Ghidra has no GUI controls; Windows P0 has no mutation authority. ### Website observation with CDP @@ -658,7 +658,7 @@ Use `--provider`, or `provider_id` in MCP, to choose Hopper, Ghidra, or IDA for With `auto`, REA selects the only available tool that supports the target. If both are available, specify one before opening the target. The session keeps that choice until you explicitly switch or close it; a failure never silently switches tools. Artifact-only analysis can work without a native analysis tool. -Run `rea providers` and `rea capabilities` to check availability and supported operations. Ghidra supports inspection and atomic function annotation edits on Linux and macOS. Its database edits are discarded on close; GUI controls require Hopper. Windows Ghidra P0 supports read-only analysis of native x86-64 PE applications on local NTFS with bundled native controls. +Run `rea providers` and `rea capabilities` to check availability and supported operations. Ghidra supports inspection and atomic function annotation edits on Linux and macOS. Its database edits are discarded on close; GUI controls require Hopper. Windows Ghidra P0 supports read-only analysis of native x86 and x86-64 PE applications on local NTFS with bundled native controls. The session also reports active work and cleanup status. If a caller times out, the analysis tool may still be busy; `analysis_activity` reports that state. A `cleanup_incomplete` result identifies resources whose shutdown or removal could not be verified. See [provider selection and analysis profiles](docs/adr/0001-provider-selection-and-analysis-profiles.md) for session, cache, and process-tracking details. @@ -743,7 +743,7 @@ Analysis runs locally. REA communicates with Hopper and Ghidra through authentic Runtime requests act on the declared target and lifecycle. Analysis tools and launched targets run with your user permissions, and native UI capture still depends on macOS Accessibility and Screen Recording access. Static JavaScript analysis does not execute extracted modules; use direct browser, Electron, or process capture when runtime behavior is needed. -Windows Ghidra P0 automatically uses native Job Objects, protected private-runtime DACLs, and handle-based admission. Its experimental scope is native x86-64 PE applications on local NTFS. Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md). +Windows Ghidra P0 automatically uses native Job Objects, protected private-runtime DACLs, and handle-based admission. Its experimental scope is native x86 and x86-64 PE applications on local NTFS. Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md). ## FAQ diff --git a/docs/installation.md b/docs/installation.md index 86f008d110..5249c20976 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -294,7 +294,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-64 PE applications on fixed local NTFS volumes. +Windows P0 admits native x86 and x86-64 PE applications 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 336cf0c535..a405b1f4b1 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -125,7 +125,7 @@ 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-64 PE applications, so it does not currently +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 experimental until REA has a matching provider and package-level verification. diff --git a/docs/provider-evaluation.md b/docs/provider-evaluation.md index 69a6214195..baf5740852 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-64 PE applications on local NTFS with +Windows x64 P0 supports native x86 and x86-64 PE applications 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 @@ -49,8 +49,8 @@ follow. - Report GUI cursor/navigation and persistent mutation operations as unavailable until their semantics and project ownership are explicitly designed. - Verify real claims on Linux with at least two distinct source-owned binaries. - Verify the Windows host claim with the deterministic source-owned x86-64 PE - fixture and all admitted operations; compare normalized semantic facts rather + Verify the Windows host claim with the deterministic source-owned x86 and x86-64 PE + fixtures and all admitted operations; compare normalized semantic facts rather than provider-specific pseudocode text. ## Shipped foundation boundary @@ -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-64 PE applications on fixed local NTFS, with the packaged +non-managed, non-DLL x86 and x86-64 PE applications on fixed local NTFS, with the packaged native authority available before launch. The launcher creates one ephemeral runtime root with project, @@ -136,7 +136,7 @@ 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` checks the source-owned native x86-64 PE +`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, and project, endpoint, process, and runtime cleanup. The independent native lane checks DACLs, handle admission, cancellation, and Job Object lifecycle. @@ -165,12 +165,12 @@ only the generic process mechanisms. ## Shortlist -| Provider | License / automation surface | What it brings | REA fit and blockers | -| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Static analysis, multiple processors and formats, scripting, and project/database workflows | 25 read-only operations shipped on Linux/macOS with installation checks, temporary projects, a serial API queue, and real ELF/PE/Mach-O conformance. Experimental Windows x64 P0 supports native x86-64 PE on local NTFS with bundled native controls and ordinary-user CLI/MCP verification; Linux/macOS additionally support atomic session function annotations; Windows mutation and all Ghidra GUI operations are unavailable. | -| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. | -| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. | -| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. | +| Provider | License / automation surface | What it brings | REA fit and blockers | +| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Static analysis, multiple processors and formats, scripting, and project/database workflows | 25 read-only operations shipped on Linux/macOS with installation checks, temporary projects, a serial API queue, and real ELF/PE/Mach-O conformance. Experimental Windows x64 P0 supports native x86 and x86-64 PE on local NTFS with bundled native controls and ordinary-user CLI/MCP verification; Linux/macOS additionally support atomic session function annotations; Windows mutation and all Ghidra GUI operations are unavailable. | +| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. | +| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. | +| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. | ## Recommended order diff --git a/docs/roadmap.md b/docs/roadmap.md index a5f86686ca..24e0f1e6cd 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -62,7 +62,7 @@ Mach-O cross-target fixtures. See [testing](testing.md) for the prerequisites and exact commands. Experimental Windows x64 P0 now supplies bundled Job Object ownership, -protected runtime DACLs, and handle-based admission for native x86-64 PE +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 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/testing.md b/docs/testing.md index be803f7d60..30635eaccd 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -197,7 +197,7 @@ prove behavior on the verified Ghidra 12.1.4 and JDK 21 build. | `npm run verify:ghidra:switch` | Linux x64 ELF; GCC/Clang optimized and stripped switch fixtures | GCC, Clang, GNU nm/objdump/strip, Ghidra 12.1.4, and full JDK 21 | | `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 | | `npm run verify:ghidra:cross-format` | Any supported Ghidra host; also analyzes AArch64 ELF, x86-64 PE, and x86-64 Mach-O | `clang`, LLD, and `lld-link` in addition to host-lane prerequisites | -| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact | +| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE; `-- --x86` selects native x86 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact | Windows native conformance runs with `npm run verify:windows-native` and does not require Ghidra or Java. An optional independently compiled Windows fixture diff --git a/docs/windows-ghidra-p0.md b/docs/windows-ghidra-p0.md index f837bae09f..ec54f5908a 100644 --- a/docs/windows-ghidra-p0.md +++ b/docs/windows-ghidra-p0.md @@ -3,6 +3,8 @@ This guide describes repository main. npm 4.1.0 includes the Windows native bundle; check the [release boundary](installation.md#released-package-and-main) before applying these instructions to an older published package. +Native x86 (32-bit) PE support is available on repository main; npm releases +through 5.0.0 admit x86-64 PE targets on Windows. Status: experimental Windows x64 support for the read-only P0 boundary. Windows package builds bundle native process, filesystem, and DACL controls. REA @@ -15,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-64 PE application; and +- an explicit native, non-managed, non-DLL x86 or x86-64 PE application; and - the 25 read-only Ghidra inventory, memory, and function-analysis operations. Loaded memory reads and file offsets preserve Ghidra's observed source mappings. @@ -98,7 +100,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-64 PE applications and rejects unsupported roles, +provider admits native x86 and x86-64 PE applications and rejects unsupported roles, architectures, managed images, and malformed headers. The original selected source coordinate is preserved for native admission. @@ -156,6 +158,8 @@ artifact and manifest. ```powershell npm run verify:windows-native npm run verify:ghidra:windows +npm run verify:ghidra:windows -- --x86 +npm run verify:ghidra:windows:package -- --x86 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 fcb1df5f2b..6538ec57f8 100644 --- a/scripts/create-ghidra-windows-fixture.mjs +++ b/scripts/create-ghidra-windows-fixture.mjs @@ -3,50 +3,68 @@ import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); -const output = resolve(root, "build", "fixtures", "rea-ghidra-windows.exe"); -const bytes = Buffer.alloc(1024); -const peOffset = 0x80; -const optionalHeader = peOffset + 24; -const sectionTable = optionalHeader + 0xf0; +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; -bytes.write("MZ", 0, "ascii"); -bytes.writeUInt32LE(peOffset, 0x3c); -bytes.write("PE\0\0", peOffset, "binary"); -bytes.writeUInt16LE(0x8664, peOffset + 4); -bytes.writeUInt16LE(1, peOffset + 6); -bytes.writeUInt16LE(0xf0, peOffset + 20); -bytes.writeUInt16LE(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, peOffset + 22); -bytes.writeUInt16LE(0x20b, optionalHeader); -bytes.writeUInt32LE(0x200, optionalHeader + 4); -bytes.writeUInt32LE(0x1000, optionalHeader + 16); -bytes.writeUInt32LE(0x1000, optionalHeader + 20); -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(0x8160, optionalHeader + 70); -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(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(0x16, sectionTable + 8); -bytes.writeUInt32LE(0x1000, sectionTable + 12); -bytes.writeUInt32LE(0x200, sectionTable + 16); -bytes.writeUInt32LE(0x200, sectionTable + 20); -bytes.writeUInt32LE(0x6000_0020, sectionTable + 36); + 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); + // 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); -process.stdout.write(`${output}\n`); + 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 1dfe3cd34d..fa17c856cd 100644 --- a/scripts/verify-packaged-ghidra-windows.mjs +++ b/scripts/verify-packaged-ghidra-windows.mjs @@ -25,10 +25,12 @@ if (process.platform !== "win32" || process.arch !== "x64") ); const workspace = await mkdtemp(join(tmpdir(), "rea-packaged-ghidra-")); const exec = promisify(execFile); -let packageRoot = resolve(process.argv[2] ?? "."); +const x86Fixture = process.argv[2] === "--x86"; +const packageRootArgument = x86Fixture ? undefined : process.argv[2]; +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. -if (process.argv[2] === undefined) { +if (packageRootArgument === undefined) { try { const npmEntry = process.env.npm_execpath ?? @@ -77,7 +79,10 @@ if (process.argv[2] === undefined) { } } const target = resolve( - process.argv[3] ?? "build/fixtures/rea-ghidra-windows.exe", + process.argv[3] ?? + (x86Fixture + ? "build/fixtures/rea-ghidra-windows-x86.exe" + : "build/fixtures/rea-ghidra-windows.exe"), ); let TOOL_CONTRACTS, native, token, sha256; try { @@ -215,7 +220,8 @@ try { }; try { await client.connect(transport); - await call("open_binary", { path: target, provider_id: "ghidra" }); + await call("open_binary", { path: target }); + await call("binary_overview", {}); assert.equal((await call("list_documents", {})).length, 1); await call("list_names", {}); const procedures = await call("list_procedures", {}); diff --git a/scripts/verify-real-ghidra-windows.mjs b/scripts/verify-real-ghidra-windows.mjs index 8dd9f6b53a..f245a4a9f9 100644 --- a/scripts/verify-real-ghidra-windows.mjs +++ b/scripts/verify-real-ghidra-windows.mjs @@ -63,12 +63,20 @@ if ( ); const root = resolve(fileURLToPath(new URL("..", import.meta.url))); -const targetPath = resolve(root, "build", "fixtures", "rea-ghidra-windows.exe"); +const architecture = process.argv[2] === "--x86" ? "x86" : "x86_64"; +const targetPath = resolve( + root, + "build", + "fixtures", + architecture === "x86" + ? "rea-ghidra-windows-x86.exe" + : "rea-ghidra-windows.exe", +); const target = await parseBinaryTarget(targetPath); if ( !target.ok || target.value.format !== "pe" || - target.value.architecture !== "x86_64" || + target.value.architecture !== architecture || target.value.executableRole !== "application" || target.value.managed !== false ) @@ -113,6 +121,9 @@ try { target.value.sha256, profile.value.profile.digest, ); + const languagePrefix = architecture === "x86" ? "x86:LE:32:" : "x86:LE:64:"; + if (!started.value.target.language_id.startsWith(languagePrefix)) + throw new Error("Windows fixture imported with the wrong Ghidra language"); coordinates = client.diagnostics(); assertRuntimeCoordinates(coordinates, target.value.sha256); @@ -137,6 +148,19 @@ try { ); if (procedure === undefined) throw new Error("Windows Ghidra fixture exposed no local procedure"); + const calleeAddress = architecture === "x86" ? 0x401010n : 0x140001010n; + const callee = procedures.find( + (item) => BigInt(item.address) === calleeAddress, + ); + if (callee === undefined) + throw new Error("Windows fixture exposed no controlled callee"); + const pseudoCode = await functionOperation("procedure_pseudo_code", { + procedure: callee.value, + }); + if (typeof pseudoCode !== "string" || !/\b(?:42|0x2a)\b/iu.test(pseudoCode)) + throw new Error( + "Windows fixture decompilation lost its known return value", + ); await inventory("inspect_native_load_image", {}); const memory = await inventory("read_bytes", { @@ -210,6 +234,7 @@ try { report = { ok: true, provider: { id: "ghidra", version: SUPPORTED_GHIDRA_VERSION }, + language_id: started.value.target.language_id, target: { format: target.value.format, architecture: target.value.architecture, @@ -228,7 +253,7 @@ try { limitations: [ "approved-non-sensitive-fixtures-only", "windows-x64-local-ntfs-only", - "native-x86-64-pe-applications-only", + "native-x86-and-x86-64-pe-applications-only", "no-gui-or-mutation-authority", ], }; diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index 7bfddef409..9cc337c9ec 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -1,15 +1,19 @@ import { createHash } from "node:crypto"; +import { execFile } from "node:child_process"; // Fake-backed provider coverage; real Ghidra verification lives in // `npm run verify:ghidra` and its focused variants. -import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; import { fixtureDosLoadImage } from "./GhidraLoadImage.fixture.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; import { parseConfig } from "../config.js"; import type { BinaryTarget } from "../domain/binaryTarget.js"; +import { parseExecutableHeader } from "../domain/binaryTarget.js"; import { createAnalysisProfile } from "../domain/analysisProfile.js"; import { GHIDRA_PROVIDER_IDENTITY, @@ -257,14 +261,18 @@ describe("Ghidra platform support", () => { const ghidra = provider({ ...installationHost(), platform: "win32" }); const nativeApplication = peTarget("x86_64"); - expect(ghidra.inspectTargetSupport(nativeApplication)).toMatchObject({ - status: "supported", - diagnostics: { - host_platform: "win32", - executable_role: "application", - managed: false, - }, - }); + for (const architecture of ["x86", "x86_64"] as const) + expect(ghidra.inspectTargetSupport(peTarget(architecture))).toMatchObject( + { + status: "supported", + diagnostics: { + host_platform: "win32", + architecture, + executable_role: "application", + managed: false, + }, + }, + ); expect(ghidra.inspectAvailability()).toMatchObject({ status: "unavailable", code: "unsupported_host", @@ -333,6 +341,46 @@ describe("Ghidra platform support", () => { code: "architecture_unsupported", }); }); + it("admits generated PE32 applications while rejecting unsupported x86 roles", 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, + }); + }); }); describe("Ghidra client projection", () => { diff --git a/src/ghidra/GhidraProvider.ts b/src/ghidra/GhidraProvider.ts index 17ef5cf202..008a3808cf 100644 --- a/src/ghidra/GhidraProvider.ts +++ b/src/ghidra/GhidraProvider.ts @@ -233,11 +233,11 @@ const inspectWindowsP0TargetSupport = ( reason: "Windows Ghidra P0 accepts PE targets only.", diagnostics, }; - if (target.architecture !== "x86_64") + if (target.architecture !== "x86" && target.architecture !== "x86_64") return { status: "unsupported", code: "architecture_unsupported", - reason: "Windows Ghidra P0 accepts x86-64 PE targets only.", + reason: "Windows Ghidra P0 accepts x86 and x86-64 PE targets only.", diagnostics, }; if (target.executableRole !== "application") diff --git a/src/ghidra/GhidraProviderCapabilities.ts b/src/ghidra/GhidraProviderCapabilities.ts index 0bec2afa04..402f6d1e89 100644 --- a/src/ghidra/GhidraProviderCapabilities.ts +++ b/src/ghidra/GhidraProviderCapabilities.ts @@ -30,7 +30,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-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 only; DLL, 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.", ]);