wowdump 0.3.9 → 0.3.11
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 +30 -8
- package/dist/adapters/{reader.js → memory.js} +2 -2
- package/dist/analysis/disassemble.js +45 -0
- package/dist/analysis/pe.js +49 -0
- package/dist/cli.js +38 -99
- package/dist/core/build-store.js +1 -21
- package/dist/{reader → memory}/broker.js +51 -44
- package/dist/memory/client.js +1 -0
- package/dist/memory/dump.js +142 -0
- package/dist/{reader → memory}/launcher.js +40 -13
- package/dist/{reader → memory}/main.js +7 -22
- package/dist/{reader → memory}/windows.js +3 -3
- package/dist/memory-main.js +2 -0
- package/dist/toolchain.js +15 -0
- package/package.json +3 -3
- package/skills/wowdump/SKILL.md +13 -13
- package/skills/wowdump/references/character-stats-case.md +7 -9
- package/skills/wowdump/references/commands.md +22 -27
- package/skills/wowdump/references/disassemble.md +14 -33
- package/skills/wowdump/references/evidence-workflow.md +2 -2
- package/skills/wowdump/references/memory.md +23 -0
- package/skills/wowdump/references/profiles.md +9 -5
- package/skills/wowdump/references/task-scope.md +16 -0
- package/skills/wowdump/references/workflow.md +22 -3
- package/dist/analysis/runtime-dump.js +0 -93
- package/dist/debug/cdb.js +0 -412
- package/dist/reader/client.js +0 -1
- package/dist/reader-main.js +0 -2
- package/skills/wowdump/references/request-schema.md +0 -12
- package/skills/wowdump/references/windbg.md +0 -63
- /package/dist/{reader → memory}/protocol.js +0 -0
package/dist/debug/cdb.js
DELETED
|
@@ -1,412 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/reader/client.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { ReaderClient, ReaderBrokerPool, ReaderProtocolError, createElevatedLaunchSpec } from "./broker.js";
|
package/dist/reader-main.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# 确定字段含义
|
|
2
|
-
|
|
3
|
-
通常在任务记录中列明目标即可;无需为每次查询创建额外请求 JSON。这些描述不是 CLI 接受的 schema。
|
|
4
|
-
|
|
5
|
-
| 用户目标 | 需要确定 |
|
|
6
|
-
| --- | --- |
|
|
7
|
-
| 角色属性 | 玩家;暴击/急速/精通/全能;等级数值还是百分比;当前有效值还是基础值 |
|
|
8
|
-
| Buff/Debuff | 玩家还是目标;有益/有害;持续时间单位和层数 |
|
|
9
|
-
| 技能冷却 | 技能 ID;剩余时间、完整冷却还是充能;区分 GCD |
|
|
10
|
-
| 附近敌人 | 范围、单位类型、敌对/存活条件;姓名板集合不等于附近全部敌人 |
|
|
11
|
-
|
|
12
|
-
用户已说“暴击全能那些”时,先按面板当前二级属性组织字段;等级与百分比分开命名。只在含义影响分析且上下文不足时询问。每个字段记录原始类型、单位、转换公式和观测条件;原始数值不带单位时不要猜换算系数。
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
# WinDbg CDB 参考
|
|
2
|
-
|
|
3
|
-
## 探测
|
|
4
|
-
|
|
5
|
-
CLI 按以下顺序寻找 `cdb.exe`:`WOWDUMP_CDB`、Windows SDK x64、WinDbg 安装目录、最后的 `where.exe cdb.exe`。只读探测,不自动下载,也不修改环境变量。找不到时返回 `CDB_NOT_FOUND`。
|
|
6
|
-
|
|
7
|
-
## 段导出
|
|
8
|
-
|
|
9
|
-
```powershell
|
|
10
|
-
wowdump analyze runtime `
|
|
11
|
-
--pid <pid> `
|
|
12
|
-
--build <buildKey> `
|
|
13
|
-
--kind dump `
|
|
14
|
-
--sections .text .rdata .pdata `
|
|
15
|
-
--confirm
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Node 从原始 PE 读取 section RVA 和 virtual size,broker 内部执行等价的:
|
|
19
|
-
|
|
20
|
-
```text
|
|
21
|
-
.writemem "<absolute output path>" <runtimeStart> <runtimeEnd>
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
输出在 `~/.wowdump/<buildKey>/runtime/<session>/dump/`,包含 `manifest.json`、段二进制和 `cdb.log`。JSON 只保存路径、大小、哈希和引擎信息,不内嵌大段字节。
|
|
25
|
-
|
|
26
|
-
## 断点
|
|
27
|
-
|
|
28
|
-
```powershell
|
|
29
|
-
wowdump analyze debug `
|
|
30
|
-
--pid <pid> `
|
|
31
|
-
--build <buildKey> `
|
|
32
|
-
--rva <rva> `
|
|
33
|
-
--kind breakpoint `
|
|
34
|
-
--max-hits 3 `
|
|
35
|
-
--duration-ms 3000 `
|
|
36
|
-
--confirm
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
默认脚本使用硬件执行断点,只记录 RIP、RCX、RDX、R8、R9、RSP、返回地址和线程 ID。当前脚本在下一次命中时才检查计数上限,达到记录上限不保证立即退出。broker 用 `VirtualQueryEx` 检查入口页和返回地址页,结果写入 session 的 `debug.json`。
|
|
40
|
-
|
|
41
|
-
## 其他调试命令
|
|
42
|
-
|
|
43
|
-
Agent 可以显式提交 CDB 命令数组,适合寄存器、线程、模块和符号诊断:
|
|
44
|
-
|
|
45
|
-
```powershell
|
|
46
|
-
wowdump analyze debug `
|
|
47
|
-
--pid <pid> `
|
|
48
|
-
--build <buildKey> `
|
|
49
|
-
--kind command `
|
|
50
|
-
--cdb-commands '["lm","r","~"]' `
|
|
51
|
-
--cdb-args '["-lines"]' `
|
|
52
|
-
--confirm
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
命令和参数通过 `spawn(file, args, {shell:false})` 传递;不要把它们拼成 shell 字符串。调试命令必须有时限,输出会截断到证据上限。
|
|
56
|
-
|
|
57
|
-
## 当前生命周期限制
|
|
58
|
-
|
|
59
|
-
当前 CLI 使用 -p 附加、-cf 命令文件,默认末尾追加 q,没有默认选择非侵入式附加。shell:false 只描述启动方式,不限制 CDB 命令本身的作用。
|
|
60
|
-
|
|
61
|
-
超时会终止 CDB,并在 Windows 使用 taskkill /T /F 清理进程树;这不能证明正常 detach。detached:true 当前是固定输出,也不是证据。不要把 q 当成经过实测的正常脱离保证。
|
|
62
|
-
|
|
63
|
-
执行前核对当前版本的附加方式、退出命令和时限。断点退出与真实目标存活尚未完成端到端验证;字段定位先使用静态证据和 Reader,调试退出验证先在可控测试进程完成。超时后记录目标是否存活和可响应,不仅看退出码。透传参数会改变附加模式,避免重复提供冲突的目标参数。
|
|
File without changes
|