wowdump 0.3.4 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -6
- package/dist/analysis/runtime-dump.js +2 -2
- package/dist/cli.js +133 -175
- package/dist/core/build-adapters.js +0 -8
- package/dist/debug/cdb.js +412 -0
- package/dist/reader/broker.js +88 -7
- package/dist/reader/launcher.js +5 -5
- package/dist/reader/main.js +18 -35
- package/dist/reader/windows.js +2 -2
- package/dist/toolchain.js +3 -3
- package/package.json +4 -5
- package/skills/wowdump/SKILL.md +18 -12
- package/skills/wowdump/references/commands.md +27 -38
- package/skills/wowdump/references/disassemble.md +3 -3
- package/skills/wowdump/references/evidence-workflow.md +6 -6
- package/skills/wowdump/references/profiles.md +2 -2
- package/skills/wowdump/references/request-schema.md +2 -2
- package/skills/wowdump/references/windbg.md +55 -0
- package/skills/wowdump/references/workflow.md +17 -32
- package/dist/analysis/frida-runtime.js +0 -715
- package/dist/analysis/runtime-script.js +0 -156
- package/dist/frida-worker.js +0 -153
- package/skills/wowdump/references/dynamic.md +0 -54
- package/skills/wowdump/scripts/dynamic-session.js +0 -133
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# wowdump
|
|
2
2
|
|
|
3
|
-
WoW 原生内存分析 CLI。用
|
|
3
|
+
WoW 原生内存分析 CLI。用 WinDbg CDB 运行时证据和 IDA Pro/iced-x86 局部反汇编定位字段,生成可复核的 Reader profile。
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
@@ -18,19 +18,24 @@ wowdump init
|
|
|
18
18
|
wowdump targets
|
|
19
19
|
wowdump database status --build "retail@VERSION"
|
|
20
20
|
|
|
21
|
-
# 2. 导出运行时模块段
|
|
21
|
+
# 2. 用 CDB 导出运行时模块段
|
|
22
22
|
wowdump analyze runtime --pid 1234 --build "retail@VERSION" --kind dump --confirm
|
|
23
23
|
|
|
24
24
|
# 3. 用 IDA Pro MCP(若可用)或 iced-x86 分析 session dump manifest,保存字段候选和证据
|
|
25
|
-
# 4.
|
|
25
|
+
# 4. 需要函数调用参数时,用 CDB 硬件断点
|
|
26
|
+
wowdump analyze debug --pid 1234 --build "retail@VERSION" --rva 0x... --kind breakpoint --confirm
|
|
27
|
+
# 5. 将候选证据交给 verify,再用 Reader 验证
|
|
26
28
|
|
|
27
|
-
#
|
|
29
|
+
# 6. 用户确认后,把 profile 保存到当前 build 的 profile 目录,再读取
|
|
28
30
|
wowdump memory read --pid 1234 --profile "$HOME\.wowdump\retail@VERSION\profile\player-state.json" --field playerAuras
|
|
31
|
+
|
|
32
|
+
# 枚举已提交虚拟内存区(复用同一个管理员 broker)
|
|
33
|
+
wowdump memory regions --pid 1234 --start 0x15000000000 --end 0x15400000000 --max-regions 20000
|
|
29
34
|
```
|
|
30
35
|
|
|
31
|
-
Windows reader 首次使用时由 Node 请求 UAC 启动 broker
|
|
36
|
+
Windows reader 首次使用时由 Node 请求 UAC 启动 broker,后续内存读取和 CDB 调试请求复用同一 broker,空闲 20 分钟后退出。CDB 路径探测不会下载或修改环境变量;所有调试请求都需要显式 `--confirm`。
|
|
32
37
|
|
|
33
|
-
`targets` 会写入 `~/.wowdump/<buildKey>/build.json` 和 `database/database.json`;同一 build 的 IDA 数据库只创建一份。`analyze runtime --kind dump` 默认把 `.text`、`.rdata`、`.pdata`
|
|
38
|
+
`targets` 会写入 `~/.wowdump/<buildKey>/build.json` 和 `database/database.json`;同一 build 的 IDA 数据库只创建一份。`analyze runtime --kind dump` 默认把 `.text`、`.rdata`、`.pdata` 写入当前 session,`.data` 需通过 `--sections` 显式加入。`analyze runtime --kind verify --profile <file>` 只验证已有 profile 的 RVA。Reader 只读取 `~/.wowdump/<buildKey>/profile/`,不读取 IDA 数据库和 dump 二进制。完整字段和 JSON 约定见 `~/.agents/skills/wowdump/references/commands.md`、`references/request-schema.md`、`references/windbg.md` 与 `references/profiles.md`。
|
|
34
39
|
|
|
35
40
|
## 开发验证
|
|
36
41
|
|
|
@@ -5,7 +5,7 @@ function safeName(name) {
|
|
|
5
5
|
const value = String(name ?? "section");
|
|
6
6
|
return /^[A-Za-z0-9_.-]+$/.test(value) ? value : "section";
|
|
7
7
|
}
|
|
8
|
-
/** Streams
|
|
8
|
+
/** Streams section bytes into files and writes a compact runtime manifest. */
|
|
9
9
|
export class RuntimeDumpWriter {
|
|
10
10
|
outputDirectory;
|
|
11
11
|
options;
|
|
@@ -61,7 +61,7 @@ export class RuntimeDumpWriter {
|
|
|
61
61
|
};
|
|
62
62
|
});
|
|
63
63
|
const manifest = {
|
|
64
|
-
schema: "wowdump.runtime-dump.
|
|
64
|
+
schema: "wowdump.runtime-dump.v3",
|
|
65
65
|
kind: "dump",
|
|
66
66
|
buildKey: this.options.buildKey,
|
|
67
67
|
pid: this.options.pid,
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { Command } from "commander";
|
|
|
9
9
|
import { WindowsBrokerManager } from "./reader/launcher.js";
|
|
10
10
|
import { initializeWowdumpHome, bootstrapToolchain, resolveToolchain } from "./toolchain.js";
|
|
11
11
|
import { buildPaths, databaseStatus, ensureBuild, findReusableDump, readBuild, resolveWowdumpHome, safeBuildKey, sha256File } from "./core/build-store.js";
|
|
12
|
-
import {
|
|
12
|
+
import { readPeLayout } from "./debug/cdb.js";
|
|
13
13
|
import { ProfileEngine } from "./core/profile-engine.js";
|
|
14
14
|
import { ReaderProfileAdapter } from "./adapters/reader.js";
|
|
15
15
|
import { enumerateWindowsProcesses } from "./reader/windows.js";
|
|
@@ -82,7 +82,7 @@ async function discoverWowTargets(selectedPid, elevatedModules) {
|
|
|
82
82
|
catch { /* try the next parent directory */ }
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
targets.push({ pid, name, path, moduleBase, moduleSize, fileVersion, product, buildKey: fileVersion ? `retail@${fileVersion}` : null, ...(buildInfoFile ? { buildInfoFile } : {}), ...(diagnostic ? { diagnostics: {
|
|
85
|
+
targets.push({ pid, name, path, moduleBase, moduleSize, fileVersion, product, buildKey: fileVersion ? `retail@${fileVersion}` : null, ...(buildInfoFile ? { buildInfoFile } : {}), ...(diagnostic ? { diagnostics: { modules: diagnostic } } : {}) });
|
|
86
86
|
}
|
|
87
87
|
return targets;
|
|
88
88
|
}
|
|
@@ -198,23 +198,6 @@ function jsonRecord(value, field) {
|
|
|
198
198
|
function writeJson(io, value) {
|
|
199
199
|
io.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
200
200
|
}
|
|
201
|
-
async function defaultSidecar(file, args, input) {
|
|
202
|
-
return new Promise(resolveResult => {
|
|
203
|
-
const child = spawn(file, args, { windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
204
|
-
let stdout = "";
|
|
205
|
-
let stderr = "";
|
|
206
|
-
child.stdout.setEncoding("utf8");
|
|
207
|
-
child.stderr.setEncoding("utf8");
|
|
208
|
-
child.stdout.on("data", value => { stdout += value; });
|
|
209
|
-
child.stderr.on("data", value => { stderr += value; });
|
|
210
|
-
child.once("error", error => resolveResult({ exitCode: 1, stdout, stderr: `${stderr}${error.message}` }));
|
|
211
|
-
child.once("exit", code => resolveResult({ exitCode: code ?? 1, stdout, stderr }));
|
|
212
|
-
if (input !== undefined)
|
|
213
|
-
child.stdin.end(input);
|
|
214
|
-
else
|
|
215
|
-
child.stdin.end();
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
201
|
function brokerCommandLine(env, packageDist) {
|
|
219
202
|
const configured = env.WOWDUMP_READER_COMMAND?.trim();
|
|
220
203
|
if (configured)
|
|
@@ -222,12 +205,23 @@ function brokerCommandLine(env, packageDist) {
|
|
|
222
205
|
const candidate = join(packageDist, "reader-main.js");
|
|
223
206
|
return existsSync(candidate) ? { file: process.execPath, args: [candidate, "--request-stdio"] } : null;
|
|
224
207
|
}
|
|
225
|
-
async function defaultBroker(invocation, env, packageDist
|
|
208
|
+
async function defaultBroker(invocation, env, packageDist) {
|
|
226
209
|
const command = brokerCommandLine(env, packageDist);
|
|
227
210
|
if (!command) {
|
|
228
211
|
throw new CliError("BROKER_NOT_CONFIGURED", "reader broker entry was not found", { expected: join(packageDist, "reader-main.js"), environment: "WOWDUMP_READER_COMMAND" });
|
|
229
212
|
}
|
|
230
|
-
const result = await
|
|
213
|
+
const result = await new Promise(resolveResult => {
|
|
214
|
+
const child = spawn(command.file, command.args, { shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
215
|
+
let stdout = "";
|
|
216
|
+
let stderr = "";
|
|
217
|
+
child.stdout?.setEncoding("utf8");
|
|
218
|
+
child.stderr?.setEncoding("utf8");
|
|
219
|
+
child.stdout?.on("data", value => { stdout += String(value); });
|
|
220
|
+
child.stderr?.on("data", value => { stderr += String(value); });
|
|
221
|
+
child.once("error", error => resolveResult({ exitCode: 1, stdout, stderr: `${stderr}${error.message}` }));
|
|
222
|
+
child.once("exit", code => resolveResult({ exitCode: code ?? 1, stdout, stderr }));
|
|
223
|
+
child.stdin?.end(`${JSON.stringify(invocation)}\n`);
|
|
224
|
+
});
|
|
231
225
|
if (result.exitCode !== 0)
|
|
232
226
|
throw new CliError("BROKER_FAILED", result.stderr.trim() || `reader broker exited with ${result.exitCode}`);
|
|
233
227
|
const line = result.stdout.split(/\r?\n/).find(value => value.trim());
|
|
@@ -240,22 +234,6 @@ async function defaultBroker(invocation, env, packageDist, sidecar) {
|
|
|
240
234
|
throw new CliError("BROKER_PROTOCOL_ERROR", "reader broker returned invalid JSON", { stdout: result.stdout });
|
|
241
235
|
}
|
|
242
236
|
}
|
|
243
|
-
async function runFridaWorker(worker, input, timeoutMs, broker) {
|
|
244
|
-
if (process.platform !== "win32") {
|
|
245
|
-
throw new CliError("PLATFORM_UNSUPPORTED", "elevated Frida broker is currently supported on Windows only");
|
|
246
|
-
}
|
|
247
|
-
const raw = await broker({ command: "frida", payload: { worker, input, timeoutMs } });
|
|
248
|
-
const response = raw && typeof raw === "object" ? raw : {};
|
|
249
|
-
if (response.ok === false) {
|
|
250
|
-
const error = response.error && typeof response.error === "object" ? response.error : {};
|
|
251
|
-
throw new CliError("BROKER_FAILED", String(error.message ?? "elevated Frida worker failed"), { response });
|
|
252
|
-
}
|
|
253
|
-
return {
|
|
254
|
-
exitCode: Number.isSafeInteger(Number(response.exitCode)) ? Number(response.exitCode) : 1,
|
|
255
|
-
stdout: typeof response.stdout === "string" ? response.stdout : "",
|
|
256
|
-
stderr: typeof response.stderr === "string" ? response.stderr : ""
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
237
|
const windowsBrokerManagers = new Map();
|
|
260
238
|
function persistentWindowsBroker(home, packageDist) {
|
|
261
239
|
const key = resolve(home).toLowerCase();
|
|
@@ -307,15 +285,13 @@ function selected(source, keys) {
|
|
|
307
285
|
function requireRuntimeConfirmation(options) {
|
|
308
286
|
if (options.confirm === true)
|
|
309
287
|
return;
|
|
310
|
-
throw new CliError("CONFIRMATION_REQUIRED", "
|
|
288
|
+
throw new CliError("CONFIRMATION_REQUIRED", "debug analysis requires --confirm", {
|
|
311
289
|
pid: options.pid,
|
|
312
290
|
buildKey: options.build,
|
|
313
291
|
operation: "runtime",
|
|
314
292
|
kind: options.kind,
|
|
315
|
-
maxHooks: options.maxHooks,
|
|
316
293
|
durationMs: options.durationMs,
|
|
317
|
-
|
|
318
|
-
cleanupDeadlineMs: options.cleanupDeadlineMs
|
|
294
|
+
maxHits: options.maxHits
|
|
319
295
|
});
|
|
320
296
|
}
|
|
321
297
|
function profileDirectoryFor(home, buildKey) {
|
|
@@ -355,16 +331,9 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
355
331
|
const cwd = resolve(dependencies.cwd ?? process.cwd());
|
|
356
332
|
const packageDist = installedDistDirectory();
|
|
357
333
|
const home = resolveWowdumpHome(env);
|
|
358
|
-
const broker = dependencies.broker ?? (process.platform === "win32"
|
|
334
|
+
const broker = dependencies.broker ?? (process.platform === "win32"
|
|
359
335
|
? (invocation => persistentWindowsBroker(home, packageDist).request(invocation))
|
|
360
|
-
: (invocation => defaultBroker(invocation, env, packageDist
|
|
361
|
-
const baseSidecar = dependencies.sidecar ?? defaultSidecar;
|
|
362
|
-
const sidecar = async (file, args, input) => {
|
|
363
|
-
const worker = args.length === 1 && /\.m?js$/i.test(args[0] ?? "") ? args[0] : undefined;
|
|
364
|
-
if (worker && input !== undefined)
|
|
365
|
-
return runFridaWorker(worker, input, 120_000, broker);
|
|
366
|
-
return baseSidecar(file, args, input);
|
|
367
|
-
};
|
|
336
|
+
: (invocation => defaultBroker(invocation, env, packageDist)));
|
|
368
337
|
const profileEngine = new ProfileEngine();
|
|
369
338
|
const profileAdapter = new ReaderProfileAdapter(broker);
|
|
370
339
|
const program = new Command()
|
|
@@ -398,7 +367,7 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
398
367
|
writeJson(io, { ...await databaseStatus(home, options.build), command: "database.status" });
|
|
399
368
|
});
|
|
400
369
|
program.command("target")
|
|
401
|
-
.description("Show target, reader broker, profile, monitor, and
|
|
370
|
+
.description("Show target, reader broker, profile, monitor, and debugger state")
|
|
402
371
|
.option("--pid <pid>", "target process ID")
|
|
403
372
|
.option("--build <buildKey>", "expected build key")
|
|
404
373
|
.action(async (options) => {
|
|
@@ -475,6 +444,20 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
475
444
|
}
|
|
476
445
|
writeJson(io, await broker({ command: "read", payload }));
|
|
477
446
|
});
|
|
447
|
+
memory.command("regions")
|
|
448
|
+
.description("Enumerate committed virtual memory regions through VirtualQueryEx")
|
|
449
|
+
.requiredOption("--pid <pid>", "target process ID")
|
|
450
|
+
.option("--start <hex>", "query start address", "0x10000")
|
|
451
|
+
.option("--end <hex>", "query end address")
|
|
452
|
+
.option("--max-regions <count>", "maximum returned regions", "4096")
|
|
453
|
+
.option("--include-free", "include free and reserved regions")
|
|
454
|
+
.action(async (options) => writeJson(io, await broker({ command: "regions", payload: {
|
|
455
|
+
pid: positiveInteger(options.pid, "pid"),
|
|
456
|
+
start: options.start,
|
|
457
|
+
...(options.end ? { end: options.end } : {}),
|
|
458
|
+
maxRegions: positiveInteger(options.maxRegions, "max-regions"),
|
|
459
|
+
includeFree: options.includeFree === true
|
|
460
|
+
} })));
|
|
478
461
|
const watch = memory.command("watch").description("Manage broker-owned memory monitors");
|
|
479
462
|
watch.command("start")
|
|
480
463
|
.description("Start a monitor")
|
|
@@ -514,165 +497,140 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
514
497
|
.description("Stop a monitor and release its resources")
|
|
515
498
|
.requiredOption("--id <watchId>", "monitor ID")
|
|
516
499
|
.action(async (options) => writeJson(io, await broker({ command: "watch.stop", payload: { watchId: options.id } })));
|
|
517
|
-
const analyze = program.command("analyze").description("Run
|
|
500
|
+
const analyze = program.command("analyze").description("Run bounded WinDbg/CDB evidence operations");
|
|
518
501
|
analyze.command("runtime")
|
|
519
|
-
.description("Dump
|
|
502
|
+
.description("Dump PE sections with CDB or verify a profile through the reader broker")
|
|
520
503
|
.requiredOption("--pid <pid>", "target process ID")
|
|
521
504
|
.requiredOption("--build <buildKey>", "build key")
|
|
522
505
|
.option("--build-key <buildKey>", "alias for --build")
|
|
523
506
|
.option("--kind <kind>", "dump or verify", "dump")
|
|
524
|
-
.option("--worker <path>", "Frida worker entry")
|
|
525
507
|
.option("--profile <path>", "reader profile containing candidate RVAs (verify only)")
|
|
526
508
|
.option("--output-dir <path>", "directory for dump manifest and section binaries")
|
|
527
509
|
.option("--sections <names...>", "sections to dump (default: .text .rdata .pdata)")
|
|
528
|
-
.option("--no-progress", "disable dump progress on stderr")
|
|
529
510
|
.option("--max-section-bytes <bytes>", "per-section dump limit", "134217728")
|
|
530
511
|
.option("--max-total-bytes <bytes>", "total dump limit", "268435456")
|
|
531
|
-
.option("--
|
|
532
|
-
.option("--
|
|
533
|
-
.option("--
|
|
534
|
-
.option("--
|
|
535
|
-
.option("--max-events <count>", "maximum events", "100")
|
|
536
|
-
.option("--cleanup-deadline-ms <ms>", "cleanup deadline", "2000")
|
|
537
|
-
.option("--confirm", "confirm the exact bounded Frida operation")
|
|
512
|
+
.option("--duration-ms <ms>", "CDB timeout", "30000")
|
|
513
|
+
.option("--cdb-args <json>", "JSON array of extra CDB arguments", "[]")
|
|
514
|
+
.option("--cdb-commands <json>", "JSON array of extra CDB commands", "[]")
|
|
515
|
+
.option("--confirm", "confirm the bounded debugger operation")
|
|
538
516
|
.action(async (options) => {
|
|
539
517
|
const normalized = {
|
|
540
518
|
pid: positiveInteger(options.pid, "pid"),
|
|
541
519
|
build: options.build ?? options.buildKey,
|
|
542
|
-
kind: options.kind,
|
|
543
|
-
maxHooks: positiveInteger(options.maxHooks, "max-hooks"),
|
|
544
|
-
durationMs: positiveInteger(options.durationMs, "duration-ms"),
|
|
545
|
-
maxEvents: positiveInteger(options.maxEvents, "max-events"),
|
|
546
|
-
cleanupDeadlineMs: positiveInteger(options.cleanupDeadlineMs, "cleanup-deadline-ms"),
|
|
520
|
+
kind: String(options.kind),
|
|
547
521
|
maxSectionBytes: positiveInteger(options.maxSectionBytes, "max-section-bytes"),
|
|
548
522
|
maxTotalBytes: positiveInteger(options.maxTotalBytes, "max-total-bytes"),
|
|
549
|
-
|
|
550
|
-
maxVerifyBytes: positiveInteger(options.maxVerifyBytes, "max-verify-bytes"),
|
|
523
|
+
durationMs: positiveInteger(options.durationMs, "duration-ms"),
|
|
551
524
|
confirm: options.confirm === true
|
|
552
525
|
};
|
|
553
526
|
if (!["dump", "verify"].includes(normalized.kind))
|
|
554
527
|
throw new CliError("ARGUMENT_INVALID", "kind must be dump or verify");
|
|
555
528
|
requireRuntimeConfirmation(normalized);
|
|
556
|
-
if (normalized.kind === "verify" && !options.profile)
|
|
557
|
-
throw new CliError("PROFILE_REQUIRED", "verify requires --profile <file>");
|
|
558
|
-
const worker = resolve(options.worker ?? env.WOWDUMP_FRIDA_WORKER ?? join(packageDist, "frida-worker.js"));
|
|
559
|
-
if (!existsSync(worker))
|
|
560
|
-
throw new CliError("FRIDA_WORKER_NOT_FOUND", `Frida worker was not found: ${worker}`);
|
|
561
|
-
const profile = options.profile
|
|
562
|
-
? jsonRecord(await readFile(resolve(options.profile), "utf8"), options.profile)
|
|
563
|
-
: undefined;
|
|
564
529
|
const buildRecord = await readBuild(home, normalized.build);
|
|
530
|
+
if (!buildRecord)
|
|
531
|
+
throw new CliError("BUILD_NOT_FOUND", `build ${normalized.build} has not been discovered`);
|
|
532
|
+
const profile = options.profile ? jsonRecord(await readFile(resolve(options.profile), "utf8"), options.profile) : undefined;
|
|
565
533
|
if (profile)
|
|
566
534
|
await validateProfileIdentity(home, normalized.build, profile, resolve(options.profile));
|
|
567
|
-
|
|
535
|
+
if (normalized.kind === "verify") {
|
|
536
|
+
if (!profile)
|
|
537
|
+
throw new CliError("PROFILE_REQUIRED", "verify requires --profile <file>");
|
|
538
|
+
const result = await profileEngine.read({ pid: normalized.pid, buildKey: normalized.build, executableSha256: buildRecord.executableSha256, profile, fields: undefined }, profileAdapter);
|
|
539
|
+
writeJson(io, { ...result, command: "analyze.runtime.verify", verified: true });
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const requestedSections = Array.isArray(options.sections) && options.sections.length > 0
|
|
568
543
|
? options.sections.map((value) => String(value).toLowerCase())
|
|
569
544
|
: [".text", ".rdata", ".pdata"];
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (reusable) {
|
|
581
|
-
writeJson(io, { ok: true, command: "analyze.runtime.dump", reused: true, manifestFile: String(reusable.manifestFile ?? ""), manifest: reusable });
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
545
|
+
const cdbArgs = JSON.parse(options.cdbArgs);
|
|
546
|
+
const cdbCommands = JSON.parse(options.cdbCommands);
|
|
547
|
+
if (!Array.isArray(cdbArgs) || !Array.isArray(cdbCommands))
|
|
548
|
+
throw new CliError("ARGUMENT_INVALID", "cdb-args and cdb-commands must be JSON arrays");
|
|
549
|
+
const reusable = requestedSections.includes(".data")
|
|
550
|
+
? null
|
|
551
|
+
: await findReusableDump(home, normalized.build, { executableSha256: buildRecord.executableSha256, sections: requestedSections, maxSectionBytes: normalized.maxSectionBytes, maxTotalBytes: normalized.maxTotalBytes, cdbArgs, commands: cdbCommands });
|
|
552
|
+
if (reusable) {
|
|
553
|
+
writeJson(io, { ok: true, command: "analyze.runtime.dump", reused: true, manifestFile: String(reusable.manifestFile ?? ""), manifest: reusable });
|
|
554
|
+
return;
|
|
584
555
|
}
|
|
556
|
+
const moduleResponse = await broker({ command: "modules", payload: { pid: normalized.pid } });
|
|
557
|
+
const modules = moduleResponse && typeof moduleResponse === "object" && Array.isArray(moduleResponse.modules) ? moduleResponse.modules : [];
|
|
558
|
+
const module = modules.find(item => /^Wow\.exe$/i.test(String(item.name ?? ""))) ?? modules[0];
|
|
559
|
+
if (!module || typeof module.base !== "string")
|
|
560
|
+
throw new CliError("MODULE_NOT_FOUND", "Wow.exe module was not found", { modules });
|
|
561
|
+
const moduleBase = module.base;
|
|
562
|
+
const layout = await readPeLayout(buildRecord.executablePath);
|
|
585
563
|
const sessionId = randomUUID();
|
|
586
|
-
const sessionDirectory =
|
|
587
|
-
? resolve(options.outputDir, "..")
|
|
588
|
-
: join(buildPaths(home, normalized.build).runtime, sessionId);
|
|
564
|
+
const sessionDirectory = join(buildPaths(home, normalized.build).runtime, sessionId);
|
|
589
565
|
const outputDirectory = resolve(options.outputDir ?? join(sessionDirectory, "dump"));
|
|
590
566
|
await mkdir(sessionDirectory, { recursive: true });
|
|
591
|
-
await writeFile(join(sessionDirectory, "target.json"), `${JSON.stringify({ schema: "wowdump.runtime-target.
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
maxSectionBytes: normalized.maxSectionBytes,
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
callArgs: [],
|
|
613
|
-
durationMs: normalized.durationMs
|
|
614
|
-
};
|
|
615
|
-
const result = await sidecar(process.execPath, [worker], `${JSON.stringify(request)}\n`);
|
|
616
|
-
if (result.stderr && options.progress !== false)
|
|
617
|
-
io.stderr.write(result.stderr);
|
|
618
|
-
if (result.exitCode !== 0)
|
|
619
|
-
throw new CliError("FRIDA_WORKER_FAILED", result.stderr.trim() || `Frida worker exited with ${result.exitCode}`);
|
|
620
|
-
const line = result.stdout.split(/\r?\n/).find(value => value.trim());
|
|
621
|
-
const value = line ? jsonRecord(line, "runtime result") : { ok: true, stdout: result.stdout };
|
|
622
|
-
if (normalized.kind === "dump" && value.ok === true) {
|
|
623
|
-
if (typeof value.manifestFile === "string") {
|
|
624
|
-
await writeFile(join(sessionDirectory, "dynamic.json"), `${JSON.stringify(value.manifest ?? value, null, 2)}\n`, "utf8").catch(() => undefined);
|
|
625
|
-
writeJson(io, { ...value, command: "analyze.runtime.dump", sessionDirectory });
|
|
626
|
-
}
|
|
627
|
-
else
|
|
628
|
-
throw new CliError("RUNTIME_DUMP_INVALID", "runtime dump worker returned no manifest");
|
|
629
|
-
}
|
|
630
|
-
else {
|
|
631
|
-
await writeFile(join(sessionDirectory, "verify.json"), `${JSON.stringify(value, null, 2)}\n`, "utf8").catch(() => undefined);
|
|
632
|
-
writeJson(io, { ...value, command: "analyze.runtime.verify" });
|
|
633
|
-
}
|
|
567
|
+
await writeFile(join(sessionDirectory, "target.json"), `${JSON.stringify({ schema: "wowdump.runtime-target.v2", pid: normalized.pid, buildKey: normalized.build, executableSha256: buildRecord.executableSha256, sessionId, createdAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
|
|
568
|
+
let total = 0;
|
|
569
|
+
const selectedSections = requestedSections.flatMap((name) => layout.sections.find((section) => section.name.toLowerCase() === name) ?? []);
|
|
570
|
+
const sections = selectedSections.map((section) => {
|
|
571
|
+
const requested = Math.min(Math.max(section.virtualSize, section.rawSize), normalized.maxSectionBytes, Math.max(0, normalized.maxTotalBytes - total));
|
|
572
|
+
total += requested;
|
|
573
|
+
return { ...section, runtimeAddress: `0x${(BigInt(moduleBase) + BigInt(section.rva)).toString(16)}`, readSize: requested, file: join(outputDirectory, `${section.name}.bin`) };
|
|
574
|
+
});
|
|
575
|
+
if (sections.length === 0)
|
|
576
|
+
throw new CliError("SECTION_NOT_FOUND", "no requested PE sections were found", { requestedSections, available: layout.sections.map(section => section.name) });
|
|
577
|
+
const raw = await broker({ command: "debug", payload: {
|
|
578
|
+
kind: "dump", pid: normalized.pid, buildKey: normalized.build, executablePath: buildRecord.executablePath,
|
|
579
|
+
executableSha256: buildRecord.executableSha256, moduleBase, preferredImageBase: buildRecord.preferredImageBase,
|
|
580
|
+
moduleSize: Number(module.size ?? buildRecord.moduleSize ?? layout.moduleSize), outputDir: outputDirectory, sections,
|
|
581
|
+
maxSectionBytes: normalized.maxSectionBytes, maxTotalBytes: normalized.maxTotalBytes, timeoutMs: normalized.durationMs,
|
|
582
|
+
cdbArgs, commands: cdbCommands
|
|
583
|
+
} });
|
|
584
|
+
const value = raw && typeof raw === "object" ? raw : {};
|
|
585
|
+
if (value.ok !== true)
|
|
586
|
+
throw new CliError(String(value.error?.code ?? "CDB_FAILED"), String(value.error?.message ?? "CDB dump failed"), value.error);
|
|
587
|
+
writeJson(io, { ...value, command: "analyze.runtime.dump", sessionDirectory });
|
|
634
588
|
});
|
|
635
|
-
analyze.command("
|
|
636
|
-
.description("Run a
|
|
589
|
+
analyze.command("debug")
|
|
590
|
+
.description("Run a bounded WinDbg/CDB command, breakpoint, or diagnostic session")
|
|
637
591
|
.requiredOption("--pid <pid>", "target process ID")
|
|
638
592
|
.requiredOption("--build <buildKey>", "build key")
|
|
639
|
-
.
|
|
640
|
-
.option("--
|
|
641
|
-
.option("--
|
|
642
|
-
.option("--
|
|
643
|
-
.option("--
|
|
644
|
-
.option("--
|
|
593
|
+
.option("--kind <kind>", "breakpoint or command", "breakpoint")
|
|
594
|
+
.option("--rva <rva>", "single statically-confirmed RVA")
|
|
595
|
+
.option("--max-hits <count>", "breakpoint hit limit", "3")
|
|
596
|
+
.option("--duration-ms <ms>", "CDB timeout", "3000")
|
|
597
|
+
.option("--cdb-args <json>", "JSON array of extra CDB arguments", "[]")
|
|
598
|
+
.option("--cdb-commands <json>", "JSON array of explicit CDB commands", "[]")
|
|
599
|
+
.option("--output <path>", "debug evidence JSON path")
|
|
600
|
+
.option("--confirm", "confirm the bounded debugger operation")
|
|
645
601
|
.action(async (options) => {
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
if (!
|
|
660
|
-
throw new CliError("
|
|
661
|
-
const
|
|
662
|
-
if (!existsSync(worker))
|
|
663
|
-
throw new CliError("FRIDA_WORKER_NOT_FOUND", `Frida worker was not found: ${worker}`);
|
|
664
|
-
const request = { command: "dynamic-script", ...selected(normalized, ["pid", "build", "script", "exportName", "args", "callArgs", "durationMs"]) };
|
|
665
|
-
const sessionId = randomUUID();
|
|
666
|
-
const sessionDirectory = join(buildPaths(home, normalized.build).runtime, sessionId);
|
|
602
|
+
const pid = positiveInteger(options.pid, "pid");
|
|
603
|
+
const build = String(options.build);
|
|
604
|
+
const durationMs = positiveInteger(options.durationMs, "duration-ms");
|
|
605
|
+
const kind = String(options.kind);
|
|
606
|
+
if (!["breakpoint", "command"].includes(kind))
|
|
607
|
+
throw new CliError("ARGUMENT_INVALID", "kind must be breakpoint or command");
|
|
608
|
+
requireRuntimeConfirmation({ pid, build, kind, durationMs, maxHits: options.maxHits, confirm: options.confirm === true });
|
|
609
|
+
const buildRecord = await readBuild(home, build);
|
|
610
|
+
if (!buildRecord)
|
|
611
|
+
throw new CliError("BUILD_NOT_FOUND", `build ${build} has not been discovered`);
|
|
612
|
+
const moduleResponse = await broker({ command: "modules", payload: { pid } });
|
|
613
|
+
const modules = moduleResponse && typeof moduleResponse === "object" && Array.isArray(moduleResponse.modules) ? moduleResponse.modules : [];
|
|
614
|
+
const module = modules.find(item => /^Wow\.exe$/i.test(String(item.name ?? ""))) ?? modules[0];
|
|
615
|
+
if (!module || typeof module.base !== "string")
|
|
616
|
+
throw new CliError("MODULE_NOT_FOUND", "Wow.exe module was not found");
|
|
617
|
+
const sessionDirectory = join(buildPaths(home, build).runtime, randomUUID());
|
|
667
618
|
await mkdir(sessionDirectory, { recursive: true });
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
619
|
+
const output = resolve(options.output ?? join(sessionDirectory, "debug.json"));
|
|
620
|
+
const parsedArgs = JSON.parse(options.cdbArgs);
|
|
621
|
+
const parsedCommands = JSON.parse(options.cdbCommands);
|
|
622
|
+
if (!Array.isArray(parsedArgs) || !Array.isArray(parsedCommands))
|
|
623
|
+
throw new CliError("ARGUMENT_INVALID", "cdb-args and cdb-commands must be JSON arrays");
|
|
624
|
+
const payload = kind === "command"
|
|
625
|
+
? { kind, pid, buildKey: build, durationMs, outputFile: output, cdbArgs: parsedArgs, commands: parsedCommands }
|
|
626
|
+
: { kind: "breakpoint", pid, buildKey: build, rva: options.rva, moduleBase: module.base, maxHits: positiveInteger(options.maxHits, "max-hits"), durationMs, outputFile: output, cdbArgs: parsedArgs, commands: parsedCommands };
|
|
627
|
+
if (kind !== "command" && !options.rva)
|
|
628
|
+
throw new CliError("RVA_REQUIRED", "breakpoint requires --rva");
|
|
629
|
+
const raw = await broker({ command: "debug", payload });
|
|
630
|
+
const value = raw && typeof raw === "object" ? raw : {};
|
|
631
|
+
if (value.ok !== true)
|
|
632
|
+
throw new CliError(String(value.error?.code ?? "CDB_FAILED"), String(value.error?.message ?? "CDB debug failed"), value.error);
|
|
633
|
+
writeJson(io, { ...value, command: "analyze.debug", sessionDirectory });
|
|
676
634
|
});
|
|
677
635
|
program.command("init")
|
|
678
636
|
.description("Initialize WOWDUMP_HOME without overwriting existing files")
|
|
@@ -30,14 +30,6 @@ function assertAdapter(adapter) {
|
|
|
30
30
|
if (typeof adapter.verification !== "string" || !adapter.verification.trim()) {
|
|
31
31
|
throw new TypeError("adapter.verification must identify the verification proof");
|
|
32
32
|
}
|
|
33
|
-
if (adapter.fridaPrelude !== undefined) {
|
|
34
|
-
if (typeof adapter.fridaPrelude !== "string") {
|
|
35
|
-
throw new TypeError("adapter.fridaPrelude must be a string when provided");
|
|
36
|
-
}
|
|
37
|
-
if (Buffer.byteLength(adapter.fridaPrelude, "utf8") > 1024 * 1024) {
|
|
38
|
-
throw new TypeError("adapter.fridaPrelude exceeds the 1 MiB limit");
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
33
|
const capabilities = adapter.capabilities;
|
|
42
34
|
if (!capabilities || typeof capabilities !== "object") {
|
|
43
35
|
throw new TypeError("adapter.capabilities must be provided");
|