wowdump 0.3.4 → 0.3.7

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # wowdump
2
2
 
3
- WoW 原生内存分析 CLI。用 Frida 运行时证据和 IDA Pro/iced-x86 局部反汇编定位字段,生成可复核的 Reader profile。
3
+ WoW 原生内存分析 CLI。用 WinDbg CDB 运行时证据和 IDA Pro/iced-x86 局部反汇编定位字段,生成可复核的 Reader profile。
4
4
 
5
5
  ## 安装
6
6
 
@@ -18,19 +18,24 @@ wowdump init
18
18
  wowdump targets
19
19
  wowdump database status --build "retail@VERSION"
20
20
 
21
- # 2. 导出运行时模块段
21
+ # 2. 用 CDB 导出运行时模块段
22
22
  wowdump analyze runtime --pid 1234 --build "retail@VERSION" --kind dump --confirm
23
23
 
24
24
  # 3. 用 IDA Pro MCP(若可用)或 iced-x86 分析 session dump manifest,保存字段候选和证据
25
- # 4. 将候选证据交给 verify,再用 Reader 验证
25
+ # 4. 需要函数调用参数时,用 CDB 硬件断点
26
+ wowdump analyze debug --pid 1234 --build "retail@VERSION" --rva 0x... --kind breakpoint --confirm
27
+ # 5. 将候选证据交给 verify,再用 Reader 验证
26
28
 
27
- # 5. 用户确认后,把 profile 保存到当前 build 的 profile 目录,再读取
29
+ # 6. 用户确认后,把 profile 保存到当前 build 的 profile 目录,再读取
28
30
  wowdump memory read --pid 1234 --profile "$HOME\.wowdump\retail@VERSION\profile\player-state.json" --field playerAuras
31
+
32
+ # 枚举已提交虚拟内存区(复用同一个管理员 broker)
33
+ wowdump memory regions --pid 1234 --start 0x15000000000 --end 0x15400000000 --max-regions 20000
29
34
  ```
30
35
 
31
- Windows reader 首次使用时由 Node 请求 UAC 启动 broker,后续调用复用同一 broker,空闲 20 分钟后退出。Frida 运行时导出需要显式 `--confirm`;高开销 Stalker 默认关闭。
36
+ Windows reader 首次使用时由 Node 请求 UAC 启动 broker,后续内存读取和 CDB 调试请求复用同一 broker,空闲 20 分钟后退出。CDB 路径探测不会下载或修改环境变量;所有调试请求都需要显式 `--confirm`。
32
37
 
33
- `targets` 会写入 `~/.wowdump/<buildKey>/build.json` 和 `database/database.json`;同一 build 的 IDA 数据库只创建一份。`analyze runtime --kind dump` 默认把 `.text`、`.rdata`、`.pdata` 流式写入当前 session,`.data` 需通过 `--sections` 显式加入。`analyze runtime --kind verify --profile <file>` 只验证已有 profile 的 RVA。Reader 只读取 `~/.wowdump/<buildKey>/profile/`,不读取 IDA 数据库和 dump 二进制。完整字段和 JSON 约定见 `~/.agents/skills/wowdump/references/commands.md`、`references/request-schema.md` 与 `references/profiles.md`;短时 Hook 示例见 `~/.agents/skills/wowdump/scripts/dynamic-session.js`。
38
+ `targets` 会写入 `~/.wowdump/<buildKey>/build.json` 和 `database/database.json`;同一 build 的 IDA 数据库只创建一份。`analyze runtime --kind dump` 默认把 `.text`、`.rdata`、`.pdata` 写入当前 session,`.data` 需通过 `--sections` 显式加入。`analyze runtime --kind verify --profile <file>` 只验证已有 profile 的 RVA。Reader 只读取 `~/.wowdump/<buildKey>/profile/`,不读取 IDA 数据库和 dump 二进制。完整字段和 JSON 约定见 `~/.agents/skills/wowdump/references/commands.md`、`references/request-schema.md`、`references/windbg.md` 与 `references/profiles.md`。
34
39
 
35
40
  ## 开发验证
36
41
 
@@ -5,7 +5,7 @@ function safeName(name) {
5
5
  const value = String(name ?? "section");
6
6
  return /^[A-Za-z0-9_.-]+$/.test(value) ? value : "section";
7
7
  }
8
- /** Streams Frida ArrayBuffer messages into section files without Base64 JSON. */
8
+ /** Streams section bytes into files and writes a compact runtime manifest. */
9
9
  export class RuntimeDumpWriter {
10
10
  outputDirectory;
11
11
  options;
@@ -61,7 +61,7 @@ export class RuntimeDumpWriter {
61
61
  };
62
62
  });
63
63
  const manifest = {
64
- schema: "wowdump.runtime-dump.v2",
64
+ schema: "wowdump.runtime-dump.v3",
65
65
  kind: "dump",
66
66
  buildKey: this.options.buildKey,
67
67
  pid: this.options.pid,
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { Command } from "commander";
9
9
  import { WindowsBrokerManager } from "./reader/launcher.js";
10
10
  import { initializeWowdumpHome, bootstrapToolchain, resolveToolchain } from "./toolchain.js";
11
11
  import { buildPaths, databaseStatus, ensureBuild, findReusableDump, readBuild, resolveWowdumpHome, safeBuildKey, sha256File } from "./core/build-store.js";
12
- import { RUNTIME_EXPORT_SCRIPT } from "./analysis/runtime-script.js";
12
+ import { readPeLayout } from "./debug/cdb.js";
13
13
  import { ProfileEngine } from "./core/profile-engine.js";
14
14
  import { ReaderProfileAdapter } from "./adapters/reader.js";
15
15
  import { enumerateWindowsProcesses } from "./reader/windows.js";
@@ -82,7 +82,7 @@ async function discoverWowTargets(selectedPid, elevatedModules) {
82
82
  catch { /* try the next parent directory */ }
83
83
  }
84
84
  }
85
- targets.push({ pid, name, path, moduleBase, moduleSize, fileVersion, product, buildKey: fileVersion ? `retail@${fileVersion}` : null, ...(buildInfoFile ? { buildInfoFile } : {}), ...(diagnostic ? { diagnostics: { frida: diagnostic } } : {}) });
85
+ targets.push({ pid, name, path, moduleBase, moduleSize, fileVersion, product, buildKey: fileVersion ? `retail@${fileVersion}` : null, ...(buildInfoFile ? { buildInfoFile } : {}), ...(diagnostic ? { diagnostics: { modules: diagnostic } } : {}) });
86
86
  }
87
87
  return targets;
88
88
  }
@@ -198,23 +198,6 @@ function jsonRecord(value, field) {
198
198
  function writeJson(io, value) {
199
199
  io.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
200
200
  }
201
- async function defaultSidecar(file, args, input) {
202
- return new Promise(resolveResult => {
203
- const child = spawn(file, args, { windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
204
- let stdout = "";
205
- let stderr = "";
206
- child.stdout.setEncoding("utf8");
207
- child.stderr.setEncoding("utf8");
208
- child.stdout.on("data", value => { stdout += value; });
209
- child.stderr.on("data", value => { stderr += value; });
210
- child.once("error", error => resolveResult({ exitCode: 1, stdout, stderr: `${stderr}${error.message}` }));
211
- child.once("exit", code => resolveResult({ exitCode: code ?? 1, stdout, stderr }));
212
- if (input !== undefined)
213
- child.stdin.end(input);
214
- else
215
- child.stdin.end();
216
- });
217
- }
218
201
  function brokerCommandLine(env, packageDist) {
219
202
  const configured = env.WOWDUMP_READER_COMMAND?.trim();
220
203
  if (configured)
@@ -222,12 +205,23 @@ function brokerCommandLine(env, packageDist) {
222
205
  const candidate = join(packageDist, "reader-main.js");
223
206
  return existsSync(candidate) ? { file: process.execPath, args: [candidate, "--request-stdio"] } : null;
224
207
  }
225
- async function defaultBroker(invocation, env, packageDist, sidecar) {
208
+ async function defaultBroker(invocation, env, packageDist) {
226
209
  const command = brokerCommandLine(env, packageDist);
227
210
  if (!command) {
228
211
  throw new CliError("BROKER_NOT_CONFIGURED", "reader broker entry was not found", { expected: join(packageDist, "reader-main.js"), environment: "WOWDUMP_READER_COMMAND" });
229
212
  }
230
- const result = await (sidecar ?? defaultSidecar)(command.file, command.args, `${JSON.stringify(invocation)}\n`);
213
+ const result = await new Promise(resolveResult => {
214
+ const child = spawn(command.file, command.args, { shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
215
+ let stdout = "";
216
+ let stderr = "";
217
+ child.stdout?.setEncoding("utf8");
218
+ child.stderr?.setEncoding("utf8");
219
+ child.stdout?.on("data", value => { stdout += String(value); });
220
+ child.stderr?.on("data", value => { stderr += String(value); });
221
+ child.once("error", error => resolveResult({ exitCode: 1, stdout, stderr: `${stderr}${error.message}` }));
222
+ child.once("exit", code => resolveResult({ exitCode: code ?? 1, stdout, stderr }));
223
+ child.stdin?.end(`${JSON.stringify(invocation)}\n`);
224
+ });
231
225
  if (result.exitCode !== 0)
232
226
  throw new CliError("BROKER_FAILED", result.stderr.trim() || `reader broker exited with ${result.exitCode}`);
233
227
  const line = result.stdout.split(/\r?\n/).find(value => value.trim());
@@ -240,22 +234,6 @@ async function defaultBroker(invocation, env, packageDist, sidecar) {
240
234
  throw new CliError("BROKER_PROTOCOL_ERROR", "reader broker returned invalid JSON", { stdout: result.stdout });
241
235
  }
242
236
  }
243
- async function runFridaWorker(worker, input, timeoutMs, broker) {
244
- if (process.platform !== "win32") {
245
- throw new CliError("PLATFORM_UNSUPPORTED", "elevated Frida broker is currently supported on Windows only");
246
- }
247
- const raw = await broker({ command: "frida", payload: { worker, input, timeoutMs } });
248
- const response = raw && typeof raw === "object" ? raw : {};
249
- if (response.ok === false) {
250
- const error = response.error && typeof response.error === "object" ? response.error : {};
251
- throw new CliError("BROKER_FAILED", String(error.message ?? "elevated Frida worker failed"), { response });
252
- }
253
- return {
254
- exitCode: Number.isSafeInteger(Number(response.exitCode)) ? Number(response.exitCode) : 1,
255
- stdout: typeof response.stdout === "string" ? response.stdout : "",
256
- stderr: typeof response.stderr === "string" ? response.stderr : ""
257
- };
258
- }
259
237
  const windowsBrokerManagers = new Map();
260
238
  function persistentWindowsBroker(home, packageDist) {
261
239
  const key = resolve(home).toLowerCase();
@@ -307,15 +285,13 @@ function selected(source, keys) {
307
285
  function requireRuntimeConfirmation(options) {
308
286
  if (options.confirm === true)
309
287
  return;
310
- throw new CliError("CONFIRMATION_REQUIRED", "runtime analysis requires --confirm", {
288
+ throw new CliError("CONFIRMATION_REQUIRED", "debug analysis requires --confirm", {
311
289
  pid: options.pid,
312
290
  buildKey: options.build,
313
291
  operation: "runtime",
314
292
  kind: options.kind,
315
- maxHooks: options.maxHooks,
316
293
  durationMs: options.durationMs,
317
- maxEvents: options.maxEvents,
318
- cleanupDeadlineMs: options.cleanupDeadlineMs
294
+ maxHits: options.maxHits
319
295
  });
320
296
  }
321
297
  function profileDirectoryFor(home, buildKey) {
@@ -355,16 +331,9 @@ export function createWowdumpCli(dependencies = {}) {
355
331
  const cwd = resolve(dependencies.cwd ?? process.cwd());
356
332
  const packageDist = installedDistDirectory();
357
333
  const home = resolveWowdumpHome(env);
358
- const broker = dependencies.broker ?? (process.platform === "win32" && !dependencies.sidecar
334
+ const broker = dependencies.broker ?? (process.platform === "win32"
359
335
  ? (invocation => persistentWindowsBroker(home, packageDist).request(invocation))
360
- : (invocation => defaultBroker(invocation, env, packageDist, dependencies.sidecar)));
361
- const baseSidecar = dependencies.sidecar ?? defaultSidecar;
362
- const sidecar = async (file, args, input) => {
363
- const worker = args.length === 1 && /\.m?js$/i.test(args[0] ?? "") ? args[0] : undefined;
364
- if (worker && input !== undefined)
365
- return runFridaWorker(worker, input, 120_000, broker);
366
- return baseSidecar(file, args, input);
367
- };
336
+ : (invocation => defaultBroker(invocation, env, packageDist)));
368
337
  const profileEngine = new ProfileEngine();
369
338
  const profileAdapter = new ReaderProfileAdapter(broker);
370
339
  const program = new Command()
@@ -398,7 +367,7 @@ export function createWowdumpCli(dependencies = {}) {
398
367
  writeJson(io, { ...await databaseStatus(home, options.build), command: "database.status" });
399
368
  });
400
369
  program.command("target")
401
- .description("Show target, reader broker, profile, monitor, and Frida state")
370
+ .description("Show target, reader broker, profile, monitor, and debugger state")
402
371
  .option("--pid <pid>", "target process ID")
403
372
  .option("--build <buildKey>", "expected build key")
404
373
  .action(async (options) => {
@@ -475,6 +444,20 @@ export function createWowdumpCli(dependencies = {}) {
475
444
  }
476
445
  writeJson(io, await broker({ command: "read", payload }));
477
446
  });
447
+ memory.command("regions")
448
+ .description("Enumerate committed virtual memory regions through VirtualQueryEx")
449
+ .requiredOption("--pid <pid>", "target process ID")
450
+ .option("--start <hex>", "query start address", "0x10000")
451
+ .option("--end <hex>", "query end address")
452
+ .option("--max-regions <count>", "maximum returned regions", "4096")
453
+ .option("--include-free", "include free and reserved regions")
454
+ .action(async (options) => writeJson(io, await broker({ command: "regions", payload: {
455
+ pid: positiveInteger(options.pid, "pid"),
456
+ start: options.start,
457
+ ...(options.end ? { end: options.end } : {}),
458
+ maxRegions: positiveInteger(options.maxRegions, "max-regions"),
459
+ includeFree: options.includeFree === true
460
+ } })));
478
461
  const watch = memory.command("watch").description("Manage broker-owned memory monitors");
479
462
  watch.command("start")
480
463
  .description("Start a monitor")
@@ -514,165 +497,140 @@ export function createWowdumpCli(dependencies = {}) {
514
497
  .description("Stop a monitor and release its resources")
515
498
  .requiredOption("--id <watchId>", "monitor ID")
516
499
  .action(async (options) => writeJson(io, await broker({ command: "watch.stop", payload: { watchId: options.id } })));
517
- const analyze = program.command("analyze").description("Run Frida evidence export and bounded runtime operations");
500
+ const analyze = program.command("analyze").description("Run bounded WinDbg/CDB evidence operations");
518
501
  analyze.command("runtime")
519
- .description("Dump runtime PE sections or verify profile candidates through the elevated Frida broker")
502
+ .description("Dump PE sections with CDB or verify a profile through the reader broker")
520
503
  .requiredOption("--pid <pid>", "target process ID")
521
504
  .requiredOption("--build <buildKey>", "build key")
522
505
  .option("--build-key <buildKey>", "alias for --build")
523
506
  .option("--kind <kind>", "dump or verify", "dump")
524
- .option("--worker <path>", "Frida worker entry")
525
507
  .option("--profile <path>", "reader profile containing candidate RVAs (verify only)")
526
508
  .option("--output-dir <path>", "directory for dump manifest and section binaries")
527
509
  .option("--sections <names...>", "sections to dump (default: .text .rdata .pdata)")
528
- .option("--no-progress", "disable dump progress on stderr")
529
510
  .option("--max-section-bytes <bytes>", "per-section dump limit", "134217728")
530
511
  .option("--max-total-bytes <bytes>", "total dump limit", "268435456")
531
- .option("--chunk-size <bytes>", "dump chunk size", "1048576")
532
- .option("--max-verify-bytes <bytes>", "maximum bytes read per candidate", "256")
533
- .option("--max-hooks <count>", "reserved compatibility limit", "1")
534
- .option("--duration-ms <ms>", "maximum duration", "5000")
535
- .option("--max-events <count>", "maximum events", "100")
536
- .option("--cleanup-deadline-ms <ms>", "cleanup deadline", "2000")
537
- .option("--confirm", "confirm the exact bounded Frida operation")
512
+ .option("--duration-ms <ms>", "CDB timeout", "30000")
513
+ .option("--cdb-args <json>", "JSON array of extra CDB arguments", "[]")
514
+ .option("--cdb-commands <json>", "JSON array of extra CDB commands", "[]")
515
+ .option("--confirm", "confirm the bounded debugger operation")
538
516
  .action(async (options) => {
539
517
  const normalized = {
540
518
  pid: positiveInteger(options.pid, "pid"),
541
519
  build: options.build ?? options.buildKey,
542
- kind: options.kind,
543
- maxHooks: positiveInteger(options.maxHooks, "max-hooks"),
544
- durationMs: positiveInteger(options.durationMs, "duration-ms"),
545
- maxEvents: positiveInteger(options.maxEvents, "max-events"),
546
- cleanupDeadlineMs: positiveInteger(options.cleanupDeadlineMs, "cleanup-deadline-ms"),
520
+ kind: String(options.kind),
547
521
  maxSectionBytes: positiveInteger(options.maxSectionBytes, "max-section-bytes"),
548
522
  maxTotalBytes: positiveInteger(options.maxTotalBytes, "max-total-bytes"),
549
- chunkSize: positiveInteger(options.chunkSize, "chunk-size"),
550
- maxVerifyBytes: positiveInteger(options.maxVerifyBytes, "max-verify-bytes"),
523
+ durationMs: positiveInteger(options.durationMs, "duration-ms"),
551
524
  confirm: options.confirm === true
552
525
  };
553
526
  if (!["dump", "verify"].includes(normalized.kind))
554
527
  throw new CliError("ARGUMENT_INVALID", "kind must be dump or verify");
555
528
  requireRuntimeConfirmation(normalized);
556
- if (normalized.kind === "verify" && !options.profile)
557
- throw new CliError("PROFILE_REQUIRED", "verify requires --profile <file>");
558
- const worker = resolve(options.worker ?? env.WOWDUMP_FRIDA_WORKER ?? join(packageDist, "frida-worker.js"));
559
- if (!existsSync(worker))
560
- throw new CliError("FRIDA_WORKER_NOT_FOUND", `Frida worker was not found: ${worker}`);
561
- const profile = options.profile
562
- ? jsonRecord(await readFile(resolve(options.profile), "utf8"), options.profile)
563
- : undefined;
564
529
  const buildRecord = await readBuild(home, normalized.build);
530
+ if (!buildRecord)
531
+ throw new CliError("BUILD_NOT_FOUND", `build ${normalized.build} has not been discovered`);
532
+ const profile = options.profile ? jsonRecord(await readFile(resolve(options.profile), "utf8"), options.profile) : undefined;
565
533
  if (profile)
566
534
  await validateProfileIdentity(home, normalized.build, profile, resolve(options.profile));
567
- const sections = Array.isArray(options.sections) && options.sections.length > 0
535
+ if (normalized.kind === "verify") {
536
+ if (!profile)
537
+ throw new CliError("PROFILE_REQUIRED", "verify requires --profile <file>");
538
+ const result = await profileEngine.read({ pid: normalized.pid, buildKey: normalized.build, executableSha256: buildRecord.executableSha256, profile, fields: undefined }, profileAdapter);
539
+ writeJson(io, { ...result, command: "analyze.runtime.verify", verified: true });
540
+ return;
541
+ }
542
+ const requestedSections = Array.isArray(options.sections) && options.sections.length > 0
568
543
  ? options.sections.map((value) => String(value).toLowerCase())
569
544
  : [".text", ".rdata", ".pdata"];
570
- if (normalized.kind === "dump") {
571
- const reusable = buildRecord?.executableSha256
572
- ? await findReusableDump(home, normalized.build, {
573
- executableSha256: buildRecord.executableSha256,
574
- sections,
575
- maxSectionBytes: normalized.maxSectionBytes,
576
- maxTotalBytes: normalized.maxTotalBytes,
577
- chunkSize: normalized.chunkSize
578
- })
579
- : null;
580
- if (reusable) {
581
- writeJson(io, { ok: true, command: "analyze.runtime.dump", reused: true, manifestFile: String(reusable.manifestFile ?? ""), manifest: reusable });
582
- return;
583
- }
545
+ const cdbArgs = JSON.parse(options.cdbArgs);
546
+ const cdbCommands = JSON.parse(options.cdbCommands);
547
+ if (!Array.isArray(cdbArgs) || !Array.isArray(cdbCommands))
548
+ throw new CliError("ARGUMENT_INVALID", "cdb-args and cdb-commands must be JSON arrays");
549
+ const reusable = requestedSections.includes(".data")
550
+ ? null
551
+ : await findReusableDump(home, normalized.build, { executableSha256: buildRecord.executableSha256, sections: requestedSections, maxSectionBytes: normalized.maxSectionBytes, maxTotalBytes: normalized.maxTotalBytes, cdbArgs, commands: cdbCommands });
552
+ if (reusable) {
553
+ writeJson(io, { ok: true, command: "analyze.runtime.dump", reused: true, manifestFile: String(reusable.manifestFile ?? ""), manifest: reusable });
554
+ return;
584
555
  }
556
+ const moduleResponse = await broker({ command: "modules", payload: { pid: normalized.pid } });
557
+ const modules = moduleResponse && typeof moduleResponse === "object" && Array.isArray(moduleResponse.modules) ? moduleResponse.modules : [];
558
+ const module = modules.find(item => /^Wow\.exe$/i.test(String(item.name ?? ""))) ?? modules[0];
559
+ if (!module || typeof module.base !== "string")
560
+ throw new CliError("MODULE_NOT_FOUND", "Wow.exe module was not found", { modules });
561
+ const moduleBase = module.base;
562
+ const layout = await readPeLayout(buildRecord.executablePath);
585
563
  const sessionId = randomUUID();
586
- const sessionDirectory = options.outputDir
587
- ? resolve(options.outputDir, "..")
588
- : join(buildPaths(home, normalized.build).runtime, sessionId);
564
+ const sessionDirectory = join(buildPaths(home, normalized.build).runtime, sessionId);
589
565
  const outputDirectory = resolve(options.outputDir ?? join(sessionDirectory, "dump"));
590
566
  await mkdir(sessionDirectory, { recursive: true });
591
- await writeFile(join(sessionDirectory, "target.json"), `${JSON.stringify({ schema: "wowdump.runtime-target.v1", pid: normalized.pid, buildKey: normalized.build, executableSha256: buildRecord?.executableSha256 ?? null, sessionId, createdAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
592
- const request = {
593
- command: normalized.kind === "dump" ? "runtime-dump" : "dynamic-script",
594
- pid: normalized.pid,
595
- build: normalized.build,
596
- source: RUNTIME_EXPORT_SCRIPT,
597
- exportName: "collect",
598
- ...(normalized.kind === "dump" ? { outputDir: outputDirectory, progress: options.progress !== false, executableSha256: buildRecord?.executableSha256 ?? null, preferredImageBase: buildRecord?.preferredImageBase ?? null, moduleSize: buildRecord?.moduleSize ?? null, dumpParameters: { executableSha256: buildRecord?.executableSha256 ?? null, sections, maxSectionBytes: normalized.maxSectionBytes, maxTotalBytes: normalized.maxTotalBytes, chunkSize: normalized.chunkSize } } : {}),
599
- args: {
600
- kind: normalized.kind,
601
- maxHooks: normalized.maxHooks,
602
- durationMs: normalized.durationMs,
603
- maxEvents: normalized.maxEvents,
604
- cleanupDeadlineMs: normalized.cleanupDeadlineMs,
605
- maxSectionBytes: normalized.maxSectionBytes,
606
- maxTotalBytes: normalized.maxTotalBytes,
607
- chunkSize: normalized.chunkSize,
608
- maxVerifyBytes: normalized.maxVerifyBytes,
609
- ...(normalized.kind === "dump" ? { sections } : {}),
610
- ...(profile ? { profile } : {})
611
- },
612
- callArgs: [],
613
- durationMs: normalized.durationMs
614
- };
615
- const result = await sidecar(process.execPath, [worker], `${JSON.stringify(request)}\n`);
616
- if (result.stderr && options.progress !== false)
617
- io.stderr.write(result.stderr);
618
- if (result.exitCode !== 0)
619
- throw new CliError("FRIDA_WORKER_FAILED", result.stderr.trim() || `Frida worker exited with ${result.exitCode}`);
620
- const line = result.stdout.split(/\r?\n/).find(value => value.trim());
621
- const value = line ? jsonRecord(line, "runtime result") : { ok: true, stdout: result.stdout };
622
- if (normalized.kind === "dump" && value.ok === true) {
623
- if (typeof value.manifestFile === "string") {
624
- await writeFile(join(sessionDirectory, "dynamic.json"), `${JSON.stringify(value.manifest ?? value, null, 2)}\n`, "utf8").catch(() => undefined);
625
- writeJson(io, { ...value, command: "analyze.runtime.dump", sessionDirectory });
626
- }
627
- else
628
- throw new CliError("RUNTIME_DUMP_INVALID", "runtime dump worker returned no manifest");
629
- }
630
- else {
631
- await writeFile(join(sessionDirectory, "verify.json"), `${JSON.stringify(value, null, 2)}\n`, "utf8").catch(() => undefined);
632
- writeJson(io, { ...value, command: "analyze.runtime.verify" });
633
- }
567
+ await writeFile(join(sessionDirectory, "target.json"), `${JSON.stringify({ schema: "wowdump.runtime-target.v2", pid: normalized.pid, buildKey: normalized.build, executableSha256: buildRecord.executableSha256, sessionId, createdAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
568
+ let total = 0;
569
+ const selectedSections = requestedSections.flatMap((name) => layout.sections.find((section) => section.name.toLowerCase() === name) ?? []);
570
+ const sections = selectedSections.map((section) => {
571
+ const requested = Math.min(Math.max(section.virtualSize, section.rawSize), normalized.maxSectionBytes, Math.max(0, normalized.maxTotalBytes - total));
572
+ total += requested;
573
+ return { ...section, runtimeAddress: `0x${(BigInt(moduleBase) + BigInt(section.rva)).toString(16)}`, readSize: requested, file: join(outputDirectory, `${section.name}.bin`) };
574
+ });
575
+ if (sections.length === 0)
576
+ throw new CliError("SECTION_NOT_FOUND", "no requested PE sections were found", { requestedSections, available: layout.sections.map(section => section.name) });
577
+ const raw = await broker({ command: "debug", payload: {
578
+ kind: "dump", pid: normalized.pid, buildKey: normalized.build, executablePath: buildRecord.executablePath,
579
+ executableSha256: buildRecord.executableSha256, moduleBase, preferredImageBase: buildRecord.preferredImageBase,
580
+ moduleSize: Number(module.size ?? buildRecord.moduleSize ?? layout.moduleSize), outputDir: outputDirectory, sections,
581
+ maxSectionBytes: normalized.maxSectionBytes, maxTotalBytes: normalized.maxTotalBytes, timeoutMs: normalized.durationMs,
582
+ cdbArgs, commands: cdbCommands
583
+ } });
584
+ const value = raw && typeof raw === "object" ? raw : {};
585
+ if (value.ok !== true)
586
+ throw new CliError(String(value.error?.code ?? "CDB_FAILED"), String(value.error?.message ?? "CDB dump failed"), value.error);
587
+ writeJson(io, { ...value, command: "analyze.runtime.dump", sessionDirectory });
634
588
  });
635
- analyze.command("dynamic")
636
- .description("Run a caller-supplied GumJS script through Frida")
589
+ analyze.command("debug")
590
+ .description("Run a bounded WinDbg/CDB command, breakpoint, or diagnostic session")
637
591
  .requiredOption("--pid <pid>", "target process ID")
638
592
  .requiredOption("--build <buildKey>", "build key")
639
- .requiredOption("--script <path>", "GumJS script file")
640
- .option("--export <name>", "rpc.exports function to call")
641
- .option("--args <json>", "JSON object exposed as __WOWDUMP_INPUT__", "{}")
642
- .option("--call-args <json>", "JSON array passed to the exported function", "[]")
643
- .option("--duration-ms <ms>", "maximum script duration", "5000")
644
- .option("--confirm", "confirm the exact Frida operation")
593
+ .option("--kind <kind>", "breakpoint or command", "breakpoint")
594
+ .option("--rva <rva>", "single statically-confirmed RVA")
595
+ .option("--max-hits <count>", "breakpoint hit limit", "3")
596
+ .option("--duration-ms <ms>", "CDB timeout", "3000")
597
+ .option("--cdb-args <json>", "JSON array of extra CDB arguments", "[]")
598
+ .option("--cdb-commands <json>", "JSON array of explicit CDB commands", "[]")
599
+ .option("--output <path>", "debug evidence JSON path")
600
+ .option("--confirm", "confirm the bounded debugger operation")
645
601
  .action(async (options) => {
646
- const normalized = {
647
- pid: positiveInteger(options.pid, "pid"),
648
- build: options.build,
649
- script: resolve(options.script),
650
- ...(options.export ? { exportName: options.export } : {}),
651
- args: jsonRecord(options.args, "args"),
652
- callArgs: JSON.parse(options.callArgs),
653
- durationMs: positiveInteger(options.durationMs, "duration-ms"),
654
- confirm: options.confirm === true
655
- };
656
- if (!Array.isArray(normalized.callArgs))
657
- throw new CliError("ARGUMENT_INVALID", "call-args must be a JSON array");
658
- requireRuntimeConfirmation(normalized);
659
- if (!existsSync(normalized.script))
660
- throw new CliError("FRIDA_SCRIPT_NOT_FOUND", `Frida script was not found: ${normalized.script}`);
661
- const worker = resolve(env.WOWDUMP_FRIDA_WORKER ?? join(packageDist, "frida-worker.js"));
662
- if (!existsSync(worker))
663
- throw new CliError("FRIDA_WORKER_NOT_FOUND", `Frida worker was not found: ${worker}`);
664
- const request = { command: "dynamic-script", ...selected(normalized, ["pid", "build", "script", "exportName", "args", "callArgs", "durationMs"]) };
665
- const sessionId = randomUUID();
666
- const sessionDirectory = join(buildPaths(home, normalized.build).runtime, sessionId);
602
+ const pid = positiveInteger(options.pid, "pid");
603
+ const build = String(options.build);
604
+ const durationMs = positiveInteger(options.durationMs, "duration-ms");
605
+ const kind = String(options.kind);
606
+ if (!["breakpoint", "command"].includes(kind))
607
+ throw new CliError("ARGUMENT_INVALID", "kind must be breakpoint or command");
608
+ requireRuntimeConfirmation({ pid, build, kind, durationMs, maxHits: options.maxHits, confirm: options.confirm === true });
609
+ const buildRecord = await readBuild(home, build);
610
+ if (!buildRecord)
611
+ throw new CliError("BUILD_NOT_FOUND", `build ${build} has not been discovered`);
612
+ const moduleResponse = await broker({ command: "modules", payload: { pid } });
613
+ const modules = moduleResponse && typeof moduleResponse === "object" && Array.isArray(moduleResponse.modules) ? moduleResponse.modules : [];
614
+ const module = modules.find(item => /^Wow\.exe$/i.test(String(item.name ?? ""))) ?? modules[0];
615
+ if (!module || typeof module.base !== "string")
616
+ throw new CliError("MODULE_NOT_FOUND", "Wow.exe module was not found");
617
+ const sessionDirectory = join(buildPaths(home, build).runtime, randomUUID());
667
618
  await mkdir(sessionDirectory, { recursive: true });
668
- await writeFile(join(sessionDirectory, "target.json"), `${JSON.stringify({ schema: "wowdump.runtime-target.v1", pid: normalized.pid, buildKey: normalized.build, sessionId, createdAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
669
- const result = await sidecar(process.execPath, [worker], `${JSON.stringify(request)}\n`);
670
- if (result.exitCode !== 0)
671
- throw new CliError("FRIDA_WORKER_FAILED", result.stderr.trim() || `Frida worker exited with ${result.exitCode}`);
672
- const line = result.stdout.split(/\r?\n/).find(value => value.trim());
673
- const value = line ? JSON.parse(line) : { ok: true, command: "analyze.dynamic", stdout: result.stdout };
674
- await writeFile(join(sessionDirectory, "dynamic.json"), `${JSON.stringify(value, null, 2)}\n`, "utf8");
675
- writeJson(io, { ...value, command: "analyze.dynamic", sessionDirectory });
619
+ const output = resolve(options.output ?? join(sessionDirectory, "debug.json"));
620
+ const parsedArgs = JSON.parse(options.cdbArgs);
621
+ const parsedCommands = JSON.parse(options.cdbCommands);
622
+ if (!Array.isArray(parsedArgs) || !Array.isArray(parsedCommands))
623
+ throw new CliError("ARGUMENT_INVALID", "cdb-args and cdb-commands must be JSON arrays");
624
+ const payload = kind === "command"
625
+ ? { kind, pid, buildKey: build, durationMs, outputFile: output, cdbArgs: parsedArgs, commands: parsedCommands }
626
+ : { kind: "breakpoint", pid, buildKey: build, rva: options.rva, moduleBase: module.base, maxHits: positiveInteger(options.maxHits, "max-hits"), durationMs, outputFile: output, cdbArgs: parsedArgs, commands: parsedCommands };
627
+ if (kind !== "command" && !options.rva)
628
+ throw new CliError("RVA_REQUIRED", "breakpoint requires --rva");
629
+ const raw = await broker({ command: "debug", payload });
630
+ const value = raw && typeof raw === "object" ? raw : {};
631
+ if (value.ok !== true)
632
+ throw new CliError(String(value.error?.code ?? "CDB_FAILED"), String(value.error?.message ?? "CDB debug failed"), value.error);
633
+ writeJson(io, { ...value, command: "analyze.debug", sessionDirectory });
676
634
  });
677
635
  program.command("init")
678
636
  .description("Initialize WOWDUMP_HOME without overwriting existing files")
@@ -30,14 +30,6 @@ function assertAdapter(adapter) {
30
30
  if (typeof adapter.verification !== "string" || !adapter.verification.trim()) {
31
31
  throw new TypeError("adapter.verification must identify the verification proof");
32
32
  }
33
- if (adapter.fridaPrelude !== undefined) {
34
- if (typeof adapter.fridaPrelude !== "string") {
35
- throw new TypeError("adapter.fridaPrelude must be a string when provided");
36
- }
37
- if (Buffer.byteLength(adapter.fridaPrelude, "utf8") > 1024 * 1024) {
38
- throw new TypeError("adapter.fridaPrelude exceeds the 1 MiB limit");
39
- }
40
- }
41
33
  const capabilities = adapter.capabilities;
42
34
  if (!capabilities || typeof capabilities !== "object") {
43
35
  throw new TypeError("adapter.capabilities must be provided");