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.
@@ -0,0 +1,412 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawn as nodeSpawn } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { dirname, isAbsolute, join, resolve } from "node:path";
7
+ export class CdbError extends Error {
8
+ code;
9
+ details;
10
+ constructor(code, message, details) {
11
+ super(message);
12
+ this.name = "CdbError";
13
+ this.code = code;
14
+ this.details = details;
15
+ }
16
+ }
17
+ function boundedInteger(value, name, minimum, maximum) {
18
+ const number = Number(value);
19
+ if (!Number.isSafeInteger(number) || number < minimum || number > maximum) {
20
+ throw new CdbError("CDB_ARGUMENT_INVALID", `${name} must be an integer between ${minimum} and ${maximum}`);
21
+ }
22
+ return number;
23
+ }
24
+ function address(value, name) {
25
+ const text = String(value ?? "").trim();
26
+ if (!/^0x[0-9a-f]+$/i.test(text))
27
+ throw new CdbError("CDB_ARGUMENT_INVALID", `${name} must be a hexadecimal address`);
28
+ const result = BigInt(text);
29
+ if (result < 0n || result > 0xffffffffffffffffn)
30
+ throw new CdbError("CDB_ARGUMENT_INVALID", `${name} is outside the 64-bit range`);
31
+ return result;
32
+ }
33
+ function hex(value) { return `0x${value.toString(16)}`; }
34
+ function validCommand(value, name) {
35
+ const command = String(value ?? "");
36
+ if (!command || command.includes("\0") || command.length > 64 * 1024)
37
+ throw new CdbError("CDB_ARGUMENT_INVALID", `${name} is empty or too large`);
38
+ return command;
39
+ }
40
+ function validCommandList(values, fallback = []) {
41
+ const source = values ?? fallback;
42
+ if (!Array.isArray(source) || source.length > 256)
43
+ throw new CdbError("CDB_ARGUMENT_INVALID", "CDB command list is too large");
44
+ const commands = source.map((value, index) => validCommand(value, `commands[${index}]`));
45
+ if (commands.join("\n").length > 512 * 1024)
46
+ throw new CdbError("CDB_ARGUMENT_INVALID", "CDB command script is too large");
47
+ return commands;
48
+ }
49
+ function validArgs(values) {
50
+ if (values === undefined)
51
+ return [];
52
+ if (!Array.isArray(values) || values.length > 128)
53
+ throw new CdbError("CDB_ARGUMENT_INVALID", "CDB argument list is too large");
54
+ return values.map((value, index) => {
55
+ const arg = String(value ?? "");
56
+ if (arg.includes("\0") || arg.length > 32 * 1024)
57
+ throw new CdbError("CDB_ARGUMENT_INVALID", `cdbArgs[${index}] is invalid`);
58
+ return arg;
59
+ });
60
+ }
61
+ async function spawnText(executable, args, timeoutMs) {
62
+ return runCdbProcess({ executable, args, cwd: dirname(executable), timeoutMs });
63
+ }
64
+ async function runWhere(name) {
65
+ if (process.platform !== "win32")
66
+ return undefined;
67
+ const result = await runCdbProcess({ executable: "where.exe", args: [name], cwd: process.cwd(), timeoutMs: 2_000 });
68
+ if (result.exitCode !== 0)
69
+ return undefined;
70
+ return result.stdout.split(/\r?\n/).map(value => value.trim()).find(value => value && existsSync(value));
71
+ }
72
+ function candidatePaths(env) {
73
+ const values = [];
74
+ const configured = env.WOWDUMP_CDB?.trim();
75
+ if (configured)
76
+ values.push({ path: configured, source: "environment" });
77
+ const programFilesX86 = env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
78
+ const programFiles = env.ProgramFiles ?? "C:\\Program Files";
79
+ const windir = env.WINDIR ?? "C:\\Windows";
80
+ values.push({ path: join(programFilesX86, "Windows Kits", "10", "Debuggers", "x64", "cdb.exe"), source: "windows-sdk" }, { path: join(programFiles, "Windows Kits", "10", "Debuggers", "x64", "cdb.exe"), source: "windows-sdk" }, { path: join(windir, "System32", "cdb.exe"), source: "windbg" }, { path: join(programFiles, "WindowsApps", "Microsoft.WinDbg_", "cdb.exe"), source: "windbg" }, { path: join(programFiles, "Windows Kits", "10", "Debuggers", "x64", "cdb.exe"), source: "windbg" });
81
+ return values;
82
+ }
83
+ async function discoverWinDbgDirectories(env) {
84
+ const roots = [env.ProgramFiles, env["ProgramFiles(x86)"], env.LOCALAPPDATA].filter((value) => Boolean(value));
85
+ const results = [];
86
+ for (const root of roots) {
87
+ for (const relative of ["Windows Kits", "WinDbg", "WindowsApps"]) {
88
+ const directory = join(root, relative);
89
+ try {
90
+ const entries = await readdir(directory, { withFileTypes: true });
91
+ for (const entry of entries.filter(item => item.isDirectory()).slice(0, 200)) {
92
+ results.push(join(directory, entry.name, "Debuggers", "x64", "cdb.exe"));
93
+ results.push(join(directory, entry.name, "cdb.exe"));
94
+ }
95
+ }
96
+ catch { /* directory is optional */ }
97
+ }
98
+ }
99
+ return results;
100
+ }
101
+ export async function discoverCdb(env = process.env) {
102
+ if (process.platform !== "win32" && env.WOWDUMP_CDB?.trim() === undefined) {
103
+ return { available: false, code: "CDB_NOT_FOUND", message: "未找到 WinDbg CDB" };
104
+ }
105
+ const candidates = [...candidatePaths(env), ...(await discoverWinDbgDirectories(env)).map(path => ({ path, source: "windbg" }))];
106
+ const seen = new Set();
107
+ for (const candidate of candidates) {
108
+ const path = resolve(candidate.path);
109
+ const key = path.toLowerCase();
110
+ if (seen.has(key) || !existsSync(path))
111
+ continue;
112
+ seen.add(key);
113
+ const version = await spawnText(path, ["-version"], 3_000).catch(() => ({ stdout: "", stderr: "" }));
114
+ return { available: true, executable: path, version: (version.stdout || version.stderr).split(/\r?\n/).map(value => value.trim()).find(Boolean) ?? null, source: candidate.source };
115
+ }
116
+ const found = await runWhere("cdb.exe");
117
+ if (found) {
118
+ const version = await spawnText(found, ["-version"], 3_000).catch(() => ({ stdout: "", stderr: "" }));
119
+ return { available: true, executable: resolve(found), version: (version.stdout || version.stderr).split(/\r?\n/).map(value => value.trim()).find(Boolean) ?? null, source: "where" };
120
+ }
121
+ return { available: false, code: "CDB_NOT_FOUND", message: "未找到 WinDbg CDB" };
122
+ }
123
+ async function terminateProcess(child) {
124
+ if (child.exitCode !== null)
125
+ return;
126
+ try {
127
+ child.kill();
128
+ }
129
+ catch { /* already exited */ }
130
+ if (process.platform === "win32" && child.pid) {
131
+ await new Promise(resolveDone => {
132
+ const killer = nodeSpawn("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
133
+ killer.once("exit", () => resolveDone());
134
+ killer.once("error", () => resolveDone());
135
+ });
136
+ }
137
+ }
138
+ export function runCdbProcess(options) {
139
+ const spawn = options.spawn ?? nodeSpawn;
140
+ return new Promise(resolveResult => {
141
+ let stdout = "";
142
+ let stderr = "";
143
+ let settled = false;
144
+ let timedOut = false;
145
+ let child;
146
+ let timer;
147
+ const finish = (exitCode, signal, started) => {
148
+ if (settled)
149
+ return;
150
+ settled = true;
151
+ if (timer)
152
+ clearTimeout(timer);
153
+ resolveResult({ exitCode, signal, stdout, stderr, timedOut, started });
154
+ };
155
+ try {
156
+ child = spawn(options.executable, [...options.args], {
157
+ shell: false,
158
+ windowsHide: true,
159
+ cwd: resolve(options.cwd),
160
+ env: options.env ?? process.env,
161
+ stdio: ["ignore", "pipe", "pipe"]
162
+ });
163
+ }
164
+ catch (error) {
165
+ stderr += error instanceof Error ? error.message : String(error);
166
+ finish(1, null, false);
167
+ return;
168
+ }
169
+ child.stdout?.setEncoding("utf8");
170
+ child.stderr?.setEncoding("utf8");
171
+ child.stdout?.on("data", value => { stdout += String(value); });
172
+ child.stderr?.on("data", value => { stderr += String(value); });
173
+ timer = setTimeout(() => {
174
+ timedOut = true;
175
+ void terminateProcess(child).finally(() => finish(1, null, true));
176
+ }, Math.max(1, options.timeoutMs));
177
+ timer.unref?.();
178
+ child.once("error", error => { stderr += error.message; finish(1, null, child.pid !== undefined); });
179
+ child.once("exit", (code, signal) => finish(code ?? 1, signal, true));
180
+ });
181
+ }
182
+ function readU16(buffer, offset) { return buffer.readUInt16LE(offset); }
183
+ function readU32(buffer, offset) { return buffer.readUInt32LE(offset); }
184
+ export async function readPeLayout(file) {
185
+ const input = resolve(file);
186
+ const buffer = await readFile(input);
187
+ if (buffer.length < 0x100 || buffer.toString("ascii", 0, 2) !== "MZ")
188
+ throw new CdbError("PE_INVALID", "Wow.exe has no DOS header");
189
+ const peOffset = readU32(buffer, 0x3c);
190
+ if (peOffset + 24 > buffer.length || buffer.toString("ascii", peOffset, peOffset + 4) !== "PE\0\0")
191
+ throw new CdbError("PE_INVALID", "Wow.exe has no PE header");
192
+ const sections = readU16(buffer, peOffset + 6);
193
+ const optionalSize = readU16(buffer, peOffset + 20);
194
+ const optional = peOffset + 24;
195
+ const magic = readU16(buffer, optional);
196
+ const imageBase = magic === 0x20b ? buffer.readBigUInt64LE(optional + 24) : BigInt(readU32(buffer, optional + 28));
197
+ const sizeOfImage = readU32(buffer, optional + 56);
198
+ const table = optional + optionalSize;
199
+ const values = [];
200
+ for (let index = 0; index < sections; index += 1) {
201
+ const offset = table + index * 40;
202
+ if (offset + 40 > buffer.length)
203
+ break;
204
+ const name = buffer.subarray(offset, offset + 8).toString("ascii").replace(/\0+$/u, "");
205
+ const virtualSize = readU32(buffer, offset + 8);
206
+ const rva = readU32(buffer, offset + 12);
207
+ const rawSize = readU32(buffer, offset + 16);
208
+ const characteristics = readU32(buffer, offset + 36);
209
+ const protection = `${characteristics & 0x40000000 ? "r" : ""}${characteristics & 0x80000000 ? "w" : ""}${characteristics & 0x20000000 ? "x" : ""}`;
210
+ if (name && (virtualSize || rawSize))
211
+ values.push({ name, rva: hex(BigInt(rva)), virtualSize, rawSize, characteristics: hex(BigInt(characteristics)), protection });
212
+ }
213
+ return { imageBase: hex(imageBase), moduleSize: sizeOfImage, sections: values };
214
+ }
215
+ function quoteCdbPath(file) {
216
+ const value = resolve(file);
217
+ if (!isAbsolute(value) || value.includes("\0"))
218
+ throw new CdbError("CDB_ARGUMENT_INVALID", "CDB output path must be absolute");
219
+ return `"${value.replace(/"/gu, "\\\"")}"`;
220
+ }
221
+ function baseCdbCommands(request, generated) {
222
+ const extra = validCommandList(request.commands);
223
+ return [...generated, ...extra, "q"];
224
+ }
225
+ function buildCdbArgs(request, commandFile) {
226
+ const extra = validArgs(request.cdbArgs);
227
+ return ["-p", String(request.pid), "-cf", commandFile, ...extra];
228
+ }
229
+ function parseHexValue(value) {
230
+ const match = value.match(/0x[0-9a-f]+/i);
231
+ return match ? `0x${BigInt(match[0]).toString(16)}` : "0x0";
232
+ }
233
+ export function parseBreakpointHits(output) {
234
+ const hits = [];
235
+ const blocks = output.split("WOWDUMP_HIT_BEGIN").slice(1);
236
+ for (const block of blocks) {
237
+ const end = block.indexOf("WOWDUMP_HIT_END");
238
+ if (end < 0)
239
+ continue;
240
+ const values = {};
241
+ for (const line of block.slice(0, end).split(/\r?\n/)) {
242
+ const match = line.match(/^\s*(rip|rcx|rdx|r8|r9|rsp|returnAddress|threadId)\s*=\s*(\S+)/u);
243
+ if (match)
244
+ values[match[1]] = parseHexValue(match[2]);
245
+ }
246
+ if (values.rip && values.rcx && values.rdx && values.r8 && values.r9 && values.rsp && values.returnAddress && values.threadId) {
247
+ hits.push(values);
248
+ }
249
+ }
250
+ return hits;
251
+ }
252
+ async function writeCdbScript(directory, commands) {
253
+ await mkdir(directory, { recursive: true });
254
+ const file = join(directory, `commands-${Date.now()}-${Math.random().toString(16).slice(2)}.cdb`);
255
+ await writeFile(file, `${commands.join("\n")}\n`, "utf8");
256
+ return file;
257
+ }
258
+ async function sha256(file) {
259
+ const hash = createHash("sha256");
260
+ hash.update(await readFile(file));
261
+ return hash.digest("hex");
262
+ }
263
+ async function dump(request, info) {
264
+ const outputDir = resolve(request.outputDir);
265
+ const maxSectionBytes = boundedInteger(request.maxSectionBytes, "maxSectionBytes", 1, 512 * 1024 * 1024);
266
+ const maxTotalBytes = boundedInteger(request.maxTotalBytes, "maxTotalBytes", 1, 768 * 1024 * 1024);
267
+ const timeoutMs = boundedInteger(request.timeoutMs, "timeoutMs", 1, 15 * 60 * 1000);
268
+ if (!Array.isArray(request.sections) || request.sections.length === 0 || request.sections.length > 64)
269
+ throw new CdbError("CDB_ARGUMENT_INVALID", "sections must contain between 1 and 64 entries");
270
+ let totalRequested = 0;
271
+ const normalizedSections = request.sections.map((section, index) => {
272
+ const readSize = boundedInteger(section.readSize, `sections[${index}].readSize`, 1, Math.min(maxSectionBytes, maxTotalBytes));
273
+ const file = resolve(section.file);
274
+ if (dirname(file).toLowerCase() !== outputDir.toLowerCase())
275
+ throw new CdbError("CDB_ARGUMENT_INVALID", `sections[${index}].file must be inside outputDir`);
276
+ totalRequested += readSize;
277
+ if (totalRequested > maxTotalBytes)
278
+ throw new CdbError("CDB_ARGUMENT_INVALID", "section sizes exceed maxTotalBytes");
279
+ return { ...section, readSize, file };
280
+ });
281
+ await mkdir(outputDir, { recursive: true });
282
+ const generated = [".echo WOWDUMP_DUMP_BEGIN"];
283
+ for (const section of normalizedSections) {
284
+ const start = address(section.runtimeAddress, `${section.name}.runtimeAddress`);
285
+ const end = start + BigInt(section.readSize);
286
+ generated.push(`.writemem ${quoteCdbPath(section.file)} ${hex(start)} ${hex(end)}`);
287
+ generated.push(`.echo WOWDUMP_DUMP_SECTION ${section.name}`);
288
+ }
289
+ const commands = baseCdbCommands(request, generated);
290
+ const commandFile = await writeCdbScript(outputDir, commands);
291
+ const logFile = join(outputDir, "cdb.log");
292
+ try {
293
+ const result = await runCdbProcess({ executable: info.executable, args: buildCdbArgs(request, commandFile), cwd: outputDir, timeoutMs });
294
+ await writeFile(logFile, `${result.stdout}${result.stderr ? `\n[stderr]\n${result.stderr}` : ""}`, "utf8");
295
+ if (!result.started)
296
+ throw new CdbError("CDB_START_FAILED", "CDB 启动失败", { executable: info.executable, stderr: result.stderr });
297
+ if (result.timedOut)
298
+ throw new CdbError("CDB_TIMEOUT", "CDB 调试请求超时", { timeoutMs, logFile });
299
+ if (result.exitCode !== 0)
300
+ throw new CdbError("CDB_EXIT_NONZERO", `CDB exited with code ${result.exitCode}`, { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr, logFile });
301
+ const manifestSections = [];
302
+ for (const section of normalizedSections) {
303
+ const bytes = await stat(section.file).then(value => value.size).catch(() => 0);
304
+ manifestSections.push({ ...section, bytes, sha256: bytes > 0 ? await sha256(section.file) : null, shortRead: bytes !== section.readSize });
305
+ }
306
+ const manifest = {
307
+ schema: "wowdump.runtime-dump.v3",
308
+ kind: "dump",
309
+ buildKey: request.buildKey,
310
+ pid: request.pid,
311
+ executableSha256: request.executableSha256 ?? null,
312
+ moduleBase: hex(address(request.moduleBase, "moduleBase")),
313
+ preferredImageBase: request.preferredImageBase ?? null,
314
+ moduleSize: request.moduleSize ?? null,
315
+ sections: manifestSections,
316
+ totalBytes: manifestSections.reduce((sum, value) => sum + Number(value.bytes ?? 0), 0),
317
+ engine: { name: "cdb", path: info.executable, version: info.version ?? null },
318
+ logFile,
319
+ dumpParameters: {
320
+ executableSha256: request.executableSha256 ?? null,
321
+ sections: normalizedSections.map(section => section.name),
322
+ maxSectionBytes,
323
+ maxTotalBytes,
324
+ cdbArgs: validArgs(request.cdbArgs),
325
+ commands: validCommandList(request.commands)
326
+ },
327
+ generatedAt: new Date().toISOString()
328
+ };
329
+ const manifestFile = join(outputDir, "manifest.json");
330
+ await writeFile(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
331
+ return { ok: true, kind: "dump", manifestFile, outputDirectory: outputDir, manifest, sections: manifestSections.map(value => ({ name: value.name, bytes: value.bytes, sha256: value.sha256 })) };
332
+ }
333
+ finally {
334
+ await rm(commandFile, { force: true }).catch(() => undefined);
335
+ }
336
+ }
337
+ async function breakpoint(request, info, queryPage) {
338
+ const maxHits = boundedInteger(request.maxHits, "maxHits", 1, 1024);
339
+ const durationMs = boundedInteger(request.durationMs, "durationMs", 1, 10 * 60 * 1000);
340
+ const runtimeAddress = address(request.moduleBase, "moduleBase") + address(request.rva, "rva");
341
+ const entryPage = queryPage ? await queryPage(runtimeAddress).catch(error => ({ error: error instanceof Error ? error.message : String(error) })) : null;
342
+ const escaped = `.printf \"WOWDUMP_HIT_BEGIN\\nrip=0x%p\\nrcx=0x%p\\nrdx=0x%p\\nr8=0x%p\\nr9=0x%p\\nrsp=0x%p\\nreturnAddress=0x%p\\nthreadId=0x%p\\nWOWDUMP_HIT_END\\n\", @rip, @rcx, @rdx, @r8, @r9, @rsp, poi(@rsp), @$tid`;
343
+ const generated = [
344
+ "r @$t0 = 0",
345
+ `ba e 1 ${hex(runtimeAddress)} \".if (@$t0 >= ${maxHits}) { bc *; q } .else { r @$t0 = @$t0 + 1; ${escaped}; gc }\"`,
346
+ "g"
347
+ ];
348
+ const commands = baseCdbCommands(request, generated);
349
+ const directory = request.outputFile ? dirname(resolve(request.outputFile)) : await mkdtemp(join(tmpdir(), "wowdump-cdb-breakpoint-"));
350
+ const commandFile = await writeCdbScript(directory, commands);
351
+ try {
352
+ const result = await runCdbProcess({ executable: info.executable, args: buildCdbArgs(request, commandFile), cwd: directory, timeoutMs: durationMs });
353
+ const hits = parseBreakpointHits(`${result.stdout}\n${result.stderr}`).slice(0, maxHits);
354
+ const enriched = await Promise.all(hits.map(async (hit) => ({ ...hit, returnAddressPage: queryPage ? await queryPage(address(hit.returnAddress, "returnAddress")).catch(error => ({ error: error instanceof Error ? error.message : String(error) })) : null })));
355
+ const value = {
356
+ ok: result.started && !result.timedOut && result.exitCode === 0,
357
+ kind: "breakpoint",
358
+ rva: hex(address(request.rva, "rva")),
359
+ runtimeAddress: hex(runtimeAddress),
360
+ maxHits,
361
+ hits: enriched,
362
+ hitCount: enriched.length,
363
+ detached: true,
364
+ timedOut: result.timedOut,
365
+ entryPage,
366
+ engine: { name: "cdb", path: info.executable, version: info.version ?? null },
367
+ stdout: result.stdout.slice(0, 256 * 1024),
368
+ stderr: result.stderr.slice(0, 256 * 1024),
369
+ evidence: [{ source: "windbg", kind: "hardware-execution-breakpoint" }]
370
+ };
371
+ if (request.outputFile)
372
+ await writeFile(resolve(request.outputFile), `${JSON.stringify(value, null, 2)}\n`, "utf8");
373
+ return value;
374
+ }
375
+ finally {
376
+ await rm(commandFile, { force: true }).catch(() => undefined);
377
+ if (!request.outputFile)
378
+ await rm(directory, { recursive: true, force: true }).catch(() => undefined);
379
+ }
380
+ }
381
+ async function command(request, info) {
382
+ const durationMs = boundedInteger(request.durationMs, "durationMs", 1, 15 * 60 * 1000);
383
+ const commands = baseCdbCommands(request, request.commands);
384
+ const directory = request.outputFile ? dirname(resolve(request.outputFile)) : await mkdtemp(join(tmpdir(), "wowdump-cdb-command-"));
385
+ const commandFile = await writeCdbScript(directory, commands);
386
+ try {
387
+ const result = await runCdbProcess({ executable: info.executable, args: buildCdbArgs(request, commandFile), cwd: directory, timeoutMs: durationMs });
388
+ const value = { ok: result.started && !result.timedOut && result.exitCode === 0, kind: "command", pid: request.pid, exitCode: result.exitCode, timedOut: result.timedOut, stdout: result.stdout.slice(0, 512 * 1024), stderr: result.stderr.slice(0, 512 * 1024), engine: { name: "cdb", path: info.executable, version: info.version ?? null }, evidence: [{ source: "windbg", kind: "cdb-command" }] };
389
+ if (request.outputFile)
390
+ await writeFile(resolve(request.outputFile), `${JSON.stringify(value, null, 2)}\n`, "utf8");
391
+ return value;
392
+ }
393
+ finally {
394
+ await rm(commandFile, { force: true }).catch(() => undefined);
395
+ if (!request.outputFile)
396
+ await rm(directory, { recursive: true, force: true }).catch(() => undefined);
397
+ }
398
+ }
399
+ export async function executeCdbDebug(request, queryPage) {
400
+ if (!request || !["dump", "breakpoint", "command"].includes(String(request.kind)))
401
+ throw new CdbError("CDB_ARGUMENT_INVALID", "kind must be dump, breakpoint, or command");
402
+ const info = await discoverCdb();
403
+ if (!info.available || !info.executable)
404
+ throw new CdbError("CDB_NOT_FOUND", info.message ?? "未找到 WinDbg CDB", { discovery: info });
405
+ if (!Number.isSafeInteger(request.pid) || request.pid <= 0)
406
+ throw new CdbError("CDB_ARGUMENT_INVALID", "pid must be positive");
407
+ if (request.kind === "dump")
408
+ return dump(request, info);
409
+ if (request.kind === "breakpoint")
410
+ return breakpoint(request, info, queryPage);
411
+ return command(request, info);
412
+ }
@@ -6,7 +6,12 @@ export const DEFAULT_RIGHTS = Object.freeze([
6
6
  "PROCESS_VM_READ"
7
7
  ]);
8
8
  const MEM_COMMIT = 0x1000;
9
- const DEFAULT_QUERY_END = BigInt("0x7fffffffffff");
9
+ // Windows rejects VirtualQueryEx at the null page (Win32 error 87). The
10
+ // lowest user-mode allocation address is 0x10000 on supported Windows hosts.
11
+ const DEFAULT_QUERY_START = 0x10000n;
12
+ // GetSystemInfo().lpMaximumApplicationAddress on supported Windows x64 hosts.
13
+ const MAX_USER_QUERY_ADDRESS = BigInt("0x7ffffffeffff");
14
+ const DEFAULT_QUERY_END = MAX_USER_QUERY_ADDRESS;
10
15
  const MAX_QUERY_REGIONS = 100_000;
11
16
  function hexAddress(value) {
12
17
  const normalized = value.trim();
@@ -97,7 +102,7 @@ export class ReaderBroker {
97
102
  backend;
98
103
  now;
99
104
  onShutdown;
100
- runFrida;
105
+ runDebug;
101
106
  handles = new Map();
102
107
  watches = new Map();
103
108
  idleTimer;
@@ -108,7 +113,7 @@ export class ReaderBroker {
108
113
  this.idleTimeoutMs = Math.max(1, Math.floor(options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS));
109
114
  this.now = options.now ?? Date.now;
110
115
  this.onShutdown = options.onShutdown;
111
- this.runFrida = options.runFrida;
116
+ this.runDebug = options.runDebug;
112
117
  this.armIdleTimer();
113
118
  }
114
119
  get isStopped() {
@@ -175,10 +180,10 @@ export class ReaderBroker {
175
180
  activeWatches: this.activeWatchCount,
176
181
  elevation: await Promise.resolve(this.backend.elevationStatus?.() ?? { state: "unknown" })
177
182
  };
178
- case "frida":
179
- if (!this.runFrida)
180
- throw new ReaderProtocolError("FRIDA_NOT_CONFIGURED", "elevated Frida runner is not configured");
181
- return this.runFrida(request);
183
+ case "debug":
184
+ if (!this.runDebug)
185
+ throw new ReaderProtocolError("CDB_NOT_CONFIGURED", "elevated CDB runner is not configured");
186
+ return this.runDebug(request);
182
187
  case "modules":
183
188
  return this.modules(request);
184
189
  case "regions":
@@ -274,8 +279,10 @@ export class ReaderBroker {
274
279
  throw new ReaderProtocolError("VIRTUAL_QUERY_UNAVAILABLE", "native VirtualQueryEx backend is not installed");
275
280
  }
276
281
  const pid = boundedPid(request.pid);
277
- const start = hexAddress(request.start ?? "0x0");
278
- const end = hexAddress(request.end ?? `0x${DEFAULT_QUERY_END.toString(16)}`);
282
+ const requestedStart = hexAddress(request.start ?? `0x${DEFAULT_QUERY_START.toString(16)}`);
283
+ const start = requestedStart === 0n ? DEFAULT_QUERY_START : requestedStart;
284
+ const requestedEnd = hexAddress(request.end ?? `0x${DEFAULT_QUERY_END.toString(16)}`);
285
+ const end = requestedEnd > MAX_USER_QUERY_ADDRESS ? MAX_USER_QUERY_ADDRESS : requestedEnd;
279
286
  if (end < start)
280
287
  throw new ReaderProtocolError("INVALID_RANGE", "end must be greater than or equal to start");
281
288
  const maxRegions = boundedCount(request.maxRegions, "maxRegions", MAX_QUERY_REGIONS, 4096);
@@ -319,6 +326,7 @@ export class ReaderBroker {
319
326
  handle: handle.id,
320
327
  start: `0x${start.toString(16)}`,
321
328
  end: `0x${end.toString(16)}`,
329
+ endClamped: requestedEnd !== end,
322
330
  includeFree,
323
331
  maxRegions,
324
332
  regions,
@@ -419,9 +427,11 @@ function serializeError(error, backend) {
419
427
  return { code: error.code, message: error.message, ...(error.details ? { details: error.details } : {}) };
420
428
  }
421
429
  const message = error instanceof Error ? error.message : String(error);
430
+ const typed = error;
422
431
  return {
423
- code: "READER_ERROR",
432
+ code: typeof typed.code === "string" ? typed.code : "READER_ERROR",
424
433
  message,
434
+ ...(typed.details && typeof typed.details === "object" ? { details: typed.details } : {}),
425
435
  elevation: backend.elevationStatus ? backend.elevationStatus() : { state: "unknown" }
426
436
  };
427
437
  }
@@ -130,17 +130,17 @@ export class WindowsBrokerManager {
130
130
  }
131
131
  async request(invocation) {
132
132
  const request = invocationRequest(invocation);
133
+ const requestTimeout = invocation.command === "debug"
134
+ ? Math.max(this.options.requestTimeoutMs ?? 30_000, Math.min(15 * 60_000, Number(invocation.payload.durationMs ?? invocation.payload.timeoutMs ?? 120_000) + 10_000))
135
+ : this.options.requestTimeoutMs ?? 30_000;
133
136
  const existing = await this.readMetadata();
134
137
  if (existing) {
135
- const requestTimeout = invocation.command === "frida"
136
- ? Math.max(this.options.requestTimeoutMs ?? 30_000, Math.min(600_000, Number(invocation.payload.timeoutMs ?? 120_000) + 10_000))
137
- : this.options.requestTimeoutMs ?? 30_000;
138
138
  try {
139
139
  return await requestPipe(existing, request, requestTimeout);
140
140
  }
141
141
  catch (error) {
142
142
  const code = error.code;
143
- // A long-running Frida request is not evidence that the broker died.
143
+ // A long-running debugger request is not evidence that the broker died.
144
144
  // Keep its metadata so the next command reuses the elevated process.
145
145
  if (code === "BROKER_REQUEST_TIMEOUT")
146
146
  throw error;
@@ -148,7 +148,7 @@ export class WindowsBrokerManager {
148
148
  }
149
149
  }
150
150
  const metadata = await this.launchBroker();
151
- return requestPipe(metadata, request, this.options.requestTimeoutMs ?? 30_000);
151
+ return requestPipe(metadata, request, requestTimeout);
152
152
  }
153
153
  async readMetadata() {
154
154
  try {
@@ -1,41 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { createServer } from "node:net";
3
- import { spawn } from "node:child_process";
4
3
  import { mkdir, rm, writeFile } from "node:fs/promises";
5
- import { dirname, isAbsolute, resolve } from "node:path";
4
+ import { dirname } from "node:path";
6
5
  import { serveReaderBroker } from "./broker.js";
6
+ import { executeCdbDebug } from "../debug/cdb.js";
7
7
  import { createWindowsNativeReader } from "./windows.js";
8
- async function runElevatedFrida(request) {
9
- const worker = resolve(request.worker);
10
- if (!isAbsolute(request.worker) || worker !== request.worker && process.platform === "win32") {
11
- throw new Error("Frida worker path must be absolute");
12
- }
13
- const timeoutMs = Math.max(1, Math.min(Number(request.timeoutMs ?? 120_000), 600_000));
14
- return new Promise(resolveResult => {
15
- const child = spawn(process.execPath, [worker], { windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
16
- let stdout = "";
17
- let stderr = "";
18
- let settled = false;
19
- const finish = (exitCode, timedOut = false) => {
20
- if (settled)
21
- return;
22
- settled = true;
23
- clearTimeout(timer);
24
- resolveResult({ exitCode, stdout, stderr, timedOut });
25
- };
26
- child.stdout.setEncoding("utf8");
27
- child.stderr.setEncoding("utf8");
28
- child.stdout.on("data", value => { stdout += value; });
29
- child.stderr.on("data", value => { stderr += value; });
30
- child.once("error", error => { stderr += error.message; finish(1); });
31
- child.once("exit", code => finish(code ?? 1));
32
- const timer = setTimeout(() => {
33
- child.kill();
34
- finish(1, true);
35
- }, timeoutMs);
36
- child.stdin.end(request.input);
37
- });
38
- }
39
8
  function argument(name) {
40
9
  const index = process.argv.indexOf(name);
41
10
  return index >= 0 ? process.argv[index + 1] : undefined;
@@ -45,10 +14,24 @@ const port = Number(argument("--port") ?? "0");
45
14
  const token = argument("--token");
46
15
  const metadata = argument("--metadata");
47
16
  let server;
17
+ const backend = process.env.WOWDUMP_USE_FAKE_READER === "1" ? undefined : createWindowsNativeReader();
48
18
  const broker = serveReaderBroker({
49
19
  attachStdio: !listen,
50
- backend: process.env.WOWDUMP_USE_FAKE_READER === "1" ? undefined : createWindowsNativeReader(),
51
- runFrida: runElevatedFrida,
20
+ backend,
21
+ runDebug: async (request) => {
22
+ const debugRequest = request;
23
+ if (!backend)
24
+ return executeCdbDebug(debugRequest);
25
+ const handle = await backend.openProcess(request.pid, ["PROCESS_QUERY_INFORMATION"]);
26
+ try {
27
+ return await executeCdbDebug(debugRequest, address => backend.virtualQueryEx
28
+ ? backend.virtualQueryEx(handle, address)
29
+ : Promise.reject(new Error("VirtualQueryEx is not installed")));
30
+ }
31
+ finally {
32
+ await Promise.resolve(handle.close()).catch(() => undefined);
33
+ }
34
+ },
52
35
  onShutdown: async () => {
53
36
  server?.close();
54
37
  if (metadata)
@@ -154,7 +154,7 @@ export class WindowsNativeReader {
154
154
  const buffer = Buffer.alloc(48);
155
155
  const result = Number(VirtualQueryEx(native.raw, address, buffer, buffer.length));
156
156
  if (result === 0)
157
- throw win32Error(`VirtualQueryEx(${native.id})`);
157
+ throw win32Error(`VirtualQueryEx(${native.id}, 0x${address.toString(16)})`);
158
158
  return {
159
159
  baseAddress: `0x${buffer.readBigUInt64LE(0).toString(16)}`,
160
160
  allocationBase: `0x${buffer.readBigUInt64LE(8).toString(16)}`,
@@ -185,7 +185,7 @@ export class WindowsNativeReader {
185
185
  };
186
186
  }
187
187
  }
188
- /** Enumerate processes without spawning a shell or depending on Frida. */
188
+ /** Enumerate processes without spawning a shell or depending on an injector. */
189
189
  export function enumerateWindowsProcesses() {
190
190
  if (process.platform !== "win32")
191
191
  return [];
package/dist/toolchain.js CHANGED
@@ -6,19 +6,19 @@ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { resolveWowdumpHome } from "./core/build-store.js";
7
7
  export const DEFAULT_WOWDUMP_SKILL = `---
8
8
  name: wowdump
9
- description: 用 Frida 运行时证据和 IDA Pro MCP 或 iced-x86 定位 WoW 字段,生成并验证 Reader profile。
9
+ description: 用 WinDbg CDB 运行时证据和 IDA Pro MCP 或 iced-x86 定位 WoW 字段,生成并验证 Reader profile。
10
10
  ---
11
11
 
12
12
  # wowdump
13
13
 
14
- 先运行 \`wowdump targets\`,再复用 \`~/.wowdump/<buildKey>/database/\`。动态脚本必须由 Agent 显式提供;runtime dump 保存当前 session 的二进制证据,Reader 只读取 build profile。详情见 references/workflow.md、references/commands.md、references/dynamic.md、references/disassemble.md 和 references/profiles.md。
14
+ 先运行 \`wowdump targets\`,再复用 \`~/.wowdump/<buildKey>/database/\`。runtime dump 保存当前 session 的二进制证据,debug 命令保存 CDB 证据,Reader 只读取 build profile。详情见 references/workflow.md、references/commands.md、references/windbg.md、references/disassemble.md 和 references/profiles.md。
15
15
  `;
16
16
  export const DEFAULT_WOWDUMP_COMMANDS = `# wowdump 命令参考
17
17
 
18
18
  1. \`wowdump targets\`
19
19
  2. \`wowdump database status --build <buildKey>\`
20
20
  3. \`wowdump analyze runtime --pid <pid> --build <buildKey> --kind dump --confirm\`
21
- 4. \`wowdump analyze dynamic --pid <pid> --build <buildKey> --script <GumJS> --export collect --confirm\`
21
+ 4. \`wowdump analyze debug --pid <pid> --build <buildKey> --rva <rva> --kind breakpoint --confirm\`
22
22
  5. \`wowdump analyze runtime --pid <pid> --build <buildKey> --kind verify --profile <profile.json> --confirm\`
23
23
  6. \`wowdump memory read --pid <pid> --build <buildKey> --profile <profile.json> --field <name>\`
24
24
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "wowdump",
3
- "version": "0.3.5",
4
- "description": "WoW native memory analysis CLI with Frida evidence, IDA/iced-x86 disassembly, and an elevated reader broker",
3
+ "version": "0.3.7",
4
+ "description": "WoW native memory analysis CLI with WinDbg CDB evidence, IDA/iced-x86 disassembly, and an elevated reader broker",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -34,7 +34,6 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "commander": "^13.1.0",
37
- "frida": "^17.2.0",
38
37
  "iced-x86": "^1.21.0",
39
38
  "koffi": "^2.16.3",
40
39
  "zod": "^3.24.2"
@@ -44,7 +43,7 @@
44
43
  "typescript": "^5.7.3"
45
44
  },
46
45
  "allowScripts": {
47
- "frida@17.17.0": true,
48
- "koffi@2.16.3": true
46
+ "koffi@2.16.3": true,
47
+ "wowdump@0.3.7": true
49
48
  }
50
49
  }