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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "wowdump",
3
- "version": "0.3.1",
4
- "description": "WoW native memory analysis CLI with Ghidra, Frida validation, and an elevated reader broker",
3
+ "version": "0.3.3",
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": {
7
7
  "type": "git",
@@ -21,27 +21,30 @@
21
21
  "node": ">=22"
22
22
  },
23
23
  "scripts": {
24
- "build": "npm run build:host && npm run build:agent",
24
+ "clean": "node scripts/clean-dist.mjs",
25
+ "build": "npm run clean && npm run build:host",
25
26
  "build:host": "tsc -p tsconfig.json",
26
- "build:agent": "frida-compile agent/index.ts -o dist/agent.js",
27
27
  "wowdump": "node dist/cli.js",
28
- "postinstall": "npm run build:host --if-present && node dist/toolchain.js --postinstall",
29
- "start": "node dist/main.js",
30
- "discover": "node dist/main.js --discover",
31
- "dry-run": "node dist/main.js --dry-run",
28
+ "postinstall": "node -e \"const fs=require('node:fs');if(fs.existsSync('dist/toolchain.js'))import('./dist/toolchain.js').then(m=>m.bootstrapToolchain())\"",
29
+ "prepack": "npm run build",
30
+ "package:verify": "node scripts/verify-package.mjs",
31
+ "release:verify": "node scripts/verify-release-workflow.mjs verify",
32
32
  "test": "npm run build:host && node --test tests/*.test.mjs",
33
33
  "typecheck": "tsc --noEmit -p tsconfig.json"
34
34
  },
35
35
  "dependencies": {
36
36
  "commander": "^13.1.0",
37
37
  "frida": "^17.2.0",
38
+ "iced-x86": "^1.21.0",
38
39
  "koffi": "^2.16.3",
39
- "pino": "^9.6.0",
40
40
  "zod": "^3.24.2"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^22.13.4",
44
- "frida-compile": "^16.4.2",
45
44
  "typescript": "^5.7.3"
45
+ },
46
+ "allowScripts": {
47
+ "frida@17.17.0": true,
48
+ "koffi@2.16.3": true
46
49
  }
47
50
  }
@@ -1,15 +1,24 @@
1
- ---
2
- name: wowdump
3
- description: "使用 wowdump Node CLI 进行只读的 WoW 原生内存分析、Ghidra 静态取证、受限 profile 读取,以及经过明确确认的运行时验证。"
4
- ---
5
-
6
- # wowdump
7
-
8
- `wowdump` 是唯一用户入口。按依赖顺序完成:
9
-
10
- 1. 用 Ghidra 静态分析发现段、函数、签名和候选 RVA。
11
- 2. 用经过确认的 Frida 短时导出运行中模块的基址和对应字节,验证这些 RVA。
12
- 3. 把通过验证的 RVA、字段、约束和证据落盘为 `reader_ready` profile。
13
- 4. 后续只用 broker-backed reader 读取和监控,不再启动 Frida
14
-
15
- 每一步都保留 build、文件哈希、模块基址、RVA、命令和证据来源。详细命令和字段格式见同目录的 `commands.md`。
1
+ ---
2
+ name: wowdump
3
+ description: Frida 运行时探测为主,按证据需要交替使用 IDA Pro MCP 或 iced-x86,生成并验证 WoW 原生内存 Reader profile
4
+ ---
5
+
6
+ # wowdump
7
+
8
+ 用于读取角色属性、Buff/Debuff、技能冷却、附近单位等原生字段。优先从当前进程取得真实值;需要长期复用时,再把已验证的线索固化为带证据的 `wowdump.profile.v1`。
9
+
10
+ ## 默认策略
11
+
12
+ 1. 先运行 `wowdump targets`,确认 PID、Wow.exe 路径和 buildKey;多个目标时让用户选择。
13
+ 2. `~/.wowdump/<buildKey>/runtime/<session>/` 保存临时证据,在 `~/.wowdump/<buildKey>/profile/` 只保存用户确认的持久化 profile
14
+ 3. 默认编写并显式传入 GumJS,使用 `wowdump analyze dynamic --script <GumJS> --confirm` 做运行时发现、原生 Hook、快照或事件采集。
15
+ 4. 若已有 `reader_ready` profile,先用 reader 读取;若动态结果已足够回答问题,直接返回,不强行做静态分析。
16
+ 5. 动态结果需要函数 RVA、对象布局或字段类型时,先用 `wowdump analyze runtime --kind dump` 导出 `.text`、`.rdata`、`.data`、`.pdata` 和 manifest,再调用 IDA Pro MCP;没有 IDA 时才用 iced-x86 输出。静态和动态可以交替执行,每轮只扩大当前字段所需的证据范围。
17
+ 6. 将确认的 RVA、指针链、类型、边界和证据写入候选 profile。字段证据不足时保持 `candidate`,回到 dynamic 补采样或回到 IDA/iced 缩小候选。
18
+ 7. 用 `wowdump analyze runtime --kind verify --profile <profile> --confirm` 或 `wowdump memory read --profile <profile> --field <name>` 做 broker-backed 验证;只有地址、类型和实际读取都成功后才标记 `reader_ready`。经用户确认后再保存到该 build 的 `profile/`。
19
+
20
+ 不要把 dynamic、IDA 或 iced 固定成一次性线性流水线。选择下一步的依据是当前字段缺少什么证据:值用 dynamic,函数/布局用 IDA/iced,稳定读取用 reader。
21
+
22
+ 具体的交替决策见 [references/workflow.md](references/workflow.md);动态 Hook 规则见 [references/dynamic.md](references/dynamic.md);IDA/iced 的选择和交替策略见 [references/disassemble.md](references/disassemble.md);请求字段和 profile 生命周期见 [references/request-schema.md](references/request-schema.md) 与 [references/profiles.md](references/profiles.md)。
23
+
24
+ IDA MCP 使用长 TTL 会话(默认 `idle_ttl_sec: 3600`)。每次查询前先做 health probe;出现 `worker not reachable` 时按 `references/disassemble.md` 重新打开并只重试一次。IDA worker 与 Windows broker 独立,broker 的 UAC 复用不会保持 IDA worker 存活。
@@ -0,0 +1,77 @@
1
+ # wowdump 命令参考
2
+
3
+ Windows 下需要内存或 Frida 权限时,CLI 会连接同一个管理员 broker;首次建立 broker 才需要 UAC,默认空闲 20 分钟后退出。
4
+
5
+ ## 发现目标
6
+
7
+ ```powershell
8
+ wowdump targets
9
+ wowdump targets --pid 33976
10
+ ```
11
+
12
+ 使用输出里的 `pid`、`path` 和 `buildKey`。`buildKey` 来自目标安装目录的 `.build.info`;不要写 `SIM` 或手填版本。
13
+
14
+ ## 运行时动态分析
15
+
16
+ ```powershell
17
+ wowdump analyze dynamic `
18
+ --pid 33976 `
19
+ --build "retail@12.1.0.69587" `
20
+ --script "C:\work\discover-state.js" `
21
+ --export collect `
22
+ --args '{"fields":["playerAuras","nearbyEnemies","cooldowns"]}' `
23
+ --duration-ms 5000 `
24
+ --confirm > $HOME\.wowdump\runtime\<session-id>\state.json
25
+ ```
26
+
27
+ 导出供静态分析的运行时模块段:
28
+
29
+ ```powershell
30
+ wowdump analyze runtime `
31
+ --pid 33976 `
32
+ --build "retail@12.1.0.69587" `
33
+ --kind dump `
34
+ --output-dir "$HOME\.wowdump\retail@12.1.0.69587\runtime\dump-01" `
35
+ --confirm
36
+ ```
37
+
38
+ 输出目录中包含 `manifest.json` 和 `.text`、`.rdata`、`.data`、`.pdata` 二进制段。stdout 只返回 manifest 路径和摘要,不打印整段十六进制。
39
+
40
+ 验证已有候选 profile:
41
+
42
+ ```powershell
43
+ wowdump analyze runtime `
44
+ --pid 33976 `
45
+ --build "retail@12.1.0.69587" `
46
+ --kind verify `
47
+ --profile "$HOME\.wowdump\retail@12.1.0.69587\runtime\candidate.profile.json" `
48
+ --confirm
49
+ ```
50
+
51
+ `verify` 不负责发现地址;没有 profile 时会直接提示缺少验证目标。
52
+
53
+ ## 需要时做静态定位
54
+
55
+ 只有 dynamic 结果缺少函数 RVA、对象布局或字段类型时,才调用 IDA Pro MCP;没有 IDA 时使用 iced-x86 兜底:
56
+
57
+ ```powershell
58
+ wowdump analyze disassemble `
59
+ --exe "D:\Game\World of Warcraft\_retail_\Wow.exe" `
60
+ --build "retail@12.1.0.69587" `
61
+ --runtime-export "$HOME\.wowdump\runtime\<session-id>\state.json" `
62
+ --output "$HOME\.wowdump\runtime\<session-id>\retail-12.1.0.69587.profile.json"
63
+ ```
64
+
65
+ ## 读取
66
+
67
+ ```powershell
68
+ wowdump memory read `
69
+ --pid 33976 `
70
+ --build "retail@12.1.0.69587" `
71
+ --profile "$HOME\.wowdump\retail@12.1.0.69587\profile\player-state.json" `
72
+ --field playerAuras nearbyEnemies cooldowns
73
+ ```
74
+
75
+ 需要连续观察时使用 `memory watch start/poll/stop`。字段未定义、模块不匹配、指针为空或发生短读时,保留 JSON 证据并停止该字段,不改写 profile。
76
+
77
+ 静态分析完成后回到 `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)。
@@ -0,0 +1,62 @@
1
+ # 反汇编与 IDA Pro 分支
2
+
3
+ 静态分析是按需使用的证据放大器,不是每个查询的必经步骤。需要静态证据时,先用 `wowdump analyze runtime --kind dump` 导出运行时 `.text`、`.rdata`、`.data`、`.pdata` 以及 manifest;运行时字节样本、Hook 调用记录或返回值也可以作为输入。先检查当前 Agent 是否能调用本机 IDA Pro MCP:
4
+
5
+ ## IDA Pro MCP 下载与安装
6
+
7
+ 官方来源:
8
+
9
+ - GitHub:https://github.com/mrexodia/ida-pro-mcp
10
+ - Hex-Rays 插件页:https://plugins.hex-rays.com/mrexodia/ida-pro-mcp
11
+
12
+ 使用 Codex 时,官方仓库提供插件市场安装方式:
13
+
14
+ ```text
15
+ codex plugin marketplace add mrexodia/codex-marketplace
16
+ codex plugin add ida-pro-mcp@mrexodia
17
+ ```
18
+
19
+ IDA Pro MCP 需要 **IDA Pro 8.3+**(不支持 IDA Free)、Python 3.11+,headless `idalib` 路径还需要 `uv`。安装后重启 IDA 和 Codex,使 MCP 工具生效。若插件市场不可用,按官方仓库 README 的 GUI 或 `idalib-mcp` 安装说明操作;不要在项目中复制或维护 MCP 的内部 Python/IDA 代码。
20
+
21
+ - 能调用时,让 MCP 只分析当前字段相关样本附近的函数、字符串、调用关系和结构,把结果保存为 JSON。随后回到 dynamic Hook 验证候选函数,不要直接把静态候选当成可读字段。
22
+
23
+ ## IDA MCP 会话保活与重连
24
+
25
+ IDA 的 headless worker 不是常驻进程。`idb_list` 中仍有 session 记录,不代表对应 worker 仍然可用;查询返回 `Worker for session ... is not reachable` 时,不能继续复用该 session。
26
+
27
+ 打开数据库时使用较长的空闲 TTL,并开启自动分析和缓存:
28
+
29
+ ```json
30
+ {
31
+ "input_path": "C:\\Games\\World of Warcraft\\_retail_\\Wow.exe",
32
+ "mode": "prefer_headless",
33
+ "run_auto_analysis": true,
34
+ "build_caches": true,
35
+ "init_hexrays": true,
36
+ "idle_ttl_sec": 3600
37
+ }
38
+ ```
39
+
40
+ 每次查询前先调用 `server_health({ database: sessionId })`,确认 `status: "ok"`、`auto_analysis_ready: true`。长查询之间也要做一次 health probe,避免 worker 被回收后才发现会话失效。
41
+
42
+ 失联时只做一次恢复:
43
+
44
+ 1. 丢弃失联的 session ID;如果服务仍列出它,调用 `idb_close`。
45
+ 2. 用相同 `input_path` 和 `idle_ttl_sec: 3600` 重新调用 `idb_open`。
46
+ 3. 对新 session 做 `server_health`,成功后重试原查询一次。
47
+ 4. 第二次仍失联时停止扩大分析范围,保存已取得的 JSON 证据并转用 iced-x86 或请求用户稍后重试。
48
+
49
+ 不要把 `session_id`、IDA 临时 worker PID 或 `0x140000000` 这类 IDA image base 写入持久化 profile。profile 只保存相对模块的 RVA、字段类型和可复核证据。IDA MCP 会话和 wowdump 的 Windows broker 是两条独立链路:前者负责反汇编,后者负责管理员内存读取;broker 复用不会延长 IDA worker 生命周期。
50
+ - IDA 不可用时执行:
51
+
52
+ ```powershell
53
+ wowdump analyze disassemble `
54
+ --exe "C:\Games\World of Warcraft\_retail_\Wow.exe" `
55
+ --build "retail@VERSION" `
56
+ --runtime-export "$HOME\.wowdump\retail@VERSION\runtime\SESSION\state.json" `
57
+ --output "$HOME\.wowdump\runtime\SESSION\candidate.profile.json"
58
+ ```
59
+
60
+ CLI 使用 `iced-x86` 解码 runtime 样本,返回模块基址、RVA、指令文本、PE sections 和证据。Agent 负责把这些线索与运行时字段对应起来,再用 dynamic 验证 `root`、`pointerChain`、`layout/type`、计数上限和停止条件。不要把一次运行的绝对地址写进持久化 profile。
61
+
62
+ 输出中的 `readerStatus: "reader_ready"` 只适用于每个字段都有可验证 RVA/地址、类型和边界,并且 reader 实际读成功的情况;否则保持 `candidate`,回到 dynamic 或继续局部静态分析。只做一次性查询时可以不生成持久化 profile。
@@ -0,0 +1,54 @@
1
+ # Dynamic GumJS 编写指南
2
+
3
+ ## 目标与优先级
4
+
5
+ dynamic 是默认主路径,负责从当前运行进程取得真实值和线索:模块基址、对象地址、函数地址、字节样本、字符串、返回值或事件证据。脚本可以使用静态阶段给出的候选 RVA,也可以自己做有界签名扫描;不要把未经验证的绝对地址当成长期 profile。
6
+
7
+ ## 脚本入口
8
+
9
+ 通过 `rpc.exports` 暴露一个小而明确的函数,例如 `collect`。CLI 的 `--args` 会放在 `globalThis.__WOWDUMP_INPUT__`,字段列表和读取上限从这里取得。
10
+
11
+ API 参考: [Frida JavaScript API](https://frida.re/docs/javascript-api/)。
12
+
13
+ 一次普通 run 由 CLI 完成 attach 并加载脚本,脚本在同一次执行中完成 Hook、采集、限时和 cleanup。需要持续观察时,由脚本内部维护事件队列、采样计时器和清理逻辑,在一次 run 结束时返回结果;不增加单独的 `analyze focus` 命令,也暂不实现跨命令的 status、pause 或 checkpoint。
14
+
15
+ 轻量模式保持不变:
16
+
17
+ ```text
18
+ wowdump analyze dynamic --script <GumJS> --confirm
19
+ ```
20
+
21
+ `--script` 必须由 Agent 显式提供。仓库中的 `scripts/dynamic-session.js` 只是可复制和修改的示例,不是 CLI 默认脚本,也不会被自动执行。
22
+
23
+ ## 允许的操作
24
+
25
+ - 枚举模块和导出
26
+ - 在指定模块或范围内扫描模式
27
+ - 读取有界内存并返回十六进制样本
28
+ - 短时 Hook、回调或事件监听
29
+ - 返回候选地址、RVA、对象关系和置信度
30
+
31
+ 每次脚本都要设置读取范围、最大 Hook 数、最大事件数和结束时间。默认禁止 `Stalker`;除非用户明确要求并给出极小范围、时长和事件上限,否则不要启用它。Hook、事件和内存读取都必须有上限。
32
+
33
+ ## 输出要求
34
+
35
+ ```json
36
+ {
37
+ "buildKey": "retail@12.1.0.69587",
38
+ "pid": 33976,
39
+ "module": { "name": "Wow.exe", "moduleBase": "0x7ff7b5040000" },
40
+ "fields": {
41
+ "playerAuras": [{ "address": "0x...", "bytesHex": "...", "source": "frida" }]
42
+ },
43
+ "evidence": [{ "source": "frida", "kind": "runtime-sample" }]
44
+ }
45
+ ```
46
+
47
+ 输出应足够让 IDA Pro MCP 或 iced-x86 在样本附近定位函数、字符串和 XREF,也应保留 Hook 的 `this`、参数、返回值和访问到的对象地址。拿不到字段时返回空数组和原因,不填充猜测地址。
48
+
49
+ ## 与静态分析交替
50
+
51
+ - 已有候选 RVA:优先直接 Hook,观察真实调用和返回值。
52
+ - 没有候选 RVA:先做小范围模块/字节扫描;仍不明确时导出最小 text 样本交给 IDA 或 iced。
53
+ - 静态给出候选后回到 dynamic 验证,不要求一次静态分析解决全部字段。
54
+ - 运行时值、用户触发动作和短时事件优先由 Frida 完成;长期稳定的 RVA、指针链和类型才需要静态证据。
@@ -0,0 +1,41 @@
1
+ # 证据工作流
2
+
3
+ ## 选择路径
4
+
5
+ 先判断用户要的是“一次当前值”还是“可复用 Reader 字段”。一次当前值优先走 Dynamic;可复用字段才在 Dynamic 结果基础上补 IDA/iced 和 Reader 验证。两者可以来回切换。
6
+
7
+ ## Dynamic(GumJS)
8
+
9
+ 脚本通过 `rpc.exports` 暴露入口;CLI 会把 `--args` 放到 `globalThis.__WOWDUMP_INPUT__`。脚本可枚举模块、读取有界内存、扫描模式、安装短时 Hook 或监听事件。输出至少包含:
10
+
11
+ - `buildKey`、进程 PID、模块名、`moduleBase`
12
+ - 候选地址或 `rva`、字节样本、来源和采样时间
13
+ - 每个请求字段对应的线索
14
+
15
+ 不要导出整段进程内存。把 `durationMs`、Hook 数量、事件数量和读取范围限制在请求所需范围内。
16
+
17
+ ## Disassemble(按需使用)
18
+
19
+ `analyze runtime --kind dump` 的 `manifest.json` 和段文件是静态分析输入;`--runtime-export` 仍可指向 dynamic JSON。只有 dynamic 线索不足以确认函数、布局或类型时才调用 IDA Pro MCP;没有时再用 iced-x86 解码。分析后回到 dynamic 或 `analyze runtime --kind verify --profile <file>` 验证。输出 profile 时,每个字段至少给出:
20
+
21
+ ```json
22
+ {
23
+ "name": "playerAuras",
24
+ "module": "Wow.exe",
25
+ "rva": "0x123456",
26
+ "pointerChain": [{ "offset": "0x20", "type": "pointer" }],
27
+ "type": "array",
28
+ "elementSize": 64,
29
+ "evidence": [{ "source": "ida-pro-mcp", "functionRva": "0x123000" }]
30
+ }
31
+ ```
32
+
33
+ 没有可靠定位时输出 `confidence: "candidate"`,不要填充猜测地址。
34
+
35
+ ## Reader
36
+
37
+ Reader 根据模块名、RVA、指针链和类型执行原子读取;它不负责猜测结构,也不执行 Frida/IDA。一次读取结果应保留实际地址、请求大小、已读字节、解析值和错误状态。
38
+
39
+ 需要连续数据时使用 `memory watch` 或重复 `memory read`。运行时模块基址可能变化,profile 保存 RVA,不保存本次运行的绝对地址。
40
+
41
+ ## Verify
@@ -0,0 +1,34 @@
1
+ # Profile 生命周期
2
+
3
+ ## 目录约定
4
+
5
+ 每次 `targets` 确认 buildKey 后,创建对应目录:
6
+
7
+ ```text
8
+ ~/.wowdump/<buildKey>/
9
+ └── profile/ # 只放用户确认保存的 profile
10
+ ├── player-state.json
11
+ └── combat-state.json
12
+ ```
13
+
14
+ buildKey 只能作为单层目录名使用。保留 `@`、点和连字符;拒绝路径分隔符、`..` 和其他会改变目录层级的字符。不同 build 必须使用不同目录。
15
+
16
+ ## 临时结果
17
+
18
+ 动态导出、反汇编候选和未确认的 profile 放到本次任务的临时目录,例如:
19
+
20
+ ```text
21
+ ~/.wowdump/runtime/<session-id>/
22
+ ```
23
+
24
+ 它们不是持久化 profile,任务结束后可以清理。不要把候选文件直接写入 `<buildKey>/profile/`。
25
+
26
+ ## 保存流程
27
+
28
+ 1. 汇总 dynamic 与必要的 IDA/iced 证据,检查 buildKey、模块、哈希、RVA 和字段证据一致;不要求固定先后顺序。
29
+ 2. 向用户展示候选 profile 的路径、字段和验证结果,询问是否保存为该 build 的持久化 profile。
30
+ 3. 用户确认后,将候选复制到 `~/.wowdump/<buildKey>/profile/<profile-id>.json`,保留 `buildKey`、来源和验证证据。
31
+ 4. 目标文件已存在时先询问覆盖,或使用新的 profile-id;不要静默覆盖。
32
+ 5. 后续读取优先使用这个绝对路径,或用 `wowdump profiles --directory ~/.wowdump/<buildKey>/profile` 列出它。
33
+
34
+ profile 只对记录的 buildKey 和模块布局有效。发现 buildKey 不匹配时停止读取并重新走分析流程,不修改旧 profile。
@@ -0,0 +1,28 @@
1
+ # 字段请求格式
2
+
3
+ 先把用户要查的内容写成一个请求文件。字段名是本次任务的目标,不是地址;地址由 dynamic 和 disassemble 阶段产生。
4
+
5
+ ```json
6
+ {
7
+ "schema": "wowdump.read-request.v1",
8
+ "target": { "pid": 33976, "buildKey": "retail@12.1.0.69587", "module": "Wow.exe" },
9
+ "fields": [
10
+ { "name": "playerAuras", "kind": "aura", "scope": "player", "direction": "both" },
11
+ { "name": "nearbyEnemies", "kind": "unit-list", "scope": "around-player" },
12
+ { "name": "cooldowns", "kind": "cooldown", "scope": "player" }
13
+ ],
14
+ "limits": { "maxUnits": 64, "maxAuras": 64, "maxEvents": 100, "durationMs": 5000 },
15
+ "dynamic": { "script": "discover-state.js", "stalker": { "enabled": false } }
16
+ }
17
+ ```
18
+
19
+ `kind` 和 `scope` 帮助选择脚本,不会被 CLI 当作固定语义。脚本可以返回不同的数据结构,但必须包含模块基址和每个候选的地址、RVA 或字节线索。
20
+
21
+ 常见字段映射:
22
+
23
+ - 我方 Buff:`kind: "aura", scope: "player", direction: "helpful"`
24
+ - 敌方 Debuff:`kind: "aura", scope: "target", direction: "harmful"`
25
+ - 技能冷却:`kind: "cooldown", scope: "player"`
26
+ - 周围敌人:`kind: "unit-list", scope: "around-player"`
27
+
28
+ 这些是查询意图。没有 reader-ready profile 定义前,不要把 dynamic 的一次性地址交给长期 reader。
@@ -0,0 +1,45 @@
1
+ # 混合分析决策
2
+
3
+ ## 先判断交付目标
4
+
5
+ - **只要当前值或短时事件**:优先 Dynamic。能从一次 GumJS 运行直接得到可信值,就结束本次查询,不生成持久化 profile。
6
+ - **要后续反复读取**:Dynamic 仍然先行,但必须补齐稳定的 RVA、指针链、类型、边界,并用 Reader 实际读通后再保存 profile。
7
+
8
+ ## 每轮只解决一个证据缺口
9
+
10
+ | 当前缺口 | 下一步 | 成功标准 |
11
+ | --- | --- | --- |
12
+ | 不知道模块/进程 | `wowdump targets` | PID、路径、buildKey、模块基址明确 |
13
+ | 不知道对象或函数是否被调用 | Dynamic GumJS 枚举、有限扫描或 Hook | 得到对象地址、候选函数或调用事件 |
14
+ | 需要给 IDA/iced 完整模块证据 | `wowdump analyze runtime --kind dump` | 得到 `.text`、`.rdata`、`.data`、`.pdata` 和 manifest |
15
+ | 有候选函数但不知道其语义 | Dynamic Hook,配合用户触发一次相关动作 | `this`、参数、返回值与目标字段相关 |
16
+ | 有调用但不知道字段偏移/类型 | IDA Pro MCP 局部反汇编;无 IDA 时 iced-x86 | 指令访问行为能解释对象和字段 |
17
+ | 静态候选需要运行时确认 | 回到 Dynamic Hook 或快照 | 候选 RVA 在当前模块命中且数据变化符合预期 |
18
+ | 字段定义已完成 | `wowdump memory read` | broker reader 实际读取成功,值与动态结果一致 |
19
+
20
+ ## 选择顺序
21
+
22
+ 1. 先检查是否已有同 build 的 `reader_ready` profile;有则直接 Reader,失败再回到 Dynamic。
23
+ 2. 没有 profile 时优先执行 Agent 编写的 GumJS。脚本可以直接 Hook 已知 RVA,也可以在限定范围内扫描候选。
24
+ 3. Dynamic 只拿到线索时,先用 `analyze runtime --kind dump` 导出相关 PE 段,再让 IDA Pro MCP 分析对应函数、字符串和交叉引用。不要把 dump 当成 profile。
25
+ 4. 没有 IDA 时使用 `analyze disassemble`/iced-x86 作为局部解码工具,得到候选后仍回到 Dynamic 验证。
26
+ 5. 每轮最多扩大一个维度:函数数量、扫描范围、读取字节数或采样时长。连续两轮没有新增证据时,停止扩大并请求用户触发相关游戏动作或确认字段范围。
27
+
28
+ ## 证据门槛
29
+
30
+ `reader_ready` 必须同时满足:
31
+
32
+ - buildKey、模块身份和模块布局匹配;
33
+ - RVA 来自同版本静态或运行时证据,且入口字节一致;
34
+ - `root`/对象获取路径、字段偏移、类型和边界有指令或运行时记录支持;
35
+ - broker-backed Reader 实际读取成功;
36
+ - 读取结果与 Dynamic 快照或 Hook 返回值一致,或差异有明确解释。
37
+
38
+ 任何一项缺失都保留 `candidate`,继续在 Dynamic 与 IDA/iced 之间切换,不填充猜测地址。
39
+
40
+ ## 运行约束
41
+
42
+ - GumJS 必须显式通过 `--script` 提供;每次运行自行负责 attach、Hook、采集、限时和 cleanup。
43
+ - Hook、扫描、事件、读取字节和总时长都设置上限;默认不启用 `Stalker`。
44
+ - 临时证据放在 `~/.wowdump/<buildKey>/runtime/<session>/`;只有用户确认的 profile 才进入 `profile/`。
45
+ - 不为单次查询修改 `src/` 或重新编译 CLI;实验逻辑放在临时 GumJS。
@@ -0,0 +1,133 @@
1
+ /*
2
+ * Copy-and-edit example for `wowdump analyze dynamic`.
3
+ * It is intentionally not selected or executed by the CLI automatically.
4
+ */
5
+ "use strict";
6
+
7
+ const input = globalThis.__WOWDUMP_INPUT__ && typeof globalThis.__WOWDUMP_INPUT__ === "object"
8
+ ? globalThis.__WOWDUMP_INPUT__
9
+ : {};
10
+
11
+ const limit = (value, fallback, maximum) => {
12
+ const number = Number(value);
13
+ if (!Number.isFinite(number) || number < 1) return fallback;
14
+ return Math.min(Math.floor(number), maximum);
15
+ };
16
+
17
+ const durationMs = limit(input.durationMs, 5000, 120000);
18
+ const maxEvents = limit(input.maxEvents, 100, 10000);
19
+ const maxHooks = limit(input.maxHooks, 1, 32);
20
+ const maxReadBytes = limit(input.maxReadBytes, 64, 256);
21
+ const events = [];
22
+ const candidates = [];
23
+ const errors = [];
24
+ const hooks = [];
25
+ let stopWaiting;
26
+
27
+ function record(event) {
28
+ if (events.length >= maxEvents) return false;
29
+ events.push({ timestamp: Date.now(), ...event });
30
+ if (events.length >= maxEvents && stopWaiting) stopWaiting();
31
+ return events.length < maxEvents;
32
+ }
33
+
34
+ function candidateAddress(candidate, module) {
35
+ if (!candidate || typeof candidate !== "object") return null;
36
+ if (candidate.address !== undefined) return ptr(String(candidate.address));
37
+ if (candidate.rva !== undefined) return module.base.add(ptr(String(candidate.rva)));
38
+ return null;
39
+ }
40
+
41
+ function inModule(address, module) {
42
+ const end = module.base.add(module.size);
43
+ return address.compare(module.base) >= 0 && address.compare(end) < 0;
44
+ }
45
+
46
+ function sampleBytes(address) {
47
+ try {
48
+ const range = Process.findRangeByAddress(address, "r--");
49
+ if (!range) return null;
50
+ const bytes = Memory.readByteArray(address, Math.min(maxReadBytes, range.size));
51
+ return bytes ? Array.from(new Uint8Array(bytes), value => value.toString(16).padStart(2, "0")).join("") : null;
52
+ } catch (error) {
53
+ errors.push(`read ${address}: ${error}`);
54
+ return null;
55
+ }
56
+ }
57
+
58
+ function installHook(address, name) {
59
+ if (hooks.length >= maxHooks) return;
60
+ try {
61
+ const listener = Interceptor.attach(address, {
62
+ onEnter(args) {
63
+ record({ kind: "enter", name, address: address.toString(), firstArg: args[0] ? args[0].toString() : null });
64
+ },
65
+ onLeave(retval) {
66
+ record({ kind: "leave", name, address: address.toString(), returnValue: retval.toString() });
67
+ }
68
+ });
69
+ hooks.push(listener);
70
+ } catch (error) {
71
+ errors.push(`hook ${name}: ${error}`);
72
+ }
73
+ }
74
+
75
+ rpc.exports = {
76
+ async collect() {
77
+ const moduleName = typeof input.module === "string" && input.module.trim() ? input.module : "Wow.exe";
78
+ const module = Process.findModuleByName(moduleName);
79
+ if (!module) {
80
+ return { ok: false, module: { name: moduleName }, candidates: [], events: [], evidence: [], errors: [`module ${moduleName} was not found`] };
81
+ }
82
+
83
+ const requested = Array.isArray(input.candidates) ? input.candidates.slice(0, maxHooks) : [];
84
+ const resolved = [];
85
+ for (const [index, value] of requested.entries()) {
86
+ try {
87
+ const address = candidateAddress(value, module);
88
+ if (!address || !inModule(address, module)) {
89
+ errors.push(`candidate ${index} is outside ${moduleName}`);
90
+ continue;
91
+ }
92
+ const item = {
93
+ name: value && typeof value.name === "string" ? value.name : `candidate_${index}`,
94
+ address: address.toString(),
95
+ rva: address.sub(module.base).toString(),
96
+ bytesHex: sampleBytes(address),
97
+ source: "frida"
98
+ };
99
+ candidates.push(item);
100
+ resolved.push({ address, name: item.name });
101
+ } catch (error) {
102
+ errors.push(`candidate ${index}: ${error}`);
103
+ }
104
+ }
105
+
106
+ for (const item of resolved) installHook(item.address, item.name);
107
+ let timer;
108
+ try {
109
+ await new Promise(resolve => {
110
+ stopWaiting = resolve;
111
+ timer = setTimeout(resolve, durationMs);
112
+ });
113
+ } finally {
114
+ stopWaiting = undefined;
115
+ if (timer) clearTimeout(timer);
116
+ for (const listener of hooks.splice(0)) {
117
+ try { listener.detach(); } catch (error) { errors.push(`cleanup: ${error}`); }
118
+ }
119
+ }
120
+
121
+ return {
122
+ ok: errors.length === 0,
123
+ module: { name: module.name, moduleBase: module.base.toString(), moduleSize: module.size },
124
+ moduleBase: module.base.toString(),
125
+ candidates,
126
+ events,
127
+ truncated: events.length >= maxEvents,
128
+ limits: { durationMs, maxEvents, maxHooks, maxReadBytes },
129
+ evidence: [{ source: "frida", kind: "dynamic-session", moduleBase: module.base.toString(), eventCount: events.length, hookCount: candidates.length }],
130
+ errors
131
+ };
132
+ }
133
+ };