wowdump 0.2.0 → 0.3.1
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/LICENSE +21 -21
- package/README.md +56 -118
- package/dist/agent.js +5 -8
- package/dist/cli.js +497 -0
- package/dist/discovery.js +1 -12
- package/dist/dry-run.js +0 -2
- package/dist/focused-session.js +61 -1329
- package/dist/frida-runtime.js +51 -73
- package/dist/frida-worker.js +100 -0
- package/dist/ghidra.js +769 -0
- package/dist/main.js +66 -0
- package/dist/processes.js +2 -5
- package/dist/reader-broker.js +460 -0
- package/dist/reader-client.js +1 -0
- package/dist/reader-main.js +67 -0
- package/dist/session.js +17 -120
- package/dist/toolchain.js +594 -0
- package/dist/windows-launcher.js +207 -0
- package/dist/windows-reader.js +102 -0
- package/dist/wow-analysis.js +211 -236
- package/package.json +18 -35
- package/skills/wowdump/SKILL.md +15 -0
- package/skills/wowdump/commands.md +44 -0
- package/dist/analysis-path.js +0 -38
- package/dist/analysis-process-log.js +0 -146
- package/dist/broker-client.js +0 -411
- package/dist/broker-codec.js +0 -148
- package/dist/broker-core.js +0 -1045
- package/dist/broker-gateway.js +0 -447
- package/dist/broker-ledger.js +0 -196
- package/dist/broker-main.js +0 -291
- package/dist/broker-protocol.js +0 -119
- package/dist/broker-runtime.js +0 -1283
- package/dist/broker-server.js +0 -466
- package/dist/build-bundle-loader.js +0 -183
- package/dist/build-bundle.js +0 -11
- package/dist/focus-errors.js +0 -63
- package/dist/focus-service.js +0 -1855
- package/dist/mcp-main.js +0 -51
- package/dist/mcp.js +0 -924
- package/dist/process-log-lock.js +0 -181
- package/dist/runtime-config.js +0 -399
- package/resources/builds/retail/12.0.7.68974/build-profile.json +0 -290
- package/resources/builds/retail/12.0.7.68974/data-sources.json +0 -1633
- package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +0 -5130
- package/resources/builds/retail/12.0.7.68974/manifest.json +0 -63
- package/resources/builds/retail/12.0.7.68974/signatures.json +0 -260
package/dist/wow-analysis.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { basename, join, resolve } from "node:path";
|
|
4
|
-
import { brokerManagedCommand } from "./frida-runtime.js";
|
|
5
|
-
import { BuildBundleStore } from "./build-bundle-loader.js";
|
|
6
4
|
export const WOW_ANALYSIS_BUILD_KEY = "retail@12.0.7.68974";
|
|
7
5
|
export const WOW_ANALYSIS_MODULE = "Wow.exe";
|
|
8
6
|
export const WOW_IDA_IMAGE_BASE = "0x140000000";
|
|
@@ -192,208 +190,206 @@ function requireSelection(input) {
|
|
|
192
190
|
throw new Error("an explicit API, namespace, glob, RVA, cluster, or all=true selection is required");
|
|
193
191
|
}
|
|
194
192
|
}
|
|
195
|
-
const CPP_TRACE_SOURCE = String.raw `
|
|
196
|
-
let catalog = null;
|
|
197
|
-
let listeners = [];
|
|
198
|
-
let events = [];
|
|
199
|
-
let nextSeq = 1;
|
|
200
|
-
let droppedEvents = 0;
|
|
201
|
-
let active = false;
|
|
202
|
-
let timer = null;
|
|
203
|
-
let startedAt = null;
|
|
204
|
-
let stoppedAt = null;
|
|
205
|
-
let stopReason = null;
|
|
206
|
-
const depthByThread = new Map();
|
|
207
|
-
function emit(event, maxEvents) {
|
|
208
|
-
event.seq = nextSeq++;
|
|
209
|
-
if (events.length >= maxEvents) { events.shift(); droppedEvents++; }
|
|
210
|
-
events.push(event);
|
|
211
|
-
}
|
|
212
|
-
function stop(reason) {
|
|
213
|
-
const errors = [];
|
|
214
|
-
if (timer !== null) { clearTimeout(timer); timer = null; }
|
|
215
|
-
const detachedHooks = listeners.length;
|
|
216
|
-
for (const listener of listeners.splice(0)) {
|
|
217
|
-
try { listener.detach(); } catch (error) { errors.push(String(error)); }
|
|
218
|
-
}
|
|
219
|
-
depthByThread.clear();
|
|
220
|
-
active = false;
|
|
221
|
-
stoppedAt = new Date().toISOString();
|
|
222
|
-
stopReason = reason || 'requested';
|
|
223
|
-
return { ...status(), detachedHooks, detachErrors: errors };
|
|
224
|
-
}
|
|
225
|
-
function status() {
|
|
226
|
-
return {
|
|
227
|
-
active, buildKey: catalog && catalog.buildKey, moduleName: catalog && catalog.moduleName,
|
|
228
|
-
installedHooks: listeners.length, queuedEvents: events.length, droppedEvents,
|
|
229
|
-
startedAt, stoppedAt, stopReason
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
rpc.exports = {
|
|
233
|
-
configureCppTrace(value) {
|
|
234
|
-
stop('reconfigured');
|
|
235
|
-
catalog = value;
|
|
236
|
-
events = []; nextSeq = 1; droppedEvents = 0;
|
|
237
|
-
return { configured: true, buildKey: value.buildKey, targets: value.targets.length };
|
|
238
|
-
},
|
|
239
|
-
wowCppTraceStart(request) {
|
|
240
|
-
if (!catalog) throw new Error('C++ trace catalog is not configured');
|
|
241
|
-
if (active) throw new Error('C++ trace is already active');
|
|
242
|
-
if (request.buildKey !== catalog.buildKey) throw new Error('build mismatch');
|
|
243
|
-
if (request.pid !== undefined && request.pid !== Process.id) throw new Error('PID mismatch');
|
|
244
|
-
const module = Process.getModuleByName(catalog.moduleName);
|
|
245
|
-
const maxEvents = request.maxEvents;
|
|
246
|
-
const sampling = request.sampling;
|
|
247
|
-
const argumentCount = request.argumentCount;
|
|
248
|
-
for (const target of catalog.targets) {
|
|
249
|
-
const address = module.base.add(target.rvaNumber);
|
|
250
|
-
const range = Process.findRangeByAddress(address);
|
|
251
|
-
if (range === null || range.protection.indexOf('x') < 0) throw new Error('target is not executable: ' + target.id);
|
|
252
|
-
const listener = Interceptor.attach(address, {
|
|
253
|
-
onEnter(args) {
|
|
254
|
-
if (sampling < 1 && Math.random() > sampling) { this.__wowCppSkipped = true; return; }
|
|
255
|
-
const threadId = this.threadId;
|
|
256
|
-
const depth = (depthByThread.get(threadId) || 0) + 1;
|
|
257
|
-
depthByThread.set(threadId, depth);
|
|
258
|
-
this.__wowCppDepth = depth;
|
|
259
|
-
const captured = [];
|
|
260
|
-
if (request.captureArguments) {
|
|
261
|
-
for (let index = 0; index < argumentCount; index++) captured.push(args[index].toString());
|
|
262
|
-
}
|
|
263
|
-
emit({ type: 'cpp_trace_enter', timestamp: new Date().toISOString(), buildKey: catalog.buildKey,
|
|
264
|
-
moduleName: module.name, moduleBase: module.base.toString(), threadId, depth, target,
|
|
265
|
-
arguments: captured }, maxEvents);
|
|
266
|
-
},
|
|
267
|
-
onLeave(retval) {
|
|
268
|
-
if (this.__wowCppSkipped) return;
|
|
269
|
-
const threadId = this.threadId;
|
|
270
|
-
const depth = this.__wowCppDepth || depthByThread.get(threadId) || 1;
|
|
271
|
-
emit({ type: 'cpp_trace_leave', timestamp: new Date().toISOString(), buildKey: catalog.buildKey,
|
|
272
|
-
moduleName: module.name, moduleBase: module.base.toString(), threadId, depth, target,
|
|
273
|
-
returnValue: request.captureReturn ? retval.toString() : undefined }, maxEvents);
|
|
274
|
-
if (depth <= 1) depthByThread.delete(threadId); else depthByThread.set(threadId, depth - 1);
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
listeners.push(listener);
|
|
278
|
-
}
|
|
279
|
-
active = true; startedAt = new Date().toISOString(); stoppedAt = null; stopReason = null;
|
|
280
|
-
if (request.durationMs > 0) timer = setTimeout(() => stop('duration_elapsed'), request.durationMs);
|
|
281
|
-
return { ...status(), pid: Process.id, moduleBase: module.base.toString(), selectedTargets: catalog.targets.length };
|
|
282
|
-
},
|
|
283
|
-
wowCppTraceRead(afterSeq, limit) {
|
|
284
|
-
const selected = events.filter(event => event.seq > afterSeq).slice(0, limit);
|
|
285
|
-
return { events: selected, oldestSeq: events[0] ? events[0].seq : null,
|
|
286
|
-
newestSeq: events.length ? events[events.length - 1].seq : null,
|
|
287
|
-
nextAfterSeq: selected.length ? selected[selected.length - 1].seq : afterSeq, droppedEvents };
|
|
288
|
-
},
|
|
289
|
-
wowCppTraceStatus() { return status(); },
|
|
290
|
-
wowCppTraceStop(reason) { return stop(reason); },
|
|
291
|
-
dispose() { const result = stop('disposed'); catalog = null; return result; }
|
|
292
|
-
};
|
|
193
|
+
const CPP_TRACE_SOURCE = String.raw `
|
|
194
|
+
let catalog = null;
|
|
195
|
+
let listeners = [];
|
|
196
|
+
let events = [];
|
|
197
|
+
let nextSeq = 1;
|
|
198
|
+
let droppedEvents = 0;
|
|
199
|
+
let active = false;
|
|
200
|
+
let timer = null;
|
|
201
|
+
let startedAt = null;
|
|
202
|
+
let stoppedAt = null;
|
|
203
|
+
let stopReason = null;
|
|
204
|
+
const depthByThread = new Map();
|
|
205
|
+
function emit(event, maxEvents) {
|
|
206
|
+
event.seq = nextSeq++;
|
|
207
|
+
if (events.length >= maxEvents) { events.shift(); droppedEvents++; }
|
|
208
|
+
events.push(event);
|
|
209
|
+
}
|
|
210
|
+
function stop(reason) {
|
|
211
|
+
const errors = [];
|
|
212
|
+
if (timer !== null) { clearTimeout(timer); timer = null; }
|
|
213
|
+
const detachedHooks = listeners.length;
|
|
214
|
+
for (const listener of listeners.splice(0)) {
|
|
215
|
+
try { listener.detach(); } catch (error) { errors.push(String(error)); }
|
|
216
|
+
}
|
|
217
|
+
depthByThread.clear();
|
|
218
|
+
active = false;
|
|
219
|
+
stoppedAt = new Date().toISOString();
|
|
220
|
+
stopReason = reason || 'requested';
|
|
221
|
+
return { ...status(), detachedHooks, detachErrors: errors };
|
|
222
|
+
}
|
|
223
|
+
function status() {
|
|
224
|
+
return {
|
|
225
|
+
active, buildKey: catalog && catalog.buildKey, moduleName: catalog && catalog.moduleName,
|
|
226
|
+
installedHooks: listeners.length, queuedEvents: events.length, droppedEvents,
|
|
227
|
+
startedAt, stoppedAt, stopReason
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
rpc.exports = {
|
|
231
|
+
configureCppTrace(value) {
|
|
232
|
+
stop('reconfigured');
|
|
233
|
+
catalog = value;
|
|
234
|
+
events = []; nextSeq = 1; droppedEvents = 0;
|
|
235
|
+
return { configured: true, buildKey: value.buildKey, targets: value.targets.length };
|
|
236
|
+
},
|
|
237
|
+
wowCppTraceStart(request) {
|
|
238
|
+
if (!catalog) throw new Error('C++ trace catalog is not configured');
|
|
239
|
+
if (active) throw new Error('C++ trace is already active');
|
|
240
|
+
if (request.buildKey !== catalog.buildKey) throw new Error('build mismatch');
|
|
241
|
+
if (request.pid !== undefined && request.pid !== Process.id) throw new Error('PID mismatch');
|
|
242
|
+
const module = Process.getModuleByName(catalog.moduleName);
|
|
243
|
+
const maxEvents = request.maxEvents;
|
|
244
|
+
const sampling = request.sampling;
|
|
245
|
+
const argumentCount = request.argumentCount;
|
|
246
|
+
for (const target of catalog.targets) {
|
|
247
|
+
const address = module.base.add(target.rvaNumber);
|
|
248
|
+
const range = Process.findRangeByAddress(address);
|
|
249
|
+
if (range === null || range.protection.indexOf('x') < 0) throw new Error('target is not executable: ' + target.id);
|
|
250
|
+
const listener = Interceptor.attach(address, {
|
|
251
|
+
onEnter(args) {
|
|
252
|
+
if (sampling < 1 && Math.random() > sampling) { this.__wowCppSkipped = true; return; }
|
|
253
|
+
const threadId = this.threadId;
|
|
254
|
+
const depth = (depthByThread.get(threadId) || 0) + 1;
|
|
255
|
+
depthByThread.set(threadId, depth);
|
|
256
|
+
this.__wowCppDepth = depth;
|
|
257
|
+
const captured = [];
|
|
258
|
+
if (request.captureArguments) {
|
|
259
|
+
for (let index = 0; index < argumentCount; index++) captured.push(args[index].toString());
|
|
260
|
+
}
|
|
261
|
+
emit({ type: 'cpp_trace_enter', timestamp: new Date().toISOString(), buildKey: catalog.buildKey,
|
|
262
|
+
moduleName: module.name, moduleBase: module.base.toString(), threadId, depth, target,
|
|
263
|
+
arguments: captured }, maxEvents);
|
|
264
|
+
},
|
|
265
|
+
onLeave(retval) {
|
|
266
|
+
if (this.__wowCppSkipped) return;
|
|
267
|
+
const threadId = this.threadId;
|
|
268
|
+
const depth = this.__wowCppDepth || depthByThread.get(threadId) || 1;
|
|
269
|
+
emit({ type: 'cpp_trace_leave', timestamp: new Date().toISOString(), buildKey: catalog.buildKey,
|
|
270
|
+
moduleName: module.name, moduleBase: module.base.toString(), threadId, depth, target,
|
|
271
|
+
returnValue: request.captureReturn ? retval.toString() : undefined }, maxEvents);
|
|
272
|
+
if (depth <= 1) depthByThread.delete(threadId); else depthByThread.set(threadId, depth - 1);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
listeners.push(listener);
|
|
276
|
+
}
|
|
277
|
+
active = true; startedAt = new Date().toISOString(); stoppedAt = null; stopReason = null;
|
|
278
|
+
if (request.durationMs > 0) timer = setTimeout(() => stop('duration_elapsed'), request.durationMs);
|
|
279
|
+
return { ...status(), pid: Process.id, moduleBase: module.base.toString(), selectedTargets: catalog.targets.length };
|
|
280
|
+
},
|
|
281
|
+
wowCppTraceRead(afterSeq, limit) {
|
|
282
|
+
const selected = events.filter(event => event.seq > afterSeq).slice(0, limit);
|
|
283
|
+
return { events: selected, oldestSeq: events[0] ? events[0].seq : null,
|
|
284
|
+
newestSeq: events.length ? events[events.length - 1].seq : null,
|
|
285
|
+
nextAfterSeq: selected.length ? selected[selected.length - 1].seq : afterSeq, droppedEvents };
|
|
286
|
+
},
|
|
287
|
+
wowCppTraceStatus() { return status(); },
|
|
288
|
+
wowCppTraceStop(reason) { return stop(reason); },
|
|
289
|
+
dispose() { const result = stop('disposed'); catalog = null; return result; }
|
|
290
|
+
};
|
|
293
291
|
`;
|
|
294
|
-
const DATA_READER_SOURCE = String.raw `
|
|
295
|
-
function readable(address, size) {
|
|
296
|
-
const range = Process.findRangeByAddress(address);
|
|
297
|
-
if (range === null || range.protection.indexOf('r') < 0) throw new Error('unreadable address ' + address);
|
|
298
|
-
const end = address.add(size);
|
|
299
|
-
if (end.compare(range.base.add(range.size)) > 0) throw new Error('read crosses range at ' + address);
|
|
300
|
-
}
|
|
301
|
-
function readField(base, field, maxStringBytes) {
|
|
302
|
-
const address = base.add(field.offset);
|
|
303
|
-
const sizeByKind = { bool: 1, u8: 1, s8: 1, u16: 2, s16: 2, u32: 4, s32: 4,
|
|
304
|
-
float: 4, u64: 8, s64: 8, double: 8, pointer: Process.pointerSize };
|
|
305
|
-
if (field.kind === 'utf8') {
|
|
306
|
-
const length = Math.min(field.maxBytes || maxStringBytes, maxStringBytes);
|
|
307
|
-
readable(address, 1);
|
|
308
|
-
return address.readUtf8String(length);
|
|
309
|
-
}
|
|
310
|
-
if (field.kind === 'bytes') {
|
|
311
|
-
const length = field.size;
|
|
312
|
-
readable(address, length);
|
|
313
|
-
const value = address.readByteArray(length);
|
|
314
|
-
return Array.from(new Uint8Array(value)).map(v => v.toString(16).padStart(2, '0')).join('');
|
|
315
|
-
}
|
|
316
|
-
const size = sizeByKind[field.kind];
|
|
317
|
-
if (!size) throw new Error('unsupported field kind ' + field.kind);
|
|
318
|
-
readable(address, size);
|
|
319
|
-
switch (field.kind) {
|
|
320
|
-
case 'bool': return address.readU8() !== 0;
|
|
321
|
-
case 'u8': return address.readU8(); case 's8': return address.readS8();
|
|
322
|
-
case 'u16': return address.readU16(); case 's16': return address.readS16();
|
|
323
|
-
case 'u32': return address.readU32(); case 's32': return address.readS32();
|
|
324
|
-
case 'float': return address.readFloat(); case 'double': return address.readDouble();
|
|
325
|
-
case 'u64': return address.readU64().toString(); case 's64': return address.readS64().toString();
|
|
326
|
-
case 'pointer': return address.readPointer().toString();
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
function validateInvariant(rule, address, fields, module) {
|
|
330
|
-
if (rule.kind === 'non_null') return { kind: rule.kind, passed: !address.isNull() };
|
|
331
|
-
if (rule.kind === 'aligned') return { kind: rule.kind, passed: address.and(rule.alignment - 1).isNull() };
|
|
332
|
-
if (rule.kind === 'field_range') {
|
|
333
|
-
const value = Number(fields[rule.field]);
|
|
334
|
-
return { kind: rule.kind, field: rule.field, passed: Number.isFinite(value) && value >= rule.minimum && value <= rule.maximum };
|
|
335
|
-
}
|
|
336
|
-
if (rule.kind === 'field_equals') return { kind: rule.kind, field: rule.field, passed: String(fields[rule.field]) === String(rule.value) };
|
|
337
|
-
if (rule.kind === 'vtable_in_module') {
|
|
338
|
-
const pointer = address.add(rule.offset || 0).readPointer();
|
|
339
|
-
const passed = pointer.compare(module.base) >= 0 && pointer.compare(module.base.add(module.size)) < 0;
|
|
340
|
-
return { kind: rule.kind, pointer: pointer.toString(), passed };
|
|
341
|
-
}
|
|
342
|
-
return { kind: rule.kind, passed: false, error: 'unsupported invariant' };
|
|
343
|
-
}
|
|
344
|
-
rpc.exports = {
|
|
345
|
-
readDataSource(plan, requestedLimit) {
|
|
346
|
-
const module = Process.getModuleByName(plan.moduleName);
|
|
347
|
-
const rva = Number(plan.root.rva);
|
|
348
|
-
if (!Number.isSafeInteger(rva) || rva < 0 || rva >= module.size) {
|
|
349
|
-
return { allPassed: false, error: 'root RVA is outside module', moduleBase: module.base.toString() };
|
|
350
|
-
}
|
|
351
|
-
try {
|
|
352
|
-
const rootAddress = module.base.add(rva);
|
|
353
|
-
let address = rootAddress;
|
|
354
|
-
if (plan.root.kind === 'global_pointer_rva') { readable(address, Process.pointerSize); address = address.readPointer(); }
|
|
355
|
-
for (const step of plan.pointerChain) {
|
|
356
|
-
address = address.add(step.offset);
|
|
357
|
-
if (step.dereference) { readable(address, Process.pointerSize); address = address.readPointer(); }
|
|
358
|
-
}
|
|
359
|
-
if (address.isNull()) throw new Error('resolved root is null');
|
|
360
|
-
const fields = {};
|
|
361
|
-
for (const field of plan.fields) fields[field.name] = readField(address, field, plan.maxStringBytes);
|
|
362
|
-
const validations = plan.invariants.map(rule => validateInvariant(rule, address, fields, module));
|
|
363
|
-
const result = { dataSourceId: plan.dataSourceId, buildKey: plan.buildKey, moduleName: module.name,
|
|
364
|
-
moduleBase: module.base.toString(), rootRva: plan.root.rva, rootAddress: rootAddress.toString(),
|
|
365
|
-
resolvedAddress: address.toString(), fields, validations, items: [] };
|
|
366
|
-
if (plan.container && plan.container.kind === 'vector') {
|
|
367
|
-
const begin = address.add(plan.container.beginOffset).readPointer();
|
|
368
|
-
const end = address.add(plan.container.endOffset).readPointer();
|
|
369
|
-
const byteLength = Number(BigInt(end.toString()) - BigInt(begin.toString()));
|
|
370
|
-
const elementSize = plan.container.elementSize;
|
|
371
|
-
const count = byteLength >= 0 && byteLength % elementSize === 0 ? byteLength / elementSize : -1;
|
|
372
|
-
const limit = Math.min(requestedLimit, plan.maxItems);
|
|
373
|
-
validations.push({ kind: 'vector_bounds', passed: count >= 0 && count <= plan.maxItems, count });
|
|
374
|
-
if (count >= 0 && count <= plan.maxItems) {
|
|
375
|
-
for (let index = 0; index < Math.min(count, limit); index++) {
|
|
376
|
-
const itemAddress = begin.add(index * elementSize);
|
|
377
|
-
const item = {};
|
|
378
|
-
for (const field of plan.container.fields || []) item[field.name] = readField(itemAddress, field, plan.maxStringBytes);
|
|
379
|
-
result.items.push({ index, address: itemAddress.toString(), fields: item });
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
result.allPassed = validations.length > 0 && validations.every(item => item.passed === true);
|
|
384
|
-
return result;
|
|
385
|
-
} catch (error) {
|
|
386
|
-
return { dataSourceId: plan.dataSourceId, buildKey: plan.buildKey, moduleName: module.name,
|
|
387
|
-
moduleBase: module.base.toString(), rootRva: plan.root.rva, allPassed: false, error: String(error) };
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
};
|
|
292
|
+
const DATA_READER_SOURCE = String.raw `
|
|
293
|
+
function readable(address, size) {
|
|
294
|
+
const range = Process.findRangeByAddress(address);
|
|
295
|
+
if (range === null || range.protection.indexOf('r') < 0) throw new Error('unreadable address ' + address);
|
|
296
|
+
const end = address.add(size);
|
|
297
|
+
if (end.compare(range.base.add(range.size)) > 0) throw new Error('read crosses range at ' + address);
|
|
298
|
+
}
|
|
299
|
+
function readField(base, field, maxStringBytes) {
|
|
300
|
+
const address = base.add(field.offset);
|
|
301
|
+
const sizeByKind = { bool: 1, u8: 1, s8: 1, u16: 2, s16: 2, u32: 4, s32: 4,
|
|
302
|
+
float: 4, u64: 8, s64: 8, double: 8, pointer: Process.pointerSize };
|
|
303
|
+
if (field.kind === 'utf8') {
|
|
304
|
+
const length = Math.min(field.maxBytes || maxStringBytes, maxStringBytes);
|
|
305
|
+
readable(address, 1);
|
|
306
|
+
return address.readUtf8String(length);
|
|
307
|
+
}
|
|
308
|
+
if (field.kind === 'bytes') {
|
|
309
|
+
const length = field.size;
|
|
310
|
+
readable(address, length);
|
|
311
|
+
const value = address.readByteArray(length);
|
|
312
|
+
return Array.from(new Uint8Array(value)).map(v => v.toString(16).padStart(2, '0')).join('');
|
|
313
|
+
}
|
|
314
|
+
const size = sizeByKind[field.kind];
|
|
315
|
+
if (!size) throw new Error('unsupported field kind ' + field.kind);
|
|
316
|
+
readable(address, size);
|
|
317
|
+
switch (field.kind) {
|
|
318
|
+
case 'bool': return address.readU8() !== 0;
|
|
319
|
+
case 'u8': return address.readU8(); case 's8': return address.readS8();
|
|
320
|
+
case 'u16': return address.readU16(); case 's16': return address.readS16();
|
|
321
|
+
case 'u32': return address.readU32(); case 's32': return address.readS32();
|
|
322
|
+
case 'float': return address.readFloat(); case 'double': return address.readDouble();
|
|
323
|
+
case 'u64': return address.readU64().toString(); case 's64': return address.readS64().toString();
|
|
324
|
+
case 'pointer': return address.readPointer().toString();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function validateInvariant(rule, address, fields, module) {
|
|
328
|
+
if (rule.kind === 'non_null') return { kind: rule.kind, passed: !address.isNull() };
|
|
329
|
+
if (rule.kind === 'aligned') return { kind: rule.kind, passed: address.and(rule.alignment - 1).isNull() };
|
|
330
|
+
if (rule.kind === 'field_range') {
|
|
331
|
+
const value = Number(fields[rule.field]);
|
|
332
|
+
return { kind: rule.kind, field: rule.field, passed: Number.isFinite(value) && value >= rule.minimum && value <= rule.maximum };
|
|
333
|
+
}
|
|
334
|
+
if (rule.kind === 'field_equals') return { kind: rule.kind, field: rule.field, passed: String(fields[rule.field]) === String(rule.value) };
|
|
335
|
+
if (rule.kind === 'vtable_in_module') {
|
|
336
|
+
const pointer = address.add(rule.offset || 0).readPointer();
|
|
337
|
+
const passed = pointer.compare(module.base) >= 0 && pointer.compare(module.base.add(module.size)) < 0;
|
|
338
|
+
return { kind: rule.kind, pointer: pointer.toString(), passed };
|
|
339
|
+
}
|
|
340
|
+
return { kind: rule.kind, passed: false, error: 'unsupported invariant' };
|
|
341
|
+
}
|
|
342
|
+
rpc.exports = {
|
|
343
|
+
readDataSource(plan, requestedLimit) {
|
|
344
|
+
const module = Process.getModuleByName(plan.moduleName);
|
|
345
|
+
const rva = Number(plan.root.rva);
|
|
346
|
+
if (!Number.isSafeInteger(rva) || rva < 0 || rva >= module.size) {
|
|
347
|
+
return { allPassed: false, error: 'root RVA is outside module', moduleBase: module.base.toString() };
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const rootAddress = module.base.add(rva);
|
|
351
|
+
let address = rootAddress;
|
|
352
|
+
if (plan.root.kind === 'global_pointer_rva') { readable(address, Process.pointerSize); address = address.readPointer(); }
|
|
353
|
+
for (const step of plan.pointerChain) {
|
|
354
|
+
address = address.add(step.offset);
|
|
355
|
+
if (step.dereference) { readable(address, Process.pointerSize); address = address.readPointer(); }
|
|
356
|
+
}
|
|
357
|
+
if (address.isNull()) throw new Error('resolved root is null');
|
|
358
|
+
const fields = {};
|
|
359
|
+
for (const field of plan.fields) fields[field.name] = readField(address, field, plan.maxStringBytes);
|
|
360
|
+
const validations = plan.invariants.map(rule => validateInvariant(rule, address, fields, module));
|
|
361
|
+
const result = { dataSourceId: plan.dataSourceId, buildKey: plan.buildKey, moduleName: module.name,
|
|
362
|
+
moduleBase: module.base.toString(), rootRva: plan.root.rva, rootAddress: rootAddress.toString(),
|
|
363
|
+
resolvedAddress: address.toString(), fields, validations, items: [] };
|
|
364
|
+
if (plan.container && plan.container.kind === 'vector') {
|
|
365
|
+
const begin = address.add(plan.container.beginOffset).readPointer();
|
|
366
|
+
const end = address.add(plan.container.endOffset).readPointer();
|
|
367
|
+
const byteLength = Number(BigInt(end.toString()) - BigInt(begin.toString()));
|
|
368
|
+
const elementSize = plan.container.elementSize;
|
|
369
|
+
const count = byteLength >= 0 && byteLength % elementSize === 0 ? byteLength / elementSize : -1;
|
|
370
|
+
const limit = Math.min(requestedLimit, plan.maxItems);
|
|
371
|
+
validations.push({ kind: 'vector_bounds', passed: count >= 0 && count <= plan.maxItems, count });
|
|
372
|
+
if (count >= 0 && count <= plan.maxItems) {
|
|
373
|
+
for (let index = 0; index < Math.min(count, limit); index++) {
|
|
374
|
+
const itemAddress = begin.add(index * elementSize);
|
|
375
|
+
const item = {};
|
|
376
|
+
for (const field of plan.container.fields || []) item[field.name] = readField(itemAddress, field, plan.maxStringBytes);
|
|
377
|
+
result.items.push({ index, address: itemAddress.toString(), fields: item });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
result.allPassed = validations.length > 0 && validations.every(item => item.passed === true);
|
|
382
|
+
return result;
|
|
383
|
+
} catch (error) {
|
|
384
|
+
return { dataSourceId: plan.dataSourceId, buildKey: plan.buildKey, moduleName: module.name,
|
|
385
|
+
moduleBase: module.base.toString(), rootRva: plan.root.rva, allPassed: false, error: String(error) };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
391
389
|
`;
|
|
392
390
|
export class WowAnalysisService {
|
|
393
391
|
executor;
|
|
394
|
-
|
|
395
|
-
packagedProfiles;
|
|
396
|
-
buildBundles;
|
|
392
|
+
artifactDir;
|
|
397
393
|
moduleName;
|
|
398
394
|
options;
|
|
399
395
|
sessions = new Map();
|
|
@@ -404,9 +400,7 @@ export class WowAnalysisService {
|
|
|
404
400
|
if (!options || typeof options.executor?.execute !== "function")
|
|
405
401
|
throw new TypeError("executor is required");
|
|
406
402
|
this.executor = options.executor;
|
|
407
|
-
this.
|
|
408
|
-
this.packagedProfiles = options.profileRoot !== undefined;
|
|
409
|
-
this.buildBundles = this.packagedProfiles ? new BuildBundleStore(this.profileRoot) : undefined;
|
|
403
|
+
this.artifactDir = resolve(requiredString(options.artifactDir, "artifactDir"));
|
|
410
404
|
this.moduleName = options.moduleName ?? WOW_ANALYSIS_MODULE;
|
|
411
405
|
this.options = options;
|
|
412
406
|
}
|
|
@@ -432,17 +426,17 @@ export class WowAnalysisService {
|
|
|
432
426
|
if (this.luaTrace)
|
|
433
427
|
throw new Error("Lua trace is already loaded; stop it before starting another trace");
|
|
434
428
|
const request = this.normalizeLuaStart(input);
|
|
435
|
-
const catalog = await this.loadLuaCatalog(input.buildKey);
|
|
436
429
|
const target = await this.acquireTarget(input.buildKey, input.pid);
|
|
437
430
|
let scriptId;
|
|
438
431
|
try {
|
|
439
432
|
const source = await this.luaSource();
|
|
440
|
-
const loaded = await this.executor.execute(
|
|
433
|
+
const loaded = await this.executor.execute({
|
|
441
434
|
operation: "script_load",
|
|
442
435
|
sessionId: target.sessionId,
|
|
443
436
|
source
|
|
444
|
-
},
|
|
437
|
+
}, target.context);
|
|
445
438
|
scriptId = requiredString(loaded.scriptId, "script_load.scriptId");
|
|
439
|
+
const catalog = await this.loadLuaCatalog(input.buildKey);
|
|
446
440
|
const configured = await this.callScript(scriptId, "configureLuaTrace", [catalog]);
|
|
447
441
|
const started = await this.callScript(scriptId, "wowLuaTraceStart", [{ ...request, pid: target.pid }]);
|
|
448
442
|
const value = isRecord(started.value) ? started.value : {};
|
|
@@ -483,11 +477,11 @@ export class WowAnalysisService {
|
|
|
483
477
|
const target = await this.acquireTarget(input.buildKey, input.pid);
|
|
484
478
|
let scriptId;
|
|
485
479
|
try {
|
|
486
|
-
const loaded = await this.executor.execute(
|
|
480
|
+
const loaded = await this.executor.execute({
|
|
487
481
|
operation: "script_load",
|
|
488
482
|
sessionId: target.sessionId,
|
|
489
483
|
source: this.options.cppTraceSource ?? CPP_TRACE_SOURCE
|
|
490
|
-
},
|
|
484
|
+
}, target.context);
|
|
491
485
|
scriptId = requiredString(loaded.scriptId, "script_load.scriptId");
|
|
492
486
|
const configured = await this.callScript(scriptId, "configureCppTrace", [catalog]);
|
|
493
487
|
const started = await this.callScript(scriptId, "wowCppTraceStart", [{ ...request, pid: target.pid }]);
|
|
@@ -566,11 +560,11 @@ export class WowAnalysisService {
|
|
|
566
560
|
const target = await this.acquireTarget(buildKey, input.pid);
|
|
567
561
|
let scriptId;
|
|
568
562
|
try {
|
|
569
|
-
const loaded = await this.executor.execute(
|
|
563
|
+
const loaded = await this.executor.execute({
|
|
570
564
|
operation: "script_load",
|
|
571
565
|
sessionId: target.sessionId,
|
|
572
566
|
source: this.options.dataReaderSource ?? DATA_READER_SOURCE
|
|
573
|
-
},
|
|
567
|
+
}, target.context);
|
|
574
568
|
scriptId = requiredString(loaded.scriptId, "script_load.scriptId");
|
|
575
569
|
const result = await this.callScript(scriptId, "readDataSource", [plan, boundedInteger(input.limit, plan.maxItems, 1, plan.maxItems, "limit")]);
|
|
576
570
|
const value = isRecord(result.value) ? result.value : { allPassed: false, error: "reader returned a non-object result" };
|
|
@@ -599,9 +593,6 @@ export class WowAnalysisService {
|
|
|
599
593
|
async wowBuildProfileValidate(input) {
|
|
600
594
|
const buildKey = requiredString(input.buildKey, "buildKey");
|
|
601
595
|
const profile = await this.readBuildProfile(buildKey);
|
|
602
|
-
const signatureSource = this.packagedProfiles
|
|
603
|
-
? await this.buildBundles.readJson(buildKey, "signatures.json")
|
|
604
|
-
: profile;
|
|
605
596
|
const target = await this.acquireTarget(buildKey, input.pid);
|
|
606
597
|
const checks = [];
|
|
607
598
|
try {
|
|
@@ -615,7 +606,7 @@ export class WowAnalysisService {
|
|
|
615
606
|
checks.push({ name: "module_name", expected: profileModule, actual: target.moduleName, passed: profileModule.toLowerCase() === target.moduleName.toLowerCase() });
|
|
616
607
|
checks.push({ name: "runtime_base_dynamic", actual: target.moduleBase, passed: /^0x[0-9a-f]+$/i.test(target.moduleBase) });
|
|
617
608
|
checks.push({ name: "image_base", expected: WOW_IDA_IMAGE_BASE, actual: profile.imageBase, passed: normalizeHex(profile.imageBase ?? WOW_IDA_IMAGE_BASE, "imageBase") === WOW_IDA_IMAGE_BASE });
|
|
618
|
-
const signatures = this.signatureRecords(
|
|
609
|
+
const signatures = this.signatureRecords(profile).slice(0, boundedInteger(input.maxChecks, 128, 1, MAX_SIGNATURE_CHECKS, "maxChecks"));
|
|
619
610
|
if (signatures.length === 0) {
|
|
620
611
|
checks.push({ name: "signature_evidence_present", passed: false, error: "build profile contains no verifiable entry signatures" });
|
|
621
612
|
}
|
|
@@ -656,7 +647,7 @@ export class WowAnalysisService {
|
|
|
656
647
|
const suffix = buildSuffix(buildKey);
|
|
657
648
|
const artifacts = [];
|
|
658
649
|
for (const spec of REQUIRED_ARTIFACTS) {
|
|
659
|
-
const path = join(this.
|
|
650
|
+
const path = join(this.artifactDir, spec.file(suffix));
|
|
660
651
|
try {
|
|
661
652
|
const data = await readFile(path);
|
|
662
653
|
let parsed;
|
|
@@ -699,7 +690,7 @@ export class WowAnalysisService {
|
|
|
699
690
|
const nonLuaRecords = recordsFrom(nonLua, ["systems", "records"]);
|
|
700
691
|
return {
|
|
701
692
|
buildKey,
|
|
702
|
-
|
|
693
|
+
artifactDir: this.artifactDir,
|
|
703
694
|
generatedAt: new Date().toISOString(),
|
|
704
695
|
requiredArtifacts: artifacts.length,
|
|
705
696
|
presentArtifacts: artifacts.filter(item => item.exists === true).length,
|
|
@@ -890,10 +881,10 @@ export class WowAnalysisService {
|
|
|
890
881
|
return readFile(path, "utf8");
|
|
891
882
|
}
|
|
892
883
|
async callScript(scriptId, exportName, args = []) {
|
|
893
|
-
return this.executor.execute(
|
|
884
|
+
return this.executor.execute({ operation: "script_call", scriptId, exportName, args });
|
|
894
885
|
}
|
|
895
886
|
async unloadScript(scriptId) {
|
|
896
|
-
await this.executor.execute(
|
|
887
|
+
await this.executor.execute({ operation: "script_unload", scriptId });
|
|
897
888
|
}
|
|
898
889
|
activeTrace(kind) {
|
|
899
890
|
return kind === "lua" ? this.luaTrace : this.cppTrace;
|
|
@@ -978,27 +969,13 @@ export class WowAnalysisService {
|
|
|
978
969
|
}
|
|
979
970
|
}
|
|
980
971
|
artifactPath(buildKey, stem) {
|
|
981
|
-
|
|
982
|
-
return join(this.profileRoot, `${stem}-${buildSuffix(buildKey)}.json`);
|
|
983
|
-
const names = {
|
|
984
|
-
"build-profile": "build-profile.json",
|
|
985
|
-
"cpp-data-sources": "data-sources.json",
|
|
986
|
-
"wow-lua-api-all-rva": "lua-targets.jsonl"
|
|
987
|
-
};
|
|
988
|
-
return names[stem];
|
|
972
|
+
return join(this.artifactDir, `${stem}-${buildSuffix(buildKey)}.json`);
|
|
989
973
|
}
|
|
990
974
|
async readArtifact(buildKey, stem) {
|
|
991
975
|
const path = this.artifactPath(buildKey, stem);
|
|
992
|
-
if (!path)
|
|
993
|
-
throw new Error(`${stem} is not included in the packaged build bundle for ${buildKey}`);
|
|
994
976
|
let value;
|
|
995
977
|
try {
|
|
996
|
-
|
|
997
|
-
? await this.buildBundles.readText(buildKey, path)
|
|
998
|
-
: await readFile(path, "utf8");
|
|
999
|
-
value = path.endsWith(".jsonl")
|
|
1000
|
-
? { buildKey, records: text.split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line)) }
|
|
1001
|
-
: JSON.parse(text);
|
|
978
|
+
value = JSON.parse(await readFile(path, "utf8"));
|
|
1002
979
|
}
|
|
1003
980
|
catch (error) {
|
|
1004
981
|
throw new Error(`failed to read ${basename(path)}: ${errorText(error)}`);
|
|
@@ -1011,8 +988,6 @@ export class WowAnalysisService {
|
|
|
1011
988
|
return value;
|
|
1012
989
|
}
|
|
1013
990
|
async readArtifactOptional(buildKey, stem) {
|
|
1014
|
-
if (this.packagedProfiles && !this.artifactPath(buildKey, stem))
|
|
1015
|
-
return undefined;
|
|
1016
991
|
try {
|
|
1017
992
|
return await this.readArtifact(buildKey, stem);
|
|
1018
993
|
}
|
package/package.json
CHANGED
|
@@ -1,37 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wowdump",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"wowdump": "dist/mcp-main.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist/**",
|
|
11
|
-
"!dist/main.js",
|
|
12
|
-
"resources/**",
|
|
13
|
-
"README.md",
|
|
14
|
-
"LICENSE"
|
|
15
|
-
],
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "WoW native memory analysis CLI with Ghidra, Frida validation, and an elevated reader broker",
|
|
16
5
|
"license": "MIT",
|
|
17
|
-
"author": "Follenfang",
|
|
18
6
|
"repository": {
|
|
19
7
|
"type": "git",
|
|
20
8
|
"url": "git+https://github.com/Follen/wowdump.git"
|
|
21
9
|
},
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"mcp",
|
|
28
|
-
"frida",
|
|
29
|
-
"world-of-warcraft",
|
|
30
|
-
"wow",
|
|
31
|
-
"runtime-analysis"
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"skills",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
32
15
|
],
|
|
33
|
-
"
|
|
34
|
-
|
|
16
|
+
"type": "module",
|
|
17
|
+
"bin": {
|
|
18
|
+
"wowdump": "dist/cli.js"
|
|
35
19
|
},
|
|
36
20
|
"engines": {
|
|
37
21
|
"node": ">=22"
|
|
@@ -40,19 +24,18 @@
|
|
|
40
24
|
"build": "npm run build:host && npm run build:agent",
|
|
41
25
|
"build:host": "tsc -p tsconfig.json",
|
|
42
26
|
"build:agent": "frida-compile agent/index.ts -o dist/agent.js",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"test": "npm run build:host && node --test tests/*.test.mjs
|
|
49
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
50
|
-
"prepack": "npm run build && npm run bundle:verify"
|
|
27
|
+
"wowdump": "node dist/cli.js",
|
|
28
|
+
"postinstall": "npm run build:host --if-present && node dist/toolchain.js --postinstall",
|
|
29
|
+
"start": "node dist/main.js",
|
|
30
|
+
"discover": "node dist/main.js --discover",
|
|
31
|
+
"dry-run": "node dist/main.js --dry-run",
|
|
32
|
+
"test": "npm run build:host && node --test tests/*.test.mjs",
|
|
33
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
51
34
|
},
|
|
52
35
|
"dependencies": {
|
|
53
|
-
"@modelcontextprotocol/server": "2.0.0",
|
|
54
36
|
"commander": "^13.1.0",
|
|
55
37
|
"frida": "^17.2.0",
|
|
38
|
+
"koffi": "^2.16.3",
|
|
56
39
|
"pino": "^9.6.0",
|
|
57
40
|
"zod": "^3.24.2"
|
|
58
41
|
},
|