wowdump 0.2.1 → 0.3.2
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/LICENSE +1 -1
- package/README.md +17 -111
- package/dist/adapters/reader.js +33 -0
- package/dist/analysis/disassemble.js +77 -0
- package/dist/{frida-runtime.js → analysis/frida-runtime.js} +3 -25
- package/dist/analysis/runtime-script.js +36 -0
- package/dist/cli.js +563 -0
- package/dist/core/profile-engine.js +238 -0
- package/dist/frida-worker.js +99 -0
- package/dist/reader/broker.js +475 -0
- package/dist/reader/client.js +1 -0
- package/dist/reader/launcher.js +219 -0
- package/dist/reader/main.js +100 -0
- package/dist/reader/protocol.js +1 -0
- package/dist/reader/windows.js +242 -0
- package/dist/reader-main.js +2 -0
- package/dist/toolchain.js +123 -0
- package/package.json +19 -37
- package/skills/wowdump/SKILL.md +22 -0
- package/skills/wowdump/references/commands.md +63 -0
- package/skills/wowdump/references/disassemble.md +18 -0
- package/skills/wowdump/references/dynamic.md +54 -0
- package/skills/wowdump/references/evidence-workflow.md +41 -0
- package/skills/wowdump/references/profiles.md +34 -0
- package/skills/wowdump/references/request-schema.md +28 -0
- package/skills/wowdump/references/workflow.md +44 -0
- package/skills/wowdump/scripts/dynamic-session.js +133 -0
- package/dist/agent.js +0 -1335
- package/dist/analysis-path.js +0 -38
- package/dist/analysis-process-log.js +0 -146
- package/dist/broker-client.js +0 -411
- package/dist/broker-codec.js +0 -148
- package/dist/broker-core.js +0 -1045
- package/dist/broker-gateway.js +0 -447
- package/dist/broker-ledger.js +0 -196
- package/dist/broker-main.js +0 -291
- package/dist/broker-protocol.js +0 -119
- package/dist/broker-runtime.js +0 -1283
- package/dist/broker-server.js +0 -466
- package/dist/build-bundle-loader.js +0 -183
- package/dist/build-bundle.js +0 -11
- package/dist/discovery.js +0 -59
- package/dist/dry-run.js +0 -38
- package/dist/error-log.js +0 -71
- package/dist/focus-errors.js +0 -63
- package/dist/focus-service.js +0 -1855
- package/dist/focused-session.js +0 -1357
- package/dist/mcp-main.js +0 -51
- package/dist/mcp.js +0 -924
- package/dist/observability.js +0 -41
- package/dist/process-log-lock.js +0 -195
- package/dist/processes.js +0 -47
- package/dist/runtime-config.js +0 -399
- package/dist/session.js +0 -145
- package/dist/storage.js +0 -12
- package/dist/wow-analysis.js +0 -1430
- package/resources/builds/retail/12.0.7.68974/build-profile.json +0 -290
- package/resources/builds/retail/12.0.7.68974/data-sources.json +0 -1633
- package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +0 -5130
- package/resources/builds/retail/12.0.7.68974/manifest.json +0 -63
- package/resources/builds/retail/12.0.7.68974/signatures.json +0 -260
- /package/dist/{adapters.js → core/build-adapters.js} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,132 +1,38 @@
|
|
|
1
1
|
# wowdump
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
WoW 原生内存分析 CLI。用 Frida 运行时证据和 IDA Pro/iced-x86 局部反汇编定位字段,生成可复核的 Reader profile。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
Run directly from an MCP client:
|
|
10
|
-
|
|
11
|
-
```json
|
|
12
|
-
{
|
|
13
|
-
"mcpServers": {
|
|
14
|
-
"wowdump": {
|
|
15
|
-
"command": "npx",
|
|
16
|
-
"args": [
|
|
17
|
-
"-y",
|
|
18
|
-
"wowdump",
|
|
19
|
-
"--game-root",
|
|
20
|
-
"D:\\Game\\World of Warcraft"
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Or install it globally:
|
|
5
|
+
## 安装
|
|
28
6
|
|
|
29
7
|
```powershell
|
|
30
8
|
npm install --global wowdump
|
|
31
|
-
wowdump
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
The `wowdump` executable only starts the stdio MCP server. It is not a separate interactive CLI.
|
|
35
|
-
|
|
36
|
-
## Game Directories
|
|
37
|
-
|
|
38
|
-
Game directories are resolved once when the MCP server starts. The first non-empty source wins:
|
|
39
|
-
|
|
40
|
-
1. Repeatable `--game-root <path>` arguments.
|
|
41
|
-
2. `WOWDUMP_GAME_ROOTS` as a semicolon-separated list on Windows.
|
|
42
|
-
3. `game_roots` in the TOML file selected by `--config <path>`.
|
|
43
|
-
4. `%LOCALAPPDATA%\wowdump\config.toml`.
|
|
44
|
-
5. Automatic detection from verified Battle.net Agent installation metadata, followed by common Windows installation locations.
|
|
45
|
-
|
|
46
|
-
Example persistent configuration:
|
|
47
|
-
|
|
48
|
-
```toml
|
|
49
|
-
game_roots = [
|
|
50
|
-
"D:/Game/World of Warcraft",
|
|
51
|
-
"E:/World of Warcraft"
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
broker_idle_ms = 1200000
|
|
9
|
+
wowdump init
|
|
55
10
|
```
|
|
56
11
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
PID, build, module base, Frida sessions, Hook IDs, and runtime addresses are never persisted in TOML. They are discovered and validated for the current process.
|
|
60
|
-
|
|
61
|
-
## Selecting A Target
|
|
62
|
-
|
|
63
|
-
Use these read-only tools before starting a dynamic session:
|
|
64
|
-
|
|
65
|
-
- `wow_install_list`: installed game directories and flavors.
|
|
66
|
-
- `wow_build_list`: detected build keys and versions.
|
|
67
|
-
- `wow_target_list`: running Wow.exe PIDs matched to an installation and build.
|
|
68
|
-
|
|
69
|
-
Focused and trace operations accept an explicit `pid` and `buildKey`. The server rejects PID/build mismatches and does not reuse a historical PID or module base.
|
|
12
|
+
安装脚本会初始化 `~/.wowdump`,并把 Skill 与 references 参考文档安装到 `~/.agents/skills/wowdump`。反汇编保底使用 npm 内置的 `iced-x86`。
|
|
70
13
|
|
|
71
|
-
##
|
|
14
|
+
## 基本流程
|
|
72
15
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
- `wow_lua_trace_*` and `wow_cpp_trace_*`: build-aware bounded trace sessions.
|
|
77
|
-
- `wow_focus_*`, `wow_watch_*`, and `wow_session_checkpoint`: explicit before/during/after analysis windows.
|
|
78
|
-
- `wow_data_source_*`, `wow_data_read`, and `wow_build_profile_validate`: verified build data and readers.
|
|
79
|
-
- `wow_broker_*`: Broker status, start, reconnect, and stop.
|
|
80
|
-
- `wow_analysis_coverage` and `wow_analysis_checkpoint`: static profile coverage and checkpoints.
|
|
81
|
-
|
|
82
|
-
The server rejects legacy MCP handshakes. Protocol output is written only to stdout; diagnostics are written to stderr.
|
|
83
|
-
|
|
84
|
-
## Runtime Layout
|
|
85
|
-
|
|
86
|
-
Production uses three distinct directory roles:
|
|
87
|
-
|
|
88
|
-
- Package `resources/builds`: read-only, build-specific profiles and signatures.
|
|
89
|
-
- `%LOCALAPPDATA%\wowdump`: writable Broker ledgers, focused sessions, logs, and cache.
|
|
90
|
-
- Repository `analyze/vm`: offline analysis inputs and intermediate evidence; it is not required by the installed package.
|
|
16
|
+
```powershell
|
|
17
|
+
# 1. 导出运行时模块或 provider
|
|
18
|
+
wowdump analyze runtime --pid 1234 --build "retail@VERSION" --kind text --duration-ms 5000 --max-events 100 --confirm > runtime.json
|
|
91
19
|
|
|
92
|
-
|
|
20
|
+
# 2. 通过 IDA Pro MCP(若可用)或 iced-x86 局部反汇编生成 profile
|
|
21
|
+
wowdump analyze disassemble --exe "C:\Games\World of Warcraft\_retail_\Wow.exe" --build "retail@VERSION" --runtime-export "$HOME\.wowdump\runtime\<session-id>\text.json" --output "$HOME\.wowdump\runtime\<session-id>\candidate.profile.json"
|
|
93
22
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
runtime_root = "C:/Users/ACCOUNT/AppData/Local/wowdump"
|
|
97
|
-
log_level = "info"
|
|
98
|
-
broker_idle_ms = 1200000
|
|
23
|
+
# 3. 用户确认后,把 profile 保存到当前 build 的 profile 目录,再读取
|
|
24
|
+
wowdump memory read --pid 1234 --profile "$HOME\.wowdump\retail@VERSION\profile\player-state.json" --field playerAuras
|
|
99
25
|
```
|
|
100
26
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
## Packaged Builds
|
|
104
|
-
|
|
105
|
-
This release includes a verified profile for:
|
|
106
|
-
|
|
107
|
-
- `retail@12.0.7.68974`
|
|
27
|
+
Windows reader 首次使用时由 Node 请求 UAC 启动 broker,后续调用复用同一 broker,空闲 20 分钟后退出。Frida 运行时导出需要显式 `--confirm`;高开销 Stalker 默认关闭。
|
|
108
28
|
|
|
109
|
-
|
|
29
|
+
当前 build 的持久化 profile 放在 `~/.wowdump/<buildKey>/profile/`;动态导出、静态候选和验证报告放在 runtime 会话临时目录。完整字段和 JSON 约定见 `~/.agents/skills/wowdump/references/commands.md`、`references/request-schema.md` 与 `references/profiles.md`;短时 Hook 示例见 `~/.agents/skills/wowdump/scripts/dynamic-session.js`。
|
|
110
30
|
|
|
111
|
-
##
|
|
31
|
+
## 开发验证
|
|
112
32
|
|
|
113
33
|
```powershell
|
|
114
|
-
npm install
|
|
115
34
|
npm run typecheck
|
|
116
|
-
npm test
|
|
117
35
|
npm run build
|
|
118
|
-
npm
|
|
119
|
-
npm run
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
To regenerate the packaged bundle from the repository-only analysis workspace:
|
|
123
|
-
|
|
124
|
-
```powershell
|
|
125
|
-
npm run bundle:build
|
|
36
|
+
npm test
|
|
37
|
+
npm pack --dry-run
|
|
126
38
|
```
|
|
127
|
-
|
|
128
|
-
`npm run package:verify` enforces the npm file whitelist, creates a real tarball, installs it in a temporary directory, starts the installed `wowdump` bin, and performs an MCP `2026-07-28` handshake.
|
|
129
|
-
|
|
130
|
-
## License
|
|
131
|
-
|
|
132
|
-
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function record(value) {
|
|
2
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3
|
+
}
|
|
4
|
+
/** Adapts the elevated reader broker to ProfileEngine's minimal memory API. */
|
|
5
|
+
export class ReaderProfileAdapter {
|
|
6
|
+
invoke;
|
|
7
|
+
constructor(invoke) {
|
|
8
|
+
this.invoke = invoke;
|
|
9
|
+
}
|
|
10
|
+
async modules(pid) {
|
|
11
|
+
const response = record(await this.invoke({ command: "modules", payload: { pid } }));
|
|
12
|
+
if (response.ok !== true)
|
|
13
|
+
throw new Error(String(record(response.error).message ?? "module enumeration failed"));
|
|
14
|
+
const values = Array.isArray(response.modules) ? response.modules : [];
|
|
15
|
+
return values.filter(value => value && typeof value === "object").map(value => {
|
|
16
|
+
const item = value;
|
|
17
|
+
return { name: String(item.name ?? ""), base: String(item.base ?? "0x0"), ...(item.size !== undefined ? { size: Number(item.size) } : {}), ...(item.path !== undefined ? { path: String(item.path) } : {}) };
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async read(pid, address, size) {
|
|
21
|
+
const response = record(await this.invoke({ command: "read", payload: { pid, address, size } }));
|
|
22
|
+
const error = response.error;
|
|
23
|
+
return {
|
|
24
|
+
ok: response.ok === true,
|
|
25
|
+
address: typeof response.address === "string" ? response.address : address,
|
|
26
|
+
requestedSize: Number(response.requestedSize ?? size),
|
|
27
|
+
bytesRead: Number(response.bytesRead ?? 0),
|
|
28
|
+
dataHex: typeof response.dataHex === "string" ? response.dataHex : undefined,
|
|
29
|
+
complete: response.complete === true,
|
|
30
|
+
...(error !== undefined ? { error } : {})
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
}
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
export const BROKER_MANAGED_COMMAND = Symbol("wowdump.broker-managed-command");
|
|
5
|
-
export function brokerManagedCommand(request, purpose) {
|
|
6
|
-
Object.defineProperty(request, BROKER_MANAGED_COMMAND, {
|
|
7
|
-
value: { purpose, irreversibleReason: "Broker-managed observation instrumentation is cleaned through the owned resource graph." },
|
|
8
|
-
enumerable: false,
|
|
9
|
-
configurable: false,
|
|
10
|
-
writable: false
|
|
11
|
-
});
|
|
12
|
-
return request;
|
|
13
|
-
}
|
|
14
4
|
const DEFAULT_MAX_SOURCE_BYTES = 4 * 1024 * 1024;
|
|
15
5
|
const DEFAULT_MAX_MEMORY_BYTES = 16 * 1024 * 1024;
|
|
16
6
|
const DEFAULT_SCRIPT_TIMEOUT_MS = 150;
|
|
@@ -196,9 +186,6 @@ function buildPrelude(context) {
|
|
|
196
186
|
buildKey: context?.buildKey,
|
|
197
187
|
flavor: context?.flavor,
|
|
198
188
|
executable: context?.executable,
|
|
199
|
-
processStartTime: context?.processStartTime,
|
|
200
|
-
moduleBase: context?.moduleBase,
|
|
201
|
-
moduleIdentity: context?.moduleIdentity,
|
|
202
189
|
adapter: context?.adapter
|
|
203
190
|
});
|
|
204
191
|
const verifiedPrelude = context?.adapter?.adapter?.fridaPrelude ?? "";
|
|
@@ -213,10 +200,7 @@ function requestContext(request, context, existing) {
|
|
|
213
200
|
...(request.buildKey === undefined ? {} : { buildKey: request.buildKey }),
|
|
214
201
|
...(request.flavor === undefined ? {} : { flavor: request.flavor }),
|
|
215
202
|
...(request.adapter === undefined ? {} : { adapter: request.adapter }),
|
|
216
|
-
...(request.executable === undefined ? {} : { executable: request.executable })
|
|
217
|
-
...(request.processStartTime === undefined ? {} : { processStartTime: request.processStartTime }),
|
|
218
|
-
...(request.moduleBase === undefined ? {} : { moduleBase: request.moduleBase }),
|
|
219
|
-
...(request.moduleIdentity === undefined ? {} : { moduleIdentity: request.moduleIdentity })
|
|
203
|
+
...(request.executable === undefined ? {} : { executable: request.executable })
|
|
220
204
|
};
|
|
221
205
|
}
|
|
222
206
|
function sessionContext(record) {
|
|
@@ -226,10 +210,7 @@ function sessionContext(record) {
|
|
|
226
210
|
...(context.buildKey === undefined ? {} : { buildKey: context.buildKey }),
|
|
227
211
|
...(context.flavor === undefined ? {} : { flavor: context.flavor }),
|
|
228
212
|
...(context.adapter === undefined ? {} : { adapter: context.adapter }),
|
|
229
|
-
...(context.executable === undefined ? {} : { executable: context.executable })
|
|
230
|
-
...(context.processStartTime === undefined ? {} : { processStartTime: context.processStartTime }),
|
|
231
|
-
...(context.moduleBase === undefined ? {} : { moduleBase: context.moduleBase }),
|
|
232
|
-
...(context.moduleIdentity === undefined ? {} : { moduleIdentity: context.moduleIdentity })
|
|
213
|
+
...(context.executable === undefined ? {} : { executable: context.executable })
|
|
233
214
|
};
|
|
234
215
|
}
|
|
235
216
|
function serializeDevice(device) {
|
|
@@ -514,10 +495,6 @@ export class FridaCommandRuntime {
|
|
|
514
495
|
const record = this.sessions.get(id);
|
|
515
496
|
if (!record)
|
|
516
497
|
throw new Error(`session ${id} was not found`);
|
|
517
|
-
// Detach is an at-most-once terminal transition. Frida may report an
|
|
518
|
-
// error after teardown has already started, so retaining the record would
|
|
519
|
-
// let Broker cleanup call detach again on a half-destroyed agent.
|
|
520
|
-
this.sessions.delete(id);
|
|
521
498
|
for (const scriptId of [...record.scripts.keys()])
|
|
522
499
|
await this.unloadScript(scriptId).catch(() => undefined);
|
|
523
500
|
try {
|
|
@@ -525,6 +502,7 @@ export class FridaCommandRuntime {
|
|
|
525
502
|
}
|
|
526
503
|
catch { /* bridge cleanup is best effort */ }
|
|
527
504
|
await record.session.detach();
|
|
505
|
+
this.sessions.delete(id);
|
|
528
506
|
}
|
|
529
507
|
async resume(request, context) {
|
|
530
508
|
const pid = this.requirePid(request, context);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Built-in bounded sampler used by `analyze runtime`; dynamic stays caller-owned. */
|
|
2
|
+
export const RUNTIME_EXPORT_SCRIPT = String.raw `
|
|
3
|
+
"use strict";
|
|
4
|
+
const input = globalThis.__WOWDUMP_INPUT__ || {};
|
|
5
|
+
const moduleName = typeof input.module === "string" && input.module ? input.module : "Wow.exe";
|
|
6
|
+
const module = Process.findModuleByName(moduleName);
|
|
7
|
+
const limit = Math.min(Math.max(Number(input.maxEvents || 16), 1), 256);
|
|
8
|
+
const sampleBytes = Math.min(Math.max(Number(input.sampleBytes || 64), 1), 256);
|
|
9
|
+
function bytes(address, size) { const value = address.readByteArray(size); return value ? Array.from(new Uint8Array(value), byte => byte.toString(16).padStart(2, "0")).join("") : ""; }
|
|
10
|
+
function collect() {
|
|
11
|
+
if (!module) return { ok: false, module: { name: moduleName }, records: [], evidence: [{ source: "frida", kind: "module-missing" }] };
|
|
12
|
+
if (input.kind === "text") {
|
|
13
|
+
const ranges = Process.enumerateRanges("r-x").filter(range => range.base.compare(module.base) >= 0 && range.base.compare(module.base.add(module.size)) < 0);
|
|
14
|
+
const textRanges = ranges.map(range => ({ base: range.base.toString(), rva: range.base.sub(module.base).toString(), size: range.size, protection: range.protection }));
|
|
15
|
+
const records = [];
|
|
16
|
+
for (const range of ranges) {
|
|
17
|
+
if (records.length >= limit) break;
|
|
18
|
+
const rva = range.base.sub(module.base);
|
|
19
|
+
const offset = rva.isNull() && range.size > 0x1000 ? 0x1000 : 0;
|
|
20
|
+
const address = range.base.add(offset);
|
|
21
|
+
records.push({ address: address.toString(), rva: address.sub(module.base).toString(), size: Math.min(sampleBytes, range.size - offset), bytesHex: bytes(address, Math.min(sampleBytes, range.size - offset)) });
|
|
22
|
+
}
|
|
23
|
+
return { ok: true, kind: "text", module: { name: module.name, moduleBase: module.base.toString(), moduleSize: module.size }, textRanges, samples: records, records, evidence: [{ source: "frida", kind: "frida-runtime-text", samples: records.length }] };
|
|
24
|
+
}
|
|
25
|
+
const profile = input.profile && typeof input.profile === "object" ? input.profile : {};
|
|
26
|
+
const candidates = Array.isArray(profile.providers) ? profile.providers : [];
|
|
27
|
+
const records = [];
|
|
28
|
+
for (const candidate of candidates.slice(0, limit)) {
|
|
29
|
+
if (!candidate || typeof candidate.rva !== "string") continue;
|
|
30
|
+
const address = module.base.add(ptr(candidate.rva));
|
|
31
|
+
records.push({ name: candidate.name || "candidate", rva: candidate.rva, runtimeAddress: address.toString(), bytesHex: bytes(address, Math.min(Number(candidate.size || 16), 256)), source: "frida" });
|
|
32
|
+
}
|
|
33
|
+
return { ok: true, module: { name: module.name, moduleBase: module.base.toString(), moduleSize: module.size }, records, evidence: [{ source: "frida", kind: "frida-runtime-read", count: records.length }] };
|
|
34
|
+
}
|
|
35
|
+
rpc.exports = { collect };
|
|
36
|
+
`;
|