wowdump 0.3.4 → 0.3.5

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
@@ -26,6 +26,9 @@ wowdump analyze runtime --pid 1234 --build "retail@VERSION" --kind dump --confir
26
26
 
27
27
  # 5. 用户确认后,把 profile 保存到当前 build 的 profile 目录,再读取
28
28
  wowdump memory read --pid 1234 --profile "$HOME\.wowdump\retail@VERSION\profile\player-state.json" --field playerAuras
29
+
30
+ # 枚举已提交虚拟内存区(复用同一个管理员 broker)
31
+ wowdump memory regions --pid 1234 --start 0x15000000000 --end 0x15400000000 --max-regions 20000
29
32
  ```
30
33
 
31
34
  Windows reader 首次使用时由 Node 请求 UAC 启动 broker,后续调用复用同一 broker,空闲 20 分钟后退出。Frida 运行时导出需要显式 `--confirm`;高开销 Stalker 默认关闭。
package/dist/cli.js CHANGED
@@ -475,6 +475,20 @@ export function createWowdumpCli(dependencies = {}) {
475
475
  }
476
476
  writeJson(io, await broker({ command: "read", payload }));
477
477
  });
478
+ memory.command("regions")
479
+ .description("Enumerate committed virtual memory regions through VirtualQueryEx")
480
+ .requiredOption("--pid <pid>", "target process ID")
481
+ .option("--start <hex>", "query start address", "0x0")
482
+ .option("--end <hex>", "query end address")
483
+ .option("--max-regions <count>", "maximum returned regions", "4096")
484
+ .option("--include-free", "include free and reserved regions")
485
+ .action(async (options) => writeJson(io, await broker({ command: "regions", payload: {
486
+ pid: positiveInteger(options.pid, "pid"),
487
+ start: options.start,
488
+ ...(options.end ? { end: options.end } : {}),
489
+ maxRegions: positiveInteger(options.maxRegions, "max-regions"),
490
+ includeFree: options.includeFree === true
491
+ } })));
478
492
  const watch = memory.command("watch").description("Manage broker-owned memory monitors");
479
493
  watch.command("start")
480
494
  .description("Start a monitor")
@@ -5,6 +5,9 @@ export const DEFAULT_RIGHTS = Object.freeze([
5
5
  "PROCESS_QUERY_INFORMATION",
6
6
  "PROCESS_VM_READ"
7
7
  ]);
8
+ const MEM_COMMIT = 0x1000;
9
+ const DEFAULT_QUERY_END = BigInt("0x7fffffffffff");
10
+ const MAX_QUERY_REGIONS = 100_000;
8
11
  function hexAddress(value) {
9
12
  const normalized = value.trim();
10
13
  if (!/^0x[0-9a-f]+$/i.test(normalized)) {
@@ -30,6 +33,15 @@ function boundedPid(value) {
30
33
  }
31
34
  return pid;
32
35
  }
36
+ function boundedCount(value, name, maximum, fallback) {
37
+ if (value === undefined)
38
+ return fallback;
39
+ const count = Number(value);
40
+ if (!Number.isSafeInteger(count) || count < 1 || count > maximum) {
41
+ throw new ReaderProtocolError("INVALID_COUNT", `${name} must be an integer between 1 and ${maximum}`);
42
+ }
43
+ return count;
44
+ }
33
45
  function toHex(bytes) {
34
46
  return Buffer.from(bytes).toString("hex");
35
47
  }
@@ -169,6 +181,8 @@ export class ReaderBroker {
169
181
  return this.runFrida(request);
170
182
  case "modules":
171
183
  return this.modules(request);
184
+ case "regions":
185
+ return this.regions(request);
172
186
  case "open":
173
187
  return this.open(request);
174
188
  case "close":
@@ -255,6 +269,63 @@ export class ReaderBroker {
255
269
  }
256
270
  return { pid, modules: await this.backend.enumerateModules(pid) };
257
271
  }
272
+ async regions(request) {
273
+ if (!this.backend.virtualQueryEx) {
274
+ throw new ReaderProtocolError("VIRTUAL_QUERY_UNAVAILABLE", "native VirtualQueryEx backend is not installed");
275
+ }
276
+ const pid = boundedPid(request.pid);
277
+ const start = hexAddress(request.start ?? "0x0");
278
+ const end = hexAddress(request.end ?? `0x${DEFAULT_QUERY_END.toString(16)}`);
279
+ if (end < start)
280
+ throw new ReaderProtocolError("INVALID_RANGE", "end must be greater than or equal to start");
281
+ const maxRegions = boundedCount(request.maxRegions, "maxRegions", MAX_QUERY_REGIONS, 4096);
282
+ const includeFree = request.includeFree === true;
283
+ const handle = await this.getHandle(pid, request.handle);
284
+ const regions = [];
285
+ let cursor = start;
286
+ let truncated = false;
287
+ while (cursor <= end) {
288
+ const raw = await this.backend.virtualQueryEx(handle, cursor);
289
+ const baseAddress = hexAddress(String(raw.baseAddress ?? "0x0"));
290
+ let regionSize;
291
+ try {
292
+ regionSize = BigInt(String(raw.regionSize ?? "0"));
293
+ }
294
+ catch {
295
+ throw new ReaderProtocolError("NATIVE_RESULT", "VirtualQueryEx returned an invalid region size");
296
+ }
297
+ if (regionSize <= 0n)
298
+ throw new ReaderProtocolError("NATIVE_RESULT", "VirtualQueryEx returned a non-positive region size");
299
+ const next = baseAddress + regionSize;
300
+ if (next <= cursor)
301
+ throw new ReaderProtocolError("NATIVE_RESULT", "VirtualQueryEx did not advance the query address");
302
+ const state = Number(raw.state);
303
+ if (includeFree || state === MEM_COMMIT) {
304
+ regions.push({
305
+ ...raw,
306
+ baseAddress: `0x${baseAddress.toString(16)}`,
307
+ regionSize: regionSize.toString(),
308
+ committed: state === MEM_COMMIT
309
+ });
310
+ }
311
+ cursor = next;
312
+ if (regions.length >= maxRegions) {
313
+ truncated = cursor <= end;
314
+ break;
315
+ }
316
+ }
317
+ return {
318
+ pid,
319
+ handle: handle.id,
320
+ start: `0x${start.toString(16)}`,
321
+ end: `0x${end.toString(16)}`,
322
+ includeFree,
323
+ maxRegions,
324
+ regions,
325
+ nextAddress: `0x${cursor.toString(16)}`,
326
+ truncated
327
+ };
328
+ }
258
329
  async watchStart(request) {
259
330
  const pid = boundedPid(request.pid);
260
331
  const address = hexAddress(request.address);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wowdump",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "WoW native memory analysis CLI with Frida evidence, IDA/iced-x86 disassembly, and an elevated reader broker",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -12,6 +12,7 @@ description: 以 Frida 运行时探测为主,按证据需要交替使用 IDA P
12
12
  1. 先运行 `wowdump targets`,确认 PID、Wow.exe 路径、哈希和 buildKey;多个目标时让用户选择。该命令会创建或复用 `~/.wowdump/<buildKey>/database/`。
13
13
  2. 先检查 `wowdump database status --build <buildKey>`。同一可执行文件哈希、首选 image base 和模块大小时复用已有 IDA 数据库;哈希变化时数据库为 `stale`。
14
14
  3. 有 `reader_ready` profile 时先用 `wowdump memory read --pid <pid> --build <buildKey> --profile <profile> --field <name>`;Reader 不读取 IDA 数据库、dump 二进制或 runtime JSON。
15
+ 需要定位堆或确认可读范围时,先用 `wowdump memory regions --pid <pid> --start <hex> --end <hex>` 枚举 `VirtualQueryEx` 返回的已提交区,再用同一 broker 做有界 `memory read`;默认不包含 free/reserved 区。
15
16
  4. 没有可用 profile 时,由 Agent 编写并显式传入 GumJS:`wowdump analyze dynamic --pid <pid> --build <buildKey> --script <GumJS> --confirm`。脚本可做快照、有限 Hook 和事件采集。
16
17
  5. 动态线索需要静态证据时运行 `wowdump analyze runtime --kind dump`。dump 只写当前 session 的 `.text/.rdata/.pdata`,`.data` 通过 `--sections` 按需加入;不创建新的静态数据库。
17
18
  6. 优先让 IDA Pro MCP 打开该 build 的 `database/ida/Wow.i64`,按 manifest 将 runtime overlay 映射到原始 Wow.exe,输出函数 RVA、字符串 RVA、XREF、结构偏移和字段类型。没有 IDA MCP 时,用 iced-x86 对相关范围局部反汇编。
@@ -78,4 +78,16 @@ wowdump memory read `
78
78
 
79
79
  需要连续观察时使用 `memory watch start/poll/stop`。字段未定义、模块不匹配、指针为空或发生短读时,保留 JSON 证据并停止该字段,不改写 profile。
80
80
 
81
+ 需要枚举目标进程的虚拟内存布局时使用 `memory regions`。它通过同一个管理员 broker 调用 Windows `VirtualQueryEx`,默认只返回 `MEM_COMMIT` 区域;查询有地址范围和数量上限,不会把整片地址空间读入内存:
82
+
83
+ ```powershell
84
+ wowdump memory regions `
85
+ --pid 33976 `
86
+ --start 0x15000000000 `
87
+ --end 0x15400000000 `
88
+ --max-regions 20000
89
+ ```
90
+
91
+ 需要诊断空闲/保留区时显式加入 `--include-free`。输出包含 `baseAddress`、`regionSize`、`state`、`protect`、`type` 和 `committed`,可将已提交的 heap 区域交给后续只读扫描。
92
+
81
93
  静态分析完成后回到 `analyze dynamic` Hook 验证候选,再生成 profile。短时 Hook/事件采集可以复制 [scripts/dynamic-session.js](../scripts/dynamic-session.js) 后按请求修改;它不会被 CLI 自动执行。详细脚本规则见 [dynamic.md](dynamic.md) 和 [disassemble.md](disassemble.md)。字段请求格式见 [request-schema.md](request-schema.md),profile 生命周期见 [profiles.md](profiles.md)。