wowdump 0.3.3 → 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.
@@ -6,9 +6,20 @@
6
6
 
7
7
  ```text
8
8
  ~/.wowdump/<buildKey>/
9
- └── profile/ # 只放用户确认保存的 profile
10
- ├── player-state.json
11
- └── combat-state.json
9
+ ├── build.json
10
+ ├── database/
11
+ │ ├── database.json
12
+ │ └── ida/Wow.i64
13
+ ├── profile/ # 只放用户确认保存的 profile
14
+ │ ├── player-state.json
15
+ │ └── combat-state.json
16
+ └── runtime/<session-id>/ # 当前任务证据
17
+ ├── target.json
18
+ ├── dynamic.json
19
+ ├── dump/manifest.json
20
+ ├── ida-evidence.json
21
+ ├── candidate.profile.json
22
+ └── verify.json
12
23
  ```
13
24
 
14
25
  buildKey 只能作为单层目录名使用。保留 `@`、点和连字符;拒绝路径分隔符、`..` 和其他会改变目录层级的字符。不同 build 必须使用不同目录。
@@ -18,7 +29,7 @@ buildKey 只能作为单层目录名使用。保留 `@`、点和连字符;拒
18
29
  动态导出、反汇编候选和未确认的 profile 放到本次任务的临时目录,例如:
19
30
 
20
31
  ```text
21
- ~/.wowdump/runtime/<session-id>/
32
+ ~/.wowdump/<buildKey>/runtime/<session-id>/
22
33
  ```
23
34
 
24
35
  它们不是持久化 profile,任务结束后可以清理。不要把候选文件直接写入 `<buildKey>/profile/`。
@@ -29,6 +40,6 @@ buildKey 只能作为单层目录名使用。保留 `@`、点和连字符;拒
29
40
  2. 向用户展示候选 profile 的路径、字段和验证结果,询问是否保存为该 build 的持久化 profile。
30
41
  3. 用户确认后,将候选复制到 `~/.wowdump/<buildKey>/profile/<profile-id>.json`,保留 `buildKey`、来源和验证证据。
31
42
  4. 目标文件已存在时先询问覆盖,或使用新的 profile-id;不要静默覆盖。
32
- 5. 后续读取优先使用这个绝对路径,或用 `wowdump profiles --directory ~/.wowdump/<buildKey>/profile` 列出它。
43
+ 5. 后续读取优先使用这个绝对路径,或用 `wowdump profiles --build <buildKey>` 列出它。Reader 只读取 profile,不读取 `database/ida` 和 `runtime/dump/*.bin`。
33
44
 
34
45
  profile 只对记录的 buildKey 和模块布局有效。发现 buildKey 不匹配时停止读取并重新走分析流程,不修改旧 profile。
@@ -1,6 +1,6 @@
1
1
  # 字段请求格式
2
2
 
3
- 先把用户要查的内容写成一个请求文件。字段名是本次任务的目标,不是地址;地址由 dynamic disassemble 阶段产生。
3
+ 先把用户要查的内容写成一个请求文件。字段名是本次任务的目标,不是地址;地址由 dynamic 证据和 IDA Pro MCP/iced-x86 局部分析产生。
4
4
 
5
5
  ```json
6
6
  {
@@ -11,7 +11,7 @@
11
11
  | --- | --- | --- |
12
12
  | 不知道模块/进程 | `wowdump targets` | PID、路径、buildKey、模块基址明确 |
13
13
  | 不知道对象或函数是否被调用 | Dynamic GumJS 枚举、有限扫描或 Hook | 得到对象地址、候选函数或调用事件 |
14
- | 需要给 IDA/iced 完整模块证据 | `wowdump analyze runtime --kind dump` | 得到 `.text`、`.rdata`、`.data`、`.pdata` 和 manifest |
14
+ | 需要给 IDA/iced 模块证据 | `wowdump analyze runtime --kind dump` | 得到默认 `.text`、`.rdata`、`.pdata` 和 manifest;`.data` 仅按需加入 |
15
15
  | 有候选函数但不知道其语义 | Dynamic Hook,配合用户触发一次相关动作 | `this`、参数、返回值与目标字段相关 |
16
16
  | 有调用但不知道字段偏移/类型 | IDA Pro MCP 局部反汇编;无 IDA 时 iced-x86 | 指令访问行为能解释对象和字段 |
17
17
  | 静态候选需要运行时确认 | 回到 Dynamic Hook 或快照 | 候选 RVA 在当前模块命中且数据变化符合预期 |
@@ -21,8 +21,8 @@
21
21
 
22
22
  1. 先检查是否已有同 build 的 `reader_ready` profile;有则直接 Reader,失败再回到 Dynamic。
23
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 验证。
24
+ 3. Dynamic 只拿到线索时,先用 `analyze runtime --kind dump` 导出当前 session 的相关 PE 段,再让 IDA Pro MCP 复用 build database 分析对应函数、字符串和交叉引用。不要把 dump 当成 profile。
25
+ 4. 没有 IDA MCP 时直接使用 iced-x86 manifest 指向的局部范围解码,得到候选后仍回到 Dynamic 验证。
26
26
  5. 每轮最多扩大一个维度:函数数量、扫描范围、读取字节数或采样时长。连续两轮没有新增证据时,停止扩大并请求用户触发相关游戏动作或确认字段范围。
27
27
 
28
28
  ## 证据门槛
@@ -1,77 +0,0 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
- import { access } from "node:fs/promises";
3
- import { constants } from "node:fs";
4
- import { resolve } from "node:path";
5
- import { Decoder, DecoderOptions, Formatter, FormatterSyntax } from "iced-x86";
6
- import { inspectPeSections, sha256File } from "../toolchain.js";
7
- function record(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : {}; }
8
- function array(value) { return Array.isArray(value) ? value.filter(item => item && typeof item === "object" && !Array.isArray(item)) : []; }
9
- function hex(value) { if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0)
10
- return BigInt(value); if (typeof value === "string" && /^0x[0-9a-f]+$/i.test(value.trim()))
11
- return BigInt(value); return undefined; }
12
- function hexText(value) { return `0x${value.toString(16)}`; }
13
- function moduleInfo(runtime) { const module = record(runtime.module); const base = hex(module.moduleBase ?? module.base ?? runtime.moduleBase) ?? 0n; const size = Number(module.moduleSize ?? module.size ?? runtime.moduleSize ?? 0); return { base, size: Number.isFinite(size) ? size : 0, name: String(module.name ?? "Wow.exe") }; }
14
- function sampleBytes(item) { const value = item.bytesHex ?? item.dataHex; if (typeof value !== "string" || !/^[0-9a-f]*$/i.test(value) || value.length % 2 !== 0)
15
- return undefined; return Buffer.from(value, "hex"); }
16
- function sampleRva(item) { return hex(item.rva) ?? (hex(item.address) !== undefined ? hex(item.address) : undefined); }
17
- function decodeSamples(runtime, maxInstructions) {
18
- const info = moduleInfo(runtime);
19
- const samples = [...array(runtime.samples), ...array(runtime.records)];
20
- const formatter = new Formatter(FormatterSyntax.Masm);
21
- const output = [];
22
- let remaining = maxInstructions;
23
- for (const sample of samples) {
24
- if (remaining <= 0)
25
- break;
26
- const bytes = sampleBytes(sample);
27
- const raw = sampleRva(sample);
28
- if (!bytes || raw === undefined)
29
- continue;
30
- const rva = raw >= info.base && info.base > 0n ? raw - info.base : raw;
31
- const ip = info.base + rva;
32
- const decoder = new Decoder(64, bytes, DecoderOptions.None);
33
- decoder.ip = ip;
34
- const instructions = [];
35
- while (remaining > 0 && decoder.canDecode) {
36
- const instruction = decoder.decode();
37
- if (instruction.code === 0)
38
- break;
39
- instructions.push(formatter.format(instruction));
40
- remaining--;
41
- }
42
- output.push({ rva: hexText(rva), address: hexText(ip), bytesHex: bytes.toString("hex"), instructions, source: "frida-text" });
43
- }
44
- return output;
45
- }
46
- function fieldCandidates(runtime, ida) {
47
- const candidates = {};
48
- const source = { ...record(runtime.fields), ...record(ida.fields) };
49
- for (const [name, value] of Object.entries(source)) {
50
- const item = record(value);
51
- const rva = item.rva ?? record(item.root).rva;
52
- const type = item.type ?? (record(item.layout).type);
53
- const layout = item.layout;
54
- if (rva !== undefined && (typeof type === "string" || (layout && typeof layout === "object" && !Array.isArray(layout))))
55
- candidates[name] = { ...item, root: item.root ?? { rva }, ...(type ? { type } : {}), evidence: [...array(item.evidence), { source: ida && Object.keys(ida).length ? "ida-pro-mcp" : "iced-x86", kind: "runtime-guided" }] };
56
- }
57
- return candidates;
58
- }
59
- export async function runDisassembly(options) {
60
- const executable = resolve(options.exe);
61
- await access(executable, constants.R_OK);
62
- if (!options.buildKey?.trim())
63
- throw new Error("buildKey is required");
64
- const runtime = record(JSON.parse(await readFile(resolve(options.runtimeExport), "utf8")));
65
- const ida = options.idaEvidence ? record(JSON.parse(await readFile(resolve(options.idaEvidence), "utf8"))) : {};
66
- const info = moduleInfo(runtime);
67
- const decoded = decodeSamples(runtime, Math.min(Math.max(options.maxInstructions ?? 256, 1), 10_000));
68
- const sections = inspectPeSections(await readFile(executable));
69
- const fields = fieldCandidates(runtime, ida);
70
- const ready = Object.keys(fields).length > 0 && Object.values(fields).every(field => { const value = record(field); return value.root !== undefined && (value.type !== undefined || value.layout !== undefined); });
71
- const outputFile = resolve(options.output ?? `${executable}.${options.buildKey.replace(/[^A-Za-z0-9_.@-]+/g, "-")}.profile.json`);
72
- const profile = { schema: "wowdump.profile.v1", id: `${options.buildKey}-runtime`, buildKey: options.buildKey, readerStatus: ready ? "reader_ready" : "candidate", confidence: ready ? "confirmed" : "candidate", module: { name: info.name, rvaBase: "0x0" }, executable: { path: executable, sha256: await sha256File(executable) }, sections, fields, evidence: [{ source: "frida", kind: "runtime-text", samples: decoded.length }, { source: options.idaEvidence ? "ida-pro-mcp" : "iced-x86", kind: "disassembly", instructions: decoded.length }], analysis: { engine: options.idaEvidence ? "ida-pro-mcp+iced-x86" : "iced-x86", generatedAt: new Date().toISOString() } };
73
- const disassembly = { module: { name: info.name, moduleBase: hexText(info.base), moduleSize: info.size }, sections, instructions: decoded, idaEvidence: options.idaEvidence ?? null };
74
- if (!options.dryRun)
75
- await writeFile(outputFile, `${JSON.stringify(profile, null, 2)}\n`, "utf8");
76
- return { ok: true, command: "analyze.disassemble", engine: options.idaEvidence ? "ida-pro-mcp" : "iced-x86", buildKey: options.buildKey, executable, outputFile, profile, disassembly };
77
- }