wowdump 0.3.10 → 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 +28 -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 +50 -43
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { stdin, stdout } from "node:process";
|
|
3
|
+
import { dumpNative } from "./dump.js";
|
|
3
4
|
export const DEFAULT_IDLE_TIMEOUT_MS = 20 * 60 * 1000;
|
|
4
5
|
export const DEFAULT_RIGHTS = Object.freeze([
|
|
5
6
|
"PROCESS_QUERY_INFORMATION",
|
|
@@ -16,25 +17,25 @@ const MAX_QUERY_REGIONS = 100_000;
|
|
|
16
17
|
function hexAddress(value) {
|
|
17
18
|
const normalized = value.trim();
|
|
18
19
|
if (!/^0x[0-9a-f]+$/i.test(normalized)) {
|
|
19
|
-
throw new
|
|
20
|
+
throw new MemoryProtocolError("INVALID_ADDRESS", "address must be a hexadecimal 0x-prefixed string");
|
|
20
21
|
}
|
|
21
22
|
const address = BigInt(normalized);
|
|
22
23
|
if (address < 0n || address > 0xffffffffffffffffn) {
|
|
23
|
-
throw new
|
|
24
|
+
throw new MemoryProtocolError("INVALID_ADDRESS", "address is outside the 64-bit range");
|
|
24
25
|
}
|
|
25
26
|
return address;
|
|
26
27
|
}
|
|
27
28
|
function boundedSize(value, name, maximum = 16 * 1024 * 1024) {
|
|
28
29
|
const size = Number(value);
|
|
29
30
|
if (!Number.isSafeInteger(size) || size <= 0 || size > maximum) {
|
|
30
|
-
throw new
|
|
31
|
+
throw new MemoryProtocolError("INVALID_SIZE", `${name} must be an integer between 1 and ${maximum}`);
|
|
31
32
|
}
|
|
32
33
|
return size;
|
|
33
34
|
}
|
|
34
35
|
function boundedPid(value) {
|
|
35
36
|
const pid = Number(value);
|
|
36
37
|
if (!Number.isSafeInteger(pid) || pid <= 0 || pid > 0x7fffffff) {
|
|
37
|
-
throw new
|
|
38
|
+
throw new MemoryProtocolError("INVALID_PID", "pid must be a positive Windows process id");
|
|
38
39
|
}
|
|
39
40
|
return pid;
|
|
40
41
|
}
|
|
@@ -43,7 +44,7 @@ function boundedCount(value, name, maximum, fallback) {
|
|
|
43
44
|
return fallback;
|
|
44
45
|
const count = Number(value);
|
|
45
46
|
if (!Number.isSafeInteger(count) || count < 1 || count > maximum) {
|
|
46
|
-
throw new
|
|
47
|
+
throw new MemoryProtocolError("INVALID_COUNT", `${name} must be an integer between 1 and ${maximum}`);
|
|
47
48
|
}
|
|
48
49
|
return count;
|
|
49
50
|
}
|
|
@@ -52,20 +53,20 @@ function toHex(bytes) {
|
|
|
52
53
|
}
|
|
53
54
|
function bytesFromResult(result) {
|
|
54
55
|
if (!result || !(result.bytes instanceof Uint8Array)) {
|
|
55
|
-
throw new
|
|
56
|
+
throw new MemoryProtocolError("NATIVE_RESULT", "native memory returned no byte buffer");
|
|
56
57
|
}
|
|
57
58
|
const bytesRead = result.bytesRead === undefined ? result.bytes.byteLength : Number(result.bytesRead);
|
|
58
59
|
if (!Number.isSafeInteger(bytesRead) || bytesRead < 0 || bytesRead > result.bytes.byteLength) {
|
|
59
|
-
throw new
|
|
60
|
+
throw new MemoryProtocolError("NATIVE_RESULT", "native memory returned an invalid bytesRead value");
|
|
60
61
|
}
|
|
61
62
|
return { bytes: result.bytes.subarray(0, bytesRead), bytesRead };
|
|
62
63
|
}
|
|
63
|
-
export class
|
|
64
|
+
export class MemoryProtocolError extends Error {
|
|
64
65
|
code;
|
|
65
66
|
details;
|
|
66
67
|
constructor(code, message, details) {
|
|
67
68
|
super(message);
|
|
68
|
-
this.name = "
|
|
69
|
+
this.name = "MemoryProtocolError";
|
|
69
70
|
this.code = code;
|
|
70
71
|
this.details = details;
|
|
71
72
|
}
|
|
@@ -74,9 +75,9 @@ export class ReaderProtocolError extends Error {
|
|
|
74
75
|
* Placeholder backend used until the platform adapter is installed. Keeping
|
|
75
76
|
* this explicit makes an unavailable native layer observable to the caller.
|
|
76
77
|
*/
|
|
77
|
-
export class
|
|
78
|
+
export class UnavailableNativeMemory {
|
|
78
79
|
diagnostic() {
|
|
79
|
-
throw new
|
|
80
|
+
throw new MemoryProtocolError("NATIVE_BACKEND_UNAVAILABLE", "Windows native memory backend is not installed", {
|
|
80
81
|
platform: process.platform,
|
|
81
82
|
requestedRights: [...DEFAULT_RIGHTS],
|
|
82
83
|
elevation: this.elevationStatus()
|
|
@@ -97,23 +98,22 @@ export class UnavailableNativeReader {
|
|
|
97
98
|
};
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
|
-
export class
|
|
101
|
+
export class MemoryBroker {
|
|
101
102
|
idleTimeoutMs;
|
|
102
103
|
backend;
|
|
103
104
|
now;
|
|
104
105
|
onShutdown;
|
|
105
|
-
runDebug;
|
|
106
106
|
handles = new Map();
|
|
107
107
|
watches = new Map();
|
|
108
108
|
idleTimer;
|
|
109
109
|
watchCounter = 0;
|
|
110
110
|
stopped = false;
|
|
111
|
+
activeRequests = 0;
|
|
111
112
|
constructor(options = {}) {
|
|
112
|
-
this.backend = options.backend ?? new
|
|
113
|
+
this.backend = options.backend ?? new UnavailableNativeMemory();
|
|
113
114
|
this.idleTimeoutMs = Math.max(1, Math.floor(options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS));
|
|
114
115
|
this.now = options.now ?? Date.now;
|
|
115
116
|
this.onShutdown = options.onShutdown;
|
|
116
|
-
this.runDebug = options.runDebug;
|
|
117
117
|
this.armIdleTimer();
|
|
118
118
|
}
|
|
119
119
|
get isStopped() {
|
|
@@ -130,17 +130,24 @@ export class ReaderBroker {
|
|
|
130
130
|
const command = typeof request?.command === "string" ? request.command : "unknown";
|
|
131
131
|
const base = { id: request?.id, ok: true, command, timestamp: this.now() };
|
|
132
132
|
if (this.stopped) {
|
|
133
|
-
return { ...base, ok: false, error: { code: "BROKER_STOPPED", message: "
|
|
133
|
+
return { ...base, ok: false, error: { code: "BROKER_STOPPED", message: "memory broker is stopped" } };
|
|
134
134
|
}
|
|
135
|
+
this.activeRequests += 1;
|
|
136
|
+
if (this.idleTimer)
|
|
137
|
+
clearTimeout(this.idleTimer);
|
|
135
138
|
try {
|
|
136
139
|
const result = await this.dispatch(request);
|
|
137
140
|
this.armIdleTimer();
|
|
138
|
-
return { ...base, ...result, ok:
|
|
141
|
+
return { ...base, ...result, ok: result.ok !== false };
|
|
139
142
|
}
|
|
140
143
|
catch (error) {
|
|
141
144
|
this.armIdleTimer();
|
|
142
145
|
return { ...base, ok: false, error: serializeError(error, this.backend) };
|
|
143
146
|
}
|
|
147
|
+
finally {
|
|
148
|
+
this.activeRequests -= 1;
|
|
149
|
+
this.armIdleTimer();
|
|
150
|
+
}
|
|
144
151
|
}
|
|
145
152
|
async shutdown(reason = "request") {
|
|
146
153
|
if (this.stopped)
|
|
@@ -160,7 +167,7 @@ export class ReaderBroker {
|
|
|
160
167
|
await Promise.resolve(this.onShutdown?.(reason)).catch(() => undefined);
|
|
161
168
|
}
|
|
162
169
|
armIdleTimer() {
|
|
163
|
-
if (this.stopped)
|
|
170
|
+
if (this.stopped || this.activeRequests > 0)
|
|
164
171
|
return;
|
|
165
172
|
if (this.idleTimer)
|
|
166
173
|
clearTimeout(this.idleTimer);
|
|
@@ -174,16 +181,16 @@ export class ReaderBroker {
|
|
|
174
181
|
switch (request.command) {
|
|
175
182
|
case "status":
|
|
176
183
|
return {
|
|
184
|
+
engine: "native-memory",
|
|
185
|
+
capabilities: ["read", "regions", "modules", "dump", "watch"],
|
|
177
186
|
pid: process.pid,
|
|
178
187
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
179
188
|
activeHandles: this.handles.size,
|
|
180
189
|
activeWatches: this.activeWatchCount,
|
|
181
190
|
elevation: await Promise.resolve(this.backend.elevationStatus?.() ?? { state: "unknown" })
|
|
182
191
|
};
|
|
183
|
-
case "
|
|
184
|
-
|
|
185
|
-
throw new ReaderProtocolError("CDB_NOT_CONFIGURED", "elevated CDB runner is not configured");
|
|
186
|
-
return this.runDebug(request);
|
|
192
|
+
case "dump":
|
|
193
|
+
return dumpNative(request, this.backend);
|
|
187
194
|
case "modules":
|
|
188
195
|
return this.modules(request);
|
|
189
196
|
case "regions":
|
|
@@ -204,7 +211,7 @@ export class ReaderBroker {
|
|
|
204
211
|
await this.shutdown("request");
|
|
205
212
|
return { stopped: true };
|
|
206
213
|
default:
|
|
207
|
-
throw new
|
|
214
|
+
throw new MemoryProtocolError("UNKNOWN_COMMAND", `unsupported memory command: ${String(request?.command)}`);
|
|
208
215
|
}
|
|
209
216
|
}
|
|
210
217
|
async getHandle(pid, requested) {
|
|
@@ -212,7 +219,7 @@ export class ReaderBroker {
|
|
|
212
219
|
if (existing && (!requested || requested === existing.id))
|
|
213
220
|
return existing;
|
|
214
221
|
if (requested && existing?.id !== requested) {
|
|
215
|
-
throw new
|
|
222
|
+
throw new MemoryProtocolError("HANDLE_MISMATCH", `handle ${requested} is not open for pid ${pid}`);
|
|
216
223
|
}
|
|
217
224
|
const handle = await this.backend.openProcess(pid, DEFAULT_RIGHTS);
|
|
218
225
|
this.handles.set(pid, handle);
|
|
@@ -270,13 +277,13 @@ export class ReaderBroker {
|
|
|
270
277
|
async modules(request) {
|
|
271
278
|
const pid = boundedPid(request.pid);
|
|
272
279
|
if (!this.backend.enumerateModules) {
|
|
273
|
-
throw new
|
|
280
|
+
throw new MemoryProtocolError("MODULE_ENUM_UNAVAILABLE", "native module enumeration is not installed");
|
|
274
281
|
}
|
|
275
282
|
return { pid, modules: await this.backend.enumerateModules(pid) };
|
|
276
283
|
}
|
|
277
284
|
async regions(request) {
|
|
278
285
|
if (!this.backend.virtualQueryEx) {
|
|
279
|
-
throw new
|
|
286
|
+
throw new MemoryProtocolError("VIRTUAL_QUERY_UNAVAILABLE", "native VirtualQueryEx backend is not installed");
|
|
280
287
|
}
|
|
281
288
|
const pid = boundedPid(request.pid);
|
|
282
289
|
const requestedStart = hexAddress(request.start ?? `0x${DEFAULT_QUERY_START.toString(16)}`);
|
|
@@ -284,7 +291,7 @@ export class ReaderBroker {
|
|
|
284
291
|
const requestedEnd = hexAddress(request.end ?? `0x${DEFAULT_QUERY_END.toString(16)}`);
|
|
285
292
|
const end = requestedEnd > MAX_USER_QUERY_ADDRESS ? MAX_USER_QUERY_ADDRESS : requestedEnd;
|
|
286
293
|
if (end < start)
|
|
287
|
-
throw new
|
|
294
|
+
throw new MemoryProtocolError("INVALID_RANGE", "end must be greater than or equal to start");
|
|
288
295
|
const maxRegions = boundedCount(request.maxRegions, "maxRegions", MAX_QUERY_REGIONS, 4096);
|
|
289
296
|
const includeFree = request.includeFree === true;
|
|
290
297
|
const handle = await this.getHandle(pid, request.handle);
|
|
@@ -299,13 +306,13 @@ export class ReaderBroker {
|
|
|
299
306
|
regionSize = BigInt(String(raw.regionSize ?? "0"));
|
|
300
307
|
}
|
|
301
308
|
catch {
|
|
302
|
-
throw new
|
|
309
|
+
throw new MemoryProtocolError("NATIVE_RESULT", "VirtualQueryEx returned an invalid region size");
|
|
303
310
|
}
|
|
304
311
|
if (regionSize <= 0n)
|
|
305
|
-
throw new
|
|
312
|
+
throw new MemoryProtocolError("NATIVE_RESULT", "VirtualQueryEx returned a non-positive region size");
|
|
306
313
|
const next = baseAddress + regionSize;
|
|
307
314
|
if (next <= cursor)
|
|
308
|
-
throw new
|
|
315
|
+
throw new MemoryProtocolError("NATIVE_RESULT", "VirtualQueryEx did not advance the query address");
|
|
309
316
|
const state = Number(raw.state);
|
|
310
317
|
if (includeFree || state === MEM_COMMIT) {
|
|
311
318
|
regions.push({
|
|
@@ -359,7 +366,7 @@ export class ReaderBroker {
|
|
|
359
366
|
}
|
|
360
367
|
async watchPoll(request) {
|
|
361
368
|
if (!request.watchId || typeof request.watchId !== "string")
|
|
362
|
-
throw new
|
|
369
|
+
throw new MemoryProtocolError("INVALID_WATCH", "watchId is required");
|
|
363
370
|
const watch = this.watches.get(request.watchId);
|
|
364
371
|
if (!watch || watch.stopped)
|
|
365
372
|
return { watchId: request.watchId, stopped: true, events: [] };
|
|
@@ -400,12 +407,12 @@ export class ReaderBroker {
|
|
|
400
407
|
}
|
|
401
408
|
catch (error) {
|
|
402
409
|
watch.stopped = true;
|
|
403
|
-
return { watchId: watch.id, stopped: true, reason: "
|
|
410
|
+
return { watchId: watch.id, stopped: true, reason: "memory_error", events: [{ type: "memory_error", watchId: watch.id, pid: watch.pid, sequence: watch.sequence + 1, timestamp: this.now(), error: serializeError(error, this.backend) }] };
|
|
404
411
|
}
|
|
405
412
|
}
|
|
406
413
|
async watchStop(request) {
|
|
407
414
|
if (!request.watchId || typeof request.watchId !== "string")
|
|
408
|
-
throw new
|
|
415
|
+
throw new MemoryProtocolError("INVALID_WATCH", "watchId is required");
|
|
409
416
|
const watch = this.watches.get(request.watchId);
|
|
410
417
|
if (!watch)
|
|
411
418
|
return { watchId: request.watchId, stopped: false };
|
|
@@ -423,21 +430,21 @@ function equalBytes(left, right) {
|
|
|
423
430
|
return true;
|
|
424
431
|
}
|
|
425
432
|
function serializeError(error, backend) {
|
|
426
|
-
if (error instanceof
|
|
433
|
+
if (error instanceof MemoryProtocolError) {
|
|
427
434
|
return { code: error.code, message: error.message, ...(error.details ? { details: error.details } : {}) };
|
|
428
435
|
}
|
|
429
436
|
const message = error instanceof Error ? error.message : String(error);
|
|
430
437
|
const typed = error;
|
|
431
438
|
return {
|
|
432
|
-
code: typeof typed.code === "string" ? typed.code : "
|
|
439
|
+
code: typeof typed.code === "string" ? typed.code : "MEMORY_ERROR",
|
|
433
440
|
message,
|
|
434
441
|
...(typed.details && typeof typed.details === "object" ? { details: typed.details } : {}),
|
|
435
442
|
elevation: backend.elevationStatus ? backend.elevationStatus() : { state: "unknown" }
|
|
436
443
|
};
|
|
437
444
|
}
|
|
438
445
|
/** Start a JSONL broker process over stdio. */
|
|
439
|
-
export function
|
|
440
|
-
const broker = new
|
|
446
|
+
export function serveMemoryBroker(options = {}) {
|
|
447
|
+
const broker = new MemoryBroker({ ...options, attachStdio: false });
|
|
441
448
|
if (options.attachStdio !== false) {
|
|
442
449
|
const input = options.input ?? stdin;
|
|
443
450
|
const output = options.output ?? stdout;
|
|
@@ -475,7 +482,7 @@ export function serveReaderBroker(options = {}) {
|
|
|
475
482
|
return broker;
|
|
476
483
|
}
|
|
477
484
|
/** Small typed client used by the CLI; transport owns process/UAC reuse. */
|
|
478
|
-
export class
|
|
485
|
+
export class MemoryClient {
|
|
479
486
|
transport;
|
|
480
487
|
requestTimeoutMs;
|
|
481
488
|
counter = 0;
|
|
@@ -488,7 +495,7 @@ export class ReaderClient {
|
|
|
488
495
|
const line = await withTimeout(this.transport.request(JSON.stringify({ ...request, id })), this.requestTimeoutMs);
|
|
489
496
|
const response = JSON.parse(line);
|
|
490
497
|
if (!response || typeof response !== "object" || typeof response.ok !== "boolean") {
|
|
491
|
-
throw new
|
|
498
|
+
throw new MemoryProtocolError("INVALID_RESPONSE", "memory broker returned an invalid JSONL response");
|
|
492
499
|
}
|
|
493
500
|
return response;
|
|
494
501
|
}
|
|
@@ -498,7 +505,7 @@ export class ReaderClient {
|
|
|
498
505
|
}
|
|
499
506
|
function withTimeout(promise, timeoutMs) {
|
|
500
507
|
return new Promise((resolve, reject) => {
|
|
501
|
-
const timer = setTimeout(() => reject(new
|
|
508
|
+
const timer = setTimeout(() => reject(new MemoryProtocolError("REQUEST_TIMEOUT", `memory request timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
502
509
|
// An in-flight request must settle even when the transport has no active handles.
|
|
503
510
|
promise.then(value => { clearTimeout(timer); resolve(value); }, error => { clearTimeout(timer); reject(error); });
|
|
504
511
|
});
|
|
@@ -509,7 +516,7 @@ export function createElevatedLaunchSpec(file, args = []) {
|
|
|
509
516
|
return Object.freeze({ file, args: [...args], verb: "runas", windowsHide: false });
|
|
510
517
|
}
|
|
511
518
|
/** Reuse one broker client and invoke the launcher only after the first miss. */
|
|
512
|
-
export class
|
|
519
|
+
export class MemoryBrokerPool {
|
|
513
520
|
client;
|
|
514
521
|
launching;
|
|
515
522
|
launcher;
|
|
@@ -541,10 +548,10 @@ export class ReaderBrokerPool {
|
|
|
541
548
|
return this.client;
|
|
542
549
|
if (!this.launching) {
|
|
543
550
|
this.launching = this.launcher.launch(this.launchSpec).then(result => {
|
|
544
|
-
this.client = new
|
|
551
|
+
this.client = new MemoryClient({ transport: result.transport, requestTimeoutMs: this.requestTimeoutMs });
|
|
545
552
|
return this.client;
|
|
546
553
|
}).catch(error => {
|
|
547
|
-
throw new
|
|
554
|
+
throw new MemoryProtocolError("ELEVATION_FAILED", error instanceof Error ? error.message : String(error), {
|
|
548
555
|
verb: this.launchSpec.verb,
|
|
549
556
|
requestedRights: [...DEFAULT_RIGHTS],
|
|
550
557
|
elevation: "launch rejected or cancelled"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MemoryClient, MemoryBrokerPool, MemoryProtocolError, createElevatedLaunchSpec } from "./broker.js";
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, open, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { readPeLayout } from "../analysis/pe.js";
|
|
5
|
+
import { sha256File } from "../core/build-store.js";
|
|
6
|
+
function fail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
7
|
+
function bounded(n, max) {
|
|
8
|
+
if (!Number.isSafeInteger(n) || n < 1 || n > max)
|
|
9
|
+
fail("DUMP_ARGUMENT_INVALID", "invalid dump limit");
|
|
10
|
+
return n;
|
|
11
|
+
}
|
|
12
|
+
/** Only the broker touches bytes. IPC carries paths and bounded summaries. */
|
|
13
|
+
export async function dumpNative(request, backend) {
|
|
14
|
+
const chunkBytes = bounded(request.chunkBytes ?? 65536, 1024 * 1024);
|
|
15
|
+
bounded(request.maxSectionBytes, 512 * 1024 * 1024);
|
|
16
|
+
bounded(request.maxTotalBytes, 768 * 1024 * 1024);
|
|
17
|
+
bounded(request.timeoutMs, 15 * 60 * 1000);
|
|
18
|
+
bounded(request.pid, 0x7fffffff);
|
|
19
|
+
if (!/^[A-Za-z0-9@._-]+$/.test(request.buildKey) || !/^[a-f0-9]{64}$/i.test(request.executableSha256))
|
|
20
|
+
fail("DUMP_ARGUMENT_INVALID", "invalid build identity");
|
|
21
|
+
if (!backend.virtualQueryEx || !backend.enumerateModules)
|
|
22
|
+
fail("NATIVE_BACKEND_UNAVAILABLE", "module enumeration and page query are required");
|
|
23
|
+
const module = (await backend.enumerateModules(request.pid)).find(m => /^Wow\.exe$/i.test(m.name));
|
|
24
|
+
if (!module?.path)
|
|
25
|
+
fail("MODULE_NOT_FOUND", "Wow.exe not found");
|
|
26
|
+
if (await sha256File(module.path) !== request.executableSha256)
|
|
27
|
+
fail("BUILD_MISMATCH", "target executable hash differs from build");
|
|
28
|
+
const layout = await readPeLayout(module.path);
|
|
29
|
+
if (module.size !== undefined && module.size !== layout.moduleSize)
|
|
30
|
+
fail("BUILD_MISMATCH", "module size differs from PE");
|
|
31
|
+
if (!Array.isArray(request.sections) || request.sections.length < 1 || request.sections.length > 64 || new Set(request.sections).size !== request.sections.length)
|
|
32
|
+
fail("DUMP_ARGUMENT_INVALID", "invalid section selection");
|
|
33
|
+
const selected = request.sections.map(name => {
|
|
34
|
+
const s = layout.sections.find(s => s.name.toLowerCase() === name.toLowerCase());
|
|
35
|
+
if (!s || !/^\.[A-Za-z0-9_-]+$/.test(s.name))
|
|
36
|
+
fail("SECTION_NOT_FOUND", `unknown section ${name}`);
|
|
37
|
+
return s;
|
|
38
|
+
});
|
|
39
|
+
let total = 0;
|
|
40
|
+
for (const s of selected) {
|
|
41
|
+
const size = s.virtualSize || s.rawSize;
|
|
42
|
+
if (size > request.maxSectionBytes || (total += size) > request.maxTotalBytes)
|
|
43
|
+
fail("DUMP_LIMIT_EXCEEDED", "raise explicit limits or select fewer sections");
|
|
44
|
+
if (BigInt(s.rva) + BigInt(size) > BigInt(layout.moduleSize))
|
|
45
|
+
fail("PE_INVALID", "section exceeds image");
|
|
46
|
+
}
|
|
47
|
+
const directory = resolve(request.outputDir);
|
|
48
|
+
// Non-recursive exclusive directory creation prevents overwriting prior evidence.
|
|
49
|
+
await mkdir(resolve(directory, ".."), { recursive: true });
|
|
50
|
+
await mkdir(directory);
|
|
51
|
+
const startedAt = new Date().toISOString();
|
|
52
|
+
const deadline = Date.now() + request.timeoutMs;
|
|
53
|
+
const sections = [];
|
|
54
|
+
let completedBytes = 0;
|
|
55
|
+
let handle;
|
|
56
|
+
try {
|
|
57
|
+
handle = await backend.openProcess(request.pid, ["PROCESS_QUERY_INFORMATION", "PROCESS_VM_READ"]);
|
|
58
|
+
for (const section of selected) {
|
|
59
|
+
const size = section.virtualSize || section.rawSize;
|
|
60
|
+
const base = BigInt(module.base) + BigInt(section.rva);
|
|
61
|
+
const file = join(directory, `${section.name}.bin`);
|
|
62
|
+
const output = await open(file, "wx");
|
|
63
|
+
const hash = createHash("sha256");
|
|
64
|
+
const gaps = [];
|
|
65
|
+
let offset = 0, readSize = 0;
|
|
66
|
+
try {
|
|
67
|
+
while (offset < size) {
|
|
68
|
+
if (Date.now() >= deadline)
|
|
69
|
+
fail("DUMP_TIMEOUT", "native dump timed out");
|
|
70
|
+
if (backend.isProcessAlive && !await backend.isProcessAlive(request.pid))
|
|
71
|
+
fail("TARGET_EXITED", "target exited during dump");
|
|
72
|
+
const address = base + BigInt(offset);
|
|
73
|
+
let count = Math.min(chunkBytes, size - offset), bytes = Buffer.alloc(0), reason = "unreadable-page";
|
|
74
|
+
try {
|
|
75
|
+
const page = await backend.virtualQueryEx(handle, address);
|
|
76
|
+
const end = BigInt(String(page.baseAddress)) + BigInt(String(page.regionSize));
|
|
77
|
+
if (end <= address)
|
|
78
|
+
fail("PAGE_INVALID", "page query made no progress");
|
|
79
|
+
count = Number(BigInt(count) < end - address ? BigInt(count) : end - address);
|
|
80
|
+
const protection = Number(page.protect);
|
|
81
|
+
if (Number(page.state) === 0x1000 && !(protection & 0x100) && [2, 4, 8, 0x20, 0x40, 0x80].includes(protection & 0xff)) {
|
|
82
|
+
const read = await backend.readProcessMemory(handle, address, count);
|
|
83
|
+
const n = read.bytesRead ?? read.bytes.length;
|
|
84
|
+
if (!Number.isSafeInteger(n) || n < 0 || n > count || n > read.bytes.length)
|
|
85
|
+
fail("NATIVE_RESULT", "invalid read length");
|
|
86
|
+
bytes = Buffer.from(read.bytes.subarray(0, n));
|
|
87
|
+
reason = "short-read";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error.code === "NATIVE_RESULT")
|
|
92
|
+
throw error;
|
|
93
|
+
count = Math.min(count, 4096 - Number(address % 4096n));
|
|
94
|
+
reason = error instanceof Error ? error.message.slice(0, 160) : "read-failed";
|
|
95
|
+
}
|
|
96
|
+
const block = Buffer.alloc(count);
|
|
97
|
+
bytes.copy(block);
|
|
98
|
+
readSize += bytes.length;
|
|
99
|
+
if (bytes.length < count) {
|
|
100
|
+
const gap = { offset: offset + bytes.length, size: count - bytes.length, reason };
|
|
101
|
+
const previous = gaps.at(-1);
|
|
102
|
+
if (previous && previous.reason === reason && previous.offset + previous.size === gap.offset)
|
|
103
|
+
previous.size += gap.size;
|
|
104
|
+
else
|
|
105
|
+
gaps.push(gap);
|
|
106
|
+
if (gaps.length > 65536)
|
|
107
|
+
fail("DUMP_GAP_LIMIT", "too many unreadable ranges");
|
|
108
|
+
}
|
|
109
|
+
let written = 0;
|
|
110
|
+
while (written < count) {
|
|
111
|
+
const r = await output.write(block, written, count - written, offset + written);
|
|
112
|
+
if (!r.bytesWritten)
|
|
113
|
+
fail("DUMP_WRITE_FAILED", "zero byte write");
|
|
114
|
+
written += r.bytesWritten;
|
|
115
|
+
}
|
|
116
|
+
hash.update(block);
|
|
117
|
+
offset += count;
|
|
118
|
+
completedBytes += count;
|
|
119
|
+
await writeFile(join(directory, "progress.json"), JSON.stringify({ completedBytes, totalBytes: total, section: section.name, offset }));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await output.close();
|
|
124
|
+
}
|
|
125
|
+
sections.push({ ...section, runtimeAddress: `0x${base.toString(16)}`, file, bytes: size, readSize, shortRead: readSize !== size, gaps, sha256: hash.digest("hex") });
|
|
126
|
+
}
|
|
127
|
+
if (Date.now() >= deadline)
|
|
128
|
+
fail("DUMP_TIMEOUT", "native dump timed out");
|
|
129
|
+
const complete = sections.every(s => !s.shortRead);
|
|
130
|
+
const manifest = { schema: "wowdump.runtime-dump.v4", engine: "native-memory", buildKey: request.buildKey, executableSha256: request.executableSha256, pid: request.pid, moduleBase: module.base, preferredImageBase: layout.imageBase, moduleSize: layout.moduleSize, startedAt, generatedAt: new Date().toISOString(), atomic: false, complete, zeroFilledGaps: true, sections, totalBytes: total };
|
|
131
|
+
const manifestFile = join(directory, "manifest.json");
|
|
132
|
+
await writeFile(manifestFile, JSON.stringify(manifest, null, 2));
|
|
133
|
+
return { ok: complete, kind: "dump", manifestFile, outputDirectory: directory, complete, totalBytes: total, sections: sections.map(s => ({ name: s.name, bytes: s.bytes, readSize: s.readSize, sha256: s.sha256, shortRead: s.shortRead })), ...(!complete ? { error: { code: "DUMP_INCOMPLETE", message: "manifest records unreadable ranges" } } : {}) };
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
await rm(directory, { recursive: true, force: true });
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
await handle?.close();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -3,7 +3,7 @@ import { createConnection } from "node:net";
|
|
|
3
3
|
import { mkdir, open, readFile, rm } from "node:fs/promises";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import koffi from "koffi";
|
|
6
|
-
import {
|
|
6
|
+
import { MemoryProtocolError } from "./broker.js";
|
|
7
7
|
function quoteWindowsArgument(value) {
|
|
8
8
|
if (value.length > 0 && !/[\s"]/u.test(value))
|
|
9
9
|
return value;
|
|
@@ -95,7 +95,7 @@ function requestPipe(metadata, request, timeoutMs) {
|
|
|
95
95
|
resolveResult(value);
|
|
96
96
|
};
|
|
97
97
|
const timer = setTimeout(() => {
|
|
98
|
-
const error = new Error(`
|
|
98
|
+
const error = new Error(`memory broker request timed out after ${timeoutMs}ms`);
|
|
99
99
|
error.code = "BROKER_REQUEST_TIMEOUT";
|
|
100
100
|
finish(error);
|
|
101
101
|
}, timeoutMs);
|
|
@@ -115,7 +115,7 @@ function requestPipe(metadata, request, timeoutMs) {
|
|
|
115
115
|
});
|
|
116
116
|
socket.once("error", error => finish(error));
|
|
117
117
|
socket.once("end", () => { if (!settled)
|
|
118
|
-
finish(new Error("
|
|
118
|
+
finish(new Error("memory broker closed without a response")); });
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
async function pause(milliseconds) {
|
|
@@ -125,12 +125,12 @@ export class WindowsBrokerManager {
|
|
|
125
125
|
metadataFile;
|
|
126
126
|
options;
|
|
127
127
|
constructor(options) {
|
|
128
|
-
this.options = { ...options, home: resolve(options.home),
|
|
129
|
-
this.metadataFile = join(this.options.home, "runtime", "
|
|
128
|
+
this.options = { ...options, home: resolve(options.home), memoryEntry: resolve(options.memoryEntry) };
|
|
129
|
+
this.metadataFile = join(this.options.home, "runtime", "memory-broker.json");
|
|
130
130
|
}
|
|
131
131
|
async request(invocation) {
|
|
132
132
|
const request = invocationRequest(invocation);
|
|
133
|
-
const requestTimeout = invocation.command === "
|
|
133
|
+
const requestTimeout = invocation.command === "dump"
|
|
134
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
135
|
: this.options.requestTimeoutMs ?? 30_000;
|
|
136
136
|
const existing = await this.readMetadata();
|
|
@@ -140,7 +140,7 @@ export class WindowsBrokerManager {
|
|
|
140
140
|
}
|
|
141
141
|
catch (error) {
|
|
142
142
|
const code = error.code;
|
|
143
|
-
// A long-running
|
|
143
|
+
// A long-running dump 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;
|
|
@@ -153,7 +153,7 @@ export class WindowsBrokerManager {
|
|
|
153
153
|
async readMetadata() {
|
|
154
154
|
try {
|
|
155
155
|
const value = JSON.parse(await readFile(this.metadataFile, "utf8"));
|
|
156
|
-
return value?.schema === "wowdump.
|
|
156
|
+
return value?.schema === "wowdump.memory-broker.v1"
|
|
157
157
|
&& value.host === "127.0.0.1"
|
|
158
158
|
&& Number.isSafeInteger(value.port)
|
|
159
159
|
&& value.port > 0
|
|
@@ -166,6 +166,32 @@ export class WindowsBrokerManager {
|
|
|
166
166
|
return undefined;
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
|
+
async retireLegacyBroker() {
|
|
170
|
+
const file = join(this.options.home, "runtime", "reader-broker.json");
|
|
171
|
+
let metadata;
|
|
172
|
+
try {
|
|
173
|
+
metadata = JSON.parse(await readFile(file, "utf8"));
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (error.code === "ENOENT")
|
|
177
|
+
return;
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
if (metadata.schema !== "wowdump.reader-broker.v1" || metadata.host !== "127.0.0.1" ||
|
|
181
|
+
!Number.isSafeInteger(metadata.port) || metadata.port < 1 || metadata.port > 65535 || typeof metadata.token !== "string") {
|
|
182
|
+
throw new MemoryProtocolError("LEGACY_BROKER_INVALID", "legacy broker metadata is invalid");
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const result = await requestPipe(metadata, { command: "shutdown" }, 3000);
|
|
186
|
+
if (!result?.ok)
|
|
187
|
+
throw new MemoryProtocolError("LEGACY_BROKER_BUSY", "legacy broker did not accept shutdown");
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if (error.code !== "ECONNREFUSED")
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
await rm(file, { force: true });
|
|
194
|
+
}
|
|
169
195
|
async launchBroker() {
|
|
170
196
|
await mkdir(dirname(this.metadataFile), { recursive: true });
|
|
171
197
|
const lockFile = `${this.metadataFile}.lock`;
|
|
@@ -187,19 +213,20 @@ export class WindowsBrokerManager {
|
|
|
187
213
|
return this.launchBroker();
|
|
188
214
|
}
|
|
189
215
|
const token = randomBytes(32).toString("hex");
|
|
190
|
-
const args = [this.options.
|
|
216
|
+
const args = [this.options.memoryEntry, "--listen", "127.0.0.1", "--port", "0", "--token", token, "--metadata", this.metadataFile];
|
|
191
217
|
const launch = this.options.launch ?? shellExecuteRunAs;
|
|
192
218
|
try {
|
|
219
|
+
await this.retireLegacyBroker();
|
|
193
220
|
try {
|
|
194
|
-
launch(this.options.nodeExecutable ?? process.execPath, args, dirname(this.options.
|
|
221
|
+
launch(this.options.nodeExecutable ?? process.execPath, args, dirname(this.options.memoryEntry));
|
|
195
222
|
}
|
|
196
223
|
catch (error) {
|
|
197
224
|
const native = error;
|
|
198
|
-
throw new
|
|
225
|
+
throw new MemoryProtocolError("ELEVATION_FAILED", native.message, {
|
|
199
226
|
verb: "runas",
|
|
200
227
|
win32: native.win32 ?? null,
|
|
201
228
|
requestedRights: ["PROCESS_QUERY_INFORMATION", "PROCESS_VM_READ"],
|
|
202
|
-
|
|
229
|
+
memoryEntry: this.options.memoryEntry
|
|
203
230
|
});
|
|
204
231
|
}
|
|
205
232
|
const deadline = Date.now() + (this.options.launchTimeoutMs ?? 30_000);
|
|
@@ -209,7 +236,7 @@ export class WindowsBrokerManager {
|
|
|
209
236
|
return metadata;
|
|
210
237
|
await pause(100);
|
|
211
238
|
}
|
|
212
|
-
throw new Error(`elevated
|
|
239
|
+
throw new Error(`elevated memory broker did not become ready within ${this.options.launchTimeoutMs ?? 30_000}ms`);
|
|
213
240
|
}
|
|
214
241
|
finally {
|
|
215
242
|
await lock.close().catch(() => undefined);
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
import { createServer } from "node:net";
|
|
3
3
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { createWindowsNativeReader } from "./windows.js";
|
|
5
|
+
import { serveMemoryBroker } from "./broker.js";
|
|
6
|
+
import { createWindowsNativeMemory } from "./windows.js";
|
|
8
7
|
function argument(name) {
|
|
9
8
|
const index = process.argv.indexOf(name);
|
|
10
9
|
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
@@ -14,24 +13,10 @@ const port = Number(argument("--port") ?? "0");
|
|
|
14
13
|
const token = argument("--token");
|
|
15
14
|
const metadata = argument("--metadata");
|
|
16
15
|
let server;
|
|
17
|
-
const backend = process.env.
|
|
18
|
-
const broker =
|
|
16
|
+
const backend = process.env.WOWDUMP_USE_FAKE_MEMORY === "1" ? undefined : createWindowsNativeMemory();
|
|
17
|
+
const broker = serveMemoryBroker({
|
|
19
18
|
attachStdio: !listen,
|
|
20
19
|
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
|
-
},
|
|
35
20
|
onShutdown: async () => {
|
|
36
21
|
server?.close();
|
|
37
22
|
if (metadata)
|
|
@@ -56,7 +41,7 @@ if (listen) {
|
|
|
56
41
|
try {
|
|
57
42
|
const envelope = JSON.parse(line);
|
|
58
43
|
if (envelope.token !== token || !envelope.request) {
|
|
59
|
-
socket.write(`${JSON.stringify({ ok: false, command: "auth", timestamp: Date.now(), error: { code: "AUTH_FAILED", message: "invalid
|
|
44
|
+
socket.write(`${JSON.stringify({ ok: false, command: "auth", timestamp: Date.now(), error: { code: "AUTH_FAILED", message: "invalid memory broker token" } })}\n`);
|
|
60
45
|
return;
|
|
61
46
|
}
|
|
62
47
|
socket.write(`${JSON.stringify(await broker.handle(envelope.request))}\n`);
|
|
@@ -72,8 +57,8 @@ if (listen) {
|
|
|
72
57
|
await mkdir(dirname(metadata), { recursive: true });
|
|
73
58
|
const address = server?.address();
|
|
74
59
|
if (!address || typeof address === "string")
|
|
75
|
-
throw new Error("
|
|
76
|
-
await writeFile(metadata, `${JSON.stringify({ schema: "wowdump.
|
|
60
|
+
throw new Error("memory broker did not acquire a TCP endpoint");
|
|
61
|
+
await writeFile(metadata, `${JSON.stringify({ schema: "wowdump.memory-broker.v1", host: listen, port: address.port, token, pid: process.pid })}\n`, { encoding: "utf8", mode: 0o600 });
|
|
77
62
|
});
|
|
78
63
|
}
|
|
79
64
|
const shutdown = () => {
|