wowdump 0.2.1 → 0.3.1

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 (47) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +56 -118
  3. package/dist/agent.js +5 -8
  4. package/dist/cli.js +497 -0
  5. package/dist/discovery.js +1 -12
  6. package/dist/dry-run.js +0 -2
  7. package/dist/focused-session.js +61 -1329
  8. package/dist/frida-runtime.js +51 -73
  9. package/dist/frida-worker.js +100 -0
  10. package/dist/ghidra.js +769 -0
  11. package/dist/main.js +66 -0
  12. package/dist/processes.js +2 -5
  13. package/dist/reader-broker.js +460 -0
  14. package/dist/reader-client.js +1 -0
  15. package/dist/reader-main.js +67 -0
  16. package/dist/session.js +17 -120
  17. package/dist/toolchain.js +594 -0
  18. package/dist/windows-launcher.js +207 -0
  19. package/dist/windows-reader.js +102 -0
  20. package/dist/wow-analysis.js +211 -236
  21. package/package.json +18 -35
  22. package/skills/wowdump/SKILL.md +15 -0
  23. package/skills/wowdump/commands.md +44 -0
  24. package/dist/analysis-path.js +0 -38
  25. package/dist/analysis-process-log.js +0 -146
  26. package/dist/broker-client.js +0 -411
  27. package/dist/broker-codec.js +0 -148
  28. package/dist/broker-core.js +0 -1045
  29. package/dist/broker-gateway.js +0 -447
  30. package/dist/broker-ledger.js +0 -196
  31. package/dist/broker-main.js +0 -291
  32. package/dist/broker-protocol.js +0 -119
  33. package/dist/broker-runtime.js +0 -1283
  34. package/dist/broker-server.js +0 -466
  35. package/dist/build-bundle-loader.js +0 -183
  36. package/dist/build-bundle.js +0 -11
  37. package/dist/focus-errors.js +0 -63
  38. package/dist/focus-service.js +0 -1855
  39. package/dist/mcp-main.js +0 -51
  40. package/dist/mcp.js +0 -924
  41. package/dist/process-log-lock.js +0 -195
  42. package/dist/runtime-config.js +0 -399
  43. package/resources/builds/retail/12.0.7.68974/build-profile.json +0 -290
  44. package/resources/builds/retail/12.0.7.68974/data-sources.json +0 -1633
  45. package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +0 -5130
  46. package/resources/builds/retail/12.0.7.68974/manifest.json +0 -63
  47. package/resources/builds/retail/12.0.7.68974/signatures.json +0 -260
@@ -1,16 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { appendFile, mkdir } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- export const BROKER_MANAGED_COMMAND = Symbol("wowdump.broker-managed-command");
5
- export function brokerManagedCommand(request, purpose) {
6
- Object.defineProperty(request, BROKER_MANAGED_COMMAND, {
7
- value: { purpose, irreversibleReason: "Broker-managed observation instrumentation is cleaned through the owned resource graph." },
8
- enumerable: false,
9
- configurable: false,
10
- writable: false
11
- });
12
- return request;
13
- }
14
4
  const DEFAULT_MAX_SOURCE_BYTES = 4 * 1024 * 1024;
15
5
  const DEFAULT_MAX_MEMORY_BYTES = 16 * 1024 * 1024;
16
6
  const DEFAULT_SCRIPT_TIMEOUT_MS = 150;
@@ -19,54 +9,54 @@ const DEFAULT_SCRIPT_TIMEOUT_MS = 150;
19
9
  * `script_load` still accept native GumJS, so a new WoW build can use any
20
10
  * Frida API without requiring a host release for every new command.
21
11
  */
22
- const BRIDGE_SOURCE = String.raw `
23
- function __wowBytes(value) {
24
- return value == null ? [] : Array.from(new Uint8Array(value));
25
- }
26
- function __wowModule(value) {
27
- return { name: value.name, base: value.base.toString(), size: value.size, path: value.path };
28
- }
29
- function __wowExport(value) {
30
- return { type: value.type, name: value.name, address: value.address.toString(), moduleName: value.moduleName };
31
- }
32
- function __wowRange(value) {
33
- return { base: value.base.toString(), size: value.size, protection: value.protection, file: value.file ?? null };
34
- }
35
- rpc.exports = {
36
- enumerateModules() {
37
- return Process.enumerateModules().map(__wowModule);
38
- },
39
- enumerateExports(moduleName) {
40
- const module = Process.getModuleByName(moduleName);
41
- return module.enumerateExports().map(__wowExport);
42
- },
43
- enumerateRanges(protection, coalesce) {
44
- try {
45
- return Process.enumerateRanges({ protection: protection || "r--", coalesce: Boolean(coalesce) }).map(__wowRange);
46
- } catch (_) {
47
- return Process.enumerateRanges(protection || "r--").map(__wowRange);
48
- }
49
- },
50
- readMemory(address, size) {
51
- return __wowBytes(ptr(address).readByteArray(size));
52
- },
53
- writeMemory(address, bytes) {
54
- const target = ptr(address);
55
- const input = Uint8Array.from(bytes || []);
56
- const before = __wowBytes(target.readByteArray(input.byteLength));
57
- target.writeByteArray(input.buffer);
58
- return { before, after: Array.from(input), address: target.toString(), size: input.byteLength };
59
- },
60
- protectMemory(address, size, protection) {
61
- const changed = Memory.protect(ptr(address), size, protection);
62
- return { address: ptr(address).toString(), size, protection, changed };
63
- },
64
- scanMemory(address, size, pattern) {
65
- return Memory.scanSync(ptr(address), size, pattern).map(match => ({
66
- address: match.address.toString(), size: match.size
67
- }));
68
- }
69
- };
12
+ const BRIDGE_SOURCE = String.raw `
13
+ function __wowBytes(value) {
14
+ return value == null ? [] : Array.from(new Uint8Array(value));
15
+ }
16
+ function __wowModule(value) {
17
+ return { name: value.name, base: value.base.toString(), size: value.size, path: value.path };
18
+ }
19
+ function __wowExport(value) {
20
+ return { type: value.type, name: value.name, address: value.address.toString(), moduleName: value.moduleName };
21
+ }
22
+ function __wowRange(value) {
23
+ return { base: value.base.toString(), size: value.size, protection: value.protection, file: value.file ?? null };
24
+ }
25
+ rpc.exports = {
26
+ enumerateModules() {
27
+ return Process.enumerateModules().map(__wowModule);
28
+ },
29
+ enumerateExports(moduleName) {
30
+ const module = Process.getModuleByName(moduleName);
31
+ return module.enumerateExports().map(__wowExport);
32
+ },
33
+ enumerateRanges(protection, coalesce) {
34
+ try {
35
+ return Process.enumerateRanges({ protection: protection || "r--", coalesce: Boolean(coalesce) }).map(__wowRange);
36
+ } catch (_) {
37
+ return Process.enumerateRanges(protection || "r--").map(__wowRange);
38
+ }
39
+ },
40
+ readMemory(address, size) {
41
+ return __wowBytes(ptr(address).readByteArray(size));
42
+ },
43
+ writeMemory(address, bytes) {
44
+ const target = ptr(address);
45
+ const input = Uint8Array.from(bytes || []);
46
+ const before = __wowBytes(target.readByteArray(input.byteLength));
47
+ target.writeByteArray(input.buffer);
48
+ return { before, after: Array.from(input), address: target.toString(), size: input.byteLength };
49
+ },
50
+ protectMemory(address, size, protection) {
51
+ const changed = Memory.protect(ptr(address), size, protection);
52
+ return { address: ptr(address).toString(), size, protection, changed };
53
+ },
54
+ scanMemory(address, size, pattern) {
55
+ return Memory.scanSync(ptr(address), size, pattern).map(match => ({
56
+ address: match.address.toString(), size: match.size
57
+ }));
58
+ }
59
+ };
70
60
  `;
71
61
  function asRecord(value) {
72
62
  return value && typeof value === "object" ? value : {};
@@ -196,9 +186,6 @@ function buildPrelude(context) {
196
186
  buildKey: context?.buildKey,
197
187
  flavor: context?.flavor,
198
188
  executable: context?.executable,
199
- processStartTime: context?.processStartTime,
200
- moduleBase: context?.moduleBase,
201
- moduleIdentity: context?.moduleIdentity,
202
189
  adapter: context?.adapter
203
190
  });
204
191
  const verifiedPrelude = context?.adapter?.adapter?.fridaPrelude ?? "";
@@ -213,10 +200,7 @@ function requestContext(request, context, existing) {
213
200
  ...(request.buildKey === undefined ? {} : { buildKey: request.buildKey }),
214
201
  ...(request.flavor === undefined ? {} : { flavor: request.flavor }),
215
202
  ...(request.adapter === undefined ? {} : { adapter: request.adapter }),
216
- ...(request.executable === undefined ? {} : { executable: request.executable }),
217
- ...(request.processStartTime === undefined ? {} : { processStartTime: request.processStartTime }),
218
- ...(request.moduleBase === undefined ? {} : { moduleBase: request.moduleBase }),
219
- ...(request.moduleIdentity === undefined ? {} : { moduleIdentity: request.moduleIdentity })
203
+ ...(request.executable === undefined ? {} : { executable: request.executable })
220
204
  };
221
205
  }
222
206
  function sessionContext(record) {
@@ -226,10 +210,7 @@ function sessionContext(record) {
226
210
  ...(context.buildKey === undefined ? {} : { buildKey: context.buildKey }),
227
211
  ...(context.flavor === undefined ? {} : { flavor: context.flavor }),
228
212
  ...(context.adapter === undefined ? {} : { adapter: context.adapter }),
229
- ...(context.executable === undefined ? {} : { executable: context.executable }),
230
- ...(context.processStartTime === undefined ? {} : { processStartTime: context.processStartTime }),
231
- ...(context.moduleBase === undefined ? {} : { moduleBase: context.moduleBase }),
232
- ...(context.moduleIdentity === undefined ? {} : { moduleIdentity: context.moduleIdentity })
213
+ ...(context.executable === undefined ? {} : { executable: context.executable })
233
214
  };
234
215
  }
235
216
  function serializeDevice(device) {
@@ -514,10 +495,6 @@ export class FridaCommandRuntime {
514
495
  const record = this.sessions.get(id);
515
496
  if (!record)
516
497
  throw new Error(`session ${id} was not found`);
517
- // Detach is an at-most-once terminal transition. Frida may report an
518
- // error after teardown has already started, so retaining the record would
519
- // let Broker cleanup call detach again on a half-destroyed agent.
520
- this.sessions.delete(id);
521
498
  for (const scriptId of [...record.scripts.keys()])
522
499
  await this.unloadScript(scriptId).catch(() => undefined);
523
500
  try {
@@ -525,6 +502,7 @@ export class FridaCommandRuntime {
525
502
  }
526
503
  catch { /* bridge cleanup is best effort */ }
527
504
  await record.session.detach();
505
+ this.sessions.delete(id);
528
506
  }
529
507
  async resume(request, context) {
530
508
  const pid = this.requirePid(request, context);
@@ -0,0 +1,100 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createInterface } from "node:readline";
3
+ import { FridaCommandRuntime } from "./frida-runtime.js";
4
+ function record(value) {
5
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
6
+ }
7
+ function requiredPositive(value, name) {
8
+ const number = typeof value === "number" ? value : Number(value);
9
+ if (!Number.isSafeInteger(number) || number < 1)
10
+ throw new Error(`${name} must be a positive integer`);
11
+ return number;
12
+ }
13
+ function hex(value, name) {
14
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0)
15
+ return `0x${value.toString(16)}`;
16
+ if (typeof value !== "string" || !/^(?:0x)?[0-9a-f]+$/i.test(value.trim()))
17
+ throw new Error(`${name} must be hexadecimal`);
18
+ return `0x${BigInt(value).toString(16)}`;
19
+ }
20
+ function add(base, offset) {
21
+ return `0x${(BigInt(base) + BigInt(offset)).toString(16)}`;
22
+ }
23
+ async function run(input) {
24
+ if (input.command !== "runtime-export")
25
+ throw new Error("unsupported worker command");
26
+ const pid = requiredPositive(input.pid, "pid");
27
+ const buildKey = String(input.build ?? input.buildKey ?? "").trim();
28
+ if (!buildKey)
29
+ throw new Error("build is required");
30
+ const profileFile = typeof input.staticProfile === "string" ? input.staticProfile : undefined;
31
+ const profile = profileFile ? record(JSON.parse(await readFile(profileFile, "utf8"))) : {};
32
+ const moduleProfile = record(profile.module);
33
+ const moduleName = typeof moduleProfile.name === "string" ? moduleProfile.name : "Wow.exe";
34
+ const runtime = new FridaCommandRuntime({ artifactDir: process.env.WOW_ANALYZE_DIR });
35
+ try {
36
+ const attached = await runtime.execute({ operation: "attach", pid, buildKey, allowUnmatched: true });
37
+ const sessionId = String(attached.sessionId);
38
+ const modules = await runtime.execute({ operation: "modules", sessionId, pid, buildKey });
39
+ const list = Array.isArray(modules.modules) ? modules.modules : [];
40
+ const module = list.find(item => String(item.name ?? "").toLowerCase() === moduleName.toLowerCase())
41
+ ?? list.find(item => String(item.name ?? "").toLowerCase() === "wow.exe");
42
+ if (!module || typeof module.base !== "string")
43
+ throw new Error(`${moduleName} module was not found`);
44
+ const moduleBase = module.base;
45
+ const moduleSize = Number(module.size ?? 0);
46
+ const candidates = [
47
+ ...(Array.isArray(profile.signatures) ? profile.signatures : []),
48
+ ...(Array.isArray(profile.providers) ? profile.providers : []),
49
+ ...(Array.isArray(profile.functions) ? profile.functions : [])
50
+ ].map(record);
51
+ const limit = Math.min(requiredPositive(input.maxEvents ?? 100, "max-events"), 10000);
52
+ const records = [];
53
+ for (const [index, candidate] of candidates.slice(0, limit).entries()) {
54
+ const rvaValue = candidate.rva;
55
+ if (rvaValue === undefined)
56
+ continue;
57
+ const rva = hex(rvaValue, `profile record ${index}.rva`);
58
+ const address = add(moduleBase, rva);
59
+ const size = Math.min(Math.max(Number(candidate.size ?? 16), 1), 256);
60
+ const read = await runtime.execute({ operation: "read_memory", sessionId, address, size, pid, buildKey });
61
+ records.push({
62
+ name: typeof candidate.name === "string" ? candidate.name : `record_${index}`,
63
+ rva,
64
+ runtimeAddress: address,
65
+ bytesHex: read.bytesHex ?? null,
66
+ source: "frida",
67
+ confidence: "unverified"
68
+ });
69
+ }
70
+ return {
71
+ ok: true,
72
+ command: "analyze.runtime-export",
73
+ pid,
74
+ buildKey,
75
+ module: { name: module.name, moduleBase, moduleSize },
76
+ moduleBase,
77
+ moduleSize,
78
+ records,
79
+ evidence: [{ kind: "frida-runtime-read", source: "frida", moduleBase, count: records.length }]
80
+ };
81
+ }
82
+ finally {
83
+ await runtime.close();
84
+ }
85
+ }
86
+ const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
87
+ let failed = false;
88
+ for await (const line of input) {
89
+ if (!line.trim())
90
+ continue;
91
+ try {
92
+ process.stdout.write(`${JSON.stringify(await run(record(JSON.parse(line))))}\n`);
93
+ }
94
+ catch (error) {
95
+ failed = true;
96
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
97
+ }
98
+ }
99
+ if (failed)
100
+ process.exitCode = 1;