wowdump 0.3.5 → 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 +8 -6
- package/dist/analysis/runtime-dump.js +2 -2
- package/dist/cli.js +120 -176
- package/dist/core/build-adapters.js +0 -8
- package/dist/debug/cdb.js +412 -0
- package/dist/reader/broker.js +20 -10
- 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 -13
- package/skills/wowdump/references/commands.md +27 -50
- 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,22 +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
|
|
29
31
|
|
|
30
32
|
# 枚举已提交虚拟内存区(复用同一个管理员 broker)
|
|
31
33
|
wowdump memory regions --pid 1234 --start 0x15000000000 --end 0x15400000000 --max-regions 20000
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
Windows reader 首次使用时由 Node 请求 UAC 启动 broker
|
|
36
|
+
Windows reader 首次使用时由 Node 请求 UAC 启动 broker,后续内存读取和 CDB 调试请求复用同一 broker,空闲 20 分钟后退出。CDB 路径探测不会下载或修改环境变量;所有调试请求都需要显式 `--confirm`。
|
|
35
37
|
|
|
36
|
-
`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`。
|
|
37
39
|
|
|
38
40
|
## 开发验证
|
|
39
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) => {
|
|
@@ -478,7 +447,7 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
478
447
|
memory.command("regions")
|
|
479
448
|
.description("Enumerate committed virtual memory regions through VirtualQueryEx")
|
|
480
449
|
.requiredOption("--pid <pid>", "target process ID")
|
|
481
|
-
.option("--start <hex>", "query start address", "
|
|
450
|
+
.option("--start <hex>", "query start address", "0x10000")
|
|
482
451
|
.option("--end <hex>", "query end address")
|
|
483
452
|
.option("--max-regions <count>", "maximum returned regions", "4096")
|
|
484
453
|
.option("--include-free", "include free and reserved regions")
|
|
@@ -528,165 +497,140 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
528
497
|
.description("Stop a monitor and release its resources")
|
|
529
498
|
.requiredOption("--id <watchId>", "monitor ID")
|
|
530
499
|
.action(async (options) => writeJson(io, await broker({ command: "watch.stop", payload: { watchId: options.id } })));
|
|
531
|
-
const analyze = program.command("analyze").description("Run
|
|
500
|
+
const analyze = program.command("analyze").description("Run bounded WinDbg/CDB evidence operations");
|
|
532
501
|
analyze.command("runtime")
|
|
533
|
-
.description("Dump
|
|
502
|
+
.description("Dump PE sections with CDB or verify a profile through the reader broker")
|
|
534
503
|
.requiredOption("--pid <pid>", "target process ID")
|
|
535
504
|
.requiredOption("--build <buildKey>", "build key")
|
|
536
505
|
.option("--build-key <buildKey>", "alias for --build")
|
|
537
506
|
.option("--kind <kind>", "dump or verify", "dump")
|
|
538
|
-
.option("--worker <path>", "Frida worker entry")
|
|
539
507
|
.option("--profile <path>", "reader profile containing candidate RVAs (verify only)")
|
|
540
508
|
.option("--output-dir <path>", "directory for dump manifest and section binaries")
|
|
541
509
|
.option("--sections <names...>", "sections to dump (default: .text .rdata .pdata)")
|
|
542
|
-
.option("--no-progress", "disable dump progress on stderr")
|
|
543
510
|
.option("--max-section-bytes <bytes>", "per-section dump limit", "134217728")
|
|
544
511
|
.option("--max-total-bytes <bytes>", "total dump limit", "268435456")
|
|
545
|
-
.option("--
|
|
546
|
-
.option("--
|
|
547
|
-
.option("--
|
|
548
|
-
.option("--
|
|
549
|
-
.option("--max-events <count>", "maximum events", "100")
|
|
550
|
-
.option("--cleanup-deadline-ms <ms>", "cleanup deadline", "2000")
|
|
551
|
-
.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")
|
|
552
516
|
.action(async (options) => {
|
|
553
517
|
const normalized = {
|
|
554
518
|
pid: positiveInteger(options.pid, "pid"),
|
|
555
519
|
build: options.build ?? options.buildKey,
|
|
556
|
-
kind: options.kind,
|
|
557
|
-
maxHooks: positiveInteger(options.maxHooks, "max-hooks"),
|
|
558
|
-
durationMs: positiveInteger(options.durationMs, "duration-ms"),
|
|
559
|
-
maxEvents: positiveInteger(options.maxEvents, "max-events"),
|
|
560
|
-
cleanupDeadlineMs: positiveInteger(options.cleanupDeadlineMs, "cleanup-deadline-ms"),
|
|
520
|
+
kind: String(options.kind),
|
|
561
521
|
maxSectionBytes: positiveInteger(options.maxSectionBytes, "max-section-bytes"),
|
|
562
522
|
maxTotalBytes: positiveInteger(options.maxTotalBytes, "max-total-bytes"),
|
|
563
|
-
|
|
564
|
-
maxVerifyBytes: positiveInteger(options.maxVerifyBytes, "max-verify-bytes"),
|
|
523
|
+
durationMs: positiveInteger(options.durationMs, "duration-ms"),
|
|
565
524
|
confirm: options.confirm === true
|
|
566
525
|
};
|
|
567
526
|
if (!["dump", "verify"].includes(normalized.kind))
|
|
568
527
|
throw new CliError("ARGUMENT_INVALID", "kind must be dump or verify");
|
|
569
528
|
requireRuntimeConfirmation(normalized);
|
|
570
|
-
if (normalized.kind === "verify" && !options.profile)
|
|
571
|
-
throw new CliError("PROFILE_REQUIRED", "verify requires --profile <file>");
|
|
572
|
-
const worker = resolve(options.worker ?? env.WOWDUMP_FRIDA_WORKER ?? join(packageDist, "frida-worker.js"));
|
|
573
|
-
if (!existsSync(worker))
|
|
574
|
-
throw new CliError("FRIDA_WORKER_NOT_FOUND", `Frida worker was not found: ${worker}`);
|
|
575
|
-
const profile = options.profile
|
|
576
|
-
? jsonRecord(await readFile(resolve(options.profile), "utf8"), options.profile)
|
|
577
|
-
: undefined;
|
|
578
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;
|
|
579
533
|
if (profile)
|
|
580
534
|
await validateProfileIdentity(home, normalized.build, profile, resolve(options.profile));
|
|
581
|
-
|
|
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
|
|
582
543
|
? options.sections.map((value) => String(value).toLowerCase())
|
|
583
544
|
: [".text", ".rdata", ".pdata"];
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (reusable) {
|
|
595
|
-
writeJson(io, { ok: true, command: "analyze.runtime.dump", reused: true, manifestFile: String(reusable.manifestFile ?? ""), manifest: reusable });
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
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;
|
|
598
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);
|
|
599
563
|
const sessionId = randomUUID();
|
|
600
|
-
const sessionDirectory =
|
|
601
|
-
? resolve(options.outputDir, "..")
|
|
602
|
-
: join(buildPaths(home, normalized.build).runtime, sessionId);
|
|
564
|
+
const sessionDirectory = join(buildPaths(home, normalized.build).runtime, sessionId);
|
|
603
565
|
const outputDirectory = resolve(options.outputDir ?? join(sessionDirectory, "dump"));
|
|
604
566
|
await mkdir(sessionDirectory, { recursive: true });
|
|
605
|
-
await writeFile(join(sessionDirectory, "target.json"), `${JSON.stringify({ schema: "wowdump.runtime-target.
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
maxSectionBytes: normalized.maxSectionBytes,
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
callArgs: [],
|
|
627
|
-
durationMs: normalized.durationMs
|
|
628
|
-
};
|
|
629
|
-
const result = await sidecar(process.execPath, [worker], `${JSON.stringify(request)}\n`);
|
|
630
|
-
if (result.stderr && options.progress !== false)
|
|
631
|
-
io.stderr.write(result.stderr);
|
|
632
|
-
if (result.exitCode !== 0)
|
|
633
|
-
throw new CliError("FRIDA_WORKER_FAILED", result.stderr.trim() || `Frida worker exited with ${result.exitCode}`);
|
|
634
|
-
const line = result.stdout.split(/\r?\n/).find(value => value.trim());
|
|
635
|
-
const value = line ? jsonRecord(line, "runtime result") : { ok: true, stdout: result.stdout };
|
|
636
|
-
if (normalized.kind === "dump" && value.ok === true) {
|
|
637
|
-
if (typeof value.manifestFile === "string") {
|
|
638
|
-
await writeFile(join(sessionDirectory, "dynamic.json"), `${JSON.stringify(value.manifest ?? value, null, 2)}\n`, "utf8").catch(() => undefined);
|
|
639
|
-
writeJson(io, { ...value, command: "analyze.runtime.dump", sessionDirectory });
|
|
640
|
-
}
|
|
641
|
-
else
|
|
642
|
-
throw new CliError("RUNTIME_DUMP_INVALID", "runtime dump worker returned no manifest");
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
await writeFile(join(sessionDirectory, "verify.json"), `${JSON.stringify(value, null, 2)}\n`, "utf8").catch(() => undefined);
|
|
646
|
-
writeJson(io, { ...value, command: "analyze.runtime.verify" });
|
|
647
|
-
}
|
|
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 });
|
|
648
588
|
});
|
|
649
|
-
analyze.command("
|
|
650
|
-
.description("Run a
|
|
589
|
+
analyze.command("debug")
|
|
590
|
+
.description("Run a bounded WinDbg/CDB command, breakpoint, or diagnostic session")
|
|
651
591
|
.requiredOption("--pid <pid>", "target process ID")
|
|
652
592
|
.requiredOption("--build <buildKey>", "build key")
|
|
653
|
-
.
|
|
654
|
-
.option("--
|
|
655
|
-
.option("--
|
|
656
|
-
.option("--
|
|
657
|
-
.option("--
|
|
658
|
-
.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")
|
|
659
601
|
.action(async (options) => {
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (!
|
|
674
|
-
throw new CliError("
|
|
675
|
-
const
|
|
676
|
-
if (!existsSync(worker))
|
|
677
|
-
throw new CliError("FRIDA_WORKER_NOT_FOUND", `Frida worker was not found: ${worker}`);
|
|
678
|
-
const request = { command: "dynamic-script", ...selected(normalized, ["pid", "build", "script", "exportName", "args", "callArgs", "durationMs"]) };
|
|
679
|
-
const sessionId = randomUUID();
|
|
680
|
-
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());
|
|
681
618
|
await mkdir(sessionDirectory, { recursive: true });
|
|
682
|
-
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
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 });
|
|
690
634
|
});
|
|
691
635
|
program.command("init")
|
|
692
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");
|