wowdump 0.3.2 → 0.3.4
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 +10 -6
- package/dist/analysis/frida-runtime.js +26 -0
- package/dist/analysis/runtime-dump.js +93 -0
- package/dist/analysis/runtime-script.js +144 -24
- package/dist/cli.js +199 -38
- package/dist/core/build-store.js +166 -0
- package/dist/core/profile-engine.js +9 -0
- package/dist/frida-worker.js +59 -5
- package/dist/toolchain.js +11 -22
- package/package.json +5 -1
- package/skills/wowdump/SKILL.md +12 -9
- package/skills/wowdump/references/commands.md +30 -12
- package/skills/wowdump/references/disassemble.md +44 -10
- package/skills/wowdump/references/evidence-workflow.md +1 -1
- package/skills/wowdump/references/profiles.md +16 -5
- package/skills/wowdump/references/request-schema.md +1 -1
- package/skills/wowdump/references/workflow.md +3 -2
- package/dist/analysis/disassemble.js +0 -77
package/dist/toolchain.js
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createReadStream, readdirSync, statSync } from "node:fs";
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
3
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
3
|
import { homedir } from "node:os";
|
|
5
4
|
import { dirname, extname, join, resolve } from "node:path";
|
|
6
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
-
import {
|
|
8
|
-
export const WOWDUMP_HOME_DIRECTORIES = ["profiles", "logs", "runtime", "cache", "skills", "monitors", "toolchains"];
|
|
6
|
+
import { resolveWowdumpHome } from "./core/build-store.js";
|
|
9
7
|
export const DEFAULT_WOWDUMP_SKILL = `---
|
|
10
8
|
name: wowdump
|
|
11
|
-
description:
|
|
9
|
+
description: 用 Frida 运行时证据和 IDA Pro MCP 或 iced-x86 定位 WoW 字段,生成并验证 Reader profile。
|
|
12
10
|
---
|
|
13
11
|
|
|
14
12
|
# wowdump
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
先运行 \`wowdump targets\`,再复用 \`~/.wowdump/<buildKey>/database/\`。动态脚本必须由 Agent 显式提供;runtime dump 保存当前 session 的二进制证据,Reader 只读取 build profile。详情见 references/workflow.md、references/commands.md、references/dynamic.md、references/disassemble.md 和 references/profiles.md。
|
|
17
15
|
`;
|
|
18
16
|
export const DEFAULT_WOWDUMP_COMMANDS = `# wowdump 命令参考
|
|
19
17
|
|
|
20
18
|
1. \`wowdump targets\`
|
|
21
|
-
2. \`wowdump
|
|
22
|
-
3. \`wowdump analyze
|
|
23
|
-
4. \`wowdump analyze
|
|
24
|
-
5. \`wowdump
|
|
19
|
+
2. \`wowdump database status --build <buildKey>\`
|
|
20
|
+
3. \`wowdump analyze runtime --pid <pid> --build <buildKey> --kind dump --confirm\`
|
|
21
|
+
4. \`wowdump analyze dynamic --pid <pid> --build <buildKey> --script <GumJS> --export collect --confirm\`
|
|
22
|
+
5. \`wowdump analyze runtime --pid <pid> --build <buildKey> --kind verify --profile <profile.json> --confirm\`
|
|
23
|
+
6. \`wowdump memory read --pid <pid> --build <buildKey> --profile <profile.json> --field <name>\`
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
\`dump\` 默认流式导出供 IDA/iced-x86 分析的 PE 段并生成 manifest;\`verify\` 只验证已有候选 RVA。未确认的候选 profile 放在 runtime session 目录;确认后才复制到当前 build 的 profile 目录。
|
|
27
26
|
`;
|
|
28
27
|
function normalizePath(value) { return resolve(value.trim().replace(/^"|"$/g, "")); }
|
|
29
28
|
function directoryExists(value) { try {
|
|
@@ -48,7 +47,7 @@ catch (error) {
|
|
|
48
47
|
else
|
|
49
48
|
throw error;
|
|
50
49
|
} }
|
|
51
|
-
export
|
|
50
|
+
export { resolveWowdumpHome, sha256File } from "./core/build-store.js";
|
|
52
51
|
export function resolveWowdumpSkillHome(env = process.env, userHome = homedir()) { return normalizePath(env.WOWDUMP_SKILL_HOME || join(userHome, ".agents", "skills")); }
|
|
53
52
|
export async function initializeWowdumpHome(options = {}) {
|
|
54
53
|
const env = options.env ?? process.env;
|
|
@@ -56,15 +55,6 @@ export async function initializeWowdumpHome(options = {}) {
|
|
|
56
55
|
const created = [];
|
|
57
56
|
const preserved = [];
|
|
58
57
|
await mkdir(home, { recursive: true });
|
|
59
|
-
for (const name of WOWDUMP_HOME_DIRECTORIES) {
|
|
60
|
-
const dir = join(home, name);
|
|
61
|
-
if (directoryExists(dir))
|
|
62
|
-
preserved.push(dir);
|
|
63
|
-
else {
|
|
64
|
-
await mkdir(dir, { recursive: true });
|
|
65
|
-
created.push(dir);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
58
|
const configFile = join(home, "config.json");
|
|
69
59
|
await createFileIfMissing(configFile, `${JSON.stringify({ schema: "wowdump.config.v1" }, null, 2)}\n`, created, preserved);
|
|
70
60
|
const skillDirectory = join(normalizePath(options.skillHome ?? resolveWowdumpSkillHome(env, options.userHome)), "wowdump");
|
|
@@ -108,7 +98,6 @@ export async function initializeWowdumpHome(options = {}) {
|
|
|
108
98
|
}
|
|
109
99
|
export async function bootstrapToolchain(options = {}) { const home = await initializeWowdumpHome(options); return { ...home, toolchain: { ready: true, disassembler: "iced-x86" }, installed: [] }; }
|
|
110
100
|
export function resolveToolchain(options = {}) { return { home: normalizePath(options.home ?? resolveWowdumpHome(options.env ?? process.env, options.userHome)), ready: true, disassembler: "iced-x86" }; }
|
|
111
|
-
export async function sha256File(file) { const hash = createHash("sha256"); await pipeline(createReadStream(file), hash); return hash.digest("hex"); }
|
|
112
101
|
export function inspectPeSections(buffer) { if (buffer.length < 0x40 || buffer.toString("ascii", 0, 2) !== "MZ")
|
|
113
102
|
return []; const pe = buffer.readUInt32LE(0x3c); if (pe + 24 > buffer.length || buffer.toString("ascii", pe, pe + 4) !== "PE\0\0")
|
|
114
103
|
return []; const count = buffer.readUInt16LE(pe + 6); const optional = buffer.readUInt16LE(pe + 20); const table = pe + 24 + optional; const out = []; for (let i = 0; i < count; i++) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wowdump",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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": {
|
|
@@ -42,5 +42,9 @@
|
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^22.13.4",
|
|
44
44
|
"typescript": "^5.7.3"
|
|
45
|
+
},
|
|
46
|
+
"allowScripts": {
|
|
47
|
+
"frida@17.17.0": true,
|
|
48
|
+
"koffi@2.16.3": true
|
|
45
49
|
}
|
|
46
50
|
}
|
package/skills/wowdump/SKILL.md
CHANGED
|
@@ -5,18 +5,21 @@ description: 以 Frida 运行时探测为主,按证据需要交替使用 IDA P
|
|
|
5
5
|
|
|
6
6
|
# wowdump
|
|
7
7
|
|
|
8
|
-
用于读取角色属性、Buff/Debuff
|
|
8
|
+
用于读取角色属性、Buff/Debuff、技能冷却、附近单位等原生字段。每个 build 有一个静态数据库,运行时证据按 session 保存,Reader 只读取已验证的 profile。
|
|
9
9
|
|
|
10
10
|
## 默认策略
|
|
11
11
|
|
|
12
|
-
1. 先运行 `wowdump targets`,确认 PID、Wow.exe
|
|
13
|
-
2.
|
|
14
|
-
3.
|
|
15
|
-
4.
|
|
16
|
-
5.
|
|
17
|
-
6.
|
|
18
|
-
7.
|
|
12
|
+
1. 先运行 `wowdump targets`,确认 PID、Wow.exe 路径、哈希和 buildKey;多个目标时让用户选择。该命令会创建或复用 `~/.wowdump/<buildKey>/database/`。
|
|
13
|
+
2. 先检查 `wowdump database status --build <buildKey>`。同一可执行文件哈希、首选 image base 和模块大小时复用已有 IDA 数据库;哈希变化时数据库为 `stale`。
|
|
14
|
+
3. 有 `reader_ready` profile 时先用 `wowdump memory read --pid <pid> --build <buildKey> --profile <profile> --field <name>`;Reader 不读取 IDA 数据库、dump 二进制或 runtime JSON。
|
|
15
|
+
4. 没有可用 profile 时,由 Agent 编写并显式传入 GumJS:`wowdump analyze dynamic --pid <pid> --build <buildKey> --script <GumJS> --confirm`。脚本可做快照、有限 Hook 和事件采集。
|
|
16
|
+
5. 动态线索需要静态证据时运行 `wowdump analyze runtime --kind dump`。dump 只写当前 session 的 `.text/.rdata/.pdata`,`.data` 通过 `--sections` 按需加入;不创建新的静态数据库。
|
|
17
|
+
6. 优先让 IDA Pro MCP 打开该 build 的 `database/ida/Wow.i64`,按 manifest 将 runtime overlay 映射到原始 Wow.exe,输出函数 RVA、字符串 RVA、XREF、结构偏移和字段类型。没有 IDA MCP 时,用 iced-x86 对相关范围局部反汇编。
|
|
18
|
+
7. 将静态结果和动态证据合并为 runtime session 下的 `candidate.profile.json`,只保存 RVA/相对偏移,不保存绝对地址。
|
|
19
|
+
8. 用 `wowdump analyze runtime --kind verify --profile <candidate> --confirm` 做 broker 实读;确认 build/hash、地址、类型、边界和读取值一致后,才把 profile 复制到 `~/.wowdump/<buildKey>/profile/`。
|
|
19
20
|
|
|
20
21
|
不要把 dynamic、IDA 或 iced 固定成一次性线性流水线。选择下一步的依据是当前字段缺少什么证据:值用 dynamic,函数/布局用 IDA/iced,稳定读取用 reader。
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
具体决策见 [references/workflow.md](references/workflow.md);命令参数见 [references/commands.md](references/commands.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)。
|
|
24
|
+
|
|
25
|
+
IDA MCP 使用长 TTL 会话(默认 `idle_ttl_sec: 3600`)。每次查询前先做 health probe;出现 `worker not reachable` 时按 `references/disassemble.md` 重新打开并只重试一次。IDA worker 与 Windows broker 独立,broker 的 UAC 复用不会保持 IDA worker 存活。
|
|
@@ -21,31 +21,49 @@ wowdump analyze dynamic `
|
|
|
21
21
|
--export collect `
|
|
22
22
|
--args '{"fields":["playerAuras","nearbyEnemies","cooldowns"]}' `
|
|
23
23
|
--duration-ms 5000 `
|
|
24
|
-
--confirm
|
|
24
|
+
--confirm
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
CLI 会把结果保存到 `~/.wowdump/<buildKey>/runtime/<session-id>/dynamic.json`,并在 stdout 返回 session 目录。
|
|
28
|
+
|
|
29
|
+
导出供静态分析的运行时模块段。默认只导出 `.text/.rdata/.pdata`;需要运行时全局状态时显式加入 `.data`:
|
|
28
30
|
|
|
29
31
|
```powershell
|
|
30
32
|
wowdump analyze runtime `
|
|
31
33
|
--pid 33976 `
|
|
32
34
|
--build "retail@12.1.0.69587" `
|
|
33
|
-
--kind
|
|
34
|
-
--
|
|
35
|
-
--
|
|
36
|
-
--confirm > $HOME\.wowdump\runtime\<session-id>\text.json
|
|
35
|
+
--kind dump `
|
|
36
|
+
--sections .text .rdata .pdata `
|
|
37
|
+
--confirm
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
输出写入 `~/.wowdump/<buildKey>/runtime/<session-id>/dump/`,包含 `manifest.json` 和二进制段文件。Frida 按块发送,worker 直接写盘;stdout 只返回 manifest 路径、段摘要、字节数和 SHA-256,不打印 Base64 或整段十六进制。相同 build 哈希、段选择和限制参数会复用已有 manifest。
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
查看静态数据库状态:
|
|
42
43
|
|
|
43
44
|
```powershell
|
|
44
|
-
wowdump
|
|
45
|
-
|
|
45
|
+
wowdump database status --build "retail@12.1.0.69587"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
验证已有候选 profile:
|
|
49
|
+
|
|
50
|
+
```powershell
|
|
51
|
+
wowdump analyze runtime `
|
|
52
|
+
--pid 33976 `
|
|
46
53
|
--build "retail@12.1.0.69587" `
|
|
47
|
-
--
|
|
48
|
-
--
|
|
54
|
+
--kind verify `
|
|
55
|
+
--profile "$HOME\.wowdump\retail@12.1.0.69587\runtime\candidate.profile.json" `
|
|
56
|
+
--confirm
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`verify` 不负责发现地址;没有 profile 时会直接提示缺少验证目标。
|
|
60
|
+
|
|
61
|
+
## 静态定位
|
|
62
|
+
|
|
63
|
+
只有 dynamic 结果缺少函数 RVA、对象布局或字段类型时,才调用 IDA Pro MCP。优先复用 `~/.wowdump/<buildKey>/database/ida/Wow.i64`;runtime manifest 只作为 overlay 证据,不作为独立程序导入。没有 IDA MCP 时使用 iced-x86 对 manifest 指向的局部范围兜底。静态结果保存到当前 session 的 `ida-evidence.json`,候选 profile 保存到 `candidate.profile.json`。
|
|
64
|
+
|
|
65
|
+
```powershell
|
|
66
|
+
IDA MCP 或 iced-x86 的输出至少包含:函数 RVA、字符串 RVA、XREF、结构偏移、字段类型、调用关系和证据来源。
|
|
49
67
|
```
|
|
50
68
|
|
|
51
69
|
## 读取
|
|
@@ -1,18 +1,52 @@
|
|
|
1
1
|
# 反汇编与 IDA Pro 分支
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
静态分析是按需使用的证据放大器,不是每个查询的必经步骤。需要静态证据时,先用 `wowdump analyze runtime --kind dump` 导出运行时 `.text`、`.rdata`、`.pdata` 以及 manifest;`.data` 只有在字段需要运行时全局状态时才通过 `--sections` 加入。原始 Wow.exe 始终是 IDA 的主输入,runtime 二进制只是 overlay 证据。先检查当前 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 代码。
|
|
4
20
|
|
|
5
21
|
- 能调用时,让 MCP 只分析当前字段相关样本附近的函数、字符串、调用关系和结构,把结果保存为 JSON。随后回到 dynamic Hook 验证候选函数,不要直接把静态候选当成可读字段。
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+
}
|
|
14
38
|
```
|
|
15
39
|
|
|
16
|
-
|
|
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 不可用时,Agent 使用 npm 内置的 `iced-x86` 对 manifest 指向的局部 RVA 反汇编,输出模块 RVA、指令文本、访问偏移和证据。Agent 负责把这些线索与运行时字段对应起来,再用 dynamic 验证 `root`、`pointerChain`、`layout/type`、计数上限和停止条件。不要把一次运行的绝对地址写进持久化 profile。
|
|
17
51
|
|
|
18
52
|
输出中的 `readerStatus: "reader_ready"` 只适用于每个字段都有可验证 RVA/地址、类型和边界,并且 reader 实际读成功的情况;否则保持 `candidate`,回到 dynamic 或继续局部静态分析。只做一次性查询时可以不生成持久化 profile。
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
## Disassemble(按需使用)
|
|
18
18
|
|
|
19
|
-
|
|
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
20
|
|
|
21
21
|
```json
|
|
22
22
|
{
|
|
@@ -6,9 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
```text
|
|
8
8
|
~/.wowdump/<buildKey>/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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 --
|
|
43
|
+
5. 后续读取优先使用这个绝对路径,或用 `wowdump profiles --build <buildKey>` 列出它。Reader 只读取 profile,不读取 `database/ida` 和 `runtime/dump/*.bin`。
|
|
33
44
|
|
|
34
45
|
profile 只对记录的 buildKey 和模块布局有效。发现 buildKey 不匹配时停止读取并重新走分析流程,不修改旧 profile。
|
|
@@ -11,6 +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`、`.pdata` 和 manifest;`.data` 仅按需加入 |
|
|
14
15
|
| 有候选函数但不知道其语义 | Dynamic Hook,配合用户触发一次相关动作 | `this`、参数、返回值与目标字段相关 |
|
|
15
16
|
| 有调用但不知道字段偏移/类型 | IDA Pro MCP 局部反汇编;无 IDA 时 iced-x86 | 指令访问行为能解释对象和字段 |
|
|
16
17
|
| 静态候选需要运行时确认 | 回到 Dynamic Hook 或快照 | 候选 RVA 在当前模块命中且数据变化符合预期 |
|
|
@@ -20,8 +21,8 @@
|
|
|
20
21
|
|
|
21
22
|
1. 先检查是否已有同 build 的 `reader_ready` profile;有则直接 Reader,失败再回到 Dynamic。
|
|
22
23
|
2. 没有 profile 时优先执行 Agent 编写的 GumJS。脚本可以直接 Hook 已知 RVA,也可以在限定范围内扫描候选。
|
|
23
|
-
3. Dynamic
|
|
24
|
-
4. 没有 IDA
|
|
24
|
+
3. Dynamic 只拿到线索时,先用 `analyze runtime --kind dump` 导出当前 session 的相关 PE 段,再让 IDA Pro MCP 复用 build database 分析对应函数、字符串和交叉引用。不要把 dump 当成 profile。
|
|
25
|
+
4. 没有 IDA MCP 时直接使用 iced-x86 对 manifest 指向的局部范围解码,得到候选后仍回到 Dynamic 验证。
|
|
25
26
|
5. 每轮最多扩大一个维度:函数数量、扫描范围、读取字节数或采样时长。连续两轮没有新增证据时,停止扩大并请求用户触发相关游戏动作或确认字段范围。
|
|
26
27
|
|
|
27
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
|
-
}
|