wowdump 0.3.1 → 0.3.3
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 +170 -0
- package/dist/cli.js +337 -192
- 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 +13 -10
- package/skills/wowdump/SKILL.md +24 -15
- package/skills/wowdump/references/commands.md +77 -0
- package/skills/wowdump/references/disassemble.md +62 -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 +45 -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,106 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { existsSync, realpathSync, readFileSync } from "node:fs";
|
|
5
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
6
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
8
|
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
|
-
|
|
9
|
+
import { WindowsBrokerManager } from "./reader/launcher.js";
|
|
10
|
+
import { initializeWowdumpHome, bootstrapToolchain, resolveToolchain, resolveWowdumpHome } from "./toolchain.js";
|
|
11
|
+
import { runDisassembly } from "./analysis/disassemble.js";
|
|
12
|
+
import { RUNTIME_EXPORT_SCRIPT } from "./analysis/runtime-script.js";
|
|
13
|
+
import { ProfileEngine } from "./core/profile-engine.js";
|
|
14
|
+
import { ReaderProfileAdapter } from "./adapters/reader.js";
|
|
15
|
+
import { enumerateWindowsProcesses } from "./reader/windows.js";
|
|
16
|
+
export function parseBuildInfo(content, file) {
|
|
17
|
+
const lines = content.split(/\r?\n/).filter(line => line.trim());
|
|
18
|
+
const headers = lines[0]?.split("|") ?? [];
|
|
19
|
+
const versionIndex = headers.indexOf("Version!STRING:0");
|
|
20
|
+
const activeIndex = headers.indexOf("Active!DEC:1");
|
|
21
|
+
const productIndex = headers.indexOf("Product!STRING:0");
|
|
22
|
+
if (versionIndex < 0 || activeIndex < 0 || productIndex < 0)
|
|
23
|
+
return null;
|
|
24
|
+
const row = lines.slice(1).map(line => line.split("|")).find(values => values[activeIndex] === "1" && values[productIndex] === "wow");
|
|
25
|
+
const version = row?.[versionIndex]?.trim();
|
|
26
|
+
const product = row?.[productIndex]?.trim();
|
|
27
|
+
if (!version || !product)
|
|
28
|
+
return null;
|
|
29
|
+
return { fileVersion: version, product, buildKey: `retail@${version}`, buildInfoFile: file };
|
|
30
|
+
}
|
|
31
|
+
async function discoverWowTargets(selectedPid, elevatedModules) {
|
|
32
|
+
if (process.platform !== "win32")
|
|
33
|
+
return [];
|
|
34
|
+
const targets = [];
|
|
35
|
+
const rows = enumerateWindowsProcesses()
|
|
36
|
+
.filter(item => /^Wow\.exe$/i.test(item.name))
|
|
37
|
+
.filter(item => selectedPid === undefined || item.pid === selectedPid);
|
|
38
|
+
for (const item of rows) {
|
|
39
|
+
const pid = item.pid;
|
|
40
|
+
const name = item.name;
|
|
41
|
+
if (!Number.isSafeInteger(pid) || pid < 1 || !/^Wow\.exe$/i.test(name) || (selectedPid !== undefined && pid !== selectedPid))
|
|
42
|
+
continue;
|
|
43
|
+
let path = item.path;
|
|
44
|
+
let moduleBase = null;
|
|
45
|
+
let moduleSize = null;
|
|
46
|
+
let diagnostic;
|
|
47
|
+
if ((!path || !moduleBase) && elevatedModules) {
|
|
48
|
+
try {
|
|
49
|
+
const elevated = elevatedModules(pid);
|
|
50
|
+
const response = await elevated;
|
|
51
|
+
const modules = response && typeof response === "object" && Array.isArray(response.modules)
|
|
52
|
+
? response.modules
|
|
53
|
+
: [];
|
|
54
|
+
const main = modules.find(module => /^Wow\.exe$/i.test(String(module.name ?? ""))) ?? modules[0];
|
|
55
|
+
if (!path && typeof main?.path === "string" && main.path)
|
|
56
|
+
path = main.path;
|
|
57
|
+
if (!moduleBase && typeof main?.base === "string")
|
|
58
|
+
moduleBase = main.base;
|
|
59
|
+
if (moduleSize === null && Number.isFinite(Number(main?.size)))
|
|
60
|
+
moduleSize = Number(main?.size);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
diagnostic = diagnostic ?? (error instanceof Error ? error.message : String(error));
|
|
36
64
|
}
|
|
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
65
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
};
|
|
66
|
+
let buildInfoFile = null;
|
|
67
|
+
let fileVersion = null;
|
|
68
|
+
let product = null;
|
|
69
|
+
if (path) {
|
|
70
|
+
const candidates = [join(dirname(path), ".build.info"), join(dirname(dirname(path)), ".build.info")];
|
|
71
|
+
for (const candidate of candidates) {
|
|
72
|
+
try {
|
|
73
|
+
const text = await readFile(candidate, "utf8");
|
|
74
|
+
const parsed = parseBuildInfo(text, candidate);
|
|
75
|
+
if (parsed) {
|
|
76
|
+
fileVersion = parsed.fileVersion;
|
|
77
|
+
product = parsed.product;
|
|
78
|
+
buildInfoFile = parsed.buildInfoFile;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch { /* try the next parent directory */ }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
targets.push({ pid, name, path, moduleBase, moduleSize, fileVersion, product, buildKey: fileVersion ? `retail@${fileVersion}` : null, ...(buildInfoFile ? { buildInfoFile } : {}), ...(diagnostic ? { diagnostics: { frida: diagnostic } } : {}) });
|
|
65
86
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
87
|
+
return targets;
|
|
88
|
+
}
|
|
89
|
+
function packageVersion() {
|
|
90
|
+
const override = process.env.WOWDUMP_VERSION?.trim();
|
|
91
|
+
if (override)
|
|
92
|
+
return override;
|
|
93
|
+
try {
|
|
94
|
+
const packageFile = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
95
|
+
const packageJson = JSON.parse(readFileSync(packageFile, "utf8"));
|
|
96
|
+
return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return "0.0.0";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function installedDistDirectory() {
|
|
103
|
+
return fileURLToPath(new URL(".", import.meta.url));
|
|
80
104
|
}
|
|
81
105
|
class CliError extends Error {
|
|
82
106
|
code;
|
|
@@ -133,17 +157,17 @@ async function defaultSidecar(file, args, input) {
|
|
|
133
157
|
child.stdin.end();
|
|
134
158
|
});
|
|
135
159
|
}
|
|
136
|
-
function brokerCommandLine(env,
|
|
160
|
+
function brokerCommandLine(env, packageDist) {
|
|
137
161
|
const configured = env.WOWDUMP_READER_COMMAND?.trim();
|
|
138
162
|
if (configured)
|
|
139
163
|
return { file: configured, args: [] };
|
|
140
|
-
const candidate =
|
|
164
|
+
const candidate = join(packageDist, "reader-main.js");
|
|
141
165
|
return existsSync(candidate) ? { file: process.execPath, args: [candidate, "--request-stdio"] } : null;
|
|
142
166
|
}
|
|
143
|
-
async function defaultBroker(invocation, env,
|
|
144
|
-
const command = brokerCommandLine(env,
|
|
167
|
+
async function defaultBroker(invocation, env, packageDist, sidecar) {
|
|
168
|
+
const command = brokerCommandLine(env, packageDist);
|
|
145
169
|
if (!command) {
|
|
146
|
-
throw new CliError("BROKER_NOT_CONFIGURED", "reader broker entry was not found", { expected:
|
|
170
|
+
throw new CliError("BROKER_NOT_CONFIGURED", "reader broker entry was not found", { expected: join(packageDist, "reader-main.js"), environment: "WOWDUMP_READER_COMMAND" });
|
|
147
171
|
}
|
|
148
172
|
const result = await (sidecar ?? defaultSidecar)(command.file, command.args, `${JSON.stringify(invocation)}\n`);
|
|
149
173
|
if (result.exitCode !== 0)
|
|
@@ -158,12 +182,28 @@ async function defaultBroker(invocation, env, cwd, sidecar) {
|
|
|
158
182
|
throw new CliError("BROKER_PROTOCOL_ERROR", "reader broker returned invalid JSON", { stdout: result.stdout });
|
|
159
183
|
}
|
|
160
184
|
}
|
|
185
|
+
async function runFridaWorker(worker, input, timeoutMs, broker) {
|
|
186
|
+
if (process.platform !== "win32") {
|
|
187
|
+
throw new CliError("PLATFORM_UNSUPPORTED", "elevated Frida broker is currently supported on Windows only");
|
|
188
|
+
}
|
|
189
|
+
const raw = await broker({ command: "frida", payload: { worker, input, timeoutMs } });
|
|
190
|
+
const response = raw && typeof raw === "object" ? raw : {};
|
|
191
|
+
if (response.ok === false) {
|
|
192
|
+
const error = response.error && typeof response.error === "object" ? response.error : {};
|
|
193
|
+
throw new CliError("BROKER_FAILED", String(error.message ?? "elevated Frida worker failed"), { response });
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
exitCode: Number.isSafeInteger(Number(response.exitCode)) ? Number(response.exitCode) : 1,
|
|
197
|
+
stdout: typeof response.stdout === "string" ? response.stdout : "",
|
|
198
|
+
stderr: typeof response.stderr === "string" ? response.stderr : ""
|
|
199
|
+
};
|
|
200
|
+
}
|
|
161
201
|
const windowsBrokerManagers = new Map();
|
|
162
|
-
function persistentWindowsBroker(home,
|
|
202
|
+
function persistentWindowsBroker(home, packageDist) {
|
|
163
203
|
const key = resolve(home).toLowerCase();
|
|
164
204
|
let manager = windowsBrokerManagers.get(key);
|
|
165
205
|
if (!manager) {
|
|
166
|
-
manager = new WindowsBrokerManager({ home, readerEntry:
|
|
206
|
+
manager = new WindowsBrokerManager({ home, readerEntry: join(packageDist, "reader-main.js") });
|
|
167
207
|
windowsBrokerManagers.set(key, manager);
|
|
168
208
|
}
|
|
169
209
|
return manager;
|
|
@@ -209,10 +249,10 @@ function selected(source, keys) {
|
|
|
209
249
|
function requireRuntimeConfirmation(options) {
|
|
210
250
|
if (options.confirm === true)
|
|
211
251
|
return;
|
|
212
|
-
throw new CliError("CONFIRMATION_REQUIRED", "runtime
|
|
252
|
+
throw new CliError("CONFIRMATION_REQUIRED", "runtime analysis requires --confirm", {
|
|
213
253
|
pid: options.pid,
|
|
214
254
|
buildKey: options.build,
|
|
215
|
-
operation: "runtime
|
|
255
|
+
operation: "runtime",
|
|
216
256
|
kind: options.kind,
|
|
217
257
|
maxHooks: options.maxHooks,
|
|
218
258
|
durationMs: options.durationMs,
|
|
@@ -220,53 +260,102 @@ function requireRuntimeConfirmation(options) {
|
|
|
220
260
|
cleanupDeadlineMs: options.cleanupDeadlineMs
|
|
221
261
|
});
|
|
222
262
|
}
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return undefined;
|
|
228
|
-
value = value[key];
|
|
229
|
-
}
|
|
263
|
+
function safeBuildDirectory(buildKey) {
|
|
264
|
+
const value = buildKey.trim();
|
|
265
|
+
if (!value || !/^[A-Za-z0-9_.@-]+$/.test(value))
|
|
266
|
+
throw new CliError("ARGUMENT_INVALID", "build key contains unsupported path characters");
|
|
230
267
|
return value;
|
|
231
268
|
}
|
|
232
|
-
function
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
269
|
+
function decodeBase64(value, expectedSize, label) {
|
|
270
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9+/]*={0,2}$/.test(value))
|
|
271
|
+
throw new CliError("RUNTIME_DUMP_INVALID", `${label} is not valid base64`);
|
|
272
|
+
const data = Buffer.from(value, "base64");
|
|
273
|
+
if (data.length !== expectedSize)
|
|
274
|
+
throw new CliError("RUNTIME_DUMP_INVALID", `${label} size does not match metadata`);
|
|
275
|
+
return data;
|
|
276
|
+
}
|
|
277
|
+
export async function persistRuntimeDump(value, outputDirectory) {
|
|
278
|
+
const sections = Array.isArray(value.sections) ? value.sections : [];
|
|
279
|
+
if (value.ok !== true || sections.length === 0)
|
|
280
|
+
return value;
|
|
281
|
+
const directory = resolve(outputDirectory);
|
|
282
|
+
await mkdir(directory, { recursive: true });
|
|
283
|
+
const summaries = [];
|
|
284
|
+
for (const raw of sections) {
|
|
285
|
+
const section = jsonRecord(JSON.stringify(raw), "runtime section");
|
|
286
|
+
const name = typeof section.name === "string" && /^[A-Za-z0-9_.-]+$/.test(section.name) ? section.name : "section";
|
|
287
|
+
const file = join(directory, `${name}.bin`);
|
|
288
|
+
const chunks = Array.isArray(section.chunks) ? section.chunks : [];
|
|
289
|
+
const buffers = chunks.map((chunk, index) => {
|
|
290
|
+
const item = jsonRecord(JSON.stringify(chunk), `runtime section ${name} chunk ${index}`);
|
|
291
|
+
return decodeBase64(item.dataBase64, Number(item.size), `${name} chunk ${index}`);
|
|
292
|
+
});
|
|
293
|
+
const data = Buffer.concat(buffers);
|
|
294
|
+
await writeFile(file, data);
|
|
295
|
+
summaries.push({
|
|
296
|
+
...selected(section, ["name", "rva", "runtimeAddress", "virtualSize", "rawSize", "characteristics", "protection", "requestedSize", "readSize", "truncated"]),
|
|
297
|
+
file,
|
|
298
|
+
bytes: data.length,
|
|
299
|
+
sha256: createHash("sha256").update(data).digest("hex")
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const manifest = {
|
|
303
|
+
schema: "wowdump.runtime-dump.v1",
|
|
304
|
+
kind: "dump",
|
|
305
|
+
buildKey: value.buildKey ?? null,
|
|
306
|
+
pid: value.pid ?? null,
|
|
307
|
+
module: value.module ?? null,
|
|
308
|
+
totalBytes: value.totalBytes ?? summaries.reduce((total, item) => total + Number(item.bytes ?? 0), 0),
|
|
309
|
+
truncated: value.truncated === true || summaries.some(item => item.truncated === true),
|
|
310
|
+
limits: value.limits ?? null,
|
|
311
|
+
sections: summaries,
|
|
312
|
+
evidence: value.evidence ?? [],
|
|
313
|
+
generatedAt: new Date().toISOString()
|
|
314
|
+
};
|
|
315
|
+
const manifestFile = join(directory, "manifest.json");
|
|
316
|
+
await writeFile(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
317
|
+
return { ...value, sections: summaries, outputDirectory: directory, manifestFile, manifest };
|
|
253
318
|
}
|
|
254
319
|
export function createWowdumpCli(dependencies = {}) {
|
|
255
320
|
const io = dependencies.io ?? { stdout: process.stdout, stderr: process.stderr };
|
|
256
321
|
const env = dependencies.env ?? process.env;
|
|
257
322
|
const cwd = resolve(dependencies.cwd ?? process.cwd());
|
|
323
|
+
const packageDist = installedDistDirectory();
|
|
258
324
|
const home = resolveWowdumpHome(env);
|
|
259
325
|
const broker = dependencies.broker ?? (process.platform === "win32" && !dependencies.sidecar
|
|
260
|
-
? (invocation => persistentWindowsBroker(home,
|
|
261
|
-
: (invocation => defaultBroker(invocation, env,
|
|
262
|
-
const
|
|
263
|
-
const sidecar =
|
|
326
|
+
? (invocation => persistentWindowsBroker(home, packageDist).request(invocation))
|
|
327
|
+
: (invocation => defaultBroker(invocation, env, packageDist, dependencies.sidecar)));
|
|
328
|
+
const baseSidecar = dependencies.sidecar ?? defaultSidecar;
|
|
329
|
+
const sidecar = async (file, args, input) => {
|
|
330
|
+
const worker = args.length === 1 && /\.m?js$/i.test(args[0] ?? "") ? args[0] : undefined;
|
|
331
|
+
if (worker && input !== undefined)
|
|
332
|
+
return runFridaWorker(worker, input, 120_000, broker);
|
|
333
|
+
return baseSidecar(file, args, input);
|
|
334
|
+
};
|
|
335
|
+
const profileEngine = new ProfileEngine();
|
|
336
|
+
const profileAdapter = new ReaderProfileAdapter(broker);
|
|
264
337
|
const program = new Command()
|
|
265
338
|
.name("wowdump")
|
|
266
339
|
.description("Build-aware WoW native memory analysis CLI")
|
|
267
|
-
.version(
|
|
340
|
+
.version(packageVersion())
|
|
268
341
|
.showHelpAfterError()
|
|
269
342
|
.configureOutput({ writeOut: value => io.stdout.write(value), writeErr: value => io.stderr.write(value) });
|
|
343
|
+
program.command("targets")
|
|
344
|
+
.description("Discover running WoW targets, executable paths, and build versions")
|
|
345
|
+
.option("--pid <pid>", "select one process ID")
|
|
346
|
+
.action(async (options) => {
|
|
347
|
+
const selectedPid = options.pid ? positiveInteger(options.pid, "pid") : undefined;
|
|
348
|
+
const targets = await discoverWowTargets(selectedPid, async (pid) => broker({ command: "modules", payload: { pid } }));
|
|
349
|
+
if (selectedPid !== undefined && targets.length === 0)
|
|
350
|
+
throw new CliError("TARGET_NOT_FOUND", `Wow.exe process ${selectedPid} was not found`);
|
|
351
|
+
writeJson(io, {
|
|
352
|
+
ok: targets.length > 0,
|
|
353
|
+
command: "targets",
|
|
354
|
+
selected: targets.length === 1 ? targets[0] : null,
|
|
355
|
+
requiresSelection: selectedPid === undefined && targets.length > 1,
|
|
356
|
+
targets
|
|
357
|
+
});
|
|
358
|
+
});
|
|
270
359
|
program.command("target")
|
|
271
360
|
.description("Show target, reader broker, profile, monitor, and Frida state")
|
|
272
361
|
.option("--pid <pid>", "target process ID")
|
|
@@ -305,7 +394,8 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
305
394
|
.option("--size <bytes>", "bounded byte count")
|
|
306
395
|
.option("--request <json>", "complete read request as JSON")
|
|
307
396
|
.action(async (options) => {
|
|
308
|
-
const
|
|
397
|
+
const requested = options.request ? jsonRecord(options.request, "request") : undefined;
|
|
398
|
+
const payload = requested ?? {
|
|
309
399
|
pid: positiveInteger(options.pid, "pid"),
|
|
310
400
|
...(options.build ? { buildKey: options.build } : {}),
|
|
311
401
|
...(options.profile ? { profileId: options.profile } : {}),
|
|
@@ -313,6 +403,20 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
313
403
|
...(options.address ? { address: options.address } : {}),
|
|
314
404
|
...(options.size ? { size: positiveInteger(options.size, "size") } : {})
|
|
315
405
|
};
|
|
406
|
+
const pid = positiveInteger(String(payload.pid ?? options.pid), "pid");
|
|
407
|
+
const profileId = typeof payload.profileId === "string" ? payload.profileId : (typeof options.profile === "string" ? options.profile : undefined);
|
|
408
|
+
if (profileId && payload.address === undefined) {
|
|
409
|
+
const profileFilePath = profileFile(join(home, "profiles"), profileId);
|
|
410
|
+
const profile = jsonRecord(await readFile(profileFilePath, "utf8"), profileFilePath);
|
|
411
|
+
const result = await profileEngine.read({
|
|
412
|
+
pid,
|
|
413
|
+
buildKey: typeof payload.buildKey === "string" ? payload.buildKey : undefined,
|
|
414
|
+
profile,
|
|
415
|
+
fields: Array.isArray(payload.fields) ? payload.fields.map(String) : undefined
|
|
416
|
+
}, profileAdapter);
|
|
417
|
+
writeJson(io, { ...result, profileFile: profileFilePath });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
316
420
|
writeJson(io, await broker({ command: "read", payload }));
|
|
317
421
|
});
|
|
318
422
|
const watch = memory.command("watch").description("Manage broker-owned memory monitors");
|
|
@@ -327,17 +431,21 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
327
431
|
.option("--interval <ms>", "sample interval", "250")
|
|
328
432
|
.option("--max-samples <count>", "sample limit", "1000")
|
|
329
433
|
.option("--all-samples", "emit unchanged samples")
|
|
330
|
-
.action(async (options) =>
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
434
|
+
.action(async (options) => {
|
|
435
|
+
if (options.profile)
|
|
436
|
+
throw new CliError("PROFILE_WATCH_UNSUPPORTED", "profile watch requires a fixed address; use repeated memory read calls for dynamic pointer fields");
|
|
437
|
+
writeJson(io, await broker({ command: "watch.start", payload: {
|
|
438
|
+
pid: positiveInteger(options.pid, "pid"),
|
|
439
|
+
...(options.build ? { buildKey: options.build } : {}),
|
|
440
|
+
...(options.profile ? { profileId: options.profile } : {}),
|
|
441
|
+
...(options.field ? { fields: options.field } : {}),
|
|
442
|
+
...(options.address ? { address: options.address } : {}),
|
|
443
|
+
...(options.size ? { size: positiveInteger(options.size, "size") } : {}),
|
|
444
|
+
intervalMs: positiveInteger(options.interval, "interval"),
|
|
445
|
+
maxSamples: positiveInteger(options.maxSamples, "max-samples"),
|
|
446
|
+
changeOnly: options.allSamples !== true
|
|
447
|
+
} }));
|
|
448
|
+
});
|
|
341
449
|
watch.command("poll")
|
|
342
450
|
.description("Poll monitor events")
|
|
343
451
|
.requiredOption("--id <watchId>", "monitor ID")
|
|
@@ -350,47 +458,39 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
350
458
|
.description("Stop a monitor and release its resources")
|
|
351
459
|
.requiredOption("--id <watchId>", "monitor ID")
|
|
352
460
|
.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("
|
|
461
|
+
const analyze = program.command("analyze").description("Run Frida evidence export and runtime-guided disassembly");
|
|
462
|
+
analyze.command("disassemble")
|
|
463
|
+
.description("Decode Frida text evidence and produce a Reader profile")
|
|
356
464
|
.requiredOption("--exe <path>", "path to Wow.exe")
|
|
357
465
|
.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"),
|
|
466
|
+
.requiredOption("--runtime-export <file>", "runtime evidence JSON")
|
|
467
|
+
.option("--ida-evidence <file>", "JSON produced by an IDA Pro MCP analysis")
|
|
468
|
+
.option("--output <file>", "profile output JSON")
|
|
469
|
+
.option("--max-instructions <count>", "maximum decoded instructions", "256")
|
|
470
|
+
.option("--dry-run", "validate inputs without writing a profile")
|
|
471
|
+
.action(async (options) => writeJson(io, await runDisassembly({
|
|
472
|
+
exe: options.exe,
|
|
473
|
+
buildKey: options.build,
|
|
474
|
+
runtimeExport: options.runtimeExport,
|
|
475
|
+
...(options.idaEvidence ? { idaEvidence: options.idaEvidence } : {}),
|
|
476
|
+
...(options.output ? { output: options.output } : {}),
|
|
477
|
+
maxInstructions: positiveInteger(options.maxInstructions, "max-instructions"),
|
|
383
478
|
dryRun: options.dryRun === true
|
|
384
479
|
})));
|
|
385
|
-
analyze.command("runtime
|
|
386
|
-
.description("
|
|
480
|
+
analyze.command("runtime")
|
|
481
|
+
.description("Dump runtime PE sections or verify profile candidates through the elevated Frida broker")
|
|
387
482
|
.requiredOption("--pid <pid>", "target process ID")
|
|
388
483
|
.requiredOption("--build <buildKey>", "build key")
|
|
389
484
|
.option("--build-key <buildKey>", "alias for --build")
|
|
390
|
-
.option("--kind <kind>", "
|
|
485
|
+
.option("--kind <kind>", "dump or verify", "dump")
|
|
391
486
|
.option("--worker <path>", "Frida worker entry")
|
|
392
|
-
.option("--
|
|
393
|
-
.option("--
|
|
487
|
+
.option("--profile <path>", "reader profile containing candidate RVAs (verify only)")
|
|
488
|
+
.option("--output-dir <path>", "directory for dump manifest and section binaries")
|
|
489
|
+
.option("--max-section-bytes <bytes>", "per-section dump limit", "134217728")
|
|
490
|
+
.option("--max-total-bytes <bytes>", "total dump limit", "268435456")
|
|
491
|
+
.option("--chunk-size <bytes>", "dump chunk size", "1048576")
|
|
492
|
+
.option("--max-verify-bytes <bytes>", "maximum bytes read per candidate", "256")
|
|
493
|
+
.option("--max-hooks <count>", "reserved compatibility limit", "1")
|
|
394
494
|
.option("--duration-ms <ms>", "maximum duration", "5000")
|
|
395
495
|
.option("--max-events <count>", "maximum events", "100")
|
|
396
496
|
.option("--cleanup-deadline-ms <ms>", "cleanup deadline", "2000")
|
|
@@ -404,54 +504,97 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
404
504
|
durationMs: positiveInteger(options.durationMs, "duration-ms"),
|
|
405
505
|
maxEvents: positiveInteger(options.maxEvents, "max-events"),
|
|
406
506
|
cleanupDeadlineMs: positiveInteger(options.cleanupDeadlineMs, "cleanup-deadline-ms"),
|
|
507
|
+
maxSectionBytes: positiveInteger(options.maxSectionBytes, "max-section-bytes"),
|
|
508
|
+
maxTotalBytes: positiveInteger(options.maxTotalBytes, "max-total-bytes"),
|
|
509
|
+
chunkSize: positiveInteger(options.chunkSize, "chunk-size"),
|
|
510
|
+
maxVerifyBytes: positiveInteger(options.maxVerifyBytes, "max-verify-bytes"),
|
|
407
511
|
confirm: options.confirm === true
|
|
408
512
|
};
|
|
409
|
-
if (!["
|
|
410
|
-
throw new CliError("ARGUMENT_INVALID", "kind must be
|
|
513
|
+
if (!["dump", "verify"].includes(normalized.kind))
|
|
514
|
+
throw new CliError("ARGUMENT_INVALID", "kind must be dump or verify");
|
|
411
515
|
requireRuntimeConfirmation(normalized);
|
|
412
|
-
|
|
516
|
+
if (normalized.kind === "verify" && !options.profile)
|
|
517
|
+
throw new CliError("PROFILE_REQUIRED", "verify requires --profile <file>");
|
|
518
|
+
const worker = resolve(options.worker ?? env.WOWDUMP_FRIDA_WORKER ?? join(packageDist, "frida-worker.js"));
|
|
413
519
|
if (!existsSync(worker))
|
|
414
520
|
throw new CliError("FRIDA_WORKER_NOT_FOUND", `Frida worker was not found: ${worker}`);
|
|
521
|
+
const profile = options.profile
|
|
522
|
+
? jsonRecord(await readFile(resolve(options.profile), "utf8"), options.profile)
|
|
523
|
+
: undefined;
|
|
415
524
|
const request = {
|
|
416
|
-
command: "
|
|
417
|
-
|
|
418
|
-
|
|
525
|
+
command: "dynamic-script",
|
|
526
|
+
pid: normalized.pid,
|
|
527
|
+
build: normalized.build,
|
|
528
|
+
source: RUNTIME_EXPORT_SCRIPT,
|
|
529
|
+
exportName: "collect",
|
|
530
|
+
args: {
|
|
531
|
+
kind: normalized.kind,
|
|
532
|
+
maxHooks: normalized.maxHooks,
|
|
533
|
+
durationMs: normalized.durationMs,
|
|
534
|
+
maxEvents: normalized.maxEvents,
|
|
535
|
+
cleanupDeadlineMs: normalized.cleanupDeadlineMs,
|
|
536
|
+
maxSectionBytes: normalized.maxSectionBytes,
|
|
537
|
+
maxTotalBytes: normalized.maxTotalBytes,
|
|
538
|
+
chunkSize: normalized.chunkSize,
|
|
539
|
+
maxVerifyBytes: normalized.maxVerifyBytes,
|
|
540
|
+
...(profile ? { profile } : {})
|
|
541
|
+
},
|
|
542
|
+
callArgs: [],
|
|
543
|
+
durationMs: normalized.durationMs
|
|
419
544
|
};
|
|
420
545
|
const result = await sidecar(process.execPath, [worker], `${JSON.stringify(request)}\n`);
|
|
421
546
|
if (result.exitCode !== 0)
|
|
422
547
|
throw new CliError("FRIDA_WORKER_FAILED", result.stderr.trim() || `Frida worker exited with ${result.exitCode}`);
|
|
423
548
|
const line = result.stdout.split(/\r?\n/).find(value => value.trim());
|
|
424
|
-
|
|
549
|
+
const value = line ? jsonRecord(line, "runtime result") : { ok: true, stdout: result.stdout };
|
|
550
|
+
if (normalized.kind === "dump" && value.ok === true) {
|
|
551
|
+
const defaultDirectory = join(home, safeBuildDirectory(normalized.build), "runtime", `dump-${Date.now()}`);
|
|
552
|
+
const persisted = await persistRuntimeDump(value, options.outputDir ?? defaultDirectory);
|
|
553
|
+
writeJson(io, { ...persisted, command: "analyze.runtime.dump" });
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
writeJson(io, { ...value, command: "analyze.runtime.verify" });
|
|
557
|
+
}
|
|
425
558
|
});
|
|
426
|
-
analyze.command("
|
|
427
|
-
.description("
|
|
428
|
-
.requiredOption("--
|
|
429
|
-
.requiredOption("--
|
|
430
|
-
.
|
|
559
|
+
analyze.command("dynamic")
|
|
560
|
+
.description("Run a caller-supplied GumJS script through Frida")
|
|
561
|
+
.requiredOption("--pid <pid>", "target process ID")
|
|
562
|
+
.requiredOption("--build <buildKey>", "build key")
|
|
563
|
+
.requiredOption("--script <path>", "GumJS script file")
|
|
564
|
+
.option("--export <name>", "rpc.exports function to call")
|
|
565
|
+
.option("--args <json>", "JSON object exposed as __WOWDUMP_INPUT__", "{}")
|
|
566
|
+
.option("--call-args <json>", "JSON array passed to the exported function", "[]")
|
|
567
|
+
.option("--duration-ms <ms>", "maximum script duration", "5000")
|
|
568
|
+
.option("--confirm", "confirm the exact Frida operation")
|
|
431
569
|
.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
|
|
570
|
+
const normalized = {
|
|
571
|
+
pid: positiveInteger(options.pid, "pid"),
|
|
572
|
+
build: options.build,
|
|
573
|
+
script: resolve(options.script),
|
|
574
|
+
...(options.export ? { exportName: options.export } : {}),
|
|
575
|
+
args: jsonRecord(options.args, "args"),
|
|
576
|
+
callArgs: JSON.parse(options.callArgs),
|
|
577
|
+
durationMs: positiveInteger(options.durationMs, "duration-ms"),
|
|
578
|
+
confirm: options.confirm === true
|
|
445
579
|
};
|
|
446
|
-
if (
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
580
|
+
if (!Array.isArray(normalized.callArgs))
|
|
581
|
+
throw new CliError("ARGUMENT_INVALID", "call-args must be a JSON array");
|
|
582
|
+
requireRuntimeConfirmation(normalized);
|
|
583
|
+
if (!existsSync(normalized.script))
|
|
584
|
+
throw new CliError("FRIDA_SCRIPT_NOT_FOUND", `Frida script was not found: ${normalized.script}`);
|
|
585
|
+
const worker = resolve(env.WOWDUMP_FRIDA_WORKER ?? join(packageDist, "frida-worker.js"));
|
|
586
|
+
if (!existsSync(worker))
|
|
587
|
+
throw new CliError("FRIDA_WORKER_NOT_FOUND", `Frida worker was not found: ${worker}`);
|
|
588
|
+
const request = { command: "dynamic-script", ...selected(normalized, ["pid", "build", "script", "exportName", "args", "callArgs", "durationMs"]) };
|
|
589
|
+
const result = await sidecar(process.execPath, [worker], `${JSON.stringify(request)}\n`);
|
|
590
|
+
if (result.exitCode !== 0)
|
|
591
|
+
throw new CliError("FRIDA_WORKER_FAILED", result.stderr.trim() || `Frida worker exited with ${result.exitCode}`);
|
|
592
|
+
const line = result.stdout.split(/\r?\n/).find(value => value.trim());
|
|
593
|
+
writeJson(io, line ? JSON.parse(line) : { ok: true, command: "analyze.dynamic", stdout: result.stdout });
|
|
451
594
|
});
|
|
452
595
|
program.command("init")
|
|
453
596
|
.description("Initialize WOWDUMP_HOME without overwriting existing files")
|
|
454
|
-
.option("--skip-toolchain-download", "
|
|
597
|
+
.option("--skip-toolchain-download", "kept for compatibility; no external toolchain is installed")
|
|
455
598
|
.action(async (options) => {
|
|
456
599
|
const installMissing = options.skipToolchainDownload !== true && env.WOWDUMP_SKIP_TOOLCHAIN_DOWNLOAD !== "1";
|
|
457
600
|
const result = await bootstrapToolchain({ home, env, installMissing });
|
|
@@ -493,5 +636,7 @@ if (process.argv[1]) {
|
|
|
493
636
|
invokedFile = pathToFileURL(invokedPath).href;
|
|
494
637
|
}
|
|
495
638
|
}
|
|
496
|
-
if (invokedFile === import.meta.url)
|
|
639
|
+
if (invokedFile === import.meta.url) {
|
|
640
|
+
await initializeWowdumpHome();
|
|
497
641
|
process.exitCode = await runWowdumpCli();
|
|
642
|
+
}
|