mirror of
https://github.com/morluto/rea.git
synced 2026-10-18 20:10:13 -05:00
feat(ghidra): admit ARM PE targets on Windows (#3965)
Admit Windows ARMNT/Thumb-2 and ARM64 PE targets using preserved COFF machine facts. Keep unsupported machine gates explicit and distinguish loader-selected versus configured language provenance. Verified with real Windows Ghidra 12.1.4 CLI/MCP workflows, ARM fixtures and Microsoft-signed Coreinfo ARM64 / MSVCR120 ARM inputs. Resource limits, analyzer overrides, startup timeout observations and verification gaps are documented in the PR. Final CI and automated review passed. Fixes #2186
This commit is contained in:
1 parent
2a2a33ad92
commit
2b5ee72d26
19 files changed
+496
-112
No files matched your search
@@ -193,7 +193,11 @@ 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 source-owned native x86-64 PE applications;
|
||||
`-- --x86` selects x86 and `-- --dll` selects DLLs, and the flags can be combined.
|
||||
`-- --x86`, `-- --arm`, or `-- --arm64` selects another target architecture;
|
||||
`-- --dll` selects DLLs. Choose one architecture and optionally combine it with
|
||||
`--dll`. These static fixtures contain source-owned encoded instructions; they
|
||||
are not compiler-produced programs. See the [Windows verification scope](windows-ghidra-p0.md#native-authority-and-target-identity)
|
||||
for real-provider coverage and exclusions.
|
||||
These lanes check all 26 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.
|
||||
|
||||
@@ -797,7 +797,7 @@ and `set_addresses_names` name a function entry or any other mapped address
|
||||
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 and DLLs, plus explicitly
|
||||
Windows P0 admits native x86, x86-64, ARMNT (Thumb-2) and ARM64 PE applications and DLLs, plus explicitly
|
||||
selected [raw NMOS 6502 images](raw-binary.md#nmos-6502) with a caller-supplied
|
||||
entry at file offset 0, on fixed local NTFS volumes. Other raw CPU families remain
|
||||
unsupported on Windows.
|
||||
|
||||
@@ -352,7 +352,7 @@ install it or its toolchain. The [NativeAOT guide](ghidra-nativeaot.md) defines
|
||||
these routes, their layout/host restrictions, derived-memory provenance, and
|
||||
verification coverage.
|
||||
|
||||
REA's experimental Windows Ghidra boundary admits native x86 and x86-64 PE
|
||||
REA's experimental Windows Ghidra boundary admits native x86, x86-64, ARMNT (Thumb-2) and ARM64 PE
|
||||
applications and DLLs for static analysis. For x64 PE NativeAOT, the read-only
|
||||
parser also runs there; the [NativeAOT guide](ghidra-nativeaot.md) lists what a
|
||||
Windows host covers.
|
||||
|
||||
@@ -497,7 +497,7 @@ is present. A denied trace reports the `debugger-attach-denied` reason.
|
||||
Install a Ghidra 12.1.x release and the 64-bit full JDK it declares, then configure REA to
|
||||
use them. Ghidra analysis supports Linux x64/arm64 and macOS x64/arm64; each installation requires
|
||||
the matching native decompiler. Experimental Windows x64 P0 admits native
|
||||
x86 and x86-64 PE applications and DLLs, plus explicitly selected
|
||||
x86, x86-64, ARMNT (Thumb-2) and ARM64 PE applications and DLLs, plus explicitly selected
|
||||
[raw NMOS 6502 images](raw-binary.md#nmos-6502) with a caller-supplied load address,
|
||||
on fixed local NTFS volumes. Other raw CPU families remain unsupported on Windows.
|
||||
The boundary uses bundled Job Object ownership, protected runtime DACLs, and
|
||||
|
||||
@@ -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 x86 or x86-64 PE application/DLL, or a
|
||||
- an explicit native, non-managed x86, x86-64, ARMNT (Thumb-2) or ARM64 PE application/DLL, or a
|
||||
[raw NMOS 6502 image](raw-binary.md#nmos-6502) with a caller-supplied entry; and
|
||||
- the 26 read-only Ghidra inventory, memory, and function-analysis operations.
|
||||
|
||||
@@ -135,9 +135,55 @@ 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 DLLs and rejects unsupported roles,
|
||||
provider admits native x86, x86-64, ARMNT (Thumb-2) and ARM64 PE applications and DLLs and rejects unsupported roles,
|
||||
architectures, managed images, and malformed headers. The original selected
|
||||
source coordinate is preserved for native admission.
|
||||
source coordinate is preserved for native admission. The original COFF machine is
|
||||
retained separately from the normalized CPU family. ARM admission requires the
|
||||
observed ARMNT machine 0x01C4; ARM64 requires 0xAA64. Legacy ARM (0x01C0), Thumb
|
||||
(0x01C2), and unavailable machine facts do not select this Windows lane.
|
||||
Without a caller-selected processor override, Ghidra's PE loader
|
||||
selects the language from the PE machine field: ARM (`IMAGE_FILE_MACHINE_ARMNT`)
|
||||
imports as `ARM:LE:32:v8` (Thumb), ARM64 (`IMAGE_FILE_MACHINE_ARM64`) as
|
||||
`AARCH64:LE:64:v8A`. Explicit language/compiler selections remain caller choices
|
||||
in the analysis profile and are recorded as explicit imports. The source
|
||||
fixture verifier compares the complete observed language ID, not just its CPU
|
||||
family prefix. ARM/ARM64 admission is a bounded extension of the x86/x64
|
||||
lane. `npm run verify:ghidra:windows -- --arm` or `-- --arm64`, optionally
|
||||
combined with `--dll`, selects encoded Thumb or ARM64 PE fixtures on a Windows
|
||||
x64 host. The packaged verifier accepts the same flags. These fixtures are not
|
||||
compiler-produced programs. ARM64EC (machine 0xA641) and ARM64X are not admitted;
|
||||
this target support does not establish Windows-on-Arm host support.
|
||||
|
||||
Real Windows x64 verification with Ghidra 12.1.4 and JDK 21 completed the ARM64
|
||||
application and ARMNT/Thumb DLL fixtures' 26 read-only operations, constant-return
|
||||
decompilation, byte searches, CLI/MCP file-offset parity, source identity, and
|
||||
owned cleanup. The DLL fixture also verifies the exported caller/callee
|
||||
relationship. The x86/x64 compact-reference value-flow battery does not cover
|
||||
ARM or ARM64 instructions. ARM64 EXE file-offset verification includes ambiguous
|
||||
mappings; the single-section ARM32 DLL cannot exercise that case.
|
||||
|
||||
The runs used two logical CPUs, 512 MiB Node/JVM heaps, a 900-second startup
|
||||
budget, and an explicit `REA_GHIDRA_DISABLED_ANALYZERS=WindowsResourceReference`
|
||||
test configuration. Default-analyzer runs exceeded the selected startup budget;
|
||||
resource-reference analysis and default-configuration completion are not
|
||||
established by these results. A Microsoft-signed Coreinfo ARM64 application also
|
||||
completed focused MCP decompilation, repeated readback, source-mapped memory
|
||||
reads, unchanged-input and cleanup checks. On this constrained host the first
|
||||
function query, including cold analysis, took about 434 seconds; its identical
|
||||
same-session read took about 23 ms and returned about 4.6 KiB of Evidence. These
|
||||
are individual observations, not general latency guarantees. Public ARM32
|
||||
DLL verification also completed against Microsoft Visual C++ 2013 ARM
|
||||
`msvcr120.dll`, including decompilation of its exported `abs`, repeated readback,
|
||||
source-mapped bytes, unchanged-input and cleanup checks. Its first function
|
||||
query took about 601 seconds; the same-session repeat took about 11 ms and
|
||||
returned about 4.8 KiB of Evidence. After preserving original COFF machines in the
|
||||
admission/profile, a further real Windows check rejected legacy ARM/Thumb and
|
||||
retained ARMNT/ARM64 admission. A rebuilt-candidate MSVCR120 query completed
|
||||
in about 407 seconds with a 15 ms repeat (4,830 bytes of Evidence), unchanged
|
||||
input and complete cleanup. That retry selected an 1,800-second startup budget
|
||||
after an additional 900-second attempt timed out. The resource bounds were
|
||||
unchanged; these observations do not establish a speedup. Configured processor
|
||||
selection is covered by profile tests, not these real-provider runs.
|
||||
|
||||
Admission opens and checks every path component without following a reparse
|
||||
point. It retains handles, verifies local NTFS semantics, and observes volume
|
||||
@@ -200,6 +246,8 @@ 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 -- --arm64
|
||||
npm run verify:ghidra:windows -- --arm64 --dll
|
||||
npm run verify:ghidra:windows:package -- --x86
|
||||
npm run verify:ghidra:windows:package -- --dll
|
||||
npm run verify:ghidra:windows:package -- --x86 --dll
|
||||
|
||||
@@ -3,42 +3,52 @@ import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { writeWindowsGhidraByteSearchFixture } from "./lib/ghidra-windows-byte-search.mjs";
|
||||
|
||||
import {
|
||||
WINDOWS_GHIDRA_FIXTURE_ARCHITECTURES,
|
||||
windowsGhidraFixtureOptions,
|
||||
} from "./lib/ghidra-windows-fixture-options.mjs";
|
||||
|
||||
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"]) {
|
||||
for (const architecture of WINDOWS_GHIDRA_FIXTURE_ARCHITECTURES) {
|
||||
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 arm = architecture === "arm";
|
||||
const arm64 = architecture === "arm64";
|
||||
const fixture = windowsGhidraFixtureOptions([
|
||||
...(architecture === "x86_64" ? [] : [`--${architecture}`]),
|
||||
...(dll ? ["--dll"] : []),
|
||||
]);
|
||||
const pe32 = fixture.pe32;
|
||||
const output = resolve(root, "build", "fixtures", fixture.filename);
|
||||
const bytes = Buffer.alloc(dll ? 1024 : 1536);
|
||||
const peOffset = 0x80;
|
||||
const optionalHeader = peOffset + 24;
|
||||
const optionalHeaderSize = x86 ? 0xe0 : 0xf0;
|
||||
const optionalHeaderSize = pe32 ? 0xe0 : 0xf0;
|
||||
const sectionTable = optionalHeader + optionalHeaderSize;
|
||||
const directories = optionalHeader + (x86 ? 96 : 112);
|
||||
const directories = optionalHeader + (pe32 ? 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);
|
||||
// The fixture descriptor owns COFF machine, width and function coordinates.
|
||||
bytes.writeUInt16LE(fixture.machine, peOffset + 4);
|
||||
bytes.writeUInt16LE(dll ? 1 : 2, peOffset + 6);
|
||||
bytes.writeUInt16LE(optionalHeaderSize, peOffset + 20);
|
||||
bytes.writeUInt16LE(
|
||||
(x86 ? 0x102 : 0x22) | (dll ? 0x2000 : 0),
|
||||
(pe32 ? 0x102 : 0x22) | (dll ? 0x2000 : 0),
|
||||
peOffset + 22,
|
||||
);
|
||||
|
||||
bytes.writeUInt16LE(x86 ? 0x10b : 0x20b, optionalHeader);
|
||||
// ARM64 PE uses the PE32+ optional header (magic 0x20b), like x86-64.
|
||||
bytes.writeUInt16LE(pe32 ? 0x10b : 0x20b, optionalHeader);
|
||||
bytes.writeUInt32LE(0x200, optionalHeader + 4);
|
||||
if (!dll) bytes.writeUInt32LE(0x200, optionalHeader + 8);
|
||||
bytes.writeUInt32LE(dll ? 0 : 0x1000, optionalHeader + 16);
|
||||
bytes.writeUInt32LE(dll ? 0 : arm ? 0x1001 : 0x1000, optionalHeader + 16);
|
||||
bytes.writeUInt32LE(0x1000, optionalHeader + 20);
|
||||
if (x86) bytes.writeUInt32LE(0x400000, optionalHeader + 28);
|
||||
else bytes.writeBigUInt64LE(0x1_4000_0000n, optionalHeader + 24);
|
||||
if (pe32)
|
||||
bytes.writeUInt32LE(Number(fixture.imageBase), optionalHeader + 28);
|
||||
else bytes.writeBigUInt64LE(fixture.imageBase, optionalHeader + 24);
|
||||
bytes.writeUInt32LE(0x1000, optionalHeader + 32);
|
||||
bytes.writeUInt32LE(0x200, optionalHeader + 36);
|
||||
bytes.writeUInt16LE(6, optionalHeader + 40);
|
||||
@@ -46,8 +56,8 @@ for (const architecture of ["x86_64", "x86"]) {
|
||||
bytes.writeUInt32LE(dll ? 0x2000 : 0x3000, optionalHeader + 56);
|
||||
bytes.writeUInt32LE(0x200, optionalHeader + 60);
|
||||
bytes.writeUInt16LE(3, optionalHeader + 68);
|
||||
bytes.writeUInt16LE(x86 ? 0x8140 : 0x8160, optionalHeader + 70);
|
||||
if (x86) {
|
||||
bytes.writeUInt16LE(pe32 ? 0x8140 : 0x8160, optionalHeader + 70);
|
||||
if (pe32) {
|
||||
bytes.writeUInt32LE(0x10_0000, optionalHeader + 72);
|
||||
bytes.writeUInt32LE(0x1000, optionalHeader + 76);
|
||||
bytes.writeUInt32LE(0x10_0000, optionalHeader + 80);
|
||||
@@ -69,18 +79,52 @@ for (const architecture of ["x86_64", "x86"]) {
|
||||
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, 0xe8, 0x16, 0, 0, 0, 0x31, 0xc0, 0xc3],
|
||||
0x200,
|
||||
);
|
||||
bytes.set(
|
||||
x86 && !dll
|
||||
? [0xb8, 0x2a, 0, 0, 0, 0xe8, 0x66, 0, 0, 0, 0xc3]
|
||||
: [0xb8, 0x2a, 0, 0, 0, 0xc3],
|
||||
0x210,
|
||||
);
|
||||
if (arm) {
|
||||
// ARMNT uses Thumb instructions and low-bit-tagged PE entry/export RVAs.
|
||||
// PUSH keeps SP aligned and preserves LR across BL from 0x1002 to 0x1020.
|
||||
bytes.set(
|
||||
[
|
||||
0x10,
|
||||
0xb5,
|
||||
0x00,
|
||||
0xf0,
|
||||
0x0d,
|
||||
0xf8,
|
||||
dll ? 0x01 : 0x00,
|
||||
dll ? 0x30 : 0x20,
|
||||
0x10,
|
||||
0xbd,
|
||||
],
|
||||
0x200,
|
||||
);
|
||||
bytes.set([0x2a, 0x20, 0x70, 0x47], 0x220);
|
||||
} else if (arm64) {
|
||||
// Preserve the caller's frame/link registers across BL. The callee
|
||||
// starts at RVA 0x1020, outside the five-instruction caller.
|
||||
const instructions = [
|
||||
0xa9bf7bfd, // STP X29, X30, [SP, #-16]!
|
||||
0x94000007, // BL +0x1c from RVA 0x1004 to RVA 0x1020
|
||||
dll ? 0x11000400 : 0x52800000, // ADD W0,W0,#1 / MOV W0,#0
|
||||
0xa8c17bfd, // LDP X29, X30, [SP], #16
|
||||
0xd65f03c0, // RET
|
||||
];
|
||||
for (const [index, instruction] of instructions.entries())
|
||||
bytes.writeUInt32LE(instruction, 0x200 + index * 4);
|
||||
bytes.set([0x40, 0x05, 0x80, 0x52, 0xc0, 0x03, 0x5f, 0xd6], 0x220);
|
||||
} else {
|
||||
bytes.set(
|
||||
dll
|
||||
? [0xe8, 0x0b, 0, 0, 0, 0x83, 0xc0, 1, 0xc3]
|
||||
: [0xe8, 0x0b, 0, 0, 0, 0xe8, 0x16, 0, 0, 0, 0x31, 0xc0, 0xc3],
|
||||
0x200,
|
||||
);
|
||||
bytes.set(
|
||||
x86 && !dll
|
||||
? [0xb8, 0x2a, 0, 0, 0, 0xe8, 0x66, 0, 0, 0, 0xc3]
|
||||
: [0xb8, 0x2a, 0, 0, 0, 0xc3],
|
||||
0x210,
|
||||
);
|
||||
}
|
||||
|
||||
if (x86 && !dll) {
|
||||
// A statically analyzed Borland-style SEH registration whose handler
|
||||
@@ -149,19 +193,31 @@ for (const architecture of ["x86_64", "x86"]) {
|
||||
"ffd7", // CALL EDI: unknown target.
|
||||
"c3",
|
||||
]
|
||||
: [
|
||||
"488d15d90f0000", // LEA RDX, [RIP + displacement]: DATA.
|
||||
"8b05d30f0000", // MOV EAX, [RIP + displacement]: READ.
|
||||
"8905d10f0000", // MOV [RIP + displacement], EAX: WRITE.
|
||||
"488d15d60f0000", // Indexed base, unknown RCX.
|
||||
"85db", // TEST EBX, EBX: two candidate bases.
|
||||
"7407", // JZ over the alternate base.
|
||||
"488d15d30f0000", // LEA RDX, alternate base.
|
||||
"c6040a01", // MOV byte [RDX + RCX], 1.
|
||||
"41ffd0", // CALL R8: unknown target.
|
||||
"c3",
|
||||
];
|
||||
bytes.set(Buffer.from(referenceInstructions.join(""), "hex"), 0x220);
|
||||
: arm
|
||||
? ["7047"] // Thumb return; x86 reference-flow controls do not apply.
|
||||
: arm64
|
||||
? [
|
||||
"010000b0", // ADRP X1, .data page (RVA 0x2000): DATA.
|
||||
"200040b9", // LDR W0, [X1]: READ.
|
||||
"200000b9", // STR W0, [X1]: WRITE.
|
||||
"c0035fd6", // RET.
|
||||
]
|
||||
: [
|
||||
"488d15d90f0000", // LEA RDX, [RIP + displacement]: DATA.
|
||||
"8b05d30f0000", // MOV EAX, [RIP + displacement]: READ.
|
||||
"8905d10f0000", // MOV [RIP + displacement], EAX: WRITE.
|
||||
"488d15d60f0000", // Indexed base, unknown RCX.
|
||||
"85db", // TEST EBX, EBX: two candidate bases.
|
||||
"7407", // JZ over the alternate base.
|
||||
"488d15d30f0000", // LEA RDX, alternate base.
|
||||
"c6040a01", // MOV byte [RDX + RCX], 1.
|
||||
"41ffd0", // CALL R8: unknown target.
|
||||
"c3",
|
||||
];
|
||||
bytes.set(
|
||||
Buffer.from(referenceInstructions.join(""), "hex"),
|
||||
arm || arm64 ? 0x230 : 0x220,
|
||||
);
|
||||
}
|
||||
|
||||
if (dll) {
|
||||
@@ -176,8 +232,8 @@ for (const architecture of ["x86_64", "x86"]) {
|
||||
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(arm ? 0x1001 : 0x1000, 0x280);
|
||||
bytes.writeUInt32LE(fixture.calleeRva + (arm ? 1 : 0), 0x284);
|
||||
bytes.writeUInt32LE(0x1120, 0x288);
|
||||
bytes.writeUInt32LE(0x1140, 0x28c);
|
||||
bytes.writeUInt16LE(0, 0x290);
|
||||
@@ -191,6 +247,6 @@ for (const architecture of ["x86_64", "x86"]) {
|
||||
|
||||
await mkdir(dirname(output), { recursive: true });
|
||||
await writeFile(output, bytes);
|
||||
if (!x86 && !dll) process.stdout.write(`${output}\n`);
|
||||
if (architecture === "x86_64" && !dll) process.stdout.write(`${output}\n`);
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,12 @@ export function writeWindowsGhidraByteSearchFixture(bytes) {
|
||||
export async function verifyWindowsGhidraByteSearch({
|
||||
invoke,
|
||||
sourceBytes,
|
||||
architecture,
|
||||
imageBase,
|
||||
sourceOwnedFixture = true,
|
||||
fallbackAddress,
|
||||
fallbackFileOffset,
|
||||
runCliPage,
|
||||
}) {
|
||||
const imageBase = architecture === "x86" ? 0x400000n : 0x140000000n;
|
||||
const query = sourceOwnedFixture
|
||||
? {
|
||||
pattern: WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE.pattern,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export const WINDOWS_GHIDRA_FIXTURE_ARCHITECTURES: readonly [
|
||||
"x86_64",
|
||||
"x86",
|
||||
"arm",
|
||||
"arm64",
|
||||
];
|
||||
export const WINDOWS_GHIDRA_FIXTURE_FLAGS: readonly string[];
|
||||
|
||||
/** Select one architecture without interpreting positional paths as flags. */
|
||||
export function windowsGhidraFixtureOptions(arguments_: readonly string[]): {
|
||||
architecture: (typeof WINDOWS_GHIDRA_FIXTURE_ARCHITECTURES)[number];
|
||||
dll: boolean;
|
||||
pe32: boolean;
|
||||
imageBase: bigint;
|
||||
calleeRva: number;
|
||||
machine: number;
|
||||
filename: string;
|
||||
languageId: string;
|
||||
callerAddress: string;
|
||||
calleeAddress: string;
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/** Source-owned PE fixtures; these are encoded bytes, not compiled programs. */
|
||||
/** @type {readonly ["x86_64", "x86", "arm", "arm64"]} */
|
||||
export const WINDOWS_GHIDRA_FIXTURE_ARCHITECTURES = Object.freeze([
|
||||
"x86_64",
|
||||
"x86",
|
||||
"arm",
|
||||
"arm64",
|
||||
]);
|
||||
|
||||
export const WINDOWS_GHIDRA_FIXTURE_FLAGS = Object.freeze([
|
||||
"--x86",
|
||||
"--arm",
|
||||
"--arm64",
|
||||
"--dll",
|
||||
]);
|
||||
|
||||
/** Select one architecture without interpreting positional paths as flags. */
|
||||
/** @param {readonly string[]} arguments_ */
|
||||
export const windowsGhidraFixtureOptions = (arguments_) => {
|
||||
const selected = WINDOWS_GHIDRA_FIXTURE_ARCHITECTURES.filter(
|
||||
(architecture) =>
|
||||
architecture !== "x86_64" && arguments_.includes(`--${architecture}`),
|
||||
);
|
||||
if (selected.length > 1)
|
||||
throw new Error("Choose one Windows PE fixture architecture.");
|
||||
const architecture = selected[0] ?? "x86_64";
|
||||
const dll = arguments_.includes("--dll");
|
||||
const pe32 = architecture === "x86" || architecture === "arm";
|
||||
const imageBase = pe32 ? 0x400000n : 0x140000000n;
|
||||
const calleeRva = architecture.startsWith("arm") ? 0x1020 : 0x1010;
|
||||
return {
|
||||
architecture,
|
||||
dll,
|
||||
pe32,
|
||||
imageBase,
|
||||
calleeRva,
|
||||
machine: { x86: 0x14c, x86_64: 0x8664, arm: 0x1c4, arm64: 0xaa64 }[
|
||||
architecture
|
||||
],
|
||||
filename: `rea-ghidra-windows${architecture === "x86_64" ? "" : `-${architecture}`}.${dll ? "dll" : "exe"}`,
|
||||
languageId:
|
||||
architecture === "arm"
|
||||
? "ARM:LE:32:v8"
|
||||
: architecture === "arm64"
|
||||
? "AARCH64:LE:64:v8A"
|
||||
: `x86:LE:${pe32 ? 32 : 64}:default`,
|
||||
callerAddress: `0x${(imageBase + 0x1000n).toString(16)}`,
|
||||
calleeAddress: `0x${(imageBase + BigInt(calleeRva)).toString(16)}`,
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
import {
|
||||
WINDOWS_GHIDRA_FIXTURE_FLAGS,
|
||||
windowsGhidraFixtureOptions,
|
||||
} from "./lib/ghidra-windows-fixture-options.mjs";
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
@@ -33,11 +37,11 @@ const exec = promisify(execFile);
|
||||
const arguments_ = process.argv.slice(2);
|
||||
const internalWorkspace = arguments_[0] === "--internal-workspace";
|
||||
const fixtureArguments = internalWorkspace ? arguments_.slice(2) : arguments_;
|
||||
const x86Fixture = fixtureArguments.includes("--x86");
|
||||
const dllFixture = fixtureArguments.includes("--dll");
|
||||
const fixture = windowsGhidraFixtureOptions(fixtureArguments);
|
||||
const dllFixture = fixture.dll;
|
||||
const [packageRootArgument, targetArgument, reportArgument] =
|
||||
fixtureArguments.filter(
|
||||
(argument) => argument !== "--x86" && argument !== "--dll",
|
||||
(argument) => !WINDOWS_GHIDRA_FIXTURE_FLAGS.includes(argument),
|
||||
);
|
||||
// Windows locks the loaded package-owned addon until the verifier exits.
|
||||
// Keep installation cleanup in a parent that never loads that addon.
|
||||
@@ -125,10 +129,7 @@ if (packageRootArgument === undefined) {
|
||||
});
|
||||
}
|
||||
}
|
||||
const target = resolve(
|
||||
targetArgument ??
|
||||
`build/fixtures/rea-ghidra-windows${x86Fixture ? "-x86" : ""}.${dllFixture ? "dll" : "exe"}`,
|
||||
);
|
||||
const target = resolve(targetArgument ?? `build/fixtures/${fixture.filename}`);
|
||||
let TOOL_CONTRACTS, native, token, sha256;
|
||||
let parsedTarget;
|
||||
try {
|
||||
@@ -160,10 +161,7 @@ try {
|
||||
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.architecture, fixture.architecture);
|
||||
assert.equal(
|
||||
parsedTarget.value.executableRole,
|
||||
dllFixture ? "shared-library" : "application",
|
||||
@@ -321,13 +319,13 @@ try {
|
||||
const procedures = await call("list_procedures", {});
|
||||
assert.ok(procedures.length > 0);
|
||||
if (targetArgument === undefined) {
|
||||
const calleeAddress = x86Fixture ? "0x401010" : "0x140001010";
|
||||
const calleeAddress = fixture.calleeAddress;
|
||||
const calleeCode = await call("procedure_pseudo_code", {
|
||||
procedure: calleeAddress,
|
||||
});
|
||||
assert.match(calleeCode, /\b(?:42|0x2a)\b/iu);
|
||||
if (dllFixture) {
|
||||
const callerAddress = x86Fixture ? "0x401000" : "0x140001000";
|
||||
const callerAddress = fixture.callerAddress;
|
||||
assert.ok(
|
||||
(
|
||||
await call("procedure_callees", { procedure: callerAddress })
|
||||
@@ -379,7 +377,7 @@ try {
|
||||
report.byteSearch = await verifyWindowsGhidraByteSearch({
|
||||
invoke: call,
|
||||
sourceBytes,
|
||||
architecture: parsedTarget.value.architecture,
|
||||
imageBase: fixture.imageBase,
|
||||
sourceOwnedFixture: targetArgument === undefined,
|
||||
fallbackAddress: procedure.address,
|
||||
fallbackFileOffset: mapping.file_offset,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
import { windowsGhidraFixtureOptions } from "./lib/ghidra-windows-fixture-options.mjs";
|
||||
|
||||
import { access, readFile } from "node:fs/promises";
|
||||
import { isAbsolute, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { parseConfig } from "../dist/config/parseConfig.js";
|
||||
import { resolveGhidraAnalysisProfile } from "../dist/ghidra/GhidraAnalysisProfile.js";
|
||||
import { GhidraClient } from "../dist/ghidra/GhidraClient.js";
|
||||
import {
|
||||
@@ -20,7 +22,10 @@ import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js"
|
||||
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallationPolicy.js";
|
||||
import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js";
|
||||
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderCapabilities.js";
|
||||
import { windowsP0Capabilities } from "../dist/ghidra/GhidraProviderCapabilities.js";
|
||||
import {
|
||||
WINDOWS_P0_NATIVE_PE_ARCHITECTURES,
|
||||
windowsP0Capabilities,
|
||||
} from "../dist/ghidra/GhidraProviderCapabilities.js";
|
||||
import {
|
||||
GHIDRA_MUTATING_OPERATIONS,
|
||||
GHIDRA_SESSION_CAPABILITIES,
|
||||
@@ -71,15 +76,11 @@ if (
|
||||
);
|
||||
|
||||
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
||||
const architecture = process.argv.includes("--x86") ? "x86" : "x86_64";
|
||||
const dllFixture = process.argv.includes("--dll");
|
||||
const fixture = windowsGhidraFixtureOptions(process.argv.slice(2));
|
||||
const architecture = fixture.architecture;
|
||||
const dllFixture = fixture.dll;
|
||||
const role = dllFixture ? "shared-library" : "application";
|
||||
const targetPath = resolve(
|
||||
root,
|
||||
"build",
|
||||
"fixtures",
|
||||
`rea-ghidra-windows${architecture === "x86" ? "-x86" : ""}.${dllFixture ? "dll" : "exe"}`,
|
||||
);
|
||||
const targetPath = resolve(root, "build", "fixtures", fixture.filename);
|
||||
const target = await parseBinaryTarget(targetPath);
|
||||
if (
|
||||
!target.ok ||
|
||||
@@ -91,10 +92,15 @@ if (
|
||||
throw new Error(
|
||||
`Controlled Windows fixture failed admission: ${JSON.stringify(target)}`,
|
||||
);
|
||||
const configuration = parseConfig(process.env);
|
||||
if (!configuration.ok) throw configuration.error;
|
||||
const profile = await resolveGhidraAnalysisProfile(
|
||||
target.value,
|
||||
GHIDRA_PROVIDER_IDENTITY,
|
||||
installation,
|
||||
undefined,
|
||||
undefined,
|
||||
configuration.value.ghidraDisabledAnalyzers,
|
||||
);
|
||||
if (!profile.ok || profile.value.profile === null)
|
||||
throw new Error("Windows Ghidra profile could not be committed");
|
||||
@@ -113,6 +119,9 @@ const client = new GhidraClient({
|
||||
new URL("../bridge/ghidra/ReaGhidraBridge.java", import.meta.url),
|
||||
),
|
||||
platform: "win32",
|
||||
...(configuration.value.ghidraDisabledAnalyzers === undefined
|
||||
? {}
|
||||
: { disabledAnalyzers: configuration.value.ghidraDisabledAnalyzers }),
|
||||
}),
|
||||
targetPath: target.value.path,
|
||||
targetSha256: target.value.sha256,
|
||||
@@ -134,8 +143,8 @@ 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))
|
||||
const languageId = fixture.languageId;
|
||||
if (started.value.target.language_id !== languageId)
|
||||
throw new Error("Windows fixture imported with the wrong Ghidra language");
|
||||
coordinates = client.diagnostics();
|
||||
assertRuntimeCoordinates(coordinates, target.value.sha256);
|
||||
@@ -161,7 +170,7 @@ try {
|
||||
);
|
||||
if (procedure === undefined)
|
||||
throw new Error("Windows Ghidra fixture exposed no local procedure");
|
||||
const calleeAddress = architecture === "x86" ? 0x401010n : 0x140001010n;
|
||||
const calleeAddress = BigInt(fixture.calleeAddress);
|
||||
const callee = procedures.find(
|
||||
(item) => BigInt(item.address) === calleeAddress,
|
||||
);
|
||||
@@ -175,7 +184,7 @@ try {
|
||||
"Windows fixture decompilation lost its known return value",
|
||||
);
|
||||
if (dllFixture) {
|
||||
const callerAddress = architecture === "x86" ? "0x401000" : "0x140001000";
|
||||
const callerAddress = fixture.callerAddress;
|
||||
const callees = await functionOperation("procedure_callees", {
|
||||
procedure: callerAddress,
|
||||
});
|
||||
@@ -223,7 +232,7 @@ try {
|
||||
const byteSearch = await verifyWindowsGhidraByteSearch({
|
||||
invoke: inventory,
|
||||
sourceBytes,
|
||||
architecture,
|
||||
imageBase: fixture.imageBase,
|
||||
});
|
||||
|
||||
for (const operation of [
|
||||
@@ -268,12 +277,16 @@ try {
|
||||
|
||||
// The reference controls occupy the EXE fixture's additional function.
|
||||
// DLL fixtures retain their independent exported-caller/return controls.
|
||||
const compactReferences = dllFixture
|
||||
? null
|
||||
: await verifyGhidraCompactReferences(
|
||||
functionOperation,
|
||||
architecture === "x86",
|
||||
);
|
||||
// ARM/ARM64 fixtures verify function entry, decompiler readback and the
|
||||
// exported caller/callee relationship; the x86/x64 compact-reference
|
||||
// value-flow battery has no ARM/ARM64 instruction encoding in this fixture.
|
||||
const compactReferences =
|
||||
dllFixture || architecture === "arm" || architecture === "arm64"
|
||||
? null
|
||||
: await verifyGhidraCompactReferences(
|
||||
functionOperation,
|
||||
architecture === "x86",
|
||||
);
|
||||
const borlandSeh =
|
||||
architecture === "x86" && !dllFixture
|
||||
? await verifyGhidraBorlandSeh(functionObservationOperation, true)
|
||||
@@ -300,10 +313,11 @@ try {
|
||||
protected_dacl: true,
|
||||
handle_admission: true,
|
||||
},
|
||||
supported_target_architectures: [...WINDOWS_P0_NATIVE_PE_ARCHITECTURES],
|
||||
limitations: [
|
||||
"approved-non-sensitive-fixtures-only",
|
||||
"windows-x64-local-ntfs-only",
|
||||
"native-x86-and-x86-64-pe-applications-and-dlls-only",
|
||||
"native-pe-applications-and-dlls-only",
|
||||
"no-gui-or-mutation-authority",
|
||||
],
|
||||
};
|
||||
@@ -323,11 +337,24 @@ try {
|
||||
}
|
||||
if (report === undefined)
|
||||
throw new Error("Windows Ghidra report was not produced");
|
||||
report.file_offsets = await verifyGhidraFileOffsets(
|
||||
targetPath,
|
||||
resolve(root, "scripts", "rea.mjs"),
|
||||
architecture === "x86" ? "0x401010" : "0x140001010",
|
||||
);
|
||||
try {
|
||||
report.file_offsets = await verifyGhidraFileOffsets(
|
||||
targetPath,
|
||||
resolve(root, "scripts", "rea.mjs"),
|
||||
fixture.calleeAddress,
|
||||
);
|
||||
} catch (cause) {
|
||||
// Preserve completed real-provider facts if a separate adapter proof fails.
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
...report,
|
||||
ok: false,
|
||||
primary_verification: "complete",
|
||||
file_offsets: { status: "incomplete", failure_code: cause.code ?? null },
|
||||
})}\n`,
|
||||
);
|
||||
throw cause;
|
||||
}
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), ...report })}\n`,
|
||||
);
|
||||
|
||||
@@ -51,6 +51,8 @@ export type ExecutableMetadata =
|
||||
}
|
||||
| {
|
||||
readonly format: "pe";
|
||||
/** Original unsigned COFF machine, before CPU-family normalization. */
|
||||
readonly peMachine: number;
|
||||
readonly architecture: HeaderArchitecture;
|
||||
readonly availableArchitectures: readonly HeaderArchitecture[];
|
||||
readonly executableRole:
|
||||
@@ -318,6 +320,7 @@ export const parsePeRecord = (
|
||||
const characteristics = record.readUInt16LE(22);
|
||||
return ok({
|
||||
format: "pe",
|
||||
peMachine: machineType,
|
||||
architecture,
|
||||
availableArchitectures: [architecture],
|
||||
executableRole:
|
||||
|
||||
@@ -195,6 +195,8 @@ export type BinaryTarget =
|
||||
})
|
||||
| (ExecutableTarget & {
|
||||
readonly format: "pe";
|
||||
/** Original COFF machine; undefined means this header fact is unavailable. */
|
||||
readonly peMachine?: number;
|
||||
/** Provider-neutral PE image role observed from COFF characteristics. */
|
||||
readonly executableRole:
|
||||
| "application"
|
||||
|
||||
@@ -227,6 +227,17 @@ export const resolveGhidraAnalysisProfile = async (
|
||||
...(target.format === "pe"
|
||||
? { pdb_observations: "program-info-v5" }
|
||||
: {}),
|
||||
...(installation.platform === "win32" &&
|
||||
target.format === "pe" &&
|
||||
(target.architecture === "arm" || target.architecture === "arm64")
|
||||
? {
|
||||
windows_arm_pe_import:
|
||||
selectedLanguage === undefined
|
||||
? "loader-selected-language-v1"
|
||||
: "explicit-language-v1",
|
||||
windows_arm_pe_machine: target.peMachine ?? null,
|
||||
}
|
||||
: {}),
|
||||
annotation_policy: "atomic-function-entry-metadata-v1",
|
||||
load_image_observations: "source-mappings-entry-context-v2",
|
||||
function_body_evidence: "complete-inclusive-ranges-v1",
|
||||
|
||||
@@ -3,7 +3,7 @@ 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, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -376,6 +376,82 @@ describe("Ghidra Mach-O slice support", () => {
|
||||
});
|
||||
|
||||
describe("Ghidra platform support", () => {
|
||||
it.each([
|
||||
{ architecture: "arm", languageId: "ARM:LE:32:v7" },
|
||||
{ architecture: "arm64", languageId: "AARCH64:LE:64:v8A" },
|
||||
] as const)(
|
||||
"records explicit Windows $architecture processor selection without claiming loader selection",
|
||||
async ({ architecture, languageId }) => {
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), "rea-arm-profile-language-"),
|
||||
);
|
||||
onTestFinished(() => rm(directory, { recursive: true, force: true }));
|
||||
const languages = join(
|
||||
directory,
|
||||
"Ghidra/Processors/fixture/data/languages",
|
||||
);
|
||||
await mkdir(languages, { recursive: true });
|
||||
await writeFile(
|
||||
join(languages, "fixture.ldefs"),
|
||||
`<language_definitions><language processor="${architecture === "arm" ? "ARM" : "AARCH64"}" endian="little" size="${architecture === "arm" ? 32 : 64}" variant="fixture" id="${languageId}"><compiler name="Visual Studio" spec="fixture.cspec" id="windows"/></language></language_definitions>`,
|
||||
);
|
||||
const config = parseConfig({
|
||||
GHIDRA_INSTALL_DIR: directory,
|
||||
REA_GHIDRA_LANGUAGE_ID: languageId,
|
||||
REA_GHIDRA_COMPILER_SPEC_ID: "windows",
|
||||
});
|
||||
if (!config.ok) throw config.error;
|
||||
const ghidra = new GhidraProvider(
|
||||
config.value,
|
||||
silentLogger,
|
||||
{},
|
||||
installationHost("win32"),
|
||||
);
|
||||
const resolved = await ghidra.resolveAnalysisProfile(
|
||||
peTarget(architecture),
|
||||
);
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
expect(resolved.value.profile?.parameters).toMatchObject({
|
||||
windows_arm_pe_import: "explicit-language-v1",
|
||||
windows_arm_pe_machine: peTarget(architecture).peMachine,
|
||||
language_id: languageId,
|
||||
compiler_spec_id: "windows",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("versions the Windows ARM PE import without changing other profile lanes", async () => {
|
||||
for (const platform of ["win32", "linux"] as const) {
|
||||
const ghidra = provider(installationHost(platform));
|
||||
for (const architecture of ["x86", "x86_64", "arm", "arm64"] as const) {
|
||||
const resolved = await ghidra.resolveAnalysisProfile(
|
||||
peTarget(architecture),
|
||||
);
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
if (resolved.value.profile === null)
|
||||
throw new Error("Expected a PE analysis profile");
|
||||
const parameters = resolved.value.profile.parameters;
|
||||
if (
|
||||
platform === "win32" &&
|
||||
(architecture === "arm" || architecture === "arm64")
|
||||
)
|
||||
expect(parameters).toHaveProperty(
|
||||
"windows_arm_pe_import",
|
||||
"loader-selected-language-v1",
|
||||
);
|
||||
else expect(parameters).not.toHaveProperty("windows_arm_pe_import");
|
||||
if (
|
||||
platform === "win32" &&
|
||||
(architecture === "arm" || architecture === "arm64")
|
||||
)
|
||||
expect(parameters.windows_arm_pe_machine).toBe(
|
||||
peTarget(architecture).peMachine,
|
||||
);
|
||||
else expect(parameters).not.toHaveProperty("windows_arm_pe_machine");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("commits the PE role and managed classification to profile identity", async () => {
|
||||
const ghidra = provider(installationHost("win32"));
|
||||
const application = peTarget("x86_64");
|
||||
@@ -520,12 +596,13 @@ describe("Ghidra Windows platform support", () => {
|
||||
status: "unsupported",
|
||||
code: "target_format_unsupported",
|
||||
});
|
||||
// ARM64 PE is admitted on Windows P0 (bounded; see docs/windows-ghidra-p0.md).
|
||||
expect(
|
||||
ghidra.inspectTargetSupport(executableTarget("pe", "arm64")),
|
||||
).toMatchObject({
|
||||
status: "unsupported",
|
||||
code: "architecture_unsupported",
|
||||
});
|
||||
ghidra.inspectTargetSupport(executableTarget("pe", "arm64")).status,
|
||||
).toBe("supported");
|
||||
expect(
|
||||
ghidra.inspectTargetSupport(executableTarget("pe", "arm")).status,
|
||||
).toBe("supported");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -625,12 +702,6 @@ describe("Windows Ghidra native failure diagnostics", () => {
|
||||
status: "unsupported",
|
||||
code: "target_format_unsupported",
|
||||
});
|
||||
expect(
|
||||
ghidra.inspectTargetSupport(executableTarget("pe", "arm64")),
|
||||
).toMatchObject({
|
||||
status: "unsupported",
|
||||
code: "architecture_unsupported",
|
||||
});
|
||||
const client = ghidra.createClient(nativeApplication);
|
||||
const health = await client.execute("health", {});
|
||||
if (health.ok)
|
||||
@@ -649,18 +720,34 @@ describe("Windows Ghidra native failure diagnostics", () => {
|
||||
});
|
||||
|
||||
describe("Ghidra Windows PE fixture admission", () => {
|
||||
it.each(["arm", "arm64"] as const)(
|
||||
"does not infer a COFF machine from an unobserved %s family",
|
||||
(architecture) => {
|
||||
const { peMachine, ...unobserved } = peTarget(architecture);
|
||||
expect(peMachine).toBeDefined();
|
||||
expect(
|
||||
provider(installationHost("win32")).inspectTargetSupport(unobserved),
|
||||
).toMatchObject({
|
||||
status: "unsupported",
|
||||
code: "architecture_unsupported",
|
||||
diagnostics: { pe_machine: null },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
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 ghidra = provider(installationHost("win32"));
|
||||
for (const architecture of ["x86", "x86_64"] as const) {
|
||||
for (const architecture of ["x86", "x86_64", "arm", "arm64"] as const) {
|
||||
for (const dll of [false, true]) {
|
||||
const suffix = architecture === "x86_64" ? "" : `-${architecture}`;
|
||||
const path = join(
|
||||
root,
|
||||
"build/fixtures",
|
||||
`rea-ghidra-windows${architecture === "x86" ? "-x86" : ""}.${dll ? "dll" : "exe"}`,
|
||||
`rea-ghidra-windows${suffix}.${dll ? "dll" : "exe"}`,
|
||||
);
|
||||
const bytes = await readFile(path);
|
||||
const metadata = parseExecutableHeader(bytes, "x64");
|
||||
@@ -684,13 +771,34 @@ describe("Ghidra Windows PE fixture admission", () => {
|
||||
...metadata.value,
|
||||
};
|
||||
expect(ghidra.inspectTargetSupport(target).status).toBe("supported");
|
||||
if (architecture === "arm") {
|
||||
for (const peMachine of [0x01c0, 0x01c2]) {
|
||||
const legacy = Buffer.from(bytes);
|
||||
legacy.writeUInt16LE(peMachine, legacy.readUInt32LE(0x3c) + 4);
|
||||
const parsed = parseExecutableHeader(legacy, "x64");
|
||||
if (!parsed.ok) throw new Error(parsed.error);
|
||||
if (parsed.value.format !== "pe")
|
||||
throw new Error("Expected a PE fixture");
|
||||
expect(parsed.value).toMatchObject({ architecture, peMachine });
|
||||
expect(
|
||||
ghidra.inspectTargetSupport({
|
||||
...target,
|
||||
...parsed.value,
|
||||
sha256: createHash("sha256").update(legacy).digest("hex"),
|
||||
}),
|
||||
).toMatchObject({
|
||||
status: "unsupported",
|
||||
code: "architecture_unsupported",
|
||||
diagnostics: { pe_machine: peMachine },
|
||||
});
|
||||
}
|
||||
}
|
||||
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",
|
||||
@@ -1947,6 +2055,9 @@ const peTarget = (
|
||||
sha256: "a".repeat(64),
|
||||
kind: "executable",
|
||||
format: "pe",
|
||||
peMachine: { x86: 0x014c, x86_64: 0x8664, arm: 0x01c4, arm64: 0xaa64 }[
|
||||
architecture
|
||||
],
|
||||
architecture,
|
||||
availableArchitectures: [architecture],
|
||||
executableRole: "application",
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
CAPABILITIES,
|
||||
windowsP0Capabilities,
|
||||
GHIDRA_PROVIDER_IDENTITY,
|
||||
WINDOWS_P0_NATIVE_PE_ARCHITECTURES,
|
||||
} from "./GhidraProviderCapabilities.js";
|
||||
import {
|
||||
createGhidraProviderClient,
|
||||
@@ -360,11 +361,12 @@ const inspectWindowsP0TargetSupport = (
|
||||
"Windows Ghidra P0 accepts native PE and explicitly selected raw 6502 targets only.",
|
||||
diagnostics,
|
||||
};
|
||||
if (target.architecture !== "x86" && target.architecture !== "x86_64")
|
||||
if (!WINDOWS_P0_NATIVE_PE_ARCHITECTURES.includes(target.architecture))
|
||||
return {
|
||||
status: "unsupported",
|
||||
code: "architecture_unsupported",
|
||||
reason: "Windows Ghidra P0 accepts x86 and x86-64 PE targets only.",
|
||||
reason:
|
||||
"Windows Ghidra P0 accepts x86, x86-64, ARM and ARM64 PE targets only.",
|
||||
diagnostics,
|
||||
};
|
||||
if (
|
||||
@@ -386,6 +388,17 @@ const inspectWindowsP0TargetSupport = (
|
||||
"Windows Ghidra P0 accepts native PE applications and DLLs; managed or unclassified PE targets are unsupported.",
|
||||
diagnostics,
|
||||
};
|
||||
if (
|
||||
(target.architecture === "arm" && target.peMachine !== 0x01c4) ||
|
||||
(target.architecture === "arm64" && target.peMachine !== 0xaa64)
|
||||
)
|
||||
return {
|
||||
status: "unsupported",
|
||||
code: "architecture_unsupported",
|
||||
reason:
|
||||
"Windows Ghidra P0 ARM PE targets require observed ARMNT (0x01c4) or ARM64 (0xaa64) COFF machines; legacy ARM/Thumb and unknown machines are unsupported.",
|
||||
diagnostics: { ...diagnostics, pe_machine: target.peMachine ?? null },
|
||||
};
|
||||
return {
|
||||
status: "supported",
|
||||
code: null,
|
||||
|
||||
@@ -10,6 +10,10 @@ import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionValues.js";
|
||||
import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryValues.js";
|
||||
import { GHIDRA_MUTATING_OPERATIONS } from "./GhidraSessionValues.js";
|
||||
|
||||
/** Target architectures admitted by the Windows P0 native PE boundary. */
|
||||
export const WINDOWS_P0_NATIVE_PE_ARCHITECTURES: readonly string[] =
|
||||
Object.freeze(["x86", "x86_64", "arm", "arm64"]);
|
||||
|
||||
/** Public identity committed by every Ghidra-backed observation. */
|
||||
export const GHIDRA_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
|
||||
id: "ghidra",
|
||||
@@ -32,7 +36,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 native x86 and x86-64 PE applications and DLLs, and explicitly selected raw 6502 images; managed, hostile, sensitive, and mutable-path targets are unsupported.",
|
||||
"Windows Ghidra P0 accepts native x86, x86-64, ARMNT (Thumb-2) and ARM64 PE applications and DLLs, and explicitly selected raw 6502 images; 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.",
|
||||
]);
|
||||
|
||||
@@ -31,6 +31,26 @@ import { parseExecutableHeader } from "../../../src/domain/binaryTarget.js";
|
||||
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
it.each([0x01c0, 0x01c2, 0x01c4, 0xaa64])(
|
||||
"retains original PE COFF machine %i through file-backed target resolution",
|
||||
async (peMachine) => {
|
||||
const directory = await createTestTempDirectory("rea-pe-machine-");
|
||||
const path = join(directory, "image.exe");
|
||||
const bytes = pe(peMachine === 0xaa64 ? 0xaa64 : 0x01c0);
|
||||
bytes.writeUInt16LE(peMachine, bytes.readUInt32LE(0x3c) + 4);
|
||||
await writeFile(path, bytes);
|
||||
const target = await parseBinaryTarget(path);
|
||||
expect(target).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
format: "pe",
|
||||
architecture: peMachine === 0xaa64 ? "arm64" : "arm",
|
||||
peMachine,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([{ cpus: [0x0100000c] }, { cpus: [0x01000007, 0x0100000c] }])(
|
||||
"preserves a FAT container's format in the resolved target and Evidence ($cpus)",
|
||||
async ({ cpus }) => {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { it } from "vitest";
|
||||
|
||||
import { windowsGhidraFixtureOptions } from "../../../../scripts/lib/ghidra-windows-fixture-options.mjs";
|
||||
|
||||
it("architecture selectors cannot silently choose a different fixture", () => {
|
||||
for (const arguments_ of [
|
||||
["--x86", "--arm"],
|
||||
["--x86", "--arm64"],
|
||||
["--arm", "--arm64"],
|
||||
])
|
||||
assert.throws(() => windowsGhidraFixtureOptions(arguments_));
|
||||
assert.equal(
|
||||
windowsGhidraFixtureOptions(["D:/tmp/rea/--arm"]).architecture,
|
||||
"x86_64",
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user