wowdump 0.3.9 → 0.3.11

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
- Windows 下的 WoW 原生内存分析 CLI。用 CDB 运行时证据和 IDA Pro MCP / iced-x86 分析字段,生成 Reader 可读取的 build profile。
3
+ Windows 下的 WoW 原生内存分析 CLI。原生内存引擎负责读取、页查询和段导出;IDA Pro MCP 优先,iced-x86 提供无需额外工具的局部 64 位反汇编。最终生成按 build 保存的 profile。
4
4
 
5
5
  ## 安装与更新
6
6
 
@@ -10,7 +10,7 @@ wowdump init
10
10
  wowdump --version
11
11
  ```
12
12
 
13
- 安装会初始化 `~/.wowdump`,把包内完整 Skill 安装到 `~/.agents/skills/wowdump`。升级或 init 会同步 Skill,差异文件先备份到 `~/.wowdump/skill-backups/`;个人新增文件和 config.json 保留。iced-x86 随 npm 安装,CDB 从本机探测,不自动下载或修改环境变量。
13
+ 安装会初始化 `~/.wowdump`,把包内完整 Skill 安装到 `~/.agents/skills/wowdump`。升级或 init 会同步 Skill,差异文件先备份到 `~/.wowdump/skill-backups/`;个人新增文件和 config.json 保留。iced-x86 随 npm 安装,无需安装调试器或 CE。
14
14
 
15
15
  ## 使用流程
16
16
 
@@ -33,9 +33,9 @@ wowdump memory read --pid <pid> --build "<buildKey>" --profile "<character-stats
33
33
  wowdump memory regions --pid <pid> --start <startAddress> --end <endAddress> --max-regions 20000
34
34
  ```
35
35
 
36
- 尖括号是待替换参数。已有匹配且适用的 profile 时直接 Reader 读取,无需重新 dump。CDB 诊断与断点不是必经步骤,执行前先查 Skill 的退出和超时限制。
36
+ 尖括号是待替换参数。已有匹配且适用的 profile 时直接 内存引擎 读取,无需重新 dump。不再提供实时调试、断点或任意调试命令。
37
37
 
38
- 内存和 CDB 请求复用同一个管理员 broker:首次请求 UAC,空闲 20 分钟退出。所有调试请求需要 `--confirm`。
38
+ 内存读取与导出复用同一个管理员 broker:首次请求 UAC,空闲 20 分钟退出。运行时证据采集需要 `--confirm`。
39
39
 
40
40
  ## 数据放在哪里
41
41
 
@@ -44,18 +44,20 @@ wowdump memory regions --pid <pid> --start <startAddress> --end <endAddress> --m
44
44
  - `build.json`:目标身份。
45
45
  - `database/`:静态分析数据库位置和状态,不因任务重复创建。
46
46
  - `runtime/<session>/`:本轮证据、dump 和候选。
47
- - `profile/`:用户确认的 Reader 接口。
47
+ - `profile/`:用户确认的 内存引擎 接口。
48
48
 
49
- Dump 默认导出 `.text/.rdata/.pdata`,`.data` 按需加入。Reader 默认选择当前 build 的 profile,也支持显式路径;它不读取 IDA 数据库或 dump 二进制。
49
+ Dump 默认导出 `.text/.rdata/.pdata`,`.data` 按需加入。内存引擎 默认选择当前 build 的 profile,也支持显式路径;它不读取 IDA 数据库或 dump 二进制。
50
50
 
51
51
  ## 0.3.9 的读取与验证
52
52
 
53
+ 0.3.10 另修复请求超时计时器提前释放活动引用的问题,确保没有其他活动句柄时仍返回 REQUEST_TIMEOUT;broker 空闲退出策略不变。
54
+
53
55
  - verify 接受 candidate 或 reader_ready;普通 memory read 要求 reader_ready。
54
56
  - 输出 `validation.read: "passed"`,替代旧版 `verified:true`;fieldSemantics、rootSemantics、crossProcess 为 not_checked,需要独立证据。命令不自动认证语义、提升状态或保存 profile。
55
- - 新增 `root.lookup`:按动态键选择有界表项,核对对象键并检查读取期间变化,避免固化本轮哈希槽位。含 lookup 的 profile 需要 0.3.9 或更新版本;旧 Reader 可能忽略未知参数。
57
+ - 新增 `root.lookup`:按动态键选择有界表项,核对对象键并检查读取期间变化,避免固化本轮哈希槽位。含 lookup 的 profile 需要 0.3.9 或更新版本;旧 内存引擎 可能忽略未知参数。
56
58
  - 校验目标 build 时,缺少 executableSha256 会报错。模块外根 RVA、空根、短读和不一致查表结果均返回结构化错误。
57
59
 
58
- 详细用法由安装的 Skill 按需加载:[命令](skills/wowdump/references/commands.md)、[Profile 格式](skills/wowdump/references/profiles.md)、[字段挖掘方法与成功案例](skills/wowdump/references/character-stats-case.md)、[CDB 限制](skills/wowdump/references/windbg.md)。
60
+ 详细用法由安装的 Skill 按需加载:[命令](skills/wowdump/references/commands.md)、[Profile 格式](skills/wowdump/references/profiles.md)、[字段挖掘方法与成功案例](skills/wowdump/references/character-stats-case.md)、[内存引擎](skills/wowdump/references/memory.md)。
59
61
 
60
62
  ## 开发验证
61
63
 
@@ -66,3 +68,23 @@ npm test
66
68
  npm run package:verify
67
69
  npm run release:verify
68
70
  ```
71
+
72
+ ## 原生导出与离线分析
73
+
74
+ `analyze runtime --kind dump` 使用 Windows API 分块写入 `.bin`,manifest v4 保留 RVA、读取缺口和哈希。每次创建新 session,超过限额报错;缺页不伪装成完整读取。`progress.json` 提供进度。运行时导出不是原子快照。
75
+
76
+ ```text
77
+ wowdump analyze disassemble --file <section.bin> --rva <startRva> --offset <fileOffset> --size 4096 --max-instructions 256
78
+ ```
79
+
80
+ 该命令仅局部解码,不替代 IDA 的反编译与交叉引用。IDA 不可用时由 Agent 基于指令和内存证据分析。
81
+
82
+ ## 模块与命名
83
+
84
+ - `src/memory/`:Memory Engine,Windows 内存访问、broker、导出与生命周期。
85
+ - `src/analysis/`:PE 解析与 iced-x86 离线反汇编。
86
+ - `src/core/profile-engine.ts`:profile 身份校验、RVA/指针链与字段解析。
87
+ - `src/adapters/memory.ts`:profile 引擎到 broker 的适配。
88
+ - `src/memory-main.ts`:管理员 broker 入口;不再发布旧 reader 入口。
89
+
90
+ 开发测试的自定义入口变量为 `WOWDUMP_MEMORY_COMMAND`,测试后端变量为 `WOWDUMP_USE_FAKE_MEMORY`。旧变量不再使用。升级后首次原生请求会认证关闭旧版 broker,然后启动新引擎;profile v1 的 `readerStatus` / `reader_ready` 保持兼容。
@@ -1,8 +1,8 @@
1
1
  function record(value) {
2
2
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
3
3
  }
4
- /** Adapts the elevated reader broker to ProfileEngine's minimal memory API. */
5
- export class ReaderProfileAdapter {
4
+ /** Adapts the elevated memory broker to ProfileEngine's minimal memory API. */
5
+ export class MemoryProfileAdapter {
6
6
  invoke;
7
7
  constructor(invoke) {
8
8
  this.invoke = invoke;
@@ -0,0 +1,45 @@
1
+ import { open } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { Decoder, DecoderOptions, Formatter, FormatterSyntax } from "iced-x86";
4
+ export async function disassembleFile(input) {
5
+ if (!/^0x[0-9a-f]+$/i.test(input.rva) || BigInt(input.rva) > 0xffffffffn ||
6
+ !Number.isSafeInteger(input.offset) || input.offset < 0 || !Number.isSafeInteger(input.size) || input.size < 1 || input.size > 1024 * 1024 ||
7
+ !Number.isSafeInteger(input.maxInstructions) || input.maxInstructions < 1 || input.maxInstructions > 10000) {
8
+ throw Object.assign(new Error("invalid bounded disassembly range"), { code: "DISASSEMBLY_ARGUMENT_INVALID" });
9
+ }
10
+ const file = await open(resolve(input.file), "r");
11
+ let bytes;
12
+ try {
13
+ const buffer = Buffer.alloc(input.size);
14
+ const result = await file.read(buffer, 0, input.size, input.offset);
15
+ bytes = buffer.subarray(0, result.bytesRead);
16
+ }
17
+ finally {
18
+ await file.close();
19
+ }
20
+ if (!bytes.length)
21
+ throw Object.assign(new Error("no bytes in selected range"), { code: "DISASSEMBLY_EMPTY" });
22
+ const decoder = new Decoder(64, bytes, DecoderOptions.None);
23
+ decoder.ip = BigInt(input.rva);
24
+ const formatter = new Formatter(FormatterSyntax.Intel);
25
+ const instructions = [];
26
+ let decodedBytes = 0;
27
+ try {
28
+ while (decoder.canDecode && instructions.length < input.maxInstructions) {
29
+ const instruction = decoder.decode();
30
+ decodedBytes += instruction.length;
31
+ try {
32
+ instructions.push({ rva: `0x${instruction.ip.toString(16)}`, length: instruction.length, text: formatter.format(instruction), invalid: instruction.isInvalid,
33
+ ...(instruction.isIpRelMemoryOperand ? { memoryRva: `0x${instruction.ipRelMemoryAddress.toString(16)}` } : {}) });
34
+ }
35
+ finally {
36
+ instruction.free();
37
+ }
38
+ }
39
+ return { ok: true, command: "analyze.disassemble", engine: "iced-x86", bitness: 64, file: resolve(input.file), offset: input.offset, rva: input.rva, bytesRead: bytes.length, decodedBytes, sourceShortRead: bytes.length < input.size, truncated: decoder.canDecode, instructions };
40
+ }
41
+ finally {
42
+ decoder.free();
43
+ formatter.free();
44
+ }
45
+ }
@@ -0,0 +1,49 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ class PeError extends Error {
4
+ code;
5
+ constructor(code, message) {
6
+ super(message);
7
+ this.code = code;
8
+ }
9
+ }
10
+ function hex(value) { return `0x${value.toString(16)}`; }
11
+ function readU16(buffer, offset) { return buffer.readUInt16LE(offset); }
12
+ function readU32(buffer, offset) { return buffer.readUInt32LE(offset); }
13
+ export async function readPeLayout(file) {
14
+ const input = resolve(file);
15
+ const buffer = await readFile(input);
16
+ if (buffer.length < 0x100 || buffer.toString("ascii", 0, 2) !== "MZ")
17
+ throw new PeError("PE_INVALID", "Wow.exe has no DOS header");
18
+ const peOffset = readU32(buffer, 0x3c);
19
+ if (peOffset + 24 > buffer.length || buffer.toString("ascii", peOffset, peOffset + 4) !== "PE\0\0")
20
+ throw new PeError("PE_INVALID", "Wow.exe has no PE header");
21
+ const sections = readU16(buffer, peOffset + 6);
22
+ const optionalSize = readU16(buffer, peOffset + 20);
23
+ const optional = peOffset + 24;
24
+ if (optionalSize < 64 || optional + optionalSize > buffer.length)
25
+ throw new PeError("PE_INVALID", "truncated optional header");
26
+ const magic = readU16(buffer, optional);
27
+ if (![0x20b, 0x10b].includes(magic))
28
+ throw new PeError("PE_INVALID", "unsupported PE format");
29
+ const imageBase = magic === 0x20b ? buffer.readBigUInt64LE(optional + 24) : BigInt(readU32(buffer, optional + 28));
30
+ const sizeOfImage = readU32(buffer, optional + 56);
31
+ const table = optional + optionalSize;
32
+ if (sections > 96 || table + sections * 40 > buffer.length)
33
+ throw new PeError("PE_INVALID", "truncated section table");
34
+ const values = [];
35
+ for (let index = 0; index < sections; index += 1) {
36
+ const offset = table + index * 40;
37
+ if (offset + 40 > buffer.length)
38
+ break;
39
+ const name = buffer.subarray(offset, offset + 8).toString("ascii").replace(/\0+$/u, "");
40
+ const virtualSize = readU32(buffer, offset + 8);
41
+ const rva = readU32(buffer, offset + 12);
42
+ const rawSize = readU32(buffer, offset + 16);
43
+ const characteristics = readU32(buffer, offset + 36);
44
+ const protection = `${characteristics & 0x40000000 ? "r" : ""}${characteristics & 0x80000000 ? "w" : ""}${characteristics & 0x20000000 ? "x" : ""}`;
45
+ if (name && (virtualSize || rawSize))
46
+ values.push({ name, rva: hex(BigInt(rva)), virtualSize, rawSize, characteristics: hex(BigInt(characteristics)), protection });
47
+ }
48
+ return { imageBase: hex(imageBase), moduleSize: sizeOfImage, sections: values };
49
+ }
package/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { disassembleFile } from "./analysis/disassemble.js";
2
3
  import { spawn } from "node:child_process";
3
4
  import { randomUUID } from "node:crypto";
4
5
  import { existsSync, realpathSync, readFileSync } from "node:fs";
@@ -6,13 +7,12 @@ import { mkdir, open, readFile, readdir, writeFile } from "node:fs/promises";
6
7
  import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
7
8
  import { fileURLToPath, pathToFileURL } from "node:url";
8
9
  import { Command } from "commander";
9
- import { WindowsBrokerManager } from "./reader/launcher.js";
10
+ import { WindowsBrokerManager } from "./memory/launcher.js";
10
11
  import { initializeWowdumpHome, bootstrapToolchain, resolveToolchain } from "./toolchain.js";
11
- import { buildPaths, databaseStatus, ensureBuild, findReusableDump, readBuild, resolveWowdumpHome, safeBuildKey, sha256File } from "./core/build-store.js";
12
- import { readPeLayout } from "./debug/cdb.js";
12
+ import { buildPaths, databaseStatus, ensureBuild, readBuild, resolveWowdumpHome, safeBuildKey, sha256File } from "./core/build-store.js";
13
13
  import { ProfileEngine } from "./core/profile-engine.js";
14
- import { ReaderProfileAdapter } from "./adapters/reader.js";
15
- import { enumerateWindowsProcesses } from "./reader/windows.js";
14
+ import { MemoryProfileAdapter } from "./adapters/memory.js";
15
+ import { enumerateWindowsProcesses } from "./memory/windows.js";
16
16
  export function parseBuildInfo(content, file) {
17
17
  const lines = content.split(/\r?\n/).filter(line => line.trim());
18
18
  const headers = lines[0]?.split("|") ?? [];
@@ -51,7 +51,7 @@ async function discoverWowTargets(selectedPid, elevatedModules) {
51
51
  const modules = response && typeof response === "object" && Array.isArray(response.modules)
52
52
  ? response.modules
53
53
  : [];
54
- const main = modules.find(module => /^Wow\.exe$/i.test(String(module.name ?? ""))) ?? modules[0];
54
+ const main = modules.find(module => /^Wow\.exe$/i.test(String(module.name ?? "")));
55
55
  if (!path && typeof main?.path === "string" && main.path)
56
56
  path = main.path;
57
57
  if (!moduleBase && typeof main?.base === "string")
@@ -199,16 +199,16 @@ function writeJson(io, value) {
199
199
  io.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
200
200
  }
201
201
  function brokerCommandLine(env, packageDist) {
202
- const configured = env.WOWDUMP_READER_COMMAND?.trim();
202
+ const configured = env.WOWDUMP_MEMORY_COMMAND?.trim();
203
203
  if (configured)
204
204
  return { file: configured, args: [] };
205
- const candidate = join(packageDist, "reader-main.js");
205
+ const candidate = join(packageDist, "memory-main.js");
206
206
  return existsSync(candidate) ? { file: process.execPath, args: [candidate, "--request-stdio"] } : null;
207
207
  }
208
208
  async function defaultBroker(invocation, env, packageDist) {
209
209
  const command = brokerCommandLine(env, packageDist);
210
210
  if (!command) {
211
- throw new CliError("BROKER_NOT_CONFIGURED", "reader broker entry was not found", { expected: join(packageDist, "reader-main.js"), environment: "WOWDUMP_READER_COMMAND" });
211
+ throw new CliError("BROKER_NOT_CONFIGURED", "memory broker entry was not found", { expected: join(packageDist, "memory-main.js"), environment: "WOWDUMP_MEMORY_COMMAND" });
212
212
  }
213
213
  const result = await new Promise(resolveResult => {
214
214
  const child = spawn(command.file, command.args, { shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
@@ -223,15 +223,15 @@ async function defaultBroker(invocation, env, packageDist) {
223
223
  child.stdin?.end(`${JSON.stringify(invocation)}\n`);
224
224
  });
225
225
  if (result.exitCode !== 0)
226
- throw new CliError("BROKER_FAILED", result.stderr.trim() || `reader broker exited with ${result.exitCode}`);
226
+ throw new CliError("BROKER_FAILED", result.stderr.trim() || `memory broker exited with ${result.exitCode}`);
227
227
  const line = result.stdout.split(/\r?\n/).find(value => value.trim());
228
228
  if (!line)
229
- throw new CliError("BROKER_PROTOCOL_ERROR", "reader broker returned no JSON response");
229
+ throw new CliError("BROKER_PROTOCOL_ERROR", "memory broker returned no JSON response");
230
230
  try {
231
231
  return JSON.parse(line);
232
232
  }
233
233
  catch {
234
- throw new CliError("BROKER_PROTOCOL_ERROR", "reader broker returned invalid JSON", { stdout: result.stdout });
234
+ throw new CliError("BROKER_PROTOCOL_ERROR", "memory broker returned invalid JSON", { stdout: result.stdout });
235
235
  }
236
236
  }
237
237
  const windowsBrokerManagers = new Map();
@@ -239,7 +239,7 @@ function persistentWindowsBroker(home, packageDist) {
239
239
  const key = resolve(home).toLowerCase();
240
240
  let manager = windowsBrokerManagers.get(key);
241
241
  if (!manager) {
242
- manager = new WindowsBrokerManager({ home, readerEntry: join(packageDist, "reader-main.js") });
242
+ manager = new WindowsBrokerManager({ home, memoryEntry: join(packageDist, "memory-main.js") });
243
243
  windowsBrokerManagers.set(key, manager);
244
244
  }
245
245
  return manager;
@@ -285,7 +285,7 @@ function selected(source, keys) {
285
285
  function requireRuntimeConfirmation(options) {
286
286
  if (options.confirm === true)
287
287
  return;
288
- throw new CliError("CONFIRMATION_REQUIRED", "debug analysis requires --confirm", {
288
+ throw new CliError("CONFIRMATION_REQUIRED", "runtime evidence collection requires --confirm", {
289
289
  pid: options.pid,
290
290
  buildKey: options.build,
291
291
  operation: "runtime",
@@ -337,7 +337,7 @@ export function createWowdumpCli(dependencies = {}) {
337
337
  ? (invocation => persistentWindowsBroker(home, packageDist).request(invocation))
338
338
  : (invocation => defaultBroker(invocation, env, packageDist)));
339
339
  const profileEngine = new ProfileEngine();
340
- const profileAdapter = new ReaderProfileAdapter(broker);
340
+ const profileAdapter = new MemoryProfileAdapter(broker);
341
341
  const program = new Command()
342
342
  .name("wowdump")
343
343
  .description("Build-aware WoW native memory analysis CLI")
@@ -369,7 +369,7 @@ export function createWowdumpCli(dependencies = {}) {
369
369
  writeJson(io, { ...await databaseStatus(home, options.build), command: "database.status" });
370
370
  });
371
371
  program.command("target")
372
- .description("Show target, reader broker, profile, monitor, and debugger state")
372
+ .description("Show target, memory broker, profile and monitor state")
373
373
  .option("--pid <pid>", "target process ID")
374
374
  .option("--build <buildKey>", "expected build key")
375
375
  .action(async (options) => {
@@ -499,22 +499,21 @@ export function createWowdumpCli(dependencies = {}) {
499
499
  .description("Stop a monitor and release its resources")
500
500
  .requiredOption("--id <watchId>", "monitor ID")
501
501
  .action(async (options) => writeJson(io, await broker({ command: "watch.stop", payload: { watchId: options.id } })));
502
- const analyze = program.command("analyze").description("Run bounded WinDbg/CDB evidence operations");
502
+ const analyze = program.command("analyze").description("Collect memory evidence and disassemble local code");
503
503
  analyze.command("runtime")
504
- .description("Dump PE sections with CDB or verify a profile through the reader broker")
504
+ .description("Dump PE sections using native memory reads or verify a profile")
505
505
  .requiredOption("--pid <pid>", "target process ID")
506
506
  .requiredOption("--build <buildKey>", "build key")
507
507
  .option("--build-key <buildKey>", "alias for --build")
508
508
  .option("--kind <kind>", "dump or verify", "dump")
509
- .option("--profile <path>", "reader profile containing candidate RVAs (verify only)")
509
+ .option("--profile <path>", "memory profile containing candidate RVAs (verify only)")
510
510
  .option("--output-dir <path>", "directory for dump manifest and section binaries")
511
511
  .option("--sections <names...>", "sections to dump (default: .text .rdata .pdata)")
512
512
  .option("--max-section-bytes <bytes>", "per-section dump limit", "134217728")
513
513
  .option("--max-total-bytes <bytes>", "total dump limit", "268435456")
514
- .option("--duration-ms <ms>", "CDB timeout", "30000")
515
- .option("--cdb-args <json>", "JSON array of extra CDB arguments", "[]")
516
- .option("--cdb-commands <json>", "JSON array of extra CDB commands", "[]")
517
- .option("--confirm", "confirm the bounded debugger operation")
514
+ .option("--duration-ms <ms>", "native dump deadline", "30000")
515
+ .option("--chunk-bytes <bytes>", "native read block size (maximum 1 MiB)", "65536")
516
+ .option("--confirm", "confirm the bounded memory operation")
518
517
  .action(async (options) => {
519
518
  const normalized = {
520
519
  pid: positiveInteger(options.pid, "pid"),
@@ -544,96 +543,36 @@ export function createWowdumpCli(dependencies = {}) {
544
543
  const requestedSections = Array.isArray(options.sections) && options.sections.length > 0
545
544
  ? options.sections.map((value) => String(value).toLowerCase())
546
545
  : [".text", ".rdata", ".pdata"];
547
- const cdbArgs = JSON.parse(options.cdbArgs);
548
- const cdbCommands = JSON.parse(options.cdbCommands);
549
- if (!Array.isArray(cdbArgs) || !Array.isArray(cdbCommands))
550
- throw new CliError("ARGUMENT_INVALID", "cdb-args and cdb-commands must be JSON arrays");
551
- const reusable = requestedSections.includes(".data")
552
- ? null
553
- : await findReusableDump(home, normalized.build, { executableSha256: buildRecord.executableSha256, sections: requestedSections, maxSectionBytes: normalized.maxSectionBytes, maxTotalBytes: normalized.maxTotalBytes, cdbArgs, commands: cdbCommands });
554
- if (reusable) {
555
- writeJson(io, { ok: true, command: "analyze.runtime.dump", reused: true, manifestFile: String(reusable.manifestFile ?? ""), manifest: reusable });
556
- return;
557
- }
558
546
  const moduleResponse = await broker({ command: "modules", payload: { pid: normalized.pid } });
559
547
  const modules = moduleResponse && typeof moduleResponse === "object" && Array.isArray(moduleResponse.modules) ? moduleResponse.modules : [];
560
- const module = modules.find(item => /^Wow\.exe$/i.test(String(item.name ?? ""))) ?? modules[0];
548
+ const module = modules.find(item => /^Wow\.exe$/i.test(String(item.name ?? "")));
561
549
  if (!module || typeof module.base !== "string")
562
550
  throw new CliError("MODULE_NOT_FOUND", "Wow.exe module was not found", { modules });
563
- const moduleBase = module.base;
564
- const layout = await readPeLayout(buildRecord.executablePath);
565
551
  const sessionId = randomUUID();
566
552
  const sessionDirectory = join(buildPaths(home, normalized.build).runtime, sessionId);
567
553
  const outputDirectory = resolve(options.outputDir ?? join(sessionDirectory, "dump"));
568
554
  await mkdir(sessionDirectory, { recursive: true });
569
555
  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");
570
- let total = 0;
571
- const selectedSections = requestedSections.flatMap((name) => layout.sections.find((section) => section.name.toLowerCase() === name) ?? []);
572
- const sections = selectedSections.map((section) => {
573
- const requested = Math.min(Math.max(section.virtualSize, section.rawSize), normalized.maxSectionBytes, Math.max(0, normalized.maxTotalBytes - total));
574
- total += requested;
575
- return { ...section, runtimeAddress: `0x${(BigInt(moduleBase) + BigInt(section.rva)).toString(16)}`, readSize: requested, file: join(outputDirectory, `${section.name}.bin`) };
576
- });
577
- if (sections.length === 0)
578
- throw new CliError("SECTION_NOT_FOUND", "no requested PE sections were found", { requestedSections, available: layout.sections.map(section => section.name) });
579
- const raw = await broker({ command: "debug", payload: {
580
- kind: "dump", pid: normalized.pid, buildKey: normalized.build, executablePath: buildRecord.executablePath,
581
- executableSha256: buildRecord.executableSha256, moduleBase, preferredImageBase: buildRecord.preferredImageBase,
582
- moduleSize: Number(module.size ?? buildRecord.moduleSize ?? layout.moduleSize), outputDir: outputDirectory, sections,
583
- maxSectionBytes: normalized.maxSectionBytes, maxTotalBytes: normalized.maxTotalBytes, timeoutMs: normalized.durationMs,
584
- cdbArgs, commands: cdbCommands
556
+ const raw = await broker({ command: "dump", payload: {
557
+ pid: normalized.pid, buildKey: normalized.build,
558
+ executableSha256: buildRecord.executableSha256, outputDir: outputDirectory, sections: requestedSections,
559
+ maxSectionBytes: normalized.maxSectionBytes, maxTotalBytes: normalized.maxTotalBytes, timeoutMs: normalized.durationMs, chunkBytes: positiveInteger(options.chunkBytes, "chunk-bytes")
585
560
  } });
586
561
  const value = raw && typeof raw === "object" ? raw : {};
587
562
  if (value.ok !== true)
588
- throw new CliError(String(value.error?.code ?? "CDB_FAILED"), String(value.error?.message ?? "CDB dump failed"), value.error);
563
+ throw new CliError(String(value.error?.code ?? "DUMP_FAILED"), String(value.error?.message ?? "native dump failed"), value);
589
564
  writeJson(io, { ...value, command: "analyze.runtime.dump", sessionDirectory });
590
565
  });
591
- analyze.command("debug")
592
- .description("Run a bounded WinDbg/CDB command, breakpoint, or diagnostic session")
593
- .requiredOption("--pid <pid>", "target process ID")
594
- .requiredOption("--build <buildKey>", "build key")
595
- .option("--kind <kind>", "breakpoint or command", "breakpoint")
596
- .option("--rva <rva>", "single statically-confirmed RVA")
597
- .option("--max-hits <count>", "breakpoint hit limit", "3")
598
- .option("--duration-ms <ms>", "CDB timeout", "3000")
599
- .option("--cdb-args <json>", "JSON array of extra CDB arguments", "[]")
600
- .option("--cdb-commands <json>", "JSON array of explicit CDB commands", "[]")
601
- .option("--output <path>", "debug evidence JSON path")
602
- .option("--confirm", "confirm the bounded debugger operation")
603
- .action(async (options) => {
604
- const pid = positiveInteger(options.pid, "pid");
605
- const build = String(options.build);
606
- const durationMs = positiveInteger(options.durationMs, "duration-ms");
607
- const kind = String(options.kind);
608
- if (!["breakpoint", "command"].includes(kind))
609
- throw new CliError("ARGUMENT_INVALID", "kind must be breakpoint or command");
610
- requireRuntimeConfirmation({ pid, build, kind, durationMs, maxHits: options.maxHits, confirm: options.confirm === true });
611
- const buildRecord = await readBuild(home, build);
612
- if (!buildRecord)
613
- throw new CliError("BUILD_NOT_FOUND", `build ${build} has not been discovered`);
614
- const moduleResponse = await broker({ command: "modules", payload: { pid } });
615
- const modules = moduleResponse && typeof moduleResponse === "object" && Array.isArray(moduleResponse.modules) ? moduleResponse.modules : [];
616
- const module = modules.find(item => /^Wow\.exe$/i.test(String(item.name ?? ""))) ?? modules[0];
617
- if (!module || typeof module.base !== "string")
618
- throw new CliError("MODULE_NOT_FOUND", "Wow.exe module was not found");
619
- const sessionDirectory = join(buildPaths(home, build).runtime, randomUUID());
620
- await mkdir(sessionDirectory, { recursive: true });
621
- const output = resolve(options.output ?? join(sessionDirectory, "debug.json"));
622
- const parsedArgs = JSON.parse(options.cdbArgs);
623
- const parsedCommands = JSON.parse(options.cdbCommands);
624
- if (!Array.isArray(parsedArgs) || !Array.isArray(parsedCommands))
625
- throw new CliError("ARGUMENT_INVALID", "cdb-args and cdb-commands must be JSON arrays");
626
- const payload = kind === "command"
627
- ? { kind, pid, buildKey: build, durationMs, outputFile: output, cdbArgs: parsedArgs, commands: parsedCommands }
628
- : { kind: "breakpoint", pid, buildKey: build, rva: options.rva, moduleBase: module.base, maxHits: positiveInteger(options.maxHits, "max-hits"), durationMs, outputFile: output, cdbArgs: parsedArgs, commands: parsedCommands };
629
- if (kind !== "command" && !options.rva)
630
- throw new CliError("RVA_REQUIRED", "breakpoint requires --rva");
631
- const raw = await broker({ command: "debug", payload });
632
- const value = raw && typeof raw === "object" ? raw : {};
633
- if (value.ok !== true)
634
- throw new CliError(String(value.error?.code ?? "CDB_FAILED"), String(value.error?.message ?? "CDB debug failed"), value.error);
635
- writeJson(io, { ...value, command: "analyze.debug", sessionDirectory });
636
- });
566
+ analyze.command("disassemble")
567
+ .description("Decode a bounded local binary range with iced-x86 (64-bit)")
568
+ .requiredOption("--file <path>", "raw section binary")
569
+ .requiredOption("--rva <hex>", "RVA of the first decoded byte")
570
+ .option("--offset <bytes>", "file offset", "0")
571
+ .option("--size <bytes>", "maximum bytes", "4096")
572
+ .option("--max-instructions <count>", "instruction limit", "256")
573
+ .action(async (options) => writeJson(io, await disassembleFile({ file: options.file, rva: options.rva,
574
+ offset: nonNegativeInteger(options.offset, "offset"), size: positiveInteger(options.size, "size"),
575
+ maxInstructions: positiveInteger(options.maxInstructions, "max-instructions") })));
637
576
  program.command("init")
638
577
  .description("Initialize WOWDUMP_HOME without overwriting existing files")
639
578
  .option("--skip-toolchain-download", "kept for compatibility; no external toolchain is installed")
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { createReadStream } from "node:fs";
3
- import { mkdir, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
3
+ import { mkdir, open, readFile, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { join, normalize, resolve } from "node:path";
6
6
  export function resolveWowdumpHome(env = process.env, userHome = homedir()) {
@@ -144,23 +144,3 @@ export async function databaseStatus(home, buildKey) {
144
144
  }
145
145
  return { ok: true, buildKey: safeBuildKey(buildKey), buildDirectory: paths.build, databasePath: paths.databaseFile, exists: Boolean(database), locked: lock, build, database };
146
146
  }
147
- export async function findReusableDump(home, buildKey, criteria) {
148
- const runtime = buildPaths(home, buildKey).runtime;
149
- let sessions;
150
- try {
151
- sessions = await readdir(runtime, { withFileTypes: true });
152
- }
153
- catch (error) {
154
- if (error.code === "ENOENT")
155
- return null;
156
- throw error;
157
- }
158
- for (const session of sessions.filter(item => item.isDirectory())) {
159
- const manifestFile = join(runtime, session.name, "dump", "manifest.json");
160
- const manifest = await readJson(manifestFile);
161
- if (!manifest || typeof manifest.moduleBase !== "string" || JSON.stringify(manifest.dumpParameters ?? null) !== JSON.stringify(criteria))
162
- continue;
163
- return { ...manifest, manifestFile };
164
- }
165
- return null;
166
- }