wowdump 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +21 -53
  3. package/dist/adapters/reader.js +33 -0
  4. package/dist/analysis/disassemble.js +77 -0
  5. package/dist/{frida-runtime.js → analysis/frida-runtime.js} +48 -48
  6. package/dist/analysis/runtime-script.js +170 -0
  7. package/dist/cli.js +337 -192
  8. package/dist/core/profile-engine.js +238 -0
  9. package/dist/frida-worker.js +54 -55
  10. package/dist/{reader-broker.js → reader/broker.js} +15 -0
  11. package/dist/{reader-client.js → reader/client.js} +1 -1
  12. package/dist/{windows-launcher.js → reader/launcher.js} +16 -4
  13. package/dist/reader/main.js +100 -0
  14. package/dist/reader/protocol.js +1 -0
  15. package/dist/reader/windows.js +242 -0
  16. package/dist/reader-main.js +1 -66
  17. package/dist/toolchain.js +102 -573
  18. package/package.json +13 -10
  19. package/skills/wowdump/SKILL.md +24 -15
  20. package/skills/wowdump/references/commands.md +77 -0
  21. package/skills/wowdump/references/disassemble.md +62 -0
  22. package/skills/wowdump/references/dynamic.md +54 -0
  23. package/skills/wowdump/references/evidence-workflow.md +41 -0
  24. package/skills/wowdump/references/profiles.md +34 -0
  25. package/skills/wowdump/references/request-schema.md +28 -0
  26. package/skills/wowdump/references/workflow.md +45 -0
  27. package/skills/wowdump/scripts/dynamic-session.js +133 -0
  28. package/dist/agent.js +0 -1332
  29. package/dist/discovery.js +0 -48
  30. package/dist/dry-run.js +0 -36
  31. package/dist/error-log.js +0 -71
  32. package/dist/focused-session.js +0 -89
  33. package/dist/ghidra.js +0 -769
  34. package/dist/main.js +0 -66
  35. package/dist/observability.js +0 -41
  36. package/dist/processes.js +0 -44
  37. package/dist/session.js +0 -42
  38. package/dist/storage.js +0 -12
  39. package/dist/windows-reader.js +0 -102
  40. package/dist/wow-analysis.js +0 -1405
  41. package/skills/wowdump/commands.md +0 -44
  42. /package/dist/{adapters.js → core/build-adapters.js} +0 -0
  43. /package/dist/{types.js → core/types.js} +0 -0
package/dist/ghidra.js DELETED
@@ -1,769 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { execFile, spawn } from "node:child_process";
3
- import { mkdir, readFile, writeFile } from "node:fs/promises";
4
- import { existsSync, readdirSync } from "node:fs";
5
- import { homedir, platform, tmpdir } from "node:os";
6
- import { basename, delimiter, dirname, extname, isAbsolute, join, normalize, resolve } from "node:path";
7
- import { promisify } from "node:util";
8
- export class GhidraError extends Error {
9
- details;
10
- constructor(message, details = {}) {
11
- super(message);
12
- this.details = details;
13
- this.name = "GhidraError";
14
- }
15
- }
16
- export class GhidraToolchainError extends GhidraError {
17
- constructor(message, details = {}) {
18
- super(message, details);
19
- this.name = "GhidraToolchainError";
20
- }
21
- }
22
- const execFileAsync = promisify(execFile);
23
- const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
24
- const DEFAULT_MAX_FUNCTIONS = 100_000;
25
- const DEFAULT_MAX_STRINGS = 100_000;
26
- const DEFAULT_MAX_XREFS = 250_000;
27
- const TOOLCHAIN_VERSION = "unknown";
28
- function stringValue(value, fallback = "") {
29
- return typeof value === "string" ? value : value == null ? fallback : String(value);
30
- }
31
- function numberValue(value) {
32
- if (typeof value === "number" && Number.isFinite(value))
33
- return value;
34
- if (typeof value === "bigint") {
35
- const number = Number(value);
36
- return Number.isSafeInteger(number) ? number : undefined;
37
- }
38
- if (typeof value === "string" && value.trim() !== "") {
39
- const number = Number(value);
40
- return Number.isFinite(number) ? number : undefined;
41
- }
42
- return undefined;
43
- }
44
- function recordValue(value) {
45
- return value && typeof value === "object" && !Array.isArray(value)
46
- ? value
47
- : {};
48
- }
49
- function arrayValue(value) {
50
- return Array.isArray(value) ? value : [];
51
- }
52
- /** Convert all addresses to strings so 64-bit values never pass through Number. */
53
- export function normalizeAddress(value) {
54
- if (value == null || value === "")
55
- return undefined;
56
- try {
57
- if (typeof value === "bigint")
58
- return `0x${value.toString(16)}`;
59
- if (typeof value === "number") {
60
- if (!Number.isSafeInteger(value) || value < 0)
61
- return undefined;
62
- return `0x${value.toString(16)}`;
63
- }
64
- const text = String(value).trim();
65
- if (!text)
66
- return undefined;
67
- const parsed = /^0x/i.test(text) ? BigInt(text) : BigInt(text);
68
- if (parsed < 0n)
69
- return undefined;
70
- return `0x${parsed.toString(16)}`;
71
- }
72
- catch {
73
- return undefined;
74
- }
75
- }
76
- function normalizedConfidence(value, fallback = "candidate") {
77
- return value === "confirmed" || value === "unverified" || value === "candidate" ? value : fallback;
78
- }
79
- function safeLimit(value, fallback, maximum) {
80
- const number = numberValue(value);
81
- if (number == null || number < 0)
82
- return fallback;
83
- return Math.min(Math.floor(number), maximum);
84
- }
85
- function firstDefined(record, keys) {
86
- for (const key of keys)
87
- if (record[key] !== undefined && record[key] !== null)
88
- return record[key];
89
- return undefined;
90
- }
91
- function normalizeSection(value) {
92
- const item = recordValue(value);
93
- const start = normalizeAddress(firstDefined(item, ["start", "address", "base", "minAddress"]));
94
- const end = normalizeAddress(firstDefined(item, ["end", "maxAddress"]));
95
- const size = numberValue(firstDefined(item, ["size", "length"]));
96
- return {
97
- name: stringValue(firstDefined(item, ["name", "block", "section"]), "unknown"),
98
- ...(start ? { start } : {}),
99
- ...(end ? { end } : {}),
100
- ...(size !== undefined ? { size } : {}),
101
- ...(stringValue(firstDefined(item, ["permissions", "permission", "flags"])) ? { permissions: stringValue(firstDefined(item, ["permissions", "permission", "flags"])) } : {}),
102
- ...(typeof item.initialized === "boolean" ? { initialized: item.initialized } : {})
103
- };
104
- }
105
- function normalizeFunction(value, source) {
106
- const item = recordValue(value);
107
- const address = normalizeAddress(firstDefined(item, ["address", "entry", "entryPoint", "start", "offset"])) ?? "0x0";
108
- const rawName = firstDefined(item, ["name", "symbol", "label"]);
109
- const name = stringValue(rawName, `sub_${address.slice(2)}`);
110
- const size = numberValue(firstDefined(item, ["size", "length", "bodySize"]));
111
- const confidence = normalizedConfidence(item.confidence);
112
- return {
113
- address,
114
- name,
115
- ...(size !== undefined ? { size } : {}),
116
- ...(stringValue(item.namespace) ? { namespace: stringValue(item.namespace) } : {}),
117
- ...(stringValue(firstDefined(item, ["signature", "prototype"])) ? { signature: stringValue(firstDefined(item, ["signature", "prototype"])) } : {}),
118
- ...(stringValue(firstDefined(item, ["callingConvention", "callConvention"])) ? { callingConvention: stringValue(firstDefined(item, ["callingConvention", "callConvention"])) } : {}),
119
- confidence,
120
- source
121
- };
122
- }
123
- function normalizeString(value, source) {
124
- const item = recordValue(value);
125
- const raw = typeof value === "string" ? value : firstDefined(item, ["value", "string", "text", "contents"]);
126
- const text = stringValue(raw);
127
- const address = typeof value === "string" ? undefined : normalizeAddress(firstDefined(item, ["address", "location", "offset"]));
128
- const length = numberValue(firstDefined(item, ["length", "byteLength"]));
129
- return {
130
- ...(address ? { address } : {}),
131
- value: text,
132
- ...(length !== undefined ? { length } : {}),
133
- ...(stringValue(firstDefined(item, ["encoding", "charset"])) ? { encoding: stringValue(firstDefined(item, ["encoding", "charset"])) } : {}),
134
- confidence: normalizedConfidence(item.confidence),
135
- source
136
- };
137
- }
138
- function normalizeXref(value, source) {
139
- const item = recordValue(value);
140
- return {
141
- ...(normalizeAddress(firstDefined(item, ["from", "source", "caller", "address"])) ? { from: normalizeAddress(firstDefined(item, ["from", "source", "caller", "address"])) } : {}),
142
- ...(normalizeAddress(firstDefined(item, ["to", "target", "callee"])) ? { to: normalizeAddress(firstDefined(item, ["to", "target", "callee"])) } : {}),
143
- ...(stringValue(firstDefined(item, ["type", "kind"])) ? { type: stringValue(firstDefined(item, ["type", "kind"])) } : {}),
144
- ...(stringValue(firstDefined(item, ["function", "functionName"])) ? { function: stringValue(firstDefined(item, ["function", "functionName"])) } : {}),
145
- confidence: normalizedConfidence(item.confidence),
146
- source
147
- };
148
- }
149
- function deriveRva(address, imageBase) {
150
- if (!address || !imageBase)
151
- return undefined;
152
- try {
153
- const value = BigInt(address);
154
- const base = BigInt(imageBase);
155
- if (value < base)
156
- return undefined;
157
- return `0x${(value - base).toString(16)}`;
158
- }
159
- catch {
160
- return undefined;
161
- }
162
- }
163
- function normalizeMatch(value, source) {
164
- const item = recordValue(value);
165
- const address = normalizeAddress(firstDefined(item, ["address", "entry", "location"]));
166
- const rva = normalizeAddress(firstDefined(item, ["rva", "relativeVirtualAddress"]));
167
- const details = recordValue(item.details ?? item.metadata);
168
- return {
169
- ...(stringValue(firstDefined(item, ["name", "id", "label"])) ? { name: stringValue(firstDefined(item, ["name", "id", "label"])) } : {}),
170
- ...(address ? { address } : {}),
171
- ...(rva ? { rva } : {}),
172
- ...(stringValue(firstDefined(item, ["pattern", "signature", "bytes"])) ? { pattern: stringValue(firstDefined(item, ["pattern", "signature", "bytes"])) } : {}),
173
- ...(stringValue(item.provider) ? { provider: stringValue(item.provider) } : {}),
174
- ...(Object.keys(details).length ? { details } : {}),
175
- confidence: normalizedConfidence(item.confidence),
176
- source
177
- };
178
- }
179
- function findArray(raw, keys) {
180
- for (const key of keys) {
181
- const value = raw[key];
182
- if (Array.isArray(value))
183
- return value;
184
- }
185
- return [];
186
- }
187
- /**
188
- * Normalize output from the repository Ghidra script or a fixture exporter.
189
- * Unknown fields are ignored intentionally; the profile contract stays stable across Ghidra releases.
190
- */
191
- export function normalizeGhidraExport(raw, context) {
192
- const source = context.source ?? "ghidra";
193
- const record = recordValue(raw);
194
- const moduleRecord = recordValue(record.module ?? record.binary ?? record.program);
195
- const sections = findArray(moduleRecord, ["sections", "memoryBlocks", "blocks"]);
196
- const startedAt = context.startedAt ?? new Date().toISOString();
197
- const finishedAt = context.finishedAt ?? new Date().toISOString();
198
- const startedMs = Date.parse(startedAt);
199
- const finishedMs = Date.parse(finishedAt);
200
- const durationMs = Number.isFinite(startedMs) && Number.isFinite(finishedMs) ? Math.max(0, finishedMs - startedMs) : 0;
201
- const moduleName = stringValue(firstDefined(moduleRecord, ["name", "module", "programName"]), basename(context.exe));
202
- const module = {
203
- name: moduleName,
204
- ...(normalizeAddress(firstDefined(moduleRecord, ["imageBase", "base", "address"])) ? { imageBase: normalizeAddress(firstDefined(moduleRecord, ["imageBase", "base", "address"])) } : {}),
205
- ...(numberValue(firstDefined(moduleRecord, ["size", "imageSize"])) !== undefined ? { size: numberValue(firstDefined(moduleRecord, ["size", "imageSize"])) } : {}),
206
- sections: sections.map(normalizeSection)
207
- };
208
- const imageBase = module.imageBase;
209
- const functions = findArray(record, ["functions", "functionRecords"]).map(value => {
210
- const item = normalizeFunction(value, source);
211
- return { ...item, ...(item.rva ? {} : { rva: deriveRva(item.address, imageBase) }) };
212
- });
213
- const strings = findArray(record, ["strings", "stringRecords"]).map(value => {
214
- const item = normalizeString(value, source);
215
- return { ...item, ...(item.rva ? {} : { rva: deriveRva(item.address, imageBase) }) };
216
- });
217
- const xrefs = findArray(record, ["xrefs", "crossReferences", "references"]).map(value => {
218
- const item = normalizeXref(value, source);
219
- return {
220
- ...item,
221
- ...(item.fromRva ? {} : { fromRva: deriveRva(item.from, imageBase) }),
222
- ...(item.toRva ? {} : { toRva: deriveRva(item.to, imageBase) })
223
- };
224
- });
225
- const providers = findArray(record, ["providers", "providerMatches", "providerEvidence"]).map(value => {
226
- const item = normalizeMatch(value, source);
227
- return { ...item, ...(item.rva ? {} : { rva: deriveRva(item.address, imageBase) }) };
228
- });
229
- const signatures = findArray(record, ["signatures", "signatureMatches", "patternMatches"]).map(value => {
230
- const item = normalizeMatch(value, source);
231
- return { ...item, ...(item.rva ? {} : { rva: deriveRva(item.address, imageBase) }) };
232
- });
233
- const evidence = Array.isArray(record.evidence)
234
- ? record.evidence.filter(item => item && typeof item === "object").map(item => ({ ...recordValue(item), source: recordValue(item).source ?? source }))
235
- : [];
236
- const confidence = normalizedConfidence(record.confidence, "candidate");
237
- const sha256 = context.executableSha256 ?? stringValue(record.executableSha256 ?? record.sha256, "unknown");
238
- const executableSize = context.executableSize ?? numberValue(record.executableSize) ?? 0;
239
- const analysisVersion = recordValue(record.analysisVersion);
240
- const analysis = {
241
- tool: "ghidra-headless",
242
- ghidraVersion: context.toolchain?.ghidraVersion ?? stringValue(analysisVersion.ghidraVersion ?? record.ghidraVersion, TOOLCHAIN_VERSION),
243
- javaVersion: context.toolchain?.javaVersion ?? stringValue(analysisVersion.javaVersion ?? record.javaVersion, TOOLCHAIN_VERSION),
244
- command: [...(context.command ?? [])],
245
- startedAt,
246
- finishedAt,
247
- durationMs
248
- };
249
- const profile = {
250
- schema: "wowdump.profile.v1",
251
- buildKey: context.buildKey,
252
- executableSha256: sha256,
253
- module,
254
- functions,
255
- strings,
256
- xrefs,
257
- providers,
258
- signatures,
259
- confidence,
260
- evidence
261
- };
262
- return {
263
- schema: "wowdump.ghidra-evidence.v1",
264
- buildKey: context.buildKey,
265
- executable: { path: resolve(context.exe), sha256, size: executableSize },
266
- module,
267
- functions,
268
- strings,
269
- xrefs,
270
- providers,
271
- signatures,
272
- analysis,
273
- confidence,
274
- evidence,
275
- profile
276
- };
277
- }
278
- function quoteWindowsArg(value) {
279
- if (!/[\s"&()\[\]{}^=;!,+'`~]/.test(value))
280
- return value;
281
- return `"${value.replace(/(\\*)"/g, "$1$1\\\"").replace(/(\\+)$/g, "$1$1")}"`;
282
- }
283
- function isBatch(file) {
284
- const extension = extname(file).toLowerCase();
285
- return extension === ".bat" || extension === ".cmd";
286
- }
287
- function commandInvocation(executable, args) {
288
- if (platform() !== "win32" || !isBatch(executable))
289
- return { file: executable, args: [...args] };
290
- const commandLine = [quoteWindowsArg(executable), ...args.map(quoteWindowsArg)].join(" ");
291
- return { file: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", commandLine] };
292
- }
293
- /** Expose the platform launcher shape for tests and diagnostics without starting a process. */
294
- export function buildGhidraProcessInvocation(executable, args, platformName = platform()) {
295
- const invocation = platformName === "win32" && isBatch(executable)
296
- ? { file: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", "call", executable, ...args] }
297
- : { file: executable, args: [...args] };
298
- return { ...invocation, shell: false };
299
- }
300
- /** Standard-library process runner used by detection and analysis. */
301
- export const defaultGhidraCommandRunner = async (executable, args, options) => {
302
- const invocation = buildGhidraProcessInvocation(executable, args);
303
- return await new Promise((resolveResult, reject) => {
304
- const child = spawn(invocation.file, invocation.args, {
305
- cwd: options.cwd,
306
- env: options.env,
307
- windowsHide: true,
308
- stdio: ["ignore", "pipe", "pipe"]
309
- });
310
- const stdout = [];
311
- const stderr = [];
312
- let settled = false;
313
- let timedOut = false;
314
- const timer = options.timeoutMs && options.timeoutMs > 0
315
- ? setTimeout(() => {
316
- timedOut = true;
317
- child.kill();
318
- }, options.timeoutMs)
319
- : undefined;
320
- child.stdout.on("data", chunk => stdout.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
321
- child.stderr.on("data", chunk => stderr.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
322
- child.once("error", error => {
323
- if (timer)
324
- clearTimeout(timer);
325
- if (settled)
326
- return;
327
- settled = true;
328
- reject(error);
329
- });
330
- child.once("close", (code, signal) => {
331
- if (timer)
332
- clearTimeout(timer);
333
- if (settled)
334
- return;
335
- settled = true;
336
- resolveResult({
337
- code: code ?? (timedOut ? 124 : 1),
338
- ...(signal ? { signal } : {}),
339
- stdout: Buffer.concat(stdout).toString("utf8"),
340
- stderr: Buffer.concat(stderr).toString("utf8"),
341
- ...(timedOut ? { timedOut: true } : {})
342
- });
343
- });
344
- });
345
- };
346
- function commandExists(command, env) {
347
- if (isAbsolute(command))
348
- return existsSync(command);
349
- const path = env.PATH ?? "";
350
- const extensions = platform() === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
351
- return path.split(delimiter).some(directory => extensions.some(extension => existsSync(join(directory, command + extension))));
352
- }
353
- function findOnPath(command, env) {
354
- if (isAbsolute(command))
355
- return existsSync(command) ? command : undefined;
356
- const path = env.PATH ?? "";
357
- const extensions = platform() === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
358
- for (const directory of path.split(delimiter)) {
359
- for (const extension of extensions) {
360
- const candidate = join(directory, command + extension);
361
- if (existsSync(candidate))
362
- return candidate;
363
- }
364
- }
365
- return undefined;
366
- }
367
- function uniqueExisting(paths) {
368
- const seen = new Set();
369
- const result = [];
370
- for (const value of paths) {
371
- if (!value)
372
- continue;
373
- const normalized = normalize(resolve(value));
374
- const key = platform() === "win32" ? normalized.toLowerCase() : normalized;
375
- if (seen.has(key))
376
- continue;
377
- seen.add(key);
378
- if (existsSync(normalized))
379
- result.push(normalized);
380
- }
381
- return result;
382
- }
383
- function discoverGhidraHomes(root) {
384
- const candidates = [];
385
- if (!existsSync(root))
386
- return candidates;
387
- // Managed installs commonly use either ghidra-<version> or ghidra/current.
388
- for (const direct of [join(root, "ghidra"), join(root, "ghidra-current"), join(root, "ghidra", "current")]) {
389
- if (existsSync(direct))
390
- candidates.push(direct);
391
- }
392
- try {
393
- for (const entry of readdirSync(root, { withFileTypes: true })) {
394
- if (!entry.isDirectory() || !/^ghidra([_-]|$)/i.test(entry.name))
395
- continue;
396
- candidates.push(join(root, entry.name));
397
- }
398
- }
399
- catch {
400
- // A protected installation directory is simply skipped; the diagnostic lists it.
401
- }
402
- return candidates;
403
- }
404
- function discoverJavaHomes(root) {
405
- if (!existsSync(root))
406
- return [];
407
- const candidates = [];
408
- for (const direct of [join(root, "jdk"), join(root, "jdk", "current"), join(root, "temurin"), join(root, "java")]) {
409
- if (javaForHome(direct))
410
- candidates.push(direct);
411
- }
412
- try {
413
- for (const entry of readdirSync(root, { withFileTypes: true })) {
414
- if (!entry.isDirectory() || !/(jdk|java|temurin)/i.test(entry.name))
415
- continue;
416
- const candidate = join(root, entry.name);
417
- if (javaForHome(candidate))
418
- candidates.push(candidate);
419
- }
420
- }
421
- catch {
422
- // Keep the managed root in the diagnostic even when enumeration is denied.
423
- }
424
- return candidates;
425
- }
426
- function analyzeHeadlessForHome(home) {
427
- const names = platform() === "win32" ? ["analyzeHeadless.bat", "analyzeHeadless.cmd", "analyzeHeadless"] : ["analyzeHeadless"];
428
- for (const relative of [join("support", names[0]), join("support", names[1] ?? names[0]), join("support", names[2] ?? names[0]), names[0]]) {
429
- const candidate = join(home, relative);
430
- if (existsSync(candidate))
431
- return candidate;
432
- }
433
- return undefined;
434
- }
435
- function javaForHome(javaHome) {
436
- const names = platform() === "win32" ? ["java.exe", "java"] : ["java"];
437
- for (const name of names) {
438
- const candidate = join(javaHome, "bin", name);
439
- if (existsSync(candidate))
440
- return candidate;
441
- }
442
- return undefined;
443
- }
444
- function parseVersion(output) {
445
- const line = output.split(/\r?\n/).find(value => /version\s+["']?[^"'\s]+/i.test(value));
446
- if (!line)
447
- return TOOLCHAIN_VERSION;
448
- const match = line.match(/version\s+["']?([^"'\s]+)/i);
449
- return match?.[1] ?? line.trim().slice(0, 120);
450
- }
451
- async function probeVersion(command, runner, env) {
452
- try {
453
- const result = await runner(command, ["-version"], { env, timeoutMs: 15_000 });
454
- return parseVersion(`${result.stdout}\n${result.stderr}`);
455
- }
456
- catch {
457
- return TOOLCHAIN_VERSION;
458
- }
459
- }
460
- function explicitHome(options, env) {
461
- return options.ghidraHome ?? env.WOWDUMP_GHIDRA_HOME ?? env.WOWDUMP_GHIDRA;
462
- }
463
- /** Locate Java and Ghidra without downloading or modifying the user's installation. */
464
- export async function detectGhidraToolchain(options = {}) {
465
- const env = options.env ?? process.env;
466
- const home = options.home ?? env.WOWDUMP_HOME ?? join(options.home ?? homedir(), ".wowdump");
467
- const runner = options.commandRunner ?? defaultGhidraCommandRunner;
468
- if (options.mock) {
469
- const mock = options.mock;
470
- const ghidraHome = resolve(mock.ghidraHome ?? options.ghidraHome ?? join(home, "toolchains", "ghidra-mock"));
471
- const analyzeHeadless = mock.analyzeHeadless ?? options.analyzeHeadless ?? join(ghidraHome, "support", platform() === "win32" ? "analyzeHeadless.bat" : "analyzeHeadless");
472
- const javaPath = mock.javaPath ?? options.javaPath ?? join(ghidraHome, "jdk", "bin", platform() === "win32" ? "java.exe" : "java");
473
- return {
474
- ghidraHome,
475
- analyzeHeadless,
476
- javaPath,
477
- ghidraVersion: mock.ghidraVersion ?? "mock",
478
- javaVersion: mock.javaVersion ?? "mock",
479
- source: "mock",
480
- searched: [ghidraHome, javaPath]
481
- };
482
- }
483
- const searched = [];
484
- const explicitGhidra = explicitHome(options, env);
485
- const explicitAnalyze = options.analyzeHeadless ?? env.WOWDUMP_ANALYZE_HEADLESS;
486
- const ghidraHomes = [];
487
- if (explicitAnalyze) {
488
- searched.push(explicitAnalyze);
489
- if (existsSync(explicitAnalyze))
490
- ghidraHomes.push({ home: dirname(dirname(explicitAnalyze)), source: "explicit" });
491
- }
492
- if (explicitGhidra) {
493
- searched.push(explicitGhidra);
494
- ghidraHomes.push({ home: explicitGhidra, source: "explicit" });
495
- }
496
- const managedRoot = join(home, "toolchains");
497
- for (const candidate of discoverGhidraHomes(managedRoot))
498
- ghidraHomes.push({ home: candidate, source: "managed" });
499
- const programRoots = platform() === "win32"
500
- ? [env.ProgramFiles, env["ProgramFiles(x86)"], env.LOCALAPPDATA, env.APPDATA].filter((value) => Boolean(value)).map(value => join(value, "Ghidra"))
501
- : ["/opt", "/usr/local", join(options.home ?? homedir(), ".local", "opt")];
502
- for (const root of programRoots) {
503
- searched.push(root);
504
- for (const candidate of discoverGhidraHomes(root))
505
- ghidraHomes.push({ home: candidate, source: "system" });
506
- if (analyzeHeadlessForHome(root))
507
- ghidraHomes.push({ home: root, source: "system" });
508
- }
509
- const pathAnalyze = explicitAnalyze && !isAbsolute(explicitAnalyze) ? findOnPath(explicitAnalyze, env) : findOnPath("analyzeHeadless", env);
510
- if (pathAnalyze)
511
- ghidraHomes.push({ home: dirname(dirname(pathAnalyze)), source: "environment" });
512
- const javaHome = options.javaHome ?? env.WOWDUMP_JAVA_HOME ?? env.JAVA_HOME;
513
- const managedJavaHomes = discoverJavaHomes(managedRoot);
514
- const javaPath = options.javaPath ?? env.WOWDUMP_JAVA ?? (javaHome ? javaForHome(javaHome) : undefined)
515
- ?? managedJavaHomes.map(javaForHome).find((value) => Boolean(value))
516
- ?? findOnPath("java", env);
517
- if (javaHome)
518
- searched.push(javaHome);
519
- if (javaPath)
520
- searched.push(javaPath);
521
- const uniqueHomes = uniqueExisting(ghidraHomes.map(item => item.home));
522
- for (const candidate of uniqueHomes) {
523
- const match = ghidraHomes.find(item => normalize(item.home).toLowerCase() === normalize(candidate).toLowerCase());
524
- const analyze = (match?.home === dirname(dirname(explicitAnalyze ?? "")) && explicitAnalyze && existsSync(explicitAnalyze))
525
- ? explicitAnalyze
526
- : analyzeHeadlessForHome(candidate);
527
- if (!analyze) {
528
- searched.push(join(candidate, "support", "analyzeHeadless"));
529
- continue;
530
- }
531
- if (!javaPath || (!isAbsolute(javaPath) && !commandExists(javaPath, env))) {
532
- continue;
533
- }
534
- const actualJava = isAbsolute(javaPath) ? javaPath : findOnPath(javaPath, env) ?? javaPath;
535
- const probe = options.probeVersions === false ? false : true;
536
- const [ghidraVersion, javaVersion] = probe
537
- ? await Promise.all([probeVersion(analyze, runner, env), probeVersion(actualJava, runner, env)])
538
- : [TOOLCHAIN_VERSION, TOOLCHAIN_VERSION];
539
- return {
540
- ghidraHome: candidate,
541
- analyzeHeadless: analyze,
542
- javaPath: actualJava,
543
- ghidraVersion,
544
- javaVersion,
545
- source: match?.source ?? "system",
546
- searched
547
- };
548
- }
549
- throw new GhidraToolchainError("Ghidra Headless toolchain was not found", { searched, hints: ["set WOWDUMP_GHIDRA_HOME and JAVA_HOME", "install Ghidra and a supported JDK", "run npm install to populate WOWDUMP_HOME/toolchains"] });
550
- }
551
- function safeProjectName(buildKey) {
552
- const value = buildKey.replace(/[^A-Za-z0-9_.-]+/g, "_").replace(/^\.+/, "");
553
- return value || "wowdump";
554
- }
555
- function shellCommand(command) {
556
- return [command.executable, ...command.args];
557
- }
558
- /** Build an argv-safe Ghidra Headless invocation. */
559
- export function buildGhidraHeadlessCommand(toolchain, options) {
560
- const projectDir = resolve(options.projectDir ?? join(tmpdir(), "wowdump-ghidra", safeProjectName(options.buildKey)));
561
- const projectName = options.projectName ?? safeProjectName(options.buildKey);
562
- const scriptPath = resolve(options.scriptPath ?? join(process.cwd(), "ghidra"));
563
- const scriptName = options.scriptName ?? "WowdumpExport.java";
564
- const output = resolve(options.output ?? join(projectDir, `${projectName}.raw.json`));
565
- const manifestPath = resolve(options.manifestPath ?? join(projectDir, `${projectName}.manifest.json`));
566
- const args = [
567
- projectDir,
568
- projectName,
569
- "-import",
570
- options.exe,
571
- "-overwrite",
572
- "-scriptPath",
573
- scriptPath,
574
- "-postScript",
575
- scriptName,
576
- manifestPath
577
- ];
578
- // Keep these values in argv too: this makes logs self-contained and helps fixture launchers.
579
- args.push("-analysisTimeoutPerFile", "0");
580
- return {
581
- executable: toolchain.analyzeHeadless,
582
- args,
583
- cwd: projectDir,
584
- env: { ...process.env, JAVA_HOME: dirname(dirname(toolchain.javaPath)), WOWDUMP_BUILD_KEY: options.buildKey, WOWDUMP_OUTPUT: output }
585
- };
586
- }
587
- function extractJson(stdout) {
588
- const trimmed = stdout.trim();
589
- if (!trimmed)
590
- return undefined;
591
- try {
592
- return JSON.parse(trimmed);
593
- }
594
- catch { /* continue with line/object extraction */ }
595
- const lines = trimmed.split(/\r?\n/).reverse();
596
- for (const line of lines) {
597
- try {
598
- return JSON.parse(line);
599
- }
600
- catch { /* ignore diagnostic lines */ }
601
- }
602
- const first = trimmed.indexOf("{");
603
- const last = trimmed.lastIndexOf("}");
604
- if (first >= 0 && last > first) {
605
- try {
606
- return JSON.parse(trimmed.slice(first, last + 1));
607
- }
608
- catch {
609
- return undefined;
610
- }
611
- }
612
- return undefined;
613
- }
614
- async function sha256File(file) {
615
- const handle = await import("node:fs/promises").then(fs => fs.open(file, "r"));
616
- const hash = createHash("sha256");
617
- let size = 0;
618
- try {
619
- for await (const chunk of handle.createReadStream()) {
620
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
621
- hash.update(buffer);
622
- size += buffer.length;
623
- }
624
- }
625
- finally {
626
- await handle.close();
627
- }
628
- return { sha256: hash.digest("hex"), size };
629
- }
630
- async function readJsonFile(file) {
631
- try {
632
- return JSON.parse(await readFile(file, "utf8"));
633
- }
634
- catch (error) {
635
- throw new GhidraError(`Ghidra exporter did not produce valid JSON: ${file}`, { file, cause: String(error) });
636
- }
637
- }
638
- /** Run Ghidra Headless and write a normalized, build-specific evidence/profile JSON file. */
639
- export async function runGhidraStaticAnalysis(options) {
640
- if (!options.exe)
641
- throw new GhidraError("an executable path is required");
642
- if (!options.buildKey)
643
- throw new GhidraError("a build key is required");
644
- const exe = resolve(options.exe);
645
- let executableInfo;
646
- try {
647
- executableInfo = await sha256File(exe);
648
- }
649
- catch (error) {
650
- throw new GhidraError(`cannot read executable: ${exe}`, { exe, cause: String(error) });
651
- }
652
- const output = resolve(options.output ?? join(options.projectDir ?? join(tmpdir(), "wowdump-ghidra", safeProjectName(options.buildKey)), `static-${safeProjectName(options.buildKey)}.json`));
653
- const projectDir = resolve(options.projectDir ?? join(tmpdir(), "wowdump-ghidra", safeProjectName(options.buildKey)));
654
- await mkdir(projectDir, { recursive: true });
655
- await mkdir(dirname(output), { recursive: true });
656
- const startedAt = new Date().toISOString();
657
- let raw;
658
- let toolchain;
659
- let command;
660
- if (options.mock !== undefined) {
661
- toolchain = {
662
- ghidraHome: join(projectDir, "mock-ghidra"),
663
- analyzeHeadless: "mock-analyzeHeadless",
664
- javaPath: "mock-java",
665
- ghidraVersion: "mock",
666
- javaVersion: "mock",
667
- source: "mock",
668
- searched: []
669
- };
670
- if (typeof options.mock === "string")
671
- raw = await readJsonFile(resolve(options.mock));
672
- else
673
- raw = options.mock;
674
- }
675
- else {
676
- toolchain = options.toolchain ?? await detectGhidraToolchain({
677
- ghidraHome: options.ghidraHome,
678
- analyzeHeadless: options.analyzeHeadless,
679
- javaHome: options.javaHome,
680
- javaPath: options.javaPath,
681
- env: options.env,
682
- home: options.home,
683
- probeVersions: options.probeVersions,
684
- commandRunner: options.commandRunner
685
- });
686
- const manifestPath = join(projectDir, `${safeProjectName(options.buildKey)}.manifest.json`);
687
- const manifest = {
688
- schema: "wowdump.ghidra-manifest.v1",
689
- buildKey: options.buildKey,
690
- executable: exe,
691
- output,
692
- limits: {
693
- functions: safeLimit(options.maxFunctions, DEFAULT_MAX_FUNCTIONS, DEFAULT_MAX_FUNCTIONS),
694
- strings: safeLimit(options.maxStrings, DEFAULT_MAX_STRINGS, DEFAULT_MAX_STRINGS),
695
- xrefs: safeLimit(options.maxXrefs, DEFAULT_MAX_XREFS, DEFAULT_MAX_XREFS)
696
- }
697
- };
698
- await writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
699
- command = buildGhidraHeadlessCommand(toolchain, {
700
- exe,
701
- buildKey: options.buildKey,
702
- projectDir,
703
- projectName: options.projectName,
704
- scriptPath: options.scriptPath,
705
- scriptName: options.scriptName,
706
- output,
707
- maxFunctions: options.maxFunctions,
708
- maxStrings: options.maxStrings,
709
- maxXrefs: options.maxXrefs,
710
- manifestPath
711
- });
712
- const runner = options.commandRunner ?? defaultGhidraCommandRunner;
713
- let result;
714
- try {
715
- result = await runner(command.executable, command.args, { cwd: command.cwd, env: command.env, timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS });
716
- }
717
- catch (error) {
718
- throw new GhidraError("failed to start Ghidra Headless", { command: shellCommand(command), cause: String(error) });
719
- }
720
- if (result.code !== 0) {
721
- throw new GhidraError("Ghidra Headless 启动失败", {
722
- command: shellCommand(command),
723
- exitCode: result.code,
724
- signal: result.signal,
725
- timedOut: result.timedOut ?? false,
726
- stdout: result.stdout.slice(-8_000),
727
- stderr: result.stderr.slice(-8_000)
728
- });
729
- }
730
- if (existsSync(output))
731
- raw = await readJsonFile(output);
732
- else
733
- raw = extractJson(result.stdout);
734
- if (raw === undefined) {
735
- throw new GhidraError("Ghidra Headless completed without an exporter JSON file", {
736
- command: shellCommand(command),
737
- output,
738
- stdout: result.stdout.slice(-8_000),
739
- stderr: result.stderr.slice(-8_000)
740
- });
741
- }
742
- if (!options.keepManifest) {
743
- try {
744
- await import("node:fs/promises").then(fs => fs.unlink(manifestPath));
745
- }
746
- catch { /* retain if locked */ }
747
- }
748
- }
749
- const finishedAt = new Date().toISOString();
750
- const evidence = normalizeGhidraExport(raw, {
751
- exe,
752
- buildKey: options.buildKey,
753
- executableSha256: executableInfo.sha256,
754
- executableSize: executableInfo.size,
755
- toolchain,
756
- command: command ? shellCommand(command) : [toolchain.analyzeHeadless, "<mock>"],
757
- startedAt,
758
- finishedAt,
759
- source: options.mock === undefined ? "ghidra" : "fixture"
760
- });
761
- await writeFile(output, JSON.stringify(evidence, null, 2) + "\n", "utf8");
762
- return evidence;
763
- }
764
- export default {
765
- detectGhidraToolchain,
766
- buildGhidraHeadlessCommand,
767
- normalizeGhidraExport,
768
- runGhidraStaticAnalysis
769
- };