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/frida-runtime.js
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
export const BROKER_MANAGED_COMMAND = Symbol("wowdump.broker-managed-command");
|
|
5
|
-
export function brokerManagedCommand(request, purpose) {
|
|
6
|
-
Object.defineProperty(request, BROKER_MANAGED_COMMAND, {
|
|
7
|
-
value: { purpose, irreversibleReason: "Broker-managed observation instrumentation is cleaned through the owned resource graph." },
|
|
8
|
-
enumerable: false,
|
|
9
|
-
configurable: false,
|
|
10
|
-
writable: false
|
|
11
|
-
});
|
|
12
|
-
return request;
|
|
13
|
-
}
|
|
14
4
|
const DEFAULT_MAX_SOURCE_BYTES = 4 * 1024 * 1024;
|
|
15
5
|
const DEFAULT_MAX_MEMORY_BYTES = 16 * 1024 * 1024;
|
|
16
6
|
const DEFAULT_SCRIPT_TIMEOUT_MS = 150;
|
|
@@ -19,54 +9,54 @@ const DEFAULT_SCRIPT_TIMEOUT_MS = 150;
|
|
|
19
9
|
* `script_load` still accept native GumJS, so a new WoW build can use any
|
|
20
10
|
* Frida API without requiring a host release for every new command.
|
|
21
11
|
*/
|
|
22
|
-
const BRIDGE_SOURCE = String.raw `
|
|
23
|
-
function __wowBytes(value) {
|
|
24
|
-
return value == null ? [] : Array.from(new Uint8Array(value));
|
|
25
|
-
}
|
|
26
|
-
function __wowModule(value) {
|
|
27
|
-
return { name: value.name, base: value.base.toString(), size: value.size, path: value.path };
|
|
28
|
-
}
|
|
29
|
-
function __wowExport(value) {
|
|
30
|
-
return { type: value.type, name: value.name, address: value.address.toString(), moduleName: value.moduleName };
|
|
31
|
-
}
|
|
32
|
-
function __wowRange(value) {
|
|
33
|
-
return { base: value.base.toString(), size: value.size, protection: value.protection, file: value.file ?? null };
|
|
34
|
-
}
|
|
35
|
-
rpc.exports = {
|
|
36
|
-
enumerateModules() {
|
|
37
|
-
return Process.enumerateModules().map(__wowModule);
|
|
38
|
-
},
|
|
39
|
-
enumerateExports(moduleName) {
|
|
40
|
-
const module = Process.getModuleByName(moduleName);
|
|
41
|
-
return module.enumerateExports().map(__wowExport);
|
|
42
|
-
},
|
|
43
|
-
enumerateRanges(protection, coalesce) {
|
|
44
|
-
try {
|
|
45
|
-
return Process.enumerateRanges({ protection: protection || "r--", coalesce: Boolean(coalesce) }).map(__wowRange);
|
|
46
|
-
} catch (_) {
|
|
47
|
-
return Process.enumerateRanges(protection || "r--").map(__wowRange);
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
readMemory(address, size) {
|
|
51
|
-
return __wowBytes(ptr(address).readByteArray(size));
|
|
52
|
-
},
|
|
53
|
-
writeMemory(address, bytes) {
|
|
54
|
-
const target = ptr(address);
|
|
55
|
-
const input = Uint8Array.from(bytes || []);
|
|
56
|
-
const before = __wowBytes(target.readByteArray(input.byteLength));
|
|
57
|
-
target.writeByteArray(input.buffer);
|
|
58
|
-
return { before, after: Array.from(input), address: target.toString(), size: input.byteLength };
|
|
59
|
-
},
|
|
60
|
-
protectMemory(address, size, protection) {
|
|
61
|
-
const changed = Memory.protect(ptr(address), size, protection);
|
|
62
|
-
return { address: ptr(address).toString(), size, protection, changed };
|
|
63
|
-
},
|
|
64
|
-
scanMemory(address, size, pattern) {
|
|
65
|
-
return Memory.scanSync(ptr(address), size, pattern).map(match => ({
|
|
66
|
-
address: match.address.toString(), size: match.size
|
|
67
|
-
}));
|
|
68
|
-
}
|
|
69
|
-
};
|
|
12
|
+
const BRIDGE_SOURCE = String.raw `
|
|
13
|
+
function __wowBytes(value) {
|
|
14
|
+
return value == null ? [] : Array.from(new Uint8Array(value));
|
|
15
|
+
}
|
|
16
|
+
function __wowModule(value) {
|
|
17
|
+
return { name: value.name, base: value.base.toString(), size: value.size, path: value.path };
|
|
18
|
+
}
|
|
19
|
+
function __wowExport(value) {
|
|
20
|
+
return { type: value.type, name: value.name, address: value.address.toString(), moduleName: value.moduleName };
|
|
21
|
+
}
|
|
22
|
+
function __wowRange(value) {
|
|
23
|
+
return { base: value.base.toString(), size: value.size, protection: value.protection, file: value.file ?? null };
|
|
24
|
+
}
|
|
25
|
+
rpc.exports = {
|
|
26
|
+
enumerateModules() {
|
|
27
|
+
return Process.enumerateModules().map(__wowModule);
|
|
28
|
+
},
|
|
29
|
+
enumerateExports(moduleName) {
|
|
30
|
+
const module = Process.getModuleByName(moduleName);
|
|
31
|
+
return module.enumerateExports().map(__wowExport);
|
|
32
|
+
},
|
|
33
|
+
enumerateRanges(protection, coalesce) {
|
|
34
|
+
try {
|
|
35
|
+
return Process.enumerateRanges({ protection: protection || "r--", coalesce: Boolean(coalesce) }).map(__wowRange);
|
|
36
|
+
} catch (_) {
|
|
37
|
+
return Process.enumerateRanges(protection || "r--").map(__wowRange);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
readMemory(address, size) {
|
|
41
|
+
return __wowBytes(ptr(address).readByteArray(size));
|
|
42
|
+
},
|
|
43
|
+
writeMemory(address, bytes) {
|
|
44
|
+
const target = ptr(address);
|
|
45
|
+
const input = Uint8Array.from(bytes || []);
|
|
46
|
+
const before = __wowBytes(target.readByteArray(input.byteLength));
|
|
47
|
+
target.writeByteArray(input.buffer);
|
|
48
|
+
return { before, after: Array.from(input), address: target.toString(), size: input.byteLength };
|
|
49
|
+
},
|
|
50
|
+
protectMemory(address, size, protection) {
|
|
51
|
+
const changed = Memory.protect(ptr(address), size, protection);
|
|
52
|
+
return { address: ptr(address).toString(), size, protection, changed };
|
|
53
|
+
},
|
|
54
|
+
scanMemory(address, size, pattern) {
|
|
55
|
+
return Memory.scanSync(ptr(address), size, pattern).map(match => ({
|
|
56
|
+
address: match.address.toString(), size: match.size
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
70
60
|
`;
|
|
71
61
|
function asRecord(value) {
|
|
72
62
|
return value && typeof value === "object" ? value : {};
|
|
@@ -196,9 +186,6 @@ function buildPrelude(context) {
|
|
|
196
186
|
buildKey: context?.buildKey,
|
|
197
187
|
flavor: context?.flavor,
|
|
198
188
|
executable: context?.executable,
|
|
199
|
-
processStartTime: context?.processStartTime,
|
|
200
|
-
moduleBase: context?.moduleBase,
|
|
201
|
-
moduleIdentity: context?.moduleIdentity,
|
|
202
189
|
adapter: context?.adapter
|
|
203
190
|
});
|
|
204
191
|
const verifiedPrelude = context?.adapter?.adapter?.fridaPrelude ?? "";
|
|
@@ -213,10 +200,7 @@ function requestContext(request, context, existing) {
|
|
|
213
200
|
...(request.buildKey === undefined ? {} : { buildKey: request.buildKey }),
|
|
214
201
|
...(request.flavor === undefined ? {} : { flavor: request.flavor }),
|
|
215
202
|
...(request.adapter === undefined ? {} : { adapter: request.adapter }),
|
|
216
|
-
...(request.executable === undefined ? {} : { executable: request.executable })
|
|
217
|
-
...(request.processStartTime === undefined ? {} : { processStartTime: request.processStartTime }),
|
|
218
|
-
...(request.moduleBase === undefined ? {} : { moduleBase: request.moduleBase }),
|
|
219
|
-
...(request.moduleIdentity === undefined ? {} : { moduleIdentity: request.moduleIdentity })
|
|
203
|
+
...(request.executable === undefined ? {} : { executable: request.executable })
|
|
220
204
|
};
|
|
221
205
|
}
|
|
222
206
|
function sessionContext(record) {
|
|
@@ -226,10 +210,7 @@ function sessionContext(record) {
|
|
|
226
210
|
...(context.buildKey === undefined ? {} : { buildKey: context.buildKey }),
|
|
227
211
|
...(context.flavor === undefined ? {} : { flavor: context.flavor }),
|
|
228
212
|
...(context.adapter === undefined ? {} : { adapter: context.adapter }),
|
|
229
|
-
...(context.executable === undefined ? {} : { executable: context.executable })
|
|
230
|
-
...(context.processStartTime === undefined ? {} : { processStartTime: context.processStartTime }),
|
|
231
|
-
...(context.moduleBase === undefined ? {} : { moduleBase: context.moduleBase }),
|
|
232
|
-
...(context.moduleIdentity === undefined ? {} : { moduleIdentity: context.moduleIdentity })
|
|
213
|
+
...(context.executable === undefined ? {} : { executable: context.executable })
|
|
233
214
|
};
|
|
234
215
|
}
|
|
235
216
|
function serializeDevice(device) {
|
|
@@ -514,10 +495,6 @@ export class FridaCommandRuntime {
|
|
|
514
495
|
const record = this.sessions.get(id);
|
|
515
496
|
if (!record)
|
|
516
497
|
throw new Error(`session ${id} was not found`);
|
|
517
|
-
// Detach is an at-most-once terminal transition. Frida may report an
|
|
518
|
-
// error after teardown has already started, so retaining the record would
|
|
519
|
-
// let Broker cleanup call detach again on a half-destroyed agent.
|
|
520
|
-
this.sessions.delete(id);
|
|
521
498
|
for (const scriptId of [...record.scripts.keys()])
|
|
522
499
|
await this.unloadScript(scriptId).catch(() => undefined);
|
|
523
500
|
try {
|
|
@@ -525,6 +502,7 @@ export class FridaCommandRuntime {
|
|
|
525
502
|
}
|
|
526
503
|
catch { /* bridge cleanup is best effort */ }
|
|
527
504
|
await record.session.detach();
|
|
505
|
+
this.sessions.delete(id);
|
|
528
506
|
}
|
|
529
507
|
async resume(request, context) {
|
|
530
508
|
const pid = this.requirePid(request, context);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { FridaCommandRuntime } from "./frida-runtime.js";
|
|
4
|
+
function record(value) {
|
|
5
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
6
|
+
}
|
|
7
|
+
function requiredPositive(value, name) {
|
|
8
|
+
const number = typeof value === "number" ? value : Number(value);
|
|
9
|
+
if (!Number.isSafeInteger(number) || number < 1)
|
|
10
|
+
throw new Error(`${name} must be a positive integer`);
|
|
11
|
+
return number;
|
|
12
|
+
}
|
|
13
|
+
function hex(value, name) {
|
|
14
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0)
|
|
15
|
+
return `0x${value.toString(16)}`;
|
|
16
|
+
if (typeof value !== "string" || !/^(?:0x)?[0-9a-f]+$/i.test(value.trim()))
|
|
17
|
+
throw new Error(`${name} must be hexadecimal`);
|
|
18
|
+
return `0x${BigInt(value).toString(16)}`;
|
|
19
|
+
}
|
|
20
|
+
function add(base, offset) {
|
|
21
|
+
return `0x${(BigInt(base) + BigInt(offset)).toString(16)}`;
|
|
22
|
+
}
|
|
23
|
+
async function run(input) {
|
|
24
|
+
if (input.command !== "runtime-export")
|
|
25
|
+
throw new Error("unsupported worker command");
|
|
26
|
+
const pid = requiredPositive(input.pid, "pid");
|
|
27
|
+
const buildKey = String(input.build ?? input.buildKey ?? "").trim();
|
|
28
|
+
if (!buildKey)
|
|
29
|
+
throw new Error("build is required");
|
|
30
|
+
const profileFile = typeof input.staticProfile === "string" ? input.staticProfile : undefined;
|
|
31
|
+
const profile = profileFile ? record(JSON.parse(await readFile(profileFile, "utf8"))) : {};
|
|
32
|
+
const moduleProfile = record(profile.module);
|
|
33
|
+
const moduleName = typeof moduleProfile.name === "string" ? moduleProfile.name : "Wow.exe";
|
|
34
|
+
const runtime = new FridaCommandRuntime({ artifactDir: process.env.WOW_ANALYZE_DIR });
|
|
35
|
+
try {
|
|
36
|
+
const attached = await runtime.execute({ operation: "attach", pid, buildKey, allowUnmatched: true });
|
|
37
|
+
const sessionId = String(attached.sessionId);
|
|
38
|
+
const modules = await runtime.execute({ operation: "modules", sessionId, pid, buildKey });
|
|
39
|
+
const list = Array.isArray(modules.modules) ? modules.modules : [];
|
|
40
|
+
const module = list.find(item => String(item.name ?? "").toLowerCase() === moduleName.toLowerCase())
|
|
41
|
+
?? list.find(item => String(item.name ?? "").toLowerCase() === "wow.exe");
|
|
42
|
+
if (!module || typeof module.base !== "string")
|
|
43
|
+
throw new Error(`${moduleName} module was not found`);
|
|
44
|
+
const moduleBase = module.base;
|
|
45
|
+
const moduleSize = Number(module.size ?? 0);
|
|
46
|
+
const candidates = [
|
|
47
|
+
...(Array.isArray(profile.signatures) ? profile.signatures : []),
|
|
48
|
+
...(Array.isArray(profile.providers) ? profile.providers : []),
|
|
49
|
+
...(Array.isArray(profile.functions) ? profile.functions : [])
|
|
50
|
+
].map(record);
|
|
51
|
+
const limit = Math.min(requiredPositive(input.maxEvents ?? 100, "max-events"), 10000);
|
|
52
|
+
const records = [];
|
|
53
|
+
for (const [index, candidate] of candidates.slice(0, limit).entries()) {
|
|
54
|
+
const rvaValue = candidate.rva;
|
|
55
|
+
if (rvaValue === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
const rva = hex(rvaValue, `profile record ${index}.rva`);
|
|
58
|
+
const address = add(moduleBase, rva);
|
|
59
|
+
const size = Math.min(Math.max(Number(candidate.size ?? 16), 1), 256);
|
|
60
|
+
const read = await runtime.execute({ operation: "read_memory", sessionId, address, size, pid, buildKey });
|
|
61
|
+
records.push({
|
|
62
|
+
name: typeof candidate.name === "string" ? candidate.name : `record_${index}`,
|
|
63
|
+
rva,
|
|
64
|
+
runtimeAddress: address,
|
|
65
|
+
bytesHex: read.bytesHex ?? null,
|
|
66
|
+
source: "frida",
|
|
67
|
+
confidence: "unverified"
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
command: "analyze.runtime-export",
|
|
73
|
+
pid,
|
|
74
|
+
buildKey,
|
|
75
|
+
module: { name: module.name, moduleBase, moduleSize },
|
|
76
|
+
moduleBase,
|
|
77
|
+
moduleSize,
|
|
78
|
+
records,
|
|
79
|
+
evidence: [{ kind: "frida-runtime-read", source: "frida", moduleBase, count: records.length }]
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await runtime.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
87
|
+
let failed = false;
|
|
88
|
+
for await (const line of input) {
|
|
89
|
+
if (!line.trim())
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
process.stdout.write(`${JSON.stringify(await run(record(JSON.parse(line))))}\n`);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
failed = true;
|
|
96
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (failed)
|
|
100
|
+
process.exitCode = 1;
|