wowdump 0.2.1 → 0.3.2

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.
Files changed (63) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +17 -111
  3. package/dist/adapters/reader.js +33 -0
  4. package/dist/analysis/disassemble.js +77 -0
  5. package/dist/{frida-runtime.js → analysis/frida-runtime.js} +3 -25
  6. package/dist/analysis/runtime-script.js +36 -0
  7. package/dist/cli.js +563 -0
  8. package/dist/core/profile-engine.js +238 -0
  9. package/dist/frida-worker.js +99 -0
  10. package/dist/reader/broker.js +475 -0
  11. package/dist/reader/client.js +1 -0
  12. package/dist/reader/launcher.js +219 -0
  13. package/dist/reader/main.js +100 -0
  14. package/dist/reader/protocol.js +1 -0
  15. package/dist/reader/windows.js +242 -0
  16. package/dist/reader-main.js +2 -0
  17. package/dist/toolchain.js +123 -0
  18. package/package.json +19 -37
  19. package/skills/wowdump/SKILL.md +22 -0
  20. package/skills/wowdump/references/commands.md +63 -0
  21. package/skills/wowdump/references/disassemble.md +18 -0
  22. package/skills/wowdump/references/dynamic.md +54 -0
  23. package/skills/wowdump/references/evidence-workflow.md +41 -0
  24. package/skills/wowdump/references/profiles.md +34 -0
  25. package/skills/wowdump/references/request-schema.md +28 -0
  26. package/skills/wowdump/references/workflow.md +44 -0
  27. package/skills/wowdump/scripts/dynamic-session.js +133 -0
  28. package/dist/agent.js +0 -1335
  29. package/dist/analysis-path.js +0 -38
  30. package/dist/analysis-process-log.js +0 -146
  31. package/dist/broker-client.js +0 -411
  32. package/dist/broker-codec.js +0 -148
  33. package/dist/broker-core.js +0 -1045
  34. package/dist/broker-gateway.js +0 -447
  35. package/dist/broker-ledger.js +0 -196
  36. package/dist/broker-main.js +0 -291
  37. package/dist/broker-protocol.js +0 -119
  38. package/dist/broker-runtime.js +0 -1283
  39. package/dist/broker-server.js +0 -466
  40. package/dist/build-bundle-loader.js +0 -183
  41. package/dist/build-bundle.js +0 -11
  42. package/dist/discovery.js +0 -59
  43. package/dist/dry-run.js +0 -38
  44. package/dist/error-log.js +0 -71
  45. package/dist/focus-errors.js +0 -63
  46. package/dist/focus-service.js +0 -1855
  47. package/dist/focused-session.js +0 -1357
  48. package/dist/mcp-main.js +0 -51
  49. package/dist/mcp.js +0 -924
  50. package/dist/observability.js +0 -41
  51. package/dist/process-log-lock.js +0 -195
  52. package/dist/processes.js +0 -47
  53. package/dist/runtime-config.js +0 -399
  54. package/dist/session.js +0 -145
  55. package/dist/storage.js +0 -12
  56. package/dist/wow-analysis.js +0 -1430
  57. package/resources/builds/retail/12.0.7.68974/build-profile.json +0 -290
  58. package/resources/builds/retail/12.0.7.68974/data-sources.json +0 -1633
  59. package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +0 -5130
  60. package/resources/builds/retail/12.0.7.68974/manifest.json +0 -63
  61. package/resources/builds/retail/12.0.7.68974/signatures.json +0 -260
  62. /package/dist/{adapters.js → core/build-adapters.js} +0 -0
  63. /package/dist/{types.js → core/types.js} +0 -0
@@ -0,0 +1,238 @@
1
+ export class ProfileEngineError extends Error {
2
+ code;
3
+ details;
4
+ constructor(code, message, details) {
5
+ super(message);
6
+ this.name = "ProfileEngineError";
7
+ this.code = code;
8
+ this.details = details;
9
+ }
10
+ }
11
+ const SCALAR_SIZES = {
12
+ u8: 1, i8: 1, u16: 2, i16: 2, u32: 4, i32: 4,
13
+ u64: 8, i64: 8, f32: 4, f64: 8, pointer: 8,
14
+ bytes: undefined, utf8: undefined, utf16: undefined
15
+ };
16
+ function record(value, name) {
17
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
18
+ throw new ProfileEngineError("PROFILE_INVALID", `${name} must be an object`);
19
+ }
20
+ return value;
21
+ }
22
+ function hex(value, name) {
23
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0)
24
+ return BigInt(value);
25
+ if (typeof value !== "string" || !/^0x[0-9a-f]+$/i.test(value.trim())) {
26
+ throw new ProfileEngineError("PROFILE_INVALID", `${name} must be a 0x-prefixed hexadecimal value`);
27
+ }
28
+ return BigInt(value);
29
+ }
30
+ function hexText(value) {
31
+ return `0x${value.toString(16)}`;
32
+ }
33
+ function integer(value, name, maximum = 16 * 1024 * 1024) {
34
+ const result = typeof value === "number" ? value : Number(value);
35
+ if (!Number.isSafeInteger(result) || result < 0 || result > maximum) {
36
+ throw new ProfileEngineError("PROFILE_INVALID", `${name} must be an integer between 0 and ${maximum}`);
37
+ }
38
+ return result;
39
+ }
40
+ function scalarType(value, name) {
41
+ if (typeof value !== "string" || !(value in SCALAR_SIZES)) {
42
+ throw new ProfileEngineError("PROFILE_INVALID", `${name} has an unsupported scalar type`);
43
+ }
44
+ return value;
45
+ }
46
+ function layoutOf(field) {
47
+ const layout = field.layout;
48
+ if (layout === undefined)
49
+ return { kind: "scalar", type: field.type ?? "pointer" };
50
+ return record(layout, "field.layout");
51
+ }
52
+ function layoutSize(layout) {
53
+ const kind = String(layout.kind ?? "scalar");
54
+ if (kind === "scalar") {
55
+ const type = scalarType(layout.type, "layout.type");
56
+ const size = SCALAR_SIZES[type];
57
+ if (size === undefined)
58
+ return integer(layout.size, "layout.size", 16 * 1024 * 1024);
59
+ return size;
60
+ }
61
+ if (kind === "struct") {
62
+ let size = layout.size === undefined ? 0 : integer(layout.size, "struct.size");
63
+ const fields = record(layout.fields ?? {}, "struct.fields");
64
+ for (const value of Object.values(fields)) {
65
+ const field = record(value, "struct field");
66
+ const offset = integer(hex(field.offset, "field.offset"), "field.offset");
67
+ size = Math.max(size, offset + layoutSize(field.layout ? record(field.layout, "field.layout") : field));
68
+ }
69
+ return size;
70
+ }
71
+ if (kind === "array") {
72
+ const item = record(layout.item, "array.item");
73
+ const stride = integer(layout.stride ?? layoutSize(item), "array.stride");
74
+ const count = layout.count;
75
+ if (typeof count === "number" || typeof count === "string")
76
+ return stride * integer(count, "array.count", 100000);
77
+ return stride * integer(layout.maxItems, "array.maxItems", 100000);
78
+ }
79
+ if (kind === "linked_list") {
80
+ const item = record(layout.item, "linked_list.item");
81
+ return Math.max(layoutSize(item), integer(hex(layout.nextOffset ?? "0x8", "nextOffset"), "nextOffset") + 8);
82
+ }
83
+ throw new ProfileEngineError("PROFILE_INVALID", `unsupported layout kind ${kind}`);
84
+ }
85
+ function decodeScalar(type, bytes) {
86
+ switch (type) {
87
+ case "u8": return bytes.readUInt8(0);
88
+ case "i8": return bytes.readInt8(0);
89
+ case "u16": return bytes.readUInt16LE(0);
90
+ case "i16": return bytes.readInt16LE(0);
91
+ case "u32": return bytes.readUInt32LE(0);
92
+ case "i32": return bytes.readInt32LE(0);
93
+ case "u64": return hexText(bytes.readBigUInt64LE(0));
94
+ case "i64": return bytes.readBigInt64LE(0).toString();
95
+ case "f32": return bytes.readFloatLE(0);
96
+ case "f64": return bytes.readDoubleLE(0);
97
+ case "pointer": return hexText(bytes.readBigUInt64LE(0));
98
+ case "bytes": return bytes.toString("hex");
99
+ case "utf8": return bytes.toString("utf8").replace(/\0.*$/s, "");
100
+ case "utf16": return bytes.toString("utf16le").replace(/\0.*$/s, "");
101
+ }
102
+ }
103
+ function decodeInline(layout, bytes, base = 0) {
104
+ const kind = String(layout.kind ?? "scalar");
105
+ if (kind === "scalar") {
106
+ const type = scalarType(layout.type, "layout.type");
107
+ const size = SCALAR_SIZES[type] ?? integer(layout.size, "layout.size");
108
+ return decodeScalar(type, bytes.subarray(base, base + size));
109
+ }
110
+ if (kind === "struct") {
111
+ const result = {};
112
+ const fields = record(layout.fields ?? {}, "struct.fields");
113
+ for (const [name, value] of Object.entries(fields)) {
114
+ const field = record(value, `struct field ${name}`);
115
+ const offset = integer(hex(field.offset, `${name}.offset`), `${name}.offset`);
116
+ result[name] = decodeInline(field.layout ? record(field.layout, `${name}.layout`) : field, bytes, base + offset);
117
+ }
118
+ return result;
119
+ }
120
+ if (kind === "array") {
121
+ const item = record(layout.item, "array.item");
122
+ const stride = integer(layout.stride ?? layoutSize(item), "array.stride");
123
+ const count = integer(layout.count ?? layout.maxItems, "array.count", 100000);
124
+ return Array.from({ length: count }, (_, index) => decodeInline(item, bytes, base + index * stride));
125
+ }
126
+ throw new ProfileEngineError("PROFILE_INVALID", `inline layout ${kind} is not supported`);
127
+ }
128
+ function profileId(profile) {
129
+ return typeof profile.id === "string" && profile.id ? profile.id : String(profile.buildKey ?? "profile");
130
+ }
131
+ function rootSpec(value, name) {
132
+ const root = record(value, `${name}.root`);
133
+ if (root.rva === undefined && root.address === undefined)
134
+ throw new ProfileEngineError("PROFILE_INVALID", `${name}.root requires rva or address`);
135
+ return root;
136
+ }
137
+ export class ProfileEngine {
138
+ async read(request, adapter) {
139
+ const profile = record(request.profile, "profile");
140
+ const buildKey = String(profile.buildKey ?? "");
141
+ if (!buildKey)
142
+ throw new ProfileEngineError("PROFILE_INVALID", "profile.buildKey is required");
143
+ if (request.buildKey && request.buildKey !== buildKey) {
144
+ throw new ProfileEngineError("BUILD_MISMATCH", `profile build ${buildKey} does not match ${request.buildKey}`);
145
+ }
146
+ const status = String(profile.readerStatus ?? profile.status ?? (profile.confidence === "reader_ready" ? "reader_ready" : ""));
147
+ if (status !== "reader_ready")
148
+ throw new ProfileEngineError("PROFILE_NOT_READY", "profile is not reader_ready", { status });
149
+ const modules = await adapter.modules(request.pid);
150
+ const moduleSpec = profile.module && typeof profile.module === "object" ? record(profile.module, "profile.module") : {};
151
+ const moduleName = String(moduleSpec.name ?? profile.moduleName ?? "Wow.exe");
152
+ const module = modules.find(item => item.name.toLowerCase() === moduleName.toLowerCase());
153
+ if (!module)
154
+ throw new ProfileEngineError("MODULE_NOT_FOUND", `${moduleName} was not found`, { modules: modules.map(item => item.name) });
155
+ const definitions = record(profile.fields ?? profile.readers, "profile.fields");
156
+ const names = request.fields?.length ? [...request.fields] : Object.keys(definitions);
157
+ const fields = {};
158
+ const reads = [];
159
+ for (const name of names) {
160
+ const definition = record(definitions[name], `field ${name}`);
161
+ fields[name] = await this.readField(request.pid, hex(module.base, "module.base"), name, definition, adapter, reads);
162
+ }
163
+ return { ok: true, command: "profile.read", pid: request.pid, buildKey, profileId: profileId(profile), module, fields, reads };
164
+ }
165
+ async readField(pid, moduleBase, fieldName, definition, adapter, reads) {
166
+ let address = definition.root === undefined ? moduleBase : await this.resolveRoot(pid, moduleBase, rootSpec(definition.root, fieldName), adapter, fieldName, reads);
167
+ const chainValue = definition.pointerChain ?? definition.chain;
168
+ const chain = Array.isArray(chainValue) ? chainValue : [];
169
+ for (const value of chain) {
170
+ const step = record(value, `${fieldName}.pointerChain`);
171
+ address += hex(step.offset ?? "0x0", `${fieldName}.pointerChain.offset`);
172
+ if (step.dereference === false)
173
+ continue;
174
+ const pointer = await this.readBytes(pid, address, 8, fieldName, adapter, reads);
175
+ address = BigInt(`0x${pointer.toString("hex").match(/../g).reverse().join("")}`);
176
+ const mask = step.mask === undefined ? 0n : hex(step.mask, `${fieldName}.pointerChain.mask`);
177
+ if (mask)
178
+ address &= mask;
179
+ if (address === 0n)
180
+ throw new ProfileEngineError("NULL_POINTER", `${fieldName} resolved to a null pointer`);
181
+ }
182
+ return this.readLayout(pid, address, layoutOf(definition), fieldName, adapter, reads);
183
+ }
184
+ async resolveRoot(pid, moduleBase, root, adapter, field, reads) {
185
+ let address = root.address === undefined ? moduleBase + hex(root.rva, `${field}.root.rva`) : hex(root.address, `${field}.root.address`);
186
+ if (root.dereference === true) {
187
+ const bytes = await this.readBytes(pid, address, 8, field, adapter, reads);
188
+ address = BigInt(`0x${bytes.toString("hex").match(/../g).reverse().join("")}`);
189
+ }
190
+ return address;
191
+ }
192
+ async readLayout(pid, address, layout, field, adapter, reads) {
193
+ const kind = String(layout.kind ?? "scalar");
194
+ if (kind === "linked_list") {
195
+ const item = record(layout.item, `${field}.item`);
196
+ const nextOffset = integer(hex(layout.nextOffset ?? "0x8", `${field}.nextOffset`), `${field}.nextOffset`);
197
+ const itemSize = Math.max(layoutSize(item), nextOffset + 8);
198
+ const maxItems = integer(layout.maxItems ?? 64, `${field}.maxItems`, 100000);
199
+ const values = [];
200
+ const seen = new Set();
201
+ while (address !== 0n && values.length < maxItems) {
202
+ const key = hexText(address);
203
+ if (seen.has(key))
204
+ throw new ProfileEngineError("POINTER_CYCLE", `${field} linked list contains a cycle`, { address: key });
205
+ seen.add(key);
206
+ if (layout.stopOnTagged !== false && (address & 1n) !== 0n)
207
+ break;
208
+ const bytes = await this.readBytes(pid, address, itemSize, field, adapter, reads);
209
+ values.push(decodeInline(item, bytes));
210
+ const nextBytes = bytes.subarray(nextOffset, nextOffset + 8);
211
+ address = nextBytes.length < 8 ? 0n : BigInt(`0x${nextBytes.toString("hex").match(/../g).reverse().join("")}`);
212
+ }
213
+ return values;
214
+ }
215
+ const size = layout.kind === "scalar" && layout.type && SCALAR_SIZES[scalarType(layout.type, `${field}.type`)] === undefined
216
+ ? integer(layout.size, `${field}.size`)
217
+ : layoutSize(layout);
218
+ const bytes = await this.readBytes(pid, address, size, field, adapter, reads);
219
+ if (kind === "array" && layout.count === undefined && layout.maxItems !== undefined) {
220
+ const item = record(layout.item, `${field}.item`);
221
+ const stride = integer(layout.stride ?? layoutSize(item), `${field}.stride`);
222
+ const count = integer(layout.maxItems, `${field}.maxItems`, 100000);
223
+ return Array.from({ length: count }, (_, index) => decodeInline(item, bytes, index * stride));
224
+ }
225
+ return decodeInline(layout, bytes);
226
+ }
227
+ async readBytes(pid, address, size, field, adapter, reads) {
228
+ const result = await adapter.read(pid, hexText(address), size);
229
+ if (!result.ok || typeof result.dataHex !== "string")
230
+ throw new ProfileEngineError("READ_FAILED", `${field} read failed`, { address: hexText(address), size, error: result.error ?? null });
231
+ const bytesRead = Number(result.bytesRead ?? result.dataHex.length / 2);
232
+ const bytes = Buffer.from(result.dataHex, "hex");
233
+ reads.push({ field, address: hexText(address), size, bytesRead, dataHex: result.dataHex, complete: result.complete === true && bytesRead === size });
234
+ if (bytesRead !== size || bytes.length < size)
235
+ throw new ProfileEngineError("PARTIAL_READ", `${field} read was incomplete`, { address: hexText(address), size, bytesRead });
236
+ return bytes.subarray(0, size);
237
+ }
238
+ }
@@ -0,0 +1,99 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createInterface } from "node:readline";
3
+ import { FridaCommandRuntime } from "./analysis/frida-runtime.js";
4
+ function record(value) {
5
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
6
+ }
7
+ function positive(value, name) {
8
+ const number = Number(value);
9
+ if (!Number.isSafeInteger(number) || number < 1)
10
+ throw new Error(`${name} must be a positive integer`);
11
+ return number;
12
+ }
13
+ /**
14
+ * Elevated Frida execution boundary. Analysis belongs to the caller-supplied
15
+ * GumJS source; this process only attaches, loads, calls, and cleans it up.
16
+ */
17
+ async function run(input) {
18
+ if (input.command !== "dynamic-script")
19
+ throw new Error("unsupported worker command");
20
+ const pid = positive(input.pid, "pid");
21
+ const buildKey = String(input.build ?? input.buildKey ?? "").trim();
22
+ if (!buildKey)
23
+ throw new Error("build is required");
24
+ const source = typeof input.source === "string"
25
+ ? input.source
26
+ : typeof input.script === "string" ? await readFile(input.script, "utf8") : "";
27
+ if (!source.trim())
28
+ throw new Error("script or source is required");
29
+ const runtime = new FridaCommandRuntime({ artifactDir: process.env.WOW_ANALYZE_DIR });
30
+ let sessionId;
31
+ let loaded = false;
32
+ try {
33
+ const attached = await runtime.execute({ operation: "attach", pid, buildKey, allowUnmatched: true });
34
+ sessionId = String(attached.sessionId);
35
+ const modules = await runtime.execute({ operation: "modules", sessionId, pid, buildKey });
36
+ const module = Array.isArray(modules.modules)
37
+ ? modules.modules.find(item => String(item.name ?? "").toLowerCase() === "wow.exe")
38
+ : undefined;
39
+ await runtime.execute({
40
+ operation: "script_load",
41
+ sessionId,
42
+ pid,
43
+ buildKey,
44
+ source: `globalThis.__WOWDUMP_INPUT__ = Object.freeze(${JSON.stringify(input.args ?? {})});\n${source}`,
45
+ scriptId: "dynamic"
46
+ });
47
+ loaded = true;
48
+ let value;
49
+ let messages = [];
50
+ if (typeof input.exportName === "string" && input.exportName.trim()) {
51
+ const called = await runtime.execute({
52
+ operation: "script_call",
53
+ sessionId,
54
+ scriptId: "dynamic",
55
+ exportName: input.exportName,
56
+ args: Array.isArray(input.callArgs) ? input.callArgs : []
57
+ });
58
+ value = called.value;
59
+ messages = Array.isArray(called.messages) ? called.messages : [];
60
+ }
61
+ const durationMs = Math.min(120_000, positive(input.durationMs ?? 100, "duration-ms"));
62
+ if (value === undefined)
63
+ await new Promise(resolve => setTimeout(resolve, durationMs));
64
+ return {
65
+ ok: true,
66
+ command: "analyze.dynamic",
67
+ pid,
68
+ buildKey,
69
+ module: module && typeof module.base === "string"
70
+ ? { name: module.name, moduleBase: module.base, moduleSize: module.size ?? null }
71
+ : null,
72
+ exportName: typeof input.exportName === "string" ? input.exportName : null,
73
+ value,
74
+ messages,
75
+ script: typeof input.script === "string" ? input.script : "<inline>",
76
+ evidence: [{ kind: "frida-gumjs", source: "frida", moduleBase: module?.base ?? null }]
77
+ };
78
+ }
79
+ finally {
80
+ if (sessionId && loaded)
81
+ await runtime.execute({ operation: "script_unload", sessionId, scriptId: "dynamic" }).catch(() => undefined);
82
+ await runtime.close();
83
+ }
84
+ }
85
+ const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
86
+ let failed = false;
87
+ for await (const line of input) {
88
+ if (!line.trim())
89
+ continue;
90
+ try {
91
+ process.stdout.write(`${JSON.stringify(await run(record(JSON.parse(line))))}\n`);
92
+ }
93
+ catch (error) {
94
+ failed = true;
95
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
96
+ }
97
+ }
98
+ if (failed)
99
+ process.exitCode = 1;