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/dist/cli.js
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, realpathSync, readFileSync } from "node:fs";
|
|
4
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { WindowsBrokerManager } from "./reader/launcher.js";
|
|
9
|
+
import { initializeWowdumpHome, bootstrapToolchain, resolveToolchain, resolveWowdumpHome } from "./toolchain.js";
|
|
10
|
+
import { runDisassembly } from "./analysis/disassemble.js";
|
|
11
|
+
import { RUNTIME_EXPORT_SCRIPT } from "./analysis/runtime-script.js";
|
|
12
|
+
import { ProfileEngine } from "./core/profile-engine.js";
|
|
13
|
+
import { ReaderProfileAdapter } from "./adapters/reader.js";
|
|
14
|
+
import { enumerateWindowsProcesses } from "./reader/windows.js";
|
|
15
|
+
export function parseBuildInfo(content, file) {
|
|
16
|
+
const lines = content.split(/\r?\n/).filter(line => line.trim());
|
|
17
|
+
const headers = lines[0]?.split("|") ?? [];
|
|
18
|
+
const versionIndex = headers.indexOf("Version!STRING:0");
|
|
19
|
+
const activeIndex = headers.indexOf("Active!DEC:1");
|
|
20
|
+
const productIndex = headers.indexOf("Product!STRING:0");
|
|
21
|
+
if (versionIndex < 0 || activeIndex < 0 || productIndex < 0)
|
|
22
|
+
return null;
|
|
23
|
+
const row = lines.slice(1).map(line => line.split("|")).find(values => values[activeIndex] === "1" && values[productIndex] === "wow");
|
|
24
|
+
const version = row?.[versionIndex]?.trim();
|
|
25
|
+
const product = row?.[productIndex]?.trim();
|
|
26
|
+
if (!version || !product)
|
|
27
|
+
return null;
|
|
28
|
+
return { fileVersion: version, product, buildKey: `retail@${version}`, buildInfoFile: file };
|
|
29
|
+
}
|
|
30
|
+
async function discoverWowTargets(selectedPid, elevatedModules) {
|
|
31
|
+
if (process.platform !== "win32")
|
|
32
|
+
return [];
|
|
33
|
+
const targets = [];
|
|
34
|
+
const rows = enumerateWindowsProcesses()
|
|
35
|
+
.filter(item => /^Wow\.exe$/i.test(item.name))
|
|
36
|
+
.filter(item => selectedPid === undefined || item.pid === selectedPid);
|
|
37
|
+
for (const item of rows) {
|
|
38
|
+
const pid = item.pid;
|
|
39
|
+
const name = item.name;
|
|
40
|
+
if (!Number.isSafeInteger(pid) || pid < 1 || !/^Wow\.exe$/i.test(name) || (selectedPid !== undefined && pid !== selectedPid))
|
|
41
|
+
continue;
|
|
42
|
+
let path = item.path;
|
|
43
|
+
let moduleBase = null;
|
|
44
|
+
let moduleSize = null;
|
|
45
|
+
let diagnostic;
|
|
46
|
+
if ((!path || !moduleBase) && elevatedModules) {
|
|
47
|
+
try {
|
|
48
|
+
const elevated = elevatedModules(pid);
|
|
49
|
+
const response = await elevated;
|
|
50
|
+
const modules = response && typeof response === "object" && Array.isArray(response.modules)
|
|
51
|
+
? response.modules
|
|
52
|
+
: [];
|
|
53
|
+
const main = modules.find(module => /^Wow\.exe$/i.test(String(module.name ?? ""))) ?? modules[0];
|
|
54
|
+
if (!path && typeof main?.path === "string" && main.path)
|
|
55
|
+
path = main.path;
|
|
56
|
+
if (!moduleBase && typeof main?.base === "string")
|
|
57
|
+
moduleBase = main.base;
|
|
58
|
+
if (moduleSize === null && Number.isFinite(Number(main?.size)))
|
|
59
|
+
moduleSize = Number(main?.size);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
diagnostic = diagnostic ?? (error instanceof Error ? error.message : String(error));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let buildInfoFile = null;
|
|
66
|
+
let fileVersion = null;
|
|
67
|
+
let product = null;
|
|
68
|
+
if (path) {
|
|
69
|
+
const candidates = [join(dirname(path), ".build.info"), join(dirname(dirname(path)), ".build.info")];
|
|
70
|
+
for (const candidate of candidates) {
|
|
71
|
+
try {
|
|
72
|
+
const text = await readFile(candidate, "utf8");
|
|
73
|
+
const parsed = parseBuildInfo(text, candidate);
|
|
74
|
+
if (parsed) {
|
|
75
|
+
fileVersion = parsed.fileVersion;
|
|
76
|
+
product = parsed.product;
|
|
77
|
+
buildInfoFile = parsed.buildInfoFile;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch { /* try the next parent directory */ }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
targets.push({ pid, name, path, moduleBase, moduleSize, fileVersion, product, buildKey: fileVersion ? `retail@${fileVersion}` : null, ...(buildInfoFile ? { buildInfoFile } : {}), ...(diagnostic ? { diagnostics: { frida: diagnostic } } : {}) });
|
|
85
|
+
}
|
|
86
|
+
return targets;
|
|
87
|
+
}
|
|
88
|
+
function packageVersion() {
|
|
89
|
+
const override = process.env.WOWDUMP_VERSION?.trim();
|
|
90
|
+
if (override)
|
|
91
|
+
return override;
|
|
92
|
+
try {
|
|
93
|
+
const packageFile = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
94
|
+
const packageJson = JSON.parse(readFileSync(packageFile, "utf8"));
|
|
95
|
+
return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return "0.0.0";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function installedDistDirectory() {
|
|
102
|
+
return fileURLToPath(new URL(".", import.meta.url));
|
|
103
|
+
}
|
|
104
|
+
class CliError extends Error {
|
|
105
|
+
code;
|
|
106
|
+
details;
|
|
107
|
+
constructor(code, message, details) {
|
|
108
|
+
super(message);
|
|
109
|
+
this.name = "CliError";
|
|
110
|
+
this.code = code;
|
|
111
|
+
this.details = details;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function positiveInteger(value, field) {
|
|
115
|
+
const result = Number(value);
|
|
116
|
+
if (!Number.isSafeInteger(result) || result <= 0)
|
|
117
|
+
throw new CliError("ARGUMENT_INVALID", `${field} must be a positive integer`);
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function nonNegativeInteger(value, field) {
|
|
121
|
+
const result = Number(value);
|
|
122
|
+
if (!Number.isSafeInteger(result) || result < 0)
|
|
123
|
+
throw new CliError("ARGUMENT_INVALID", `${field} must be a non-negative integer`);
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
function jsonRecord(value, field) {
|
|
127
|
+
let decoded;
|
|
128
|
+
try {
|
|
129
|
+
decoded = JSON.parse(value);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
throw new CliError("ARGUMENT_INVALID", `${field} must be valid JSON`);
|
|
133
|
+
}
|
|
134
|
+
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
|
|
135
|
+
throw new CliError("ARGUMENT_INVALID", `${field} must be a JSON object`);
|
|
136
|
+
}
|
|
137
|
+
return decoded;
|
|
138
|
+
}
|
|
139
|
+
function writeJson(io, value) {
|
|
140
|
+
io.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
141
|
+
}
|
|
142
|
+
async function defaultSidecar(file, args, input) {
|
|
143
|
+
return new Promise(resolveResult => {
|
|
144
|
+
const child = spawn(file, args, { windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
145
|
+
let stdout = "";
|
|
146
|
+
let stderr = "";
|
|
147
|
+
child.stdout.setEncoding("utf8");
|
|
148
|
+
child.stderr.setEncoding("utf8");
|
|
149
|
+
child.stdout.on("data", value => { stdout += value; });
|
|
150
|
+
child.stderr.on("data", value => { stderr += value; });
|
|
151
|
+
child.once("error", error => resolveResult({ exitCode: 1, stdout, stderr: `${stderr}${error.message}` }));
|
|
152
|
+
child.once("exit", code => resolveResult({ exitCode: code ?? 1, stdout, stderr }));
|
|
153
|
+
if (input !== undefined)
|
|
154
|
+
child.stdin.end(input);
|
|
155
|
+
else
|
|
156
|
+
child.stdin.end();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function brokerCommandLine(env, packageDist) {
|
|
160
|
+
const configured = env.WOWDUMP_READER_COMMAND?.trim();
|
|
161
|
+
if (configured)
|
|
162
|
+
return { file: configured, args: [] };
|
|
163
|
+
const candidate = join(packageDist, "reader-main.js");
|
|
164
|
+
return existsSync(candidate) ? { file: process.execPath, args: [candidate, "--request-stdio"] } : null;
|
|
165
|
+
}
|
|
166
|
+
async function defaultBroker(invocation, env, packageDist, sidecar) {
|
|
167
|
+
const command = brokerCommandLine(env, packageDist);
|
|
168
|
+
if (!command) {
|
|
169
|
+
throw new CliError("BROKER_NOT_CONFIGURED", "reader broker entry was not found", { expected: join(packageDist, "reader-main.js"), environment: "WOWDUMP_READER_COMMAND" });
|
|
170
|
+
}
|
|
171
|
+
const result = await (sidecar ?? defaultSidecar)(command.file, command.args, `${JSON.stringify(invocation)}\n`);
|
|
172
|
+
if (result.exitCode !== 0)
|
|
173
|
+
throw new CliError("BROKER_FAILED", result.stderr.trim() || `reader broker exited with ${result.exitCode}`);
|
|
174
|
+
const line = result.stdout.split(/\r?\n/).find(value => value.trim());
|
|
175
|
+
if (!line)
|
|
176
|
+
throw new CliError("BROKER_PROTOCOL_ERROR", "reader broker returned no JSON response");
|
|
177
|
+
try {
|
|
178
|
+
return JSON.parse(line);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
throw new CliError("BROKER_PROTOCOL_ERROR", "reader broker returned invalid JSON", { stdout: result.stdout });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function runFridaWorker(worker, input, timeoutMs, broker) {
|
|
185
|
+
if (process.platform !== "win32") {
|
|
186
|
+
throw new CliError("PLATFORM_UNSUPPORTED", "elevated Frida broker is currently supported on Windows only");
|
|
187
|
+
}
|
|
188
|
+
const raw = await broker({ command: "frida", payload: { worker, input, timeoutMs } });
|
|
189
|
+
const response = raw && typeof raw === "object" ? raw : {};
|
|
190
|
+
if (response.ok === false) {
|
|
191
|
+
const error = response.error && typeof response.error === "object" ? response.error : {};
|
|
192
|
+
throw new CliError("BROKER_FAILED", String(error.message ?? "elevated Frida worker failed"), { response });
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
exitCode: Number.isSafeInteger(Number(response.exitCode)) ? Number(response.exitCode) : 1,
|
|
196
|
+
stdout: typeof response.stdout === "string" ? response.stdout : "",
|
|
197
|
+
stderr: typeof response.stderr === "string" ? response.stderr : ""
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const windowsBrokerManagers = new Map();
|
|
201
|
+
function persistentWindowsBroker(home, packageDist) {
|
|
202
|
+
const key = resolve(home).toLowerCase();
|
|
203
|
+
let manager = windowsBrokerManagers.get(key);
|
|
204
|
+
if (!manager) {
|
|
205
|
+
manager = new WindowsBrokerManager({ home, readerEntry: join(packageDist, "reader-main.js") });
|
|
206
|
+
windowsBrokerManagers.set(key, manager);
|
|
207
|
+
}
|
|
208
|
+
return manager;
|
|
209
|
+
}
|
|
210
|
+
async function listProfileFiles(directory) {
|
|
211
|
+
try {
|
|
212
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
213
|
+
return entries
|
|
214
|
+
.filter(entry => entry.isFile() && extname(entry.name).toLowerCase() === ".json")
|
|
215
|
+
.map(entry => join(directory, entry.name))
|
|
216
|
+
.sort((left, right) => left.localeCompare(right));
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (error.code === "ENOENT")
|
|
220
|
+
return [];
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function readProfileSummary(file) {
|
|
225
|
+
const profile = jsonRecord(await readFile(file, "utf8"), file);
|
|
226
|
+
return {
|
|
227
|
+
id: typeof profile.id === "string" ? profile.id : basename(file, extname(file)),
|
|
228
|
+
file,
|
|
229
|
+
buildKey: profile.buildKey ?? null,
|
|
230
|
+
confidence: profile.confidence ?? "unverified",
|
|
231
|
+
executableSha256: profile.executableSha256
|
|
232
|
+
?? (profile.executable && typeof profile.executable === "object" && !Array.isArray(profile.executable)
|
|
233
|
+
? profile.executable.sha256 ?? null
|
|
234
|
+
: null)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function profileFile(profileRoot, idOrFile) {
|
|
238
|
+
if (isAbsolute(idOrFile))
|
|
239
|
+
return resolve(idOrFile);
|
|
240
|
+
const safeId = idOrFile.replace(/\.json$/i, "");
|
|
241
|
+
if (!/^[A-Za-z0-9_.@-]+$/.test(safeId))
|
|
242
|
+
throw new CliError("ARGUMENT_INVALID", "profile ID contains unsupported characters");
|
|
243
|
+
return join(profileRoot, `${safeId}.json`);
|
|
244
|
+
}
|
|
245
|
+
function selected(source, keys) {
|
|
246
|
+
return Object.fromEntries(keys.filter(key => source[key] !== undefined).map(key => [key, source[key]]));
|
|
247
|
+
}
|
|
248
|
+
function requireRuntimeConfirmation(options) {
|
|
249
|
+
if (options.confirm === true)
|
|
250
|
+
return;
|
|
251
|
+
throw new CliError("CONFIRMATION_REQUIRED", "runtime export requires --confirm", {
|
|
252
|
+
pid: options.pid,
|
|
253
|
+
buildKey: options.build,
|
|
254
|
+
operation: "runtime",
|
|
255
|
+
kind: options.kind,
|
|
256
|
+
maxHooks: options.maxHooks,
|
|
257
|
+
durationMs: options.durationMs,
|
|
258
|
+
maxEvents: options.maxEvents,
|
|
259
|
+
cleanupDeadlineMs: options.cleanupDeadlineMs
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
export function createWowdumpCli(dependencies = {}) {
|
|
263
|
+
const io = dependencies.io ?? { stdout: process.stdout, stderr: process.stderr };
|
|
264
|
+
const env = dependencies.env ?? process.env;
|
|
265
|
+
const cwd = resolve(dependencies.cwd ?? process.cwd());
|
|
266
|
+
const packageDist = installedDistDirectory();
|
|
267
|
+
const home = resolveWowdumpHome(env);
|
|
268
|
+
const broker = dependencies.broker ?? (process.platform === "win32" && !dependencies.sidecar
|
|
269
|
+
? (invocation => persistentWindowsBroker(home, packageDist).request(invocation))
|
|
270
|
+
: (invocation => defaultBroker(invocation, env, packageDist, dependencies.sidecar)));
|
|
271
|
+
const baseSidecar = dependencies.sidecar ?? defaultSidecar;
|
|
272
|
+
const sidecar = async (file, args, input) => {
|
|
273
|
+
const worker = args.length === 1 && /\.m?js$/i.test(args[0] ?? "") ? args[0] : undefined;
|
|
274
|
+
if (worker && input !== undefined)
|
|
275
|
+
return runFridaWorker(worker, input, 120_000, broker);
|
|
276
|
+
return baseSidecar(file, args, input);
|
|
277
|
+
};
|
|
278
|
+
const profileEngine = new ProfileEngine();
|
|
279
|
+
const profileAdapter = new ReaderProfileAdapter(broker);
|
|
280
|
+
const program = new Command()
|
|
281
|
+
.name("wowdump")
|
|
282
|
+
.description("Build-aware WoW native memory analysis CLI")
|
|
283
|
+
.version(packageVersion())
|
|
284
|
+
.showHelpAfterError()
|
|
285
|
+
.configureOutput({ writeOut: value => io.stdout.write(value), writeErr: value => io.stderr.write(value) });
|
|
286
|
+
program.command("targets")
|
|
287
|
+
.description("Discover running WoW targets, executable paths, and build versions")
|
|
288
|
+
.option("--pid <pid>", "select one process ID")
|
|
289
|
+
.action(async (options) => {
|
|
290
|
+
const selectedPid = options.pid ? positiveInteger(options.pid, "pid") : undefined;
|
|
291
|
+
const targets = await discoverWowTargets(selectedPid, async (pid) => broker({ command: "modules", payload: { pid } }));
|
|
292
|
+
if (selectedPid !== undefined && targets.length === 0)
|
|
293
|
+
throw new CliError("TARGET_NOT_FOUND", `Wow.exe process ${selectedPid} was not found`);
|
|
294
|
+
writeJson(io, {
|
|
295
|
+
ok: targets.length > 0,
|
|
296
|
+
command: "targets",
|
|
297
|
+
selected: targets.length === 1 ? targets[0] : null,
|
|
298
|
+
requiresSelection: selectedPid === undefined && targets.length > 1,
|
|
299
|
+
targets
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
program.command("target")
|
|
303
|
+
.description("Show target, reader broker, profile, monitor, and Frida state")
|
|
304
|
+
.option("--pid <pid>", "target process ID")
|
|
305
|
+
.option("--build <buildKey>", "expected build key")
|
|
306
|
+
.action(async (options) => {
|
|
307
|
+
const payload = {
|
|
308
|
+
...(options.pid ? { pid: positiveInteger(options.pid, "pid") } : {}),
|
|
309
|
+
...(options.build ? { buildKey: options.build } : {})
|
|
310
|
+
};
|
|
311
|
+
const status = await broker({ command: "status", payload });
|
|
312
|
+
writeJson(io, { ok: true, command: "target", home, broker: status, toolchain: resolveToolchain({ home, env }) });
|
|
313
|
+
});
|
|
314
|
+
program.command("profiles")
|
|
315
|
+
.description("List profiles or describe one profile")
|
|
316
|
+
.argument("[id-or-file]", "profile ID or absolute JSON file")
|
|
317
|
+
.option("--directory <path>", "profile directory", join(home, "profiles"))
|
|
318
|
+
.action(async (idOrFile, options) => {
|
|
319
|
+
const directory = resolve(options.directory);
|
|
320
|
+
if (idOrFile) {
|
|
321
|
+
const file = profileFile(directory, idOrFile);
|
|
322
|
+
const profile = jsonRecord(await readFile(file, "utf8"), file);
|
|
323
|
+
writeJson(io, { ok: true, command: "profiles.describe", file, profile });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const files = await listProfileFiles(directory);
|
|
327
|
+
writeJson(io, { ok: true, command: "profiles.list", directory, profiles: await Promise.all(files.map(readProfileSummary)) });
|
|
328
|
+
});
|
|
329
|
+
const memory = program.command("memory").description("Read or monitor profile-defined native memory");
|
|
330
|
+
memory.command("read")
|
|
331
|
+
.description("Perform one bounded read")
|
|
332
|
+
.requiredOption("--pid <pid>", "target process ID")
|
|
333
|
+
.option("--build <buildKey>", "expected build key")
|
|
334
|
+
.option("--profile <id>", "profile ID")
|
|
335
|
+
.option("--field <name...>", "field names")
|
|
336
|
+
.option("--address <hex>", "explicit address as hexadecimal text")
|
|
337
|
+
.option("--size <bytes>", "bounded byte count")
|
|
338
|
+
.option("--request <json>", "complete read request as JSON")
|
|
339
|
+
.action(async (options) => {
|
|
340
|
+
const requested = options.request ? jsonRecord(options.request, "request") : undefined;
|
|
341
|
+
const payload = requested ?? {
|
|
342
|
+
pid: positiveInteger(options.pid, "pid"),
|
|
343
|
+
...(options.build ? { buildKey: options.build } : {}),
|
|
344
|
+
...(options.profile ? { profileId: options.profile } : {}),
|
|
345
|
+
...(options.field ? { fields: options.field } : {}),
|
|
346
|
+
...(options.address ? { address: options.address } : {}),
|
|
347
|
+
...(options.size ? { size: positiveInteger(options.size, "size") } : {})
|
|
348
|
+
};
|
|
349
|
+
const pid = positiveInteger(String(payload.pid ?? options.pid), "pid");
|
|
350
|
+
const profileId = typeof payload.profileId === "string" ? payload.profileId : (typeof options.profile === "string" ? options.profile : undefined);
|
|
351
|
+
if (profileId && payload.address === undefined) {
|
|
352
|
+
const profileFilePath = profileFile(join(home, "profiles"), profileId);
|
|
353
|
+
const profile = jsonRecord(await readFile(profileFilePath, "utf8"), profileFilePath);
|
|
354
|
+
const result = await profileEngine.read({
|
|
355
|
+
pid,
|
|
356
|
+
buildKey: typeof payload.buildKey === "string" ? payload.buildKey : undefined,
|
|
357
|
+
profile,
|
|
358
|
+
fields: Array.isArray(payload.fields) ? payload.fields.map(String) : undefined
|
|
359
|
+
}, profileAdapter);
|
|
360
|
+
writeJson(io, { ...result, profileFile: profileFilePath });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
writeJson(io, await broker({ command: "read", payload }));
|
|
364
|
+
});
|
|
365
|
+
const watch = memory.command("watch").description("Manage broker-owned memory monitors");
|
|
366
|
+
watch.command("start")
|
|
367
|
+
.description("Start a monitor")
|
|
368
|
+
.requiredOption("--pid <pid>", "target process ID")
|
|
369
|
+
.option("--build <buildKey>", "expected build key")
|
|
370
|
+
.option("--profile <id>", "profile ID")
|
|
371
|
+
.option("--field <name...>", "field names")
|
|
372
|
+
.option("--address <hex>", "explicit address as hexadecimal text")
|
|
373
|
+
.option("--size <bytes>", "bounded byte count")
|
|
374
|
+
.option("--interval <ms>", "sample interval", "250")
|
|
375
|
+
.option("--max-samples <count>", "sample limit", "1000")
|
|
376
|
+
.option("--all-samples", "emit unchanged samples")
|
|
377
|
+
.action(async (options) => {
|
|
378
|
+
if (options.profile)
|
|
379
|
+
throw new CliError("PROFILE_WATCH_UNSUPPORTED", "profile watch requires a fixed address; use repeated memory read calls for dynamic pointer fields");
|
|
380
|
+
writeJson(io, await broker({ command: "watch.start", payload: {
|
|
381
|
+
pid: positiveInteger(options.pid, "pid"),
|
|
382
|
+
...(options.build ? { buildKey: options.build } : {}),
|
|
383
|
+
...(options.profile ? { profileId: options.profile } : {}),
|
|
384
|
+
...(options.field ? { fields: options.field } : {}),
|
|
385
|
+
...(options.address ? { address: options.address } : {}),
|
|
386
|
+
...(options.size ? { size: positiveInteger(options.size, "size") } : {}),
|
|
387
|
+
intervalMs: positiveInteger(options.interval, "interval"),
|
|
388
|
+
maxSamples: positiveInteger(options.maxSamples, "max-samples"),
|
|
389
|
+
changeOnly: options.allSamples !== true
|
|
390
|
+
} }));
|
|
391
|
+
});
|
|
392
|
+
watch.command("poll")
|
|
393
|
+
.description("Poll monitor events")
|
|
394
|
+
.requiredOption("--id <watchId>", "monitor ID")
|
|
395
|
+
.option("--after <sequence>", "sequence cursor", "0")
|
|
396
|
+
.action(async (options) => writeJson(io, await broker({ command: "watch.poll", payload: {
|
|
397
|
+
watchId: options.id,
|
|
398
|
+
afterSequence: nonNegativeInteger(options.after, "after")
|
|
399
|
+
} })));
|
|
400
|
+
watch.command("stop")
|
|
401
|
+
.description("Stop a monitor and release its resources")
|
|
402
|
+
.requiredOption("--id <watchId>", "monitor ID")
|
|
403
|
+
.action(async (options) => writeJson(io, await broker({ command: "watch.stop", payload: { watchId: options.id } })));
|
|
404
|
+
const analyze = program.command("analyze").description("Run Frida evidence export and runtime-guided disassembly");
|
|
405
|
+
analyze.command("disassemble")
|
|
406
|
+
.description("Decode Frida text evidence and produce a Reader profile")
|
|
407
|
+
.requiredOption("--exe <path>", "path to Wow.exe")
|
|
408
|
+
.requiredOption("--build <buildKey>", "build key")
|
|
409
|
+
.requiredOption("--runtime-export <file>", "Frida runtime text JSON")
|
|
410
|
+
.option("--ida-evidence <file>", "JSON produced by an IDA Pro MCP analysis")
|
|
411
|
+
.option("--output <file>", "profile output JSON")
|
|
412
|
+
.option("--max-instructions <count>", "maximum decoded instructions", "256")
|
|
413
|
+
.option("--dry-run", "validate inputs without writing a profile")
|
|
414
|
+
.action(async (options) => writeJson(io, await runDisassembly({
|
|
415
|
+
exe: options.exe,
|
|
416
|
+
buildKey: options.build,
|
|
417
|
+
runtimeExport: options.runtimeExport,
|
|
418
|
+
...(options.idaEvidence ? { idaEvidence: options.idaEvidence } : {}),
|
|
419
|
+
...(options.output ? { output: options.output } : {}),
|
|
420
|
+
maxInstructions: positiveInteger(options.maxInstructions, "max-instructions"),
|
|
421
|
+
dryRun: options.dryRun === true
|
|
422
|
+
})));
|
|
423
|
+
analyze.command("runtime")
|
|
424
|
+
.description("Read runtime evidence through the elevated Frida broker")
|
|
425
|
+
.requiredOption("--pid <pid>", "target process ID")
|
|
426
|
+
.requiredOption("--build <buildKey>", "build key")
|
|
427
|
+
.option("--build-key <buildKey>", "alias for --build")
|
|
428
|
+
.option("--kind <kind>", "text or providers", "providers")
|
|
429
|
+
.option("--worker <path>", "Frida worker entry")
|
|
430
|
+
.option("--profile <path>", "existing reader profile containing candidate RVAs")
|
|
431
|
+
.option("--max-hooks <count>", "maximum hooks", "1")
|
|
432
|
+
.option("--duration-ms <ms>", "maximum duration", "5000")
|
|
433
|
+
.option("--max-events <count>", "maximum events", "100")
|
|
434
|
+
.option("--cleanup-deadline-ms <ms>", "cleanup deadline", "2000")
|
|
435
|
+
.option("--confirm", "confirm the exact bounded Frida operation")
|
|
436
|
+
.action(async (options) => {
|
|
437
|
+
const normalized = {
|
|
438
|
+
pid: positiveInteger(options.pid, "pid"),
|
|
439
|
+
build: options.build ?? options.buildKey,
|
|
440
|
+
kind: options.kind,
|
|
441
|
+
maxHooks: positiveInteger(options.maxHooks, "max-hooks"),
|
|
442
|
+
durationMs: positiveInteger(options.durationMs, "duration-ms"),
|
|
443
|
+
maxEvents: positiveInteger(options.maxEvents, "max-events"),
|
|
444
|
+
cleanupDeadlineMs: positiveInteger(options.cleanupDeadlineMs, "cleanup-deadline-ms"),
|
|
445
|
+
confirm: options.confirm === true
|
|
446
|
+
};
|
|
447
|
+
if (!["text", "providers"].includes(normalized.kind))
|
|
448
|
+
throw new CliError("ARGUMENT_INVALID", "kind must be text or providers");
|
|
449
|
+
requireRuntimeConfirmation(normalized);
|
|
450
|
+
const worker = resolve(options.worker ?? env.WOWDUMP_FRIDA_WORKER ?? join(packageDist, "frida-worker.js"));
|
|
451
|
+
if (!existsSync(worker))
|
|
452
|
+
throw new CliError("FRIDA_WORKER_NOT_FOUND", `Frida worker was not found: ${worker}`);
|
|
453
|
+
const profile = options.profile
|
|
454
|
+
? jsonRecord(await readFile(resolve(options.profile), "utf8"), options.profile)
|
|
455
|
+
: undefined;
|
|
456
|
+
const request = {
|
|
457
|
+
command: "dynamic-script",
|
|
458
|
+
pid: normalized.pid,
|
|
459
|
+
build: normalized.build,
|
|
460
|
+
source: RUNTIME_EXPORT_SCRIPT,
|
|
461
|
+
exportName: "collect",
|
|
462
|
+
args: {
|
|
463
|
+
kind: normalized.kind,
|
|
464
|
+
maxHooks: normalized.maxHooks,
|
|
465
|
+
durationMs: normalized.durationMs,
|
|
466
|
+
maxEvents: normalized.maxEvents,
|
|
467
|
+
cleanupDeadlineMs: normalized.cleanupDeadlineMs,
|
|
468
|
+
...(profile ? { profile } : {})
|
|
469
|
+
},
|
|
470
|
+
callArgs: [],
|
|
471
|
+
durationMs: normalized.durationMs
|
|
472
|
+
};
|
|
473
|
+
const result = await sidecar(process.execPath, [worker], `${JSON.stringify(request)}\n`);
|
|
474
|
+
if (result.exitCode !== 0)
|
|
475
|
+
throw new CliError("FRIDA_WORKER_FAILED", result.stderr.trim() || `Frida worker exited with ${result.exitCode}`);
|
|
476
|
+
const line = result.stdout.split(/\r?\n/).find(value => value.trim());
|
|
477
|
+
const value = line ? jsonRecord(line, "runtime result") : { ok: true, stdout: result.stdout };
|
|
478
|
+
writeJson(io, { ...value, command: "analyze.runtime" });
|
|
479
|
+
});
|
|
480
|
+
analyze.command("dynamic")
|
|
481
|
+
.description("Run a caller-supplied GumJS script through Frida")
|
|
482
|
+
.requiredOption("--pid <pid>", "target process ID")
|
|
483
|
+
.requiredOption("--build <buildKey>", "build key")
|
|
484
|
+
.requiredOption("--script <path>", "GumJS script file")
|
|
485
|
+
.option("--export <name>", "rpc.exports function to call")
|
|
486
|
+
.option("--args <json>", "JSON object exposed as __WOWDUMP_INPUT__", "{}")
|
|
487
|
+
.option("--call-args <json>", "JSON array passed to the exported function", "[]")
|
|
488
|
+
.option("--duration-ms <ms>", "maximum script duration", "5000")
|
|
489
|
+
.option("--confirm", "confirm the exact Frida operation")
|
|
490
|
+
.action(async (options) => {
|
|
491
|
+
const normalized = {
|
|
492
|
+
pid: positiveInteger(options.pid, "pid"),
|
|
493
|
+
build: options.build,
|
|
494
|
+
script: resolve(options.script),
|
|
495
|
+
...(options.export ? { exportName: options.export } : {}),
|
|
496
|
+
args: jsonRecord(options.args, "args"),
|
|
497
|
+
callArgs: JSON.parse(options.callArgs),
|
|
498
|
+
durationMs: positiveInteger(options.durationMs, "duration-ms"),
|
|
499
|
+
confirm: options.confirm === true
|
|
500
|
+
};
|
|
501
|
+
if (!Array.isArray(normalized.callArgs))
|
|
502
|
+
throw new CliError("ARGUMENT_INVALID", "call-args must be a JSON array");
|
|
503
|
+
requireRuntimeConfirmation(normalized);
|
|
504
|
+
if (!existsSync(normalized.script))
|
|
505
|
+
throw new CliError("FRIDA_SCRIPT_NOT_FOUND", `Frida script was not found: ${normalized.script}`);
|
|
506
|
+
const worker = resolve(env.WOWDUMP_FRIDA_WORKER ?? join(packageDist, "frida-worker.js"));
|
|
507
|
+
if (!existsSync(worker))
|
|
508
|
+
throw new CliError("FRIDA_WORKER_NOT_FOUND", `Frida worker was not found: ${worker}`);
|
|
509
|
+
const request = { command: "dynamic-script", ...selected(normalized, ["pid", "build", "script", "exportName", "args", "callArgs", "durationMs"]) };
|
|
510
|
+
const result = await sidecar(process.execPath, [worker], `${JSON.stringify(request)}\n`);
|
|
511
|
+
if (result.exitCode !== 0)
|
|
512
|
+
throw new CliError("FRIDA_WORKER_FAILED", result.stderr.trim() || `Frida worker exited with ${result.exitCode}`);
|
|
513
|
+
const line = result.stdout.split(/\r?\n/).find(value => value.trim());
|
|
514
|
+
writeJson(io, line ? JSON.parse(line) : { ok: true, command: "analyze.dynamic", stdout: result.stdout });
|
|
515
|
+
});
|
|
516
|
+
program.command("init")
|
|
517
|
+
.description("Initialize WOWDUMP_HOME without overwriting existing files")
|
|
518
|
+
.option("--skip-toolchain-download", "kept for compatibility; no external toolchain is installed")
|
|
519
|
+
.action(async (options) => {
|
|
520
|
+
const installMissing = options.skipToolchainDownload !== true && env.WOWDUMP_SKIP_TOOLCHAIN_DOWNLOAD !== "1";
|
|
521
|
+
const result = await bootstrapToolchain({ home, env, installMissing });
|
|
522
|
+
writeJson(io, {
|
|
523
|
+
ok: result.toolchain.ready,
|
|
524
|
+
command: "init",
|
|
525
|
+
...result,
|
|
526
|
+
toolchain: result.toolchain
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
return program;
|
|
530
|
+
}
|
|
531
|
+
export async function runWowdumpCli(argv = process.argv, dependencies = {}) {
|
|
532
|
+
const io = dependencies.io ?? { stdout: process.stdout, stderr: process.stderr };
|
|
533
|
+
try {
|
|
534
|
+
await createWowdumpCli(dependencies).parseAsync([...argv]);
|
|
535
|
+
return 0;
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
const failure = error;
|
|
539
|
+
io.stderr.write(`${JSON.stringify({
|
|
540
|
+
ok: false,
|
|
541
|
+
error: {
|
|
542
|
+
code: typeof failure.code === "string" ? failure.code : "CLI_FAILED",
|
|
543
|
+
message: error instanceof Error ? error.message : String(error),
|
|
544
|
+
...(failure.details !== undefined ? { details: failure.details } : {})
|
|
545
|
+
}
|
|
546
|
+
})}\n`);
|
|
547
|
+
return 1;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
let invokedFile = "";
|
|
551
|
+
if (process.argv[1]) {
|
|
552
|
+
const invokedPath = resolve(process.argv[1]);
|
|
553
|
+
try {
|
|
554
|
+
invokedFile = pathToFileURL(realpathSync(invokedPath)).href;
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
invokedFile = pathToFileURL(invokedPath).href;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (invokedFile === import.meta.url) {
|
|
561
|
+
await initializeWowdumpHome();
|
|
562
|
+
process.exitCode = await runWowdumpCli();
|
|
563
|
+
}
|