wowdump 0.3.4 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -6
- package/dist/analysis/runtime-dump.js +2 -2
- package/dist/cli.js +133 -175
- package/dist/core/build-adapters.js +0 -8
- package/dist/debug/cdb.js +412 -0
- package/dist/reader/broker.js +88 -7
- package/dist/reader/launcher.js +5 -5
- package/dist/reader/main.js +18 -35
- package/dist/reader/windows.js +2 -2
- package/dist/toolchain.js +3 -3
- package/package.json +4 -5
- package/skills/wowdump/SKILL.md +18 -12
- package/skills/wowdump/references/commands.md +27 -38
- package/skills/wowdump/references/disassemble.md +3 -3
- package/skills/wowdump/references/evidence-workflow.md +6 -6
- package/skills/wowdump/references/profiles.md +2 -2
- package/skills/wowdump/references/request-schema.md +2 -2
- package/skills/wowdump/references/windbg.md +55 -0
- package/skills/wowdump/references/workflow.md +17 -32
- package/dist/analysis/frida-runtime.js +0 -715
- package/dist/analysis/runtime-script.js +0 -156
- package/dist/frida-worker.js +0 -153
- package/skills/wowdump/references/dynamic.md +0 -54
- package/skills/wowdump/scripts/dynamic-session.js +0 -133
|
@@ -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
|
+
}
|
package/dist/reader/broker.js
CHANGED
|
@@ -5,6 +5,14 @@ export const DEFAULT_RIGHTS = Object.freeze([
|
|
|
5
5
|
"PROCESS_QUERY_INFORMATION",
|
|
6
6
|
"PROCESS_VM_READ"
|
|
7
7
|
]);
|
|
8
|
+
const MEM_COMMIT = 0x1000;
|
|
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;
|
|
15
|
+
const MAX_QUERY_REGIONS = 100_000;
|
|
8
16
|
function hexAddress(value) {
|
|
9
17
|
const normalized = value.trim();
|
|
10
18
|
if (!/^0x[0-9a-f]+$/i.test(normalized)) {
|
|
@@ -30,6 +38,15 @@ function boundedPid(value) {
|
|
|
30
38
|
}
|
|
31
39
|
return pid;
|
|
32
40
|
}
|
|
41
|
+
function boundedCount(value, name, maximum, fallback) {
|
|
42
|
+
if (value === undefined)
|
|
43
|
+
return fallback;
|
|
44
|
+
const count = Number(value);
|
|
45
|
+
if (!Number.isSafeInteger(count) || count < 1 || count > maximum) {
|
|
46
|
+
throw new ReaderProtocolError("INVALID_COUNT", `${name} must be an integer between 1 and ${maximum}`);
|
|
47
|
+
}
|
|
48
|
+
return count;
|
|
49
|
+
}
|
|
33
50
|
function toHex(bytes) {
|
|
34
51
|
return Buffer.from(bytes).toString("hex");
|
|
35
52
|
}
|
|
@@ -85,7 +102,7 @@ export class ReaderBroker {
|
|
|
85
102
|
backend;
|
|
86
103
|
now;
|
|
87
104
|
onShutdown;
|
|
88
|
-
|
|
105
|
+
runDebug;
|
|
89
106
|
handles = new Map();
|
|
90
107
|
watches = new Map();
|
|
91
108
|
idleTimer;
|
|
@@ -96,7 +113,7 @@ export class ReaderBroker {
|
|
|
96
113
|
this.idleTimeoutMs = Math.max(1, Math.floor(options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS));
|
|
97
114
|
this.now = options.now ?? Date.now;
|
|
98
115
|
this.onShutdown = options.onShutdown;
|
|
99
|
-
this.
|
|
116
|
+
this.runDebug = options.runDebug;
|
|
100
117
|
this.armIdleTimer();
|
|
101
118
|
}
|
|
102
119
|
get isStopped() {
|
|
@@ -163,12 +180,14 @@ export class ReaderBroker {
|
|
|
163
180
|
activeWatches: this.activeWatchCount,
|
|
164
181
|
elevation: await Promise.resolve(this.backend.elevationStatus?.() ?? { state: "unknown" })
|
|
165
182
|
};
|
|
166
|
-
case "
|
|
167
|
-
if (!this.
|
|
168
|
-
throw new ReaderProtocolError("
|
|
169
|
-
return this.
|
|
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);
|
|
170
187
|
case "modules":
|
|
171
188
|
return this.modules(request);
|
|
189
|
+
case "regions":
|
|
190
|
+
return this.regions(request);
|
|
172
191
|
case "open":
|
|
173
192
|
return this.open(request);
|
|
174
193
|
case "close":
|
|
@@ -255,6 +274,66 @@ export class ReaderBroker {
|
|
|
255
274
|
}
|
|
256
275
|
return { pid, modules: await this.backend.enumerateModules(pid) };
|
|
257
276
|
}
|
|
277
|
+
async regions(request) {
|
|
278
|
+
if (!this.backend.virtualQueryEx) {
|
|
279
|
+
throw new ReaderProtocolError("VIRTUAL_QUERY_UNAVAILABLE", "native VirtualQueryEx backend is not installed");
|
|
280
|
+
}
|
|
281
|
+
const pid = boundedPid(request.pid);
|
|
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;
|
|
286
|
+
if (end < start)
|
|
287
|
+
throw new ReaderProtocolError("INVALID_RANGE", "end must be greater than or equal to start");
|
|
288
|
+
const maxRegions = boundedCount(request.maxRegions, "maxRegions", MAX_QUERY_REGIONS, 4096);
|
|
289
|
+
const includeFree = request.includeFree === true;
|
|
290
|
+
const handle = await this.getHandle(pid, request.handle);
|
|
291
|
+
const regions = [];
|
|
292
|
+
let cursor = start;
|
|
293
|
+
let truncated = false;
|
|
294
|
+
while (cursor <= end) {
|
|
295
|
+
const raw = await this.backend.virtualQueryEx(handle, cursor);
|
|
296
|
+
const baseAddress = hexAddress(String(raw.baseAddress ?? "0x0"));
|
|
297
|
+
let regionSize;
|
|
298
|
+
try {
|
|
299
|
+
regionSize = BigInt(String(raw.regionSize ?? "0"));
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
throw new ReaderProtocolError("NATIVE_RESULT", "VirtualQueryEx returned an invalid region size");
|
|
303
|
+
}
|
|
304
|
+
if (regionSize <= 0n)
|
|
305
|
+
throw new ReaderProtocolError("NATIVE_RESULT", "VirtualQueryEx returned a non-positive region size");
|
|
306
|
+
const next = baseAddress + regionSize;
|
|
307
|
+
if (next <= cursor)
|
|
308
|
+
throw new ReaderProtocolError("NATIVE_RESULT", "VirtualQueryEx did not advance the query address");
|
|
309
|
+
const state = Number(raw.state);
|
|
310
|
+
if (includeFree || state === MEM_COMMIT) {
|
|
311
|
+
regions.push({
|
|
312
|
+
...raw,
|
|
313
|
+
baseAddress: `0x${baseAddress.toString(16)}`,
|
|
314
|
+
regionSize: regionSize.toString(),
|
|
315
|
+
committed: state === MEM_COMMIT
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
cursor = next;
|
|
319
|
+
if (regions.length >= maxRegions) {
|
|
320
|
+
truncated = cursor <= end;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
pid,
|
|
326
|
+
handle: handle.id,
|
|
327
|
+
start: `0x${start.toString(16)}`,
|
|
328
|
+
end: `0x${end.toString(16)}`,
|
|
329
|
+
endClamped: requestedEnd !== end,
|
|
330
|
+
includeFree,
|
|
331
|
+
maxRegions,
|
|
332
|
+
regions,
|
|
333
|
+
nextAddress: `0x${cursor.toString(16)}`,
|
|
334
|
+
truncated
|
|
335
|
+
};
|
|
336
|
+
}
|
|
258
337
|
async watchStart(request) {
|
|
259
338
|
const pid = boundedPid(request.pid);
|
|
260
339
|
const address = hexAddress(request.address);
|
|
@@ -348,9 +427,11 @@ function serializeError(error, backend) {
|
|
|
348
427
|
return { code: error.code, message: error.message, ...(error.details ? { details: error.details } : {}) };
|
|
349
428
|
}
|
|
350
429
|
const message = error instanceof Error ? error.message : String(error);
|
|
430
|
+
const typed = error;
|
|
351
431
|
return {
|
|
352
|
-
code: "READER_ERROR",
|
|
432
|
+
code: typeof typed.code === "string" ? typed.code : "READER_ERROR",
|
|
353
433
|
message,
|
|
434
|
+
...(typed.details && typeof typed.details === "object" ? { details: typed.details } : {}),
|
|
354
435
|
elevation: backend.elevationStatus ? backend.elevationStatus() : { state: "unknown" }
|
|
355
436
|
};
|
|
356
437
|
}
|
package/dist/reader/launcher.js
CHANGED
|
@@ -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
|
|
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,
|
|
151
|
+
return requestPipe(metadata, request, requestTimeout);
|
|
152
152
|
}
|
|
153
153
|
async readMetadata() {
|
|
154
154
|
try {
|
package/dist/reader/main.js
CHANGED
|
@@ -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
|
|
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
|
|
51
|
-
|
|
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)
|