wowdump 0.3.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 +21 -21
- package/README.md +21 -53
- package/dist/adapters/reader.js +33 -0
- package/dist/analysis/disassemble.js +77 -0
- package/dist/{frida-runtime.js → analysis/frida-runtime.js} +48 -48
- package/dist/analysis/runtime-script.js +36 -0
- package/dist/cli.js +255 -189
- package/dist/core/profile-engine.js +238 -0
- package/dist/frida-worker.js +54 -55
- package/dist/{reader-broker.js → reader/broker.js} +15 -0
- package/dist/{reader-client.js → reader/client.js} +1 -1
- package/dist/{windows-launcher.js → reader/launcher.js} +16 -4
- 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 +1 -66
- package/dist/toolchain.js +102 -573
- package/package.json +9 -10
- package/skills/wowdump/SKILL.md +22 -15
- 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 -1332
- package/dist/discovery.js +0 -48
- package/dist/dry-run.js +0 -36
- package/dist/error-log.js +0 -71
- package/dist/focused-session.js +0 -89
- package/dist/ghidra.js +0 -769
- package/dist/main.js +0 -66
- package/dist/observability.js +0 -41
- package/dist/processes.js +0 -44
- package/dist/session.js +0 -42
- package/dist/storage.js +0 -12
- package/dist/windows-reader.js +0 -102
- package/dist/wow-analysis.js +0 -1405
- package/skills/wowdump/commands.md +0 -44
- /package/dist/{adapters.js → core/build-adapters.js} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,82 +1,105 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { existsSync, realpathSync } from "node:fs";
|
|
3
|
+
import { existsSync, realpathSync, readFileSync } from "node:fs";
|
|
4
4
|
import { readFile, readdir } from "node:fs/promises";
|
|
5
|
-
import { basename, extname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
-
import { WindowsBrokerManager } from "./
|
|
9
|
-
import { bootstrapToolchain, resolveToolchain, resolveWowdumpHome } from "./toolchain.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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));
|
|
36
63
|
}
|
|
37
|
-
: undefined
|
|
38
|
-
};
|
|
39
|
-
if (options.dryRun) {
|
|
40
|
-
const resolved = resolveToolchain(options);
|
|
41
|
-
if (!resolved.ghidra || !resolved.java) {
|
|
42
|
-
throw new CliError("TOOLCHAIN_INCOMPLETE", `Ghidra toolchain is incomplete: missing ${resolved.missing.join(", ")}`, resolved);
|
|
43
64
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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";
|
|
65
99
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
command: "analyze.static",
|
|
70
|
-
dryRun: false,
|
|
71
|
-
buildKey: result.buildKey,
|
|
72
|
-
executable: result.executable.path,
|
|
73
|
-
executableSha256: result.executable.sha256,
|
|
74
|
-
outputFile: options.evidenceFile ?? join(options.outputDirectory ?? resolveWowdumpHome(options.env ?? process.env), "profiles", `${options.buildKey.replace(/[^A-Za-z0-9_.-]+/g, "-")}.json`),
|
|
75
|
-
commandLine: { file: result.analysis.command[0] ?? "", args: result.analysis.command.slice(1), cwd: options.projectDirectory ?? resolveWowdumpHome(options.env ?? process.env) },
|
|
76
|
-
toolchain: resolveToolchain(options),
|
|
77
|
-
profile: result.profile,
|
|
78
|
-
evidence: result
|
|
79
|
-
};
|
|
100
|
+
}
|
|
101
|
+
function installedDistDirectory() {
|
|
102
|
+
return fileURLToPath(new URL(".", import.meta.url));
|
|
80
103
|
}
|
|
81
104
|
class CliError extends Error {
|
|
82
105
|
code;
|
|
@@ -133,17 +156,17 @@ async function defaultSidecar(file, args, input) {
|
|
|
133
156
|
child.stdin.end();
|
|
134
157
|
});
|
|
135
158
|
}
|
|
136
|
-
function brokerCommandLine(env,
|
|
159
|
+
function brokerCommandLine(env, packageDist) {
|
|
137
160
|
const configured = env.WOWDUMP_READER_COMMAND?.trim();
|
|
138
161
|
if (configured)
|
|
139
162
|
return { file: configured, args: [] };
|
|
140
|
-
const candidate =
|
|
163
|
+
const candidate = join(packageDist, "reader-main.js");
|
|
141
164
|
return existsSync(candidate) ? { file: process.execPath, args: [candidate, "--request-stdio"] } : null;
|
|
142
165
|
}
|
|
143
|
-
async function defaultBroker(invocation, env,
|
|
144
|
-
const command = brokerCommandLine(env,
|
|
166
|
+
async function defaultBroker(invocation, env, packageDist, sidecar) {
|
|
167
|
+
const command = brokerCommandLine(env, packageDist);
|
|
145
168
|
if (!command) {
|
|
146
|
-
throw new CliError("BROKER_NOT_CONFIGURED", "reader broker entry was not found", { expected:
|
|
169
|
+
throw new CliError("BROKER_NOT_CONFIGURED", "reader broker entry was not found", { expected: join(packageDist, "reader-main.js"), environment: "WOWDUMP_READER_COMMAND" });
|
|
147
170
|
}
|
|
148
171
|
const result = await (sidecar ?? defaultSidecar)(command.file, command.args, `${JSON.stringify(invocation)}\n`);
|
|
149
172
|
if (result.exitCode !== 0)
|
|
@@ -158,12 +181,28 @@ async function defaultBroker(invocation, env, cwd, sidecar) {
|
|
|
158
181
|
throw new CliError("BROKER_PROTOCOL_ERROR", "reader broker returned invalid JSON", { stdout: result.stdout });
|
|
159
182
|
}
|
|
160
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
|
+
}
|
|
161
200
|
const windowsBrokerManagers = new Map();
|
|
162
|
-
function persistentWindowsBroker(home,
|
|
201
|
+
function persistentWindowsBroker(home, packageDist) {
|
|
163
202
|
const key = resolve(home).toLowerCase();
|
|
164
203
|
let manager = windowsBrokerManagers.get(key);
|
|
165
204
|
if (!manager) {
|
|
166
|
-
manager = new WindowsBrokerManager({ home, readerEntry:
|
|
205
|
+
manager = new WindowsBrokerManager({ home, readerEntry: join(packageDist, "reader-main.js") });
|
|
167
206
|
windowsBrokerManagers.set(key, manager);
|
|
168
207
|
}
|
|
169
208
|
return manager;
|
|
@@ -212,7 +251,7 @@ function requireRuntimeConfirmation(options) {
|
|
|
212
251
|
throw new CliError("CONFIRMATION_REQUIRED", "runtime export requires --confirm", {
|
|
213
252
|
pid: options.pid,
|
|
214
253
|
buildKey: options.build,
|
|
215
|
-
operation: "runtime
|
|
254
|
+
operation: "runtime",
|
|
216
255
|
kind: options.kind,
|
|
217
256
|
maxHooks: options.maxHooks,
|
|
218
257
|
durationMs: options.durationMs,
|
|
@@ -220,53 +259,46 @@ function requireRuntimeConfirmation(options) {
|
|
|
220
259
|
cleanupDeadlineMs: options.cleanupDeadlineMs
|
|
221
260
|
});
|
|
222
261
|
}
|
|
223
|
-
function nested(record, ...path) {
|
|
224
|
-
let value = record;
|
|
225
|
-
for (const key of path) {
|
|
226
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
227
|
-
return undefined;
|
|
228
|
-
value = value[key];
|
|
229
|
-
}
|
|
230
|
-
return value;
|
|
231
|
-
}
|
|
232
|
-
function compareEvidence(staticProfile, runtimeExport) {
|
|
233
|
-
const definitions = [
|
|
234
|
-
{ name: "buildKey", left: staticProfile.buildKey, right: runtimeExport.buildKey },
|
|
235
|
-
{
|
|
236
|
-
name: "executableSha256",
|
|
237
|
-
left: staticProfile.executableSha256 ?? nested(staticProfile, "executable", "sha256"),
|
|
238
|
-
right: runtimeExport.executableSha256 ?? nested(runtimeExport, "executable", "sha256")
|
|
239
|
-
},
|
|
240
|
-
{ name: "moduleBase", left: staticProfile.moduleBase ?? nested(staticProfile, "module", "base"), right: runtimeExport.moduleBase ?? nested(runtimeExport, "module", "base") },
|
|
241
|
-
{ name: "sectionBounds", left: staticProfile.sectionBounds ?? staticProfile.sections, right: runtimeExport.sectionBounds ?? runtimeExport.sections },
|
|
242
|
-
{ name: "entryBytes", left: staticProfile.entryBytes, right: runtimeExport.entryBytes },
|
|
243
|
-
{ name: "matchCount", left: staticProfile.matchCount, right: runtimeExport.matchCount }
|
|
244
|
-
];
|
|
245
|
-
return definitions.map(item => ({
|
|
246
|
-
name: item.name,
|
|
247
|
-
static: item.left ?? null,
|
|
248
|
-
runtime: item.right ?? null,
|
|
249
|
-
status: item.left === undefined || item.right === undefined
|
|
250
|
-
? "missing"
|
|
251
|
-
: JSON.stringify(item.left) === JSON.stringify(item.right) ? "match" : "mismatch"
|
|
252
|
-
}));
|
|
253
|
-
}
|
|
254
262
|
export function createWowdumpCli(dependencies = {}) {
|
|
255
263
|
const io = dependencies.io ?? { stdout: process.stdout, stderr: process.stderr };
|
|
256
264
|
const env = dependencies.env ?? process.env;
|
|
257
265
|
const cwd = resolve(dependencies.cwd ?? process.cwd());
|
|
266
|
+
const packageDist = installedDistDirectory();
|
|
258
267
|
const home = resolveWowdumpHome(env);
|
|
259
268
|
const broker = dependencies.broker ?? (process.platform === "win32" && !dependencies.sidecar
|
|
260
|
-
? (invocation => persistentWindowsBroker(home,
|
|
261
|
-
: (invocation => defaultBroker(invocation, env,
|
|
262
|
-
const
|
|
263
|
-
const 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);
|
|
264
280
|
const program = new Command()
|
|
265
281
|
.name("wowdump")
|
|
266
282
|
.description("Build-aware WoW native memory analysis CLI")
|
|
267
|
-
.version(
|
|
283
|
+
.version(packageVersion())
|
|
268
284
|
.showHelpAfterError()
|
|
269
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
|
+
});
|
|
270
302
|
program.command("target")
|
|
271
303
|
.description("Show target, reader broker, profile, monitor, and Frida state")
|
|
272
304
|
.option("--pid <pid>", "target process ID")
|
|
@@ -305,7 +337,8 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
305
337
|
.option("--size <bytes>", "bounded byte count")
|
|
306
338
|
.option("--request <json>", "complete read request as JSON")
|
|
307
339
|
.action(async (options) => {
|
|
308
|
-
const
|
|
340
|
+
const requested = options.request ? jsonRecord(options.request, "request") : undefined;
|
|
341
|
+
const payload = requested ?? {
|
|
309
342
|
pid: positiveInteger(options.pid, "pid"),
|
|
310
343
|
...(options.build ? { buildKey: options.build } : {}),
|
|
311
344
|
...(options.profile ? { profileId: options.profile } : {}),
|
|
@@ -313,6 +346,20 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
313
346
|
...(options.address ? { address: options.address } : {}),
|
|
314
347
|
...(options.size ? { size: positiveInteger(options.size, "size") } : {})
|
|
315
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
|
+
}
|
|
316
363
|
writeJson(io, await broker({ command: "read", payload }));
|
|
317
364
|
});
|
|
318
365
|
const watch = memory.command("watch").description("Manage broker-owned memory monitors");
|
|
@@ -327,17 +374,21 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
327
374
|
.option("--interval <ms>", "sample interval", "250")
|
|
328
375
|
.option("--max-samples <count>", "sample limit", "1000")
|
|
329
376
|
.option("--all-samples", "emit unchanged samples")
|
|
330
|
-
.action(async (options) =>
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
});
|
|
341
392
|
watch.command("poll")
|
|
342
393
|
.description("Poll monitor events")
|
|
343
394
|
.requiredOption("--id <watchId>", "monitor ID")
|
|
@@ -350,46 +401,33 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
350
401
|
.description("Stop a monitor and release its resources")
|
|
351
402
|
.requiredOption("--id <watchId>", "monitor ID")
|
|
352
403
|
.action(async (options) => writeJson(io, await broker({ command: "watch.stop", payload: { watchId: options.id } })));
|
|
353
|
-
const analyze = program.command("analyze").description("Run
|
|
354
|
-
analyze.command("
|
|
355
|
-
.description("
|
|
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")
|
|
356
407
|
.requiredOption("--exe <path>", "path to Wow.exe")
|
|
357
408
|
.requiredOption("--build <buildKey>", "build key")
|
|
358
|
-
.
|
|
359
|
-
.option("--
|
|
360
|
-
.option("--
|
|
361
|
-
.option("--
|
|
362
|
-
.option("--
|
|
363
|
-
.
|
|
364
|
-
.
|
|
365
|
-
.
|
|
366
|
-
.
|
|
367
|
-
.
|
|
368
|
-
.
|
|
369
|
-
|
|
370
|
-
executable: options.exe,
|
|
371
|
-
buildKey: options.build ?? options.buildKey,
|
|
372
|
-
home,
|
|
373
|
-
env,
|
|
374
|
-
java: options.java,
|
|
375
|
-
ghidra: options.ghidra,
|
|
376
|
-
outputDirectory: options.output,
|
|
377
|
-
projectDirectory: options.projectDirectory,
|
|
378
|
-
projectName: options.projectName,
|
|
379
|
-
scriptDirectory: options.scriptDirectory,
|
|
380
|
-
exporterScript: options.exporterScript,
|
|
381
|
-
evidenceFile: options.evidence,
|
|
382
|
-
timeoutSeconds: positiveInteger(options.timeout, "timeout"),
|
|
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"),
|
|
383
421
|
dryRun: options.dryRun === true
|
|
384
422
|
})));
|
|
385
|
-
analyze.command("runtime
|
|
386
|
-
.description("
|
|
423
|
+
analyze.command("runtime")
|
|
424
|
+
.description("Read runtime evidence through the elevated Frida broker")
|
|
387
425
|
.requiredOption("--pid <pid>", "target process ID")
|
|
388
426
|
.requiredOption("--build <buildKey>", "build key")
|
|
389
427
|
.option("--build-key <buildKey>", "alias for --build")
|
|
390
428
|
.option("--kind <kind>", "text or providers", "providers")
|
|
391
429
|
.option("--worker <path>", "Frida worker entry")
|
|
392
|
-
.option("--
|
|
430
|
+
.option("--profile <path>", "existing reader profile containing candidate RVAs")
|
|
393
431
|
.option("--max-hooks <count>", "maximum hooks", "1")
|
|
394
432
|
.option("--duration-ms <ms>", "maximum duration", "5000")
|
|
395
433
|
.option("--max-events <count>", "maximum events", "100")
|
|
@@ -409,49 +447,75 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
409
447
|
if (!["text", "providers"].includes(normalized.kind))
|
|
410
448
|
throw new CliError("ARGUMENT_INVALID", "kind must be text or providers");
|
|
411
449
|
requireRuntimeConfirmation(normalized);
|
|
412
|
-
const worker = resolve(options.worker ?? env.WOWDUMP_FRIDA_WORKER ?? join(
|
|
450
|
+
const worker = resolve(options.worker ?? env.WOWDUMP_FRIDA_WORKER ?? join(packageDist, "frida-worker.js"));
|
|
413
451
|
if (!existsSync(worker))
|
|
414
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;
|
|
415
456
|
const request = {
|
|
416
|
-
command: "
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
419
472
|
};
|
|
420
473
|
const result = await sidecar(process.execPath, [worker], `${JSON.stringify(request)}\n`);
|
|
421
474
|
if (result.exitCode !== 0)
|
|
422
475
|
throw new CliError("FRIDA_WORKER_FAILED", result.stderr.trim() || `Frida worker exited with ${result.exitCode}`);
|
|
423
476
|
const line = result.stdout.split(/\r?\n/).find(value => value.trim());
|
|
424
|
-
|
|
477
|
+
const value = line ? jsonRecord(line, "runtime result") : { ok: true, stdout: result.stdout };
|
|
478
|
+
writeJson(io, { ...value, command: "analyze.runtime" });
|
|
425
479
|
});
|
|
426
|
-
analyze.command("
|
|
427
|
-
.description("
|
|
428
|
-
.requiredOption("--
|
|
429
|
-
.requiredOption("--
|
|
430
|
-
.
|
|
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")
|
|
431
490
|
.action(async (options) => {
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
runtimeExport: runtimeFile,
|
|
442
|
-
checks,
|
|
443
|
-
confidence: staticProfile.confidence ?? "unverified",
|
|
444
|
-
promoted: false
|
|
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
|
|
445
500
|
};
|
|
446
|
-
if (
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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 });
|
|
451
515
|
});
|
|
452
516
|
program.command("init")
|
|
453
517
|
.description("Initialize WOWDUMP_HOME without overwriting existing files")
|
|
454
|
-
.option("--skip-toolchain-download", "
|
|
518
|
+
.option("--skip-toolchain-download", "kept for compatibility; no external toolchain is installed")
|
|
455
519
|
.action(async (options) => {
|
|
456
520
|
const installMissing = options.skipToolchainDownload !== true && env.WOWDUMP_SKIP_TOOLCHAIN_DOWNLOAD !== "1";
|
|
457
521
|
const result = await bootstrapToolchain({ home, env, installMissing });
|
|
@@ -493,5 +557,7 @@ if (process.argv[1]) {
|
|
|
493
557
|
invokedFile = pathToFileURL(invokedPath).href;
|
|
494
558
|
}
|
|
495
559
|
}
|
|
496
|
-
if (invokedFile === import.meta.url)
|
|
560
|
+
if (invokedFile === import.meta.url) {
|
|
561
|
+
await initializeWowdumpHome();
|
|
497
562
|
process.exitCode = await runWowdumpCli();
|
|
563
|
+
}
|