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.
- package/LICENSE +1 -1
- package/README.md +17 -111
- package/dist/adapters/reader.js +33 -0
- package/dist/analysis/disassemble.js +77 -0
- package/dist/{frida-runtime.js → analysis/frida-runtime.js} +3 -25
- package/dist/analysis/runtime-script.js +36 -0
- package/dist/cli.js +563 -0
- package/dist/core/profile-engine.js +238 -0
- package/dist/frida-worker.js +99 -0
- package/dist/reader/broker.js +475 -0
- package/dist/reader/client.js +1 -0
- package/dist/reader/launcher.js +219 -0
- package/dist/reader/main.js +100 -0
- package/dist/reader/protocol.js +1 -0
- package/dist/reader/windows.js +242 -0
- package/dist/reader-main.js +2 -0
- package/dist/toolchain.js +123 -0
- package/package.json +19 -37
- package/skills/wowdump/SKILL.md +22 -0
- package/skills/wowdump/references/commands.md +63 -0
- package/skills/wowdump/references/disassemble.md +18 -0
- package/skills/wowdump/references/dynamic.md +54 -0
- package/skills/wowdump/references/evidence-workflow.md +41 -0
- package/skills/wowdump/references/profiles.md +34 -0
- package/skills/wowdump/references/request-schema.md +28 -0
- package/skills/wowdump/references/workflow.md +44 -0
- package/skills/wowdump/scripts/dynamic-session.js +133 -0
- package/dist/agent.js +0 -1335
- 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/discovery.js +0 -59
- package/dist/dry-run.js +0 -38
- package/dist/error-log.js +0 -71
- package/dist/focus-errors.js +0 -63
- package/dist/focus-service.js +0 -1855
- package/dist/focused-session.js +0 -1357
- package/dist/mcp-main.js +0 -51
- package/dist/mcp.js +0 -924
- package/dist/observability.js +0 -41
- package/dist/process-log-lock.js +0 -195
- package/dist/processes.js +0 -47
- package/dist/runtime-config.js +0 -399
- package/dist/session.js +0 -145
- package/dist/storage.js +0 -12
- package/dist/wow-analysis.js +0 -1430
- 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/{adapters.js → core/build-adapters.js} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
package/dist/wow-analysis.js
DELETED
|
@@ -1,1430 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { basename, join, resolve } from "node:path";
|
|
4
|
-
import { brokerManagedCommand } from "./frida-runtime.js";
|
|
5
|
-
import { BuildBundleStore } from "./build-bundle-loader.js";
|
|
6
|
-
export const WOW_ANALYSIS_BUILD_KEY = "retail@12.0.7.68974";
|
|
7
|
-
export const WOW_ANALYSIS_MODULE = "Wow.exe";
|
|
8
|
-
export const WOW_IDA_IMAGE_BASE = "0x140000000";
|
|
9
|
-
const REQUIRED_ARTIFACTS = [
|
|
10
|
-
{ key: "luaAbi", role: "Lua ABI", file: suffix => `lua-abi-${suffix}.json`, json: true },
|
|
11
|
-
{ key: "luaTrace", role: "Lua trace events", file: suffix => `lua-trace-${suffix}.jsonl`, json: false },
|
|
12
|
-
{ key: "wrapperCoverage", role: "Lua wrapper coverage", file: suffix => `lua-wrapper-coverage-${suffix}.json`, json: true },
|
|
13
|
-
{ key: "callgraph", role: "Lua wrapper call graph", file: suffix => `lua-wrapper-callgraph-${suffix}.json`, json: true },
|
|
14
|
-
{ key: "clusters", role: "C++ implementation clusters", file: suffix => `cpp-implementation-clusters-${suffix}.json`, json: true },
|
|
15
|
-
{ key: "dataSources", role: "C++ data sources", file: suffix => `cpp-data-sources-${suffix}.json`, json: true },
|
|
16
|
-
{ key: "dataRootCoverage", role: "Data-root coverage", file: suffix => `data-root-coverage-${suffix}.json`, json: true },
|
|
17
|
-
{ key: "buildProfile", role: "Build profile", file: suffix => `build-profile-${suffix}.json`, json: true },
|
|
18
|
-
{ key: "readerVerification", role: "Reader verification", file: suffix => `data-reader-verification-${suffix}.json`, json: true },
|
|
19
|
-
{ key: "nonLuaIndex", role: "Non-Lua system index", file: suffix => `non-lua-system-index-${suffix}.json`, json: true },
|
|
20
|
-
{ key: "nonLuaCoverage", role: "Non-Lua system coverage", file: suffix => `non-lua-system-coverage-${suffix}.json`, json: true },
|
|
21
|
-
{ key: "unresolved", role: "Unresolved ledger", file: suffix => `unresolved-ledger-${suffix}.json`, json: true },
|
|
22
|
-
{ key: "process", role: "Process event log", file: suffix => `wow-analysis-process-${suffix}.jsonl`, json: false },
|
|
23
|
-
{ key: "report", role: "Runtime analysis report", file: suffix => `WOW-runtime-analysis-${suffix}.md`, json: false },
|
|
24
|
-
{ key: "manifest", role: "Analysis artifact manifest", file: suffix => `analysis-artifact-manifest-${suffix}.json`, json: true }
|
|
25
|
-
];
|
|
26
|
-
const DEFAULT_MAX_HOOKS = 64;
|
|
27
|
-
// The default is intentionally small; an explicit caller may request the
|
|
28
|
-
// complete indexed set (the agent still validates every target before attach).
|
|
29
|
-
const MAX_LUA_HOOKS = 5130;
|
|
30
|
-
const MAX_CPP_HOOKS = 5130;
|
|
31
|
-
const MAX_EVENTS = 100_000;
|
|
32
|
-
const MAX_READ_ITEMS = 10_000;
|
|
33
|
-
const MAX_STRING_BYTES = 65_536;
|
|
34
|
-
const MAX_SIGNATURE_CHECKS = 512;
|
|
35
|
-
function isRecord(value) {
|
|
36
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
37
|
-
}
|
|
38
|
-
function stringValue(value) {
|
|
39
|
-
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
40
|
-
}
|
|
41
|
-
function integerValue(value) {
|
|
42
|
-
return typeof value === "number" && Number.isSafeInteger(value) ? value : undefined;
|
|
43
|
-
}
|
|
44
|
-
function booleanValue(value) {
|
|
45
|
-
return typeof value === "boolean" ? value : undefined;
|
|
46
|
-
}
|
|
47
|
-
function requiredString(value, field) {
|
|
48
|
-
const result = stringValue(value);
|
|
49
|
-
if (result === undefined)
|
|
50
|
-
throw new Error(`${field} is required`);
|
|
51
|
-
return result;
|
|
52
|
-
}
|
|
53
|
-
function boundedInteger(value, fallback, minimum, maximum, field) {
|
|
54
|
-
if (value === undefined)
|
|
55
|
-
return fallback;
|
|
56
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
57
|
-
throw new Error(`${field} must be an integer in [${minimum}, ${maximum}]`);
|
|
58
|
-
}
|
|
59
|
-
return value;
|
|
60
|
-
}
|
|
61
|
-
function boundedSampling(value) {
|
|
62
|
-
if (value === undefined)
|
|
63
|
-
return 1;
|
|
64
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1) {
|
|
65
|
-
throw new Error("sampling must be greater than 0 and at most 1");
|
|
66
|
-
}
|
|
67
|
-
return value;
|
|
68
|
-
}
|
|
69
|
-
function normalizeHex(value, field) {
|
|
70
|
-
if (typeof value === "number") {
|
|
71
|
-
if (!Number.isSafeInteger(value) || value < 0)
|
|
72
|
-
throw new Error(`${field} must be a non-negative RVA`);
|
|
73
|
-
return `0x${value.toString(16)}`;
|
|
74
|
-
}
|
|
75
|
-
const text = requiredString(value, field).trim();
|
|
76
|
-
if (!/^(?:0x)?[0-9a-f]+$/i.test(text))
|
|
77
|
-
throw new Error(`${field} must be hexadecimal`);
|
|
78
|
-
return `0x${BigInt(text.startsWith("0x") || text.startsWith("0X") ? text : `0x${text}`).toString(16)}`;
|
|
79
|
-
}
|
|
80
|
-
function hexToNumber(value, field) {
|
|
81
|
-
const normalized = normalizeHex(value, field);
|
|
82
|
-
const parsed = Number(BigInt(normalized));
|
|
83
|
-
if (!Number.isSafeInteger(parsed))
|
|
84
|
-
throw new Error(`${field} exceeds JavaScript's safe integer range`);
|
|
85
|
-
return parsed;
|
|
86
|
-
}
|
|
87
|
-
function hexAdd(base, offset) {
|
|
88
|
-
return `0x${(BigInt(base) + BigInt(typeof offset === "number" ? offset : normalizeHex(offset, "offset"))).toString(16)}`;
|
|
89
|
-
}
|
|
90
|
-
function buildSuffix(buildKey) {
|
|
91
|
-
const value = requiredString(buildKey, "buildKey");
|
|
92
|
-
const tail = value.split(".").at(-1)?.split("@").at(-1) ?? "";
|
|
93
|
-
if (!/^[A-Za-z0-9_-]+$/.test(tail))
|
|
94
|
-
throw new Error("buildKey has no safe artifact suffix");
|
|
95
|
-
return tail;
|
|
96
|
-
}
|
|
97
|
-
function flavorForBuild(buildKey) {
|
|
98
|
-
const prefix = buildKey.split("@", 1)[0];
|
|
99
|
-
return prefix && /^[A-Za-z0-9_-]+$/.test(prefix) ? prefix : undefined;
|
|
100
|
-
}
|
|
101
|
-
function arrayValue(value) {
|
|
102
|
-
return Array.isArray(value) ? value : [];
|
|
103
|
-
}
|
|
104
|
-
function recordsFrom(value, preferred = []) {
|
|
105
|
-
if (Array.isArray(value))
|
|
106
|
-
return value.filter(isRecord);
|
|
107
|
-
if (!isRecord(value))
|
|
108
|
-
return [];
|
|
109
|
-
for (const key of [...preferred, "records", "dataSources", "sources", "wrappers", "targets", "clusters", "systems", "items"]) {
|
|
110
|
-
const candidate = value[key];
|
|
111
|
-
if (Array.isArray(candidate))
|
|
112
|
-
return candidate.filter(isRecord);
|
|
113
|
-
}
|
|
114
|
-
return [];
|
|
115
|
-
}
|
|
116
|
-
function resultArray(result, key) {
|
|
117
|
-
const direct = result[key];
|
|
118
|
-
const value = direct ?? result.value;
|
|
119
|
-
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
120
|
-
}
|
|
121
|
-
function normalizeModuleName(value, fallback) {
|
|
122
|
-
return stringValue(value) ?? fallback;
|
|
123
|
-
}
|
|
124
|
-
function globExpression(pattern) {
|
|
125
|
-
if (pattern.length > 256)
|
|
126
|
-
throw new Error("glob exceeds 256 characters");
|
|
127
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
128
|
-
return new RegExp(`^${escaped}$`, "i");
|
|
129
|
-
}
|
|
130
|
-
function sha256(value) {
|
|
131
|
-
return createHash("sha256").update(value).digest("hex");
|
|
132
|
-
}
|
|
133
|
-
function pickPath(record, paths) {
|
|
134
|
-
for (const path of paths) {
|
|
135
|
-
let current = record;
|
|
136
|
-
for (const key of path) {
|
|
137
|
-
if (!isRecord(current)) {
|
|
138
|
-
current = undefined;
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
current = current[key];
|
|
142
|
-
}
|
|
143
|
-
if (current !== undefined && current !== null)
|
|
144
|
-
return current;
|
|
145
|
-
}
|
|
146
|
-
return undefined;
|
|
147
|
-
}
|
|
148
|
-
function statusValue(record, ...keys) {
|
|
149
|
-
for (const key of keys) {
|
|
150
|
-
const value = stringValue(record[key]);
|
|
151
|
-
if (value !== undefined)
|
|
152
|
-
return value;
|
|
153
|
-
}
|
|
154
|
-
return undefined;
|
|
155
|
-
}
|
|
156
|
-
function summarizeRecord(record) {
|
|
157
|
-
const dataSourceId = stringValue(record.dataSourceId ?? record.id);
|
|
158
|
-
return {
|
|
159
|
-
...(dataSourceId ? { dataSourceId } : {}),
|
|
160
|
-
...(stringValue(record.name) ? { name: record.name } : {}),
|
|
161
|
-
...(stringValue(record.system) ? { system: record.system } : {}),
|
|
162
|
-
...(stringValue(record.buildKey) ? { buildKey: record.buildKey } : {}),
|
|
163
|
-
status: statusValue(record, "status", "resolutionStatus") ?? "unresolved",
|
|
164
|
-
readerStatus: statusValue(record, "readerStatus", "reader_status")
|
|
165
|
-
?? (isRecord(record.reader) ? statusValue(record.reader, "status") : undefined)
|
|
166
|
-
?? "unresolved",
|
|
167
|
-
...(record.sourceApis !== undefined ? { sourceApis: record.sourceApis } : {}),
|
|
168
|
-
...(record.sourceAPIs !== undefined ? { sourceAPIs: record.sourceAPIs } : {})
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
function normalizeApiList(input) {
|
|
172
|
-
const values = input.APIs ?? input.apis ?? [];
|
|
173
|
-
if (!Array.isArray(values) || values.length > 512)
|
|
174
|
-
throw new Error("APIs must contain at most 512 entries");
|
|
175
|
-
return values.map((value, index) => requiredString(value, `APIs[${index}]`));
|
|
176
|
-
}
|
|
177
|
-
function normalizeRvaSelection(value) {
|
|
178
|
-
if (value === undefined)
|
|
179
|
-
return [];
|
|
180
|
-
const values = Array.isArray(value) ? value : [value];
|
|
181
|
-
if (values.length > 512)
|
|
182
|
-
throw new Error("rva must contain at most 512 entries");
|
|
183
|
-
return values.map((item, index) => normalizeHex(item, `rva[${index}]`));
|
|
184
|
-
}
|
|
185
|
-
function requireSelection(input) {
|
|
186
|
-
if (normalizeApiList(input).length === 0
|
|
187
|
-
&& input.namespace === undefined
|
|
188
|
-
&& input.glob === undefined
|
|
189
|
-
&& normalizeRvaSelection(input.rva).length === 0
|
|
190
|
-
&& (!("clusterIds" in input) || !Array.isArray(input.clusterIds) || input.clusterIds.length === 0)
|
|
191
|
-
&& input.all !== true) {
|
|
192
|
-
throw new Error("an explicit API, namespace, glob, RVA, cluster, or all=true selection is required");
|
|
193
|
-
}
|
|
194
|
-
}
|
|
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
|
-
};
|
|
293
|
-
`;
|
|
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
|
-
};
|
|
391
|
-
`;
|
|
392
|
-
export class WowAnalysisService {
|
|
393
|
-
executor;
|
|
394
|
-
profileRoot;
|
|
395
|
-
packagedProfiles;
|
|
396
|
-
buildBundles;
|
|
397
|
-
moduleName;
|
|
398
|
-
options;
|
|
399
|
-
sessions = new Map();
|
|
400
|
-
luaTrace;
|
|
401
|
-
cppTrace;
|
|
402
|
-
closing = false;
|
|
403
|
-
constructor(options) {
|
|
404
|
-
if (!options || typeof options.executor?.execute !== "function")
|
|
405
|
-
throw new TypeError("executor is required");
|
|
406
|
-
this.executor = options.executor;
|
|
407
|
-
this.profileRoot = resolve(requiredString(options.profileRoot ?? options.artifactDir, "profileRoot"));
|
|
408
|
-
this.packagedProfiles = options.profileRoot !== undefined;
|
|
409
|
-
this.buildBundles = this.packagedProfiles ? new BuildBundleStore(this.profileRoot) : undefined;
|
|
410
|
-
this.moduleName = options.moduleName ?? WOW_ANALYSIS_MODULE;
|
|
411
|
-
this.options = options;
|
|
412
|
-
}
|
|
413
|
-
async invoke(operation, input = {}) {
|
|
414
|
-
switch (operation) {
|
|
415
|
-
case "wow_lua_trace_start": return this.wowLuaTraceStart(input);
|
|
416
|
-
case "wow_lua_trace_read": return this.wowLuaTraceRead(input);
|
|
417
|
-
case "wow_lua_trace_status": return this.wowLuaTraceStatus();
|
|
418
|
-
case "wow_lua_trace_stop": return this.wowLuaTraceStop(input);
|
|
419
|
-
case "wow_cpp_trace_start": return this.wowCppTraceStart(input);
|
|
420
|
-
case "wow_cpp_trace_read": return this.wowCppTraceRead(input);
|
|
421
|
-
case "wow_cpp_trace_status": return this.wowCppTraceStatus();
|
|
422
|
-
case "wow_cpp_trace_stop": return this.wowCppTraceStop(input);
|
|
423
|
-
case "wow_data_source_list": return this.wowDataSourceList(input);
|
|
424
|
-
case "wow_data_source_describe": return this.wowDataSourceDescribe(input);
|
|
425
|
-
case "wow_data_read": return this.wowDataRead(input);
|
|
426
|
-
case "wow_build_profile_validate": return this.wowBuildProfileValidate(input);
|
|
427
|
-
case "wow_analysis_coverage": return this.wowAnalysisCoverage(input);
|
|
428
|
-
case "wow_analysis_checkpoint": return this.wowAnalysisCheckpoint(input);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
async wowLuaTraceStart(input) {
|
|
432
|
-
if (this.luaTrace)
|
|
433
|
-
throw new Error("Lua trace is already loaded; stop it before starting another trace");
|
|
434
|
-
const request = this.normalizeLuaStart(input);
|
|
435
|
-
const catalog = await this.loadLuaCatalog(input.buildKey);
|
|
436
|
-
const target = await this.acquireTarget(input.buildKey, input.pid);
|
|
437
|
-
let scriptId;
|
|
438
|
-
try {
|
|
439
|
-
const source = await this.luaSource();
|
|
440
|
-
const loaded = await this.executor.execute(brokerManagedCommand({
|
|
441
|
-
operation: "script_load",
|
|
442
|
-
sessionId: target.sessionId,
|
|
443
|
-
source
|
|
444
|
-
}, "wow.lua_trace.script_load"), target.context);
|
|
445
|
-
scriptId = requiredString(loaded.scriptId, "script_load.scriptId");
|
|
446
|
-
const configured = await this.callScript(scriptId, "configureLuaTrace", [catalog]);
|
|
447
|
-
const started = await this.callScript(scriptId, "wowLuaTraceStart", [{ ...request, pid: target.pid }]);
|
|
448
|
-
const value = isRecord(started.value) ? started.value : {};
|
|
449
|
-
this.assertRuntimeBase(value.moduleBase, target.moduleBase, "Lua trace");
|
|
450
|
-
this.luaTrace = { kind: "lua", scriptId, target, startedAt: new Date().toISOString() };
|
|
451
|
-
return {
|
|
452
|
-
...value,
|
|
453
|
-
configured: configured.value,
|
|
454
|
-
sessionId: target.sessionId,
|
|
455
|
-
scriptId,
|
|
456
|
-
discoveredPid: target.pid,
|
|
457
|
-
discoveredModuleBase: target.moduleBase,
|
|
458
|
-
catalogRecords: arrayValue(catalog.targets).length,
|
|
459
|
-
abiProfiles: arrayValue(catalog.abiProfiles).length
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
catch (error) {
|
|
463
|
-
if (scriptId)
|
|
464
|
-
await this.unloadScript(scriptId).catch(() => undefined);
|
|
465
|
-
await this.releaseTarget(target).catch(() => undefined);
|
|
466
|
-
throw error;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
async wowLuaTraceRead(input = {}) {
|
|
470
|
-
return this.readTrace("lua", input);
|
|
471
|
-
}
|
|
472
|
-
async wowLuaTraceStatus() {
|
|
473
|
-
return this.traceStatus("lua");
|
|
474
|
-
}
|
|
475
|
-
async wowLuaTraceStop(input = {}) {
|
|
476
|
-
return this.stopTrace("lua", input.reason ?? "requested");
|
|
477
|
-
}
|
|
478
|
-
async wowCppTraceStart(input) {
|
|
479
|
-
if (this.cppTrace)
|
|
480
|
-
throw new Error("C++ trace is already loaded; stop it before starting another trace");
|
|
481
|
-
const request = this.normalizeCppStart(input);
|
|
482
|
-
const catalog = await this.loadCppCatalog(input, request.maxHooks);
|
|
483
|
-
const target = await this.acquireTarget(input.buildKey, input.pid);
|
|
484
|
-
let scriptId;
|
|
485
|
-
try {
|
|
486
|
-
const loaded = await this.executor.execute(brokerManagedCommand({
|
|
487
|
-
operation: "script_load",
|
|
488
|
-
sessionId: target.sessionId,
|
|
489
|
-
source: this.options.cppTraceSource ?? CPP_TRACE_SOURCE
|
|
490
|
-
}, "wow.cpp_trace.script_load"), target.context);
|
|
491
|
-
scriptId = requiredString(loaded.scriptId, "script_load.scriptId");
|
|
492
|
-
const configured = await this.callScript(scriptId, "configureCppTrace", [catalog]);
|
|
493
|
-
const started = await this.callScript(scriptId, "wowCppTraceStart", [{ ...request, pid: target.pid }]);
|
|
494
|
-
const value = isRecord(started.value) ? started.value : {};
|
|
495
|
-
this.assertRuntimeBase(value.moduleBase, target.moduleBase, "C++ trace");
|
|
496
|
-
this.cppTrace = { kind: "cpp", scriptId, target, startedAt: new Date().toISOString() };
|
|
497
|
-
return {
|
|
498
|
-
...value,
|
|
499
|
-
configured: configured.value,
|
|
500
|
-
sessionId: target.sessionId,
|
|
501
|
-
scriptId,
|
|
502
|
-
discoveredPid: target.pid,
|
|
503
|
-
discoveredModuleBase: target.moduleBase
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
catch (error) {
|
|
507
|
-
if (scriptId)
|
|
508
|
-
await this.unloadScript(scriptId).catch(() => undefined);
|
|
509
|
-
await this.releaseTarget(target).catch(() => undefined);
|
|
510
|
-
throw error;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
async wowCppTraceRead(input = {}) {
|
|
514
|
-
return this.readTrace("cpp", input);
|
|
515
|
-
}
|
|
516
|
-
async wowCppTraceStatus() {
|
|
517
|
-
return this.traceStatus("cpp");
|
|
518
|
-
}
|
|
519
|
-
async wowCppTraceStop(input = {}) {
|
|
520
|
-
return this.stopTrace("cpp", input.reason ?? "requested");
|
|
521
|
-
}
|
|
522
|
-
async wowDataSourceList(input) {
|
|
523
|
-
const buildKey = requiredString(input.buildKey, "buildKey");
|
|
524
|
-
const records = await this.loadDataSources(buildKey);
|
|
525
|
-
const offset = boundedInteger(input.offset, 0, 0, Number.MAX_SAFE_INTEGER, "offset");
|
|
526
|
-
const limit = boundedInteger(input.limit, 100, 1, 1000, "limit");
|
|
527
|
-
const filtered = records.filter(record => {
|
|
528
|
-
const summary = summarizeRecord(record);
|
|
529
|
-
return (input.status === undefined || summary.status === input.status)
|
|
530
|
-
&& (input.readerStatus === undefined || summary.readerStatus === input.readerStatus)
|
|
531
|
-
&& (input.system === undefined || record.system === input.system);
|
|
532
|
-
});
|
|
533
|
-
return {
|
|
534
|
-
buildKey,
|
|
535
|
-
total: filtered.length,
|
|
536
|
-
offset,
|
|
537
|
-
limit,
|
|
538
|
-
dataSources: filtered.slice(offset, offset + limit).map(summarizeRecord)
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
async wowDataSourceDescribe(input) {
|
|
542
|
-
const buildKey = requiredString(input.buildKey, "buildKey");
|
|
543
|
-
const dataSourceId = requiredString(input.dataSourceId, "dataSourceId");
|
|
544
|
-
const source = await this.requireDataSource(buildKey, dataSourceId);
|
|
545
|
-
const profile = await this.readBuildProfile(buildKey);
|
|
546
|
-
const profileEntry = this.profileDataSource(profile, dataSourceId);
|
|
547
|
-
return {
|
|
548
|
-
buildKey,
|
|
549
|
-
dataSourceId,
|
|
550
|
-
summary: summarizeRecord(source),
|
|
551
|
-
source,
|
|
552
|
-
buildProfile: profileEntry ?? null,
|
|
553
|
-
formalReaderEligible: this.isReaderReady(source, profileEntry)
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
async wowDataRead(input) {
|
|
557
|
-
const buildKey = requiredString(input.buildKey, "buildKey");
|
|
558
|
-
const dataSourceId = requiredString(input.dataSourceId, "dataSourceId");
|
|
559
|
-
const source = await this.requireDataSource(buildKey, dataSourceId);
|
|
560
|
-
const profile = await this.readBuildProfile(buildKey);
|
|
561
|
-
const profileEntry = this.profileDataSource(profile, dataSourceId);
|
|
562
|
-
if (!this.isReaderReady(source, profileEntry)) {
|
|
563
|
-
throw new Error(`data source ${dataSourceId} is not reader_ready`);
|
|
564
|
-
}
|
|
565
|
-
const plan = this.normalizeReaderPlan(buildKey, dataSourceId, source, profileEntry);
|
|
566
|
-
const target = await this.acquireTarget(buildKey, input.pid);
|
|
567
|
-
let scriptId;
|
|
568
|
-
try {
|
|
569
|
-
const loaded = await this.executor.execute(brokerManagedCommand({
|
|
570
|
-
operation: "script_load",
|
|
571
|
-
sessionId: target.sessionId,
|
|
572
|
-
source: this.options.dataReaderSource ?? DATA_READER_SOURCE
|
|
573
|
-
}, "wow.data_reader.script_load"), target.context);
|
|
574
|
-
scriptId = requiredString(loaded.scriptId, "script_load.scriptId");
|
|
575
|
-
const result = await this.callScript(scriptId, "readDataSource", [plan, boundedInteger(input.limit, plan.maxItems, 1, plan.maxItems, "limit")]);
|
|
576
|
-
const value = isRecord(result.value) ? result.value : { allPassed: false, error: "reader returned a non-object result" };
|
|
577
|
-
this.assertRuntimeBase(value.moduleBase, target.moduleBase, "data reader");
|
|
578
|
-
return {
|
|
579
|
-
...value,
|
|
580
|
-
dataSourceId,
|
|
581
|
-
buildKey,
|
|
582
|
-
runtime: {
|
|
583
|
-
pid: target.pid,
|
|
584
|
-
module: target.moduleName,
|
|
585
|
-
moduleBase: target.moduleBase,
|
|
586
|
-
rootRva: plan.root.rva,
|
|
587
|
-
rootRuntimeAddress: hexAdd(target.moduleBase, plan.root.rva)
|
|
588
|
-
},
|
|
589
|
-
readerDefinition: plan,
|
|
590
|
-
crossVerificationEvidence: source.liveEvidence ?? source.verificationEvidence ?? profileEntry?.liveEvidence ?? []
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
finally {
|
|
594
|
-
if (scriptId)
|
|
595
|
-
await this.unloadScript(scriptId).catch(() => undefined);
|
|
596
|
-
await this.releaseTarget(target).catch(() => undefined);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
async wowBuildProfileValidate(input) {
|
|
600
|
-
const buildKey = requiredString(input.buildKey, "buildKey");
|
|
601
|
-
const profile = await this.readBuildProfile(buildKey);
|
|
602
|
-
const signatureSource = this.packagedProfiles
|
|
603
|
-
? await this.buildBundles.readJson(buildKey, "signatures.json")
|
|
604
|
-
: profile;
|
|
605
|
-
const target = await this.acquireTarget(buildKey, input.pid);
|
|
606
|
-
const checks = [];
|
|
607
|
-
try {
|
|
608
|
-
checks.push({
|
|
609
|
-
name: "build_key",
|
|
610
|
-
expected: buildKey,
|
|
611
|
-
actual: profile.buildKey,
|
|
612
|
-
passed: profile.buildKey === buildKey
|
|
613
|
-
});
|
|
614
|
-
const profileModule = normalizeModuleName(profile.module ?? profile.moduleName, this.moduleName);
|
|
615
|
-
checks.push({ name: "module_name", expected: profileModule, actual: target.moduleName, passed: profileModule.toLowerCase() === target.moduleName.toLowerCase() });
|
|
616
|
-
checks.push({ name: "runtime_base_dynamic", actual: target.moduleBase, passed: /^0x[0-9a-f]+$/i.test(target.moduleBase) });
|
|
617
|
-
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(signatureSource).slice(0, boundedInteger(input.maxChecks, 128, 1, MAX_SIGNATURE_CHECKS, "maxChecks"));
|
|
619
|
-
if (signatures.length === 0) {
|
|
620
|
-
checks.push({ name: "signature_evidence_present", passed: false, error: "build profile contains no verifiable entry signatures" });
|
|
621
|
-
}
|
|
622
|
-
for (let index = 0; index < signatures.length; index++) {
|
|
623
|
-
checks.push(await this.verifySignature(target, signatures[index], index));
|
|
624
|
-
}
|
|
625
|
-
const profileSources = recordsFrom(profile, ["dataSources", "dataRoots"]);
|
|
626
|
-
for (const record of profileSources.filter(item => this.isReaderReady(item)).slice(0, MAX_SIGNATURE_CHECKS)) {
|
|
627
|
-
const id = stringValue(record.dataSourceId ?? record.id) ?? "unknown";
|
|
628
|
-
try {
|
|
629
|
-
this.normalizeReaderPlan(buildKey, id, record, record);
|
|
630
|
-
checks.push({ name: `reader_definition:${id}`, passed: true });
|
|
631
|
-
}
|
|
632
|
-
catch (error) {
|
|
633
|
-
checks.push({ name: `reader_definition:${id}`, passed: false, error: errorText(error) });
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
const allPassed = checks.length > 0 && checks.every(check => check.passed === true);
|
|
637
|
-
return {
|
|
638
|
-
buildKey,
|
|
639
|
-
pid: target.pid,
|
|
640
|
-
module: target.moduleName,
|
|
641
|
-
moduleBase: target.moduleBase,
|
|
642
|
-
moduleSize: target.moduleSize,
|
|
643
|
-
imageBase: WOW_IDA_IMAGE_BASE,
|
|
644
|
-
checks,
|
|
645
|
-
checkedSignatures: signatures.length,
|
|
646
|
-
allPassed,
|
|
647
|
-
status: allPassed ? "passed" : "failed"
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
finally {
|
|
651
|
-
await this.releaseTarget(target).catch(() => undefined);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
async wowAnalysisCoverage(input) {
|
|
655
|
-
const buildKey = requiredString(input.buildKey, "buildKey");
|
|
656
|
-
const suffix = buildSuffix(buildKey);
|
|
657
|
-
const artifacts = [];
|
|
658
|
-
for (const spec of REQUIRED_ARTIFACTS) {
|
|
659
|
-
const path = join(this.profileRoot, spec.file(suffix));
|
|
660
|
-
try {
|
|
661
|
-
const data = await readFile(path);
|
|
662
|
-
let parsed;
|
|
663
|
-
let parsePassed = true;
|
|
664
|
-
if (spec.json) {
|
|
665
|
-
try {
|
|
666
|
-
parsed = JSON.parse(data.toString("utf8"));
|
|
667
|
-
}
|
|
668
|
-
catch {
|
|
669
|
-
parsePassed = false;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
artifacts.push({
|
|
673
|
-
key: spec.key,
|
|
674
|
-
role: spec.role,
|
|
675
|
-
file: path,
|
|
676
|
-
exists: true,
|
|
677
|
-
size: data.byteLength,
|
|
678
|
-
sha256: sha256(data),
|
|
679
|
-
parsePassed,
|
|
680
|
-
...(isRecord(parsed) && parsed.buildKey !== undefined ? { artifactBuildKey: parsed.buildKey, buildMatches: parsed.buildKey === buildKey } : {}),
|
|
681
|
-
...(isRecord(parsed) && isRecord(parsed.counts) ? { counts: parsed.counts } : {}),
|
|
682
|
-
...(isRecord(parsed) && parsed.summary !== undefined ? { summary: parsed.summary } : {})
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
catch (error) {
|
|
686
|
-
const code = isRecord(error) ? error.code : undefined;
|
|
687
|
-
artifacts.push({ key: spec.key, role: spec.role, file: path, exists: false, error: code === "ENOENT" ? null : errorText(error) });
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
const wrapper = await this.readArtifactOptional(buildKey, "lua-wrapper-coverage");
|
|
691
|
-
const callgraph = await this.readArtifactOptional(buildKey, "lua-wrapper-callgraph");
|
|
692
|
-
const clusters = await this.readArtifactOptional(buildKey, "cpp-implementation-clusters");
|
|
693
|
-
const dataSources = await this.readArtifactOptional(buildKey, "cpp-data-sources");
|
|
694
|
-
const nonLua = await this.readArtifactOptional(buildKey, "non-lua-system-coverage");
|
|
695
|
-
const wrapperRecords = recordsFrom(wrapper, ["records", "wrappers"]);
|
|
696
|
-
const callgraphRecords = recordsFrom(callgraph, ["records", "wrappers"]);
|
|
697
|
-
const clusterRecords = recordsFrom(clusters, ["clusters"]);
|
|
698
|
-
const dataSourceRecords = recordsFrom(dataSources, ["dataSources"]);
|
|
699
|
-
const nonLuaRecords = recordsFrom(nonLua, ["systems", "records"]);
|
|
700
|
-
return {
|
|
701
|
-
buildKey,
|
|
702
|
-
profileRoot: this.profileRoot,
|
|
703
|
-
generatedAt: new Date().toISOString(),
|
|
704
|
-
requiredArtifacts: artifacts.length,
|
|
705
|
-
presentArtifacts: artifacts.filter(item => item.exists === true).length,
|
|
706
|
-
missingArtifacts: artifacts.filter(item => item.exists !== true).map(item => item.file),
|
|
707
|
-
artifacts,
|
|
708
|
-
matrix: {
|
|
709
|
-
luaWrappers: this.statusCounts(wrapperRecords),
|
|
710
|
-
callgraph: this.statusCounts(callgraphRecords),
|
|
711
|
-
clusters: this.statusCounts(clusterRecords),
|
|
712
|
-
dataSources: this.statusCounts(dataSourceRecords, "readerStatus"),
|
|
713
|
-
nonLuaSystems: this.statusCounts(nonLuaRecords)
|
|
714
|
-
},
|
|
715
|
-
completion: {
|
|
716
|
-
wrapperRecords: wrapperRecords.length,
|
|
717
|
-
wrapperDenominator: 5130,
|
|
718
|
-
allWrappersPresent: wrapperRecords.length === 5130,
|
|
719
|
-
callgraphRecords: callgraphRecords.length,
|
|
720
|
-
clusters: clusterRecords.length,
|
|
721
|
-
dataSources: dataSourceRecords.length,
|
|
722
|
-
nonLuaSystems: nonLuaRecords.length
|
|
723
|
-
}
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
async wowAnalysisCheckpoint(input) {
|
|
727
|
-
const coverage = await this.wowAnalysisCoverage(input);
|
|
728
|
-
const luaTrace = await this.wowLuaTraceStatus().catch(error => ({ active: false, error: errorText(error) }));
|
|
729
|
-
const cppTrace = await this.wowCppTraceStatus().catch(error => ({ active: false, error: errorText(error) }));
|
|
730
|
-
const timestamp = new Date().toISOString();
|
|
731
|
-
return {
|
|
732
|
-
buildKey: input.buildKey,
|
|
733
|
-
label: input.label ?? "manual",
|
|
734
|
-
timestamp,
|
|
735
|
-
coverage,
|
|
736
|
-
traces: { lua: luaTrace, cpp: cppTrace },
|
|
737
|
-
suggestedProcessEvent: {
|
|
738
|
-
timestamp,
|
|
739
|
-
buildKey: input.buildKey,
|
|
740
|
-
phase: "verify",
|
|
741
|
-
action: "checkpoint",
|
|
742
|
-
tool: "wow-analysis-service",
|
|
743
|
-
target: { name: this.moduleName, imageBase: WOW_IDA_IMAGE_BASE },
|
|
744
|
-
result: {
|
|
745
|
-
label: input.label ?? "manual",
|
|
746
|
-
presentArtifacts: coverage.presentArtifacts,
|
|
747
|
-
requiredArtifacts: coverage.requiredArtifacts,
|
|
748
|
-
completion: coverage.completion
|
|
749
|
-
},
|
|
750
|
-
status: coverage.missingArtifacts instanceof Array && coverage.missingArtifacts.length === 0 ? "passed" : "partial",
|
|
751
|
-
artifacts: (coverage.artifacts instanceof Array ? coverage.artifacts : []).filter(isRecord).filter(item => item.exists === true).map(item => item.file),
|
|
752
|
-
error: null,
|
|
753
|
-
nextAction: "continue unresolved and partial coverage entries"
|
|
754
|
-
}
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
async close() {
|
|
758
|
-
if (this.closing)
|
|
759
|
-
return;
|
|
760
|
-
this.closing = true;
|
|
761
|
-
try {
|
|
762
|
-
await this.stopTrace("lua", "service_close").catch(() => undefined);
|
|
763
|
-
await this.stopTrace("cpp", "service_close").catch(() => undefined);
|
|
764
|
-
for (const lease of [...this.sessions.values()]) {
|
|
765
|
-
if (lease.owned) {
|
|
766
|
-
await this.executor.execute({ operation: "detach", sessionId: lease.sessionId }, lease.context).catch(() => undefined);
|
|
767
|
-
}
|
|
768
|
-
this.sessions.delete(lease.sessionId);
|
|
769
|
-
}
|
|
770
|
-
if (this.options.closeExecutorOnClose)
|
|
771
|
-
await this.executor.close();
|
|
772
|
-
}
|
|
773
|
-
finally {
|
|
774
|
-
this.closing = false;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
normalizeLuaStart(input) {
|
|
778
|
-
requiredString(input.buildKey, "buildKey");
|
|
779
|
-
requireSelection(input);
|
|
780
|
-
const APIs = normalizeApiList(input);
|
|
781
|
-
const rva = normalizeRvaSelection(input.rva);
|
|
782
|
-
return {
|
|
783
|
-
buildKey: input.buildKey,
|
|
784
|
-
...(APIs.length ? { APIs } : {}),
|
|
785
|
-
...(input.namespace === undefined ? {} : { namespace: requiredString(input.namespace, "namespace") }),
|
|
786
|
-
...(input.glob === undefined ? {} : { glob: requiredString(input.glob, "glob") }),
|
|
787
|
-
...(rva.length ? { rva } : {}),
|
|
788
|
-
all: input.all === true,
|
|
789
|
-
captureArgs: input.captureArgs ?? true,
|
|
790
|
-
captureReturns: input.captureReturns ?? true,
|
|
791
|
-
captureNativeArguments: input.captureNativeArguments ?? false,
|
|
792
|
-
tableDepth: boundedInteger(input.tableDepth, 2, 0, 8, "tableDepth"),
|
|
793
|
-
maxItems: boundedInteger(input.maxItems, 64, 1, 1000, "maxItems"),
|
|
794
|
-
maxStringBytes: boundedInteger(input.maxStringBytes, 4096, 1, MAX_STRING_BYTES, "maxStringBytes"),
|
|
795
|
-
maxStackValues: boundedInteger(input.maxStackValues, 128, 1, 1000, "maxStackValues"),
|
|
796
|
-
maxInvocationDepth: boundedInteger(input.maxInvocationDepth, 64, 1, 1024, "maxInvocationDepth"),
|
|
797
|
-
maxDecodeMs: boundedInteger(input.maxDecodeMs, 25, 1, 1000, "maxDecodeMs"),
|
|
798
|
-
maxEvents: boundedInteger(input.maxEvents, 10_000, 1, MAX_EVENTS, "maxEvents"),
|
|
799
|
-
maxHooks: boundedInteger(input.maxHooks, DEFAULT_MAX_HOOKS, 1, MAX_LUA_HOOKS, "maxHooks"),
|
|
800
|
-
sampling: boundedSampling(input.sampling),
|
|
801
|
-
durationMs: boundedInteger(input.durationMs, 0, 0, 24 * 60 * 60 * 1000, "durationMs"),
|
|
802
|
-
allowTableIteration: input.allowTableIteration ?? false,
|
|
803
|
-
staleInvocationMs: boundedInteger(input.staleInvocationMs, 600_000, 1000, 24 * 60 * 60 * 1000, "staleInvocationMs")
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
normalizeCppStart(input) {
|
|
807
|
-
requiredString(input.buildKey, "buildKey");
|
|
808
|
-
requireSelection(input);
|
|
809
|
-
return {
|
|
810
|
-
buildKey: input.buildKey,
|
|
811
|
-
captureArguments: input.captureArguments ?? true,
|
|
812
|
-
captureReturn: input.captureReturn ?? true,
|
|
813
|
-
argumentCount: boundedInteger(input.argumentCount, 4, 0, 8, "argumentCount"),
|
|
814
|
-
maxEvents: boundedInteger(input.maxEvents, 10_000, 1, MAX_EVENTS, "maxEvents"),
|
|
815
|
-
maxHooks: boundedInteger(input.maxHooks, 32, 1, MAX_CPP_HOOKS, "maxHooks"),
|
|
816
|
-
sampling: boundedSampling(input.sampling),
|
|
817
|
-
durationMs: boundedInteger(input.durationMs, 0, 0, 24 * 60 * 60 * 1000, "durationMs")
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
async acquireTarget(buildKeyValue, requestedPid) {
|
|
821
|
-
const buildKey = requiredString(buildKeyValue, "buildKey");
|
|
822
|
-
if (requestedPid !== undefined)
|
|
823
|
-
boundedInteger(requestedPid, requestedPid, 1, Number.MAX_SAFE_INTEGER, "pid");
|
|
824
|
-
const processesResult = await this.executor.execute({ operation: "processes" });
|
|
825
|
-
const wowProcesses = resultArray(processesResult, "processes").filter(process => {
|
|
826
|
-
const name = stringValue(process.name) ?? "";
|
|
827
|
-
const pid = integerValue(process.pid);
|
|
828
|
-
return name.toLowerCase() === this.moduleName.toLowerCase()
|
|
829
|
-
&& pid !== undefined
|
|
830
|
-
&& (requestedPid === undefined || pid === requestedPid);
|
|
831
|
-
});
|
|
832
|
-
if (wowProcesses.length === 0 && requestedPid === undefined) {
|
|
833
|
-
throw new Error(`${this.moduleName} is not running or is hidden from Frida process enumeration`);
|
|
834
|
-
}
|
|
835
|
-
if (wowProcesses.length > 1 && requestedPid === undefined) {
|
|
836
|
-
throw new Error(`multiple ${this.moduleName} processes are running; specify pid`);
|
|
837
|
-
}
|
|
838
|
-
const pid = requestedPid ?? integerValue(wowProcesses[0].pid);
|
|
839
|
-
const context = { pid, buildKey, flavor: flavorForBuild(buildKey) };
|
|
840
|
-
const attached = await this.executor.execute({ operation: "attach", pid, buildKey }, context);
|
|
841
|
-
const sessionId = requiredString(attached.sessionId, "attach.sessionId");
|
|
842
|
-
const existingLease = this.sessions.get(sessionId);
|
|
843
|
-
const newlyOwned = existingLease === undefined && attached.reused !== true;
|
|
844
|
-
try {
|
|
845
|
-
const modulesResult = await this.executor.execute({ operation: "modules", sessionId }, context);
|
|
846
|
-
const module = resultArray(modulesResult, "modules").find(item => stringValue(item.name)?.toLowerCase() === this.moduleName.toLowerCase());
|
|
847
|
-
if (!module)
|
|
848
|
-
throw new Error(`${this.moduleName} was not found in PID ${pid}`);
|
|
849
|
-
const moduleBase = normalizeHex(module.base, "module.base");
|
|
850
|
-
const moduleSize = boundedInteger(module.size, 0, 1, Number.MAX_SAFE_INTEGER, "module.size");
|
|
851
|
-
const target = {
|
|
852
|
-
sessionId,
|
|
853
|
-
pid,
|
|
854
|
-
buildKey,
|
|
855
|
-
context,
|
|
856
|
-
moduleName: stringValue(module.name) ?? this.moduleName,
|
|
857
|
-
moduleBase,
|
|
858
|
-
moduleSize,
|
|
859
|
-
owned: existingLease?.owned ?? newlyOwned
|
|
860
|
-
};
|
|
861
|
-
if (existingLease)
|
|
862
|
-
existingLease.references++;
|
|
863
|
-
else
|
|
864
|
-
this.sessions.set(sessionId, { ...target, references: 1 });
|
|
865
|
-
return target;
|
|
866
|
-
}
|
|
867
|
-
catch (error) {
|
|
868
|
-
if (newlyOwned)
|
|
869
|
-
await this.executor.execute({ operation: "detach", sessionId }, context).catch(() => undefined);
|
|
870
|
-
throw error;
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
async releaseTarget(target) {
|
|
874
|
-
const lease = this.sessions.get(target.sessionId);
|
|
875
|
-
if (!lease)
|
|
876
|
-
return;
|
|
877
|
-
lease.references--;
|
|
878
|
-
if (lease.references > 0)
|
|
879
|
-
return;
|
|
880
|
-
this.sessions.delete(target.sessionId);
|
|
881
|
-
if (lease.owned)
|
|
882
|
-
await this.executor.execute({ operation: "detach", sessionId: lease.sessionId }, lease.context);
|
|
883
|
-
}
|
|
884
|
-
async luaSource() {
|
|
885
|
-
if (typeof this.options.luaTraceSource === "string")
|
|
886
|
-
return this.options.luaTraceSource;
|
|
887
|
-
if (typeof this.options.luaTraceSource === "function")
|
|
888
|
-
return this.options.luaTraceSource();
|
|
889
|
-
const path = this.options.luaTraceScriptFile ?? resolve("dist", "agent.js");
|
|
890
|
-
return readFile(path, "utf8");
|
|
891
|
-
}
|
|
892
|
-
async callScript(scriptId, exportName, args = []) {
|
|
893
|
-
return this.executor.execute(brokerManagedCommand({ operation: "script_call", scriptId, exportName, args }, `wow.analysis.${exportName}`));
|
|
894
|
-
}
|
|
895
|
-
async unloadScript(scriptId) {
|
|
896
|
-
await this.executor.execute(brokerManagedCommand({ operation: "script_unload", scriptId }, "wow.analysis.script_unload"));
|
|
897
|
-
}
|
|
898
|
-
activeTrace(kind) {
|
|
899
|
-
return kind === "lua" ? this.luaTrace : this.cppTrace;
|
|
900
|
-
}
|
|
901
|
-
setActiveTrace(kind, value) {
|
|
902
|
-
if (kind === "lua")
|
|
903
|
-
this.luaTrace = value;
|
|
904
|
-
else
|
|
905
|
-
this.cppTrace = value;
|
|
906
|
-
}
|
|
907
|
-
rpcName(kind, operation) {
|
|
908
|
-
return `wow${kind === "lua" ? "Lua" : "Cpp"}Trace${operation}`;
|
|
909
|
-
}
|
|
910
|
-
async readTrace(kind, input) {
|
|
911
|
-
const trace = this.activeTrace(kind);
|
|
912
|
-
if (!trace)
|
|
913
|
-
return { active: false, events: [], oldestSeq: null, newestSeq: null, nextAfterSeq: input.afterSeq ?? 0, droppedEvents: 0 };
|
|
914
|
-
const afterSeq = boundedInteger(input.afterSeq, 0, 0, Number.MAX_SAFE_INTEGER, "afterSeq");
|
|
915
|
-
const limit = boundedInteger(input.limit, 1000, 1, 10_000, "limit");
|
|
916
|
-
const result = await this.callScript(trace.scriptId, this.rpcName(kind, "Read"), [afterSeq, limit]);
|
|
917
|
-
return { active: true, sessionId: trace.target.sessionId, scriptId: trace.scriptId, ...(isRecord(result.value) ? result.value : { value: result.value }) };
|
|
918
|
-
}
|
|
919
|
-
async traceStatus(kind) {
|
|
920
|
-
const trace = this.activeTrace(kind);
|
|
921
|
-
if (!trace)
|
|
922
|
-
return { active: false, loaded: false, kind };
|
|
923
|
-
const result = await this.callScript(trace.scriptId, this.rpcName(kind, "Status"));
|
|
924
|
-
return {
|
|
925
|
-
loaded: true,
|
|
926
|
-
kind,
|
|
927
|
-
sessionId: trace.target.sessionId,
|
|
928
|
-
scriptId: trace.scriptId,
|
|
929
|
-
discoveredPid: trace.target.pid,
|
|
930
|
-
discoveredModuleBase: trace.target.moduleBase,
|
|
931
|
-
...(isRecord(result.value) ? result.value : { value: result.value })
|
|
932
|
-
};
|
|
933
|
-
}
|
|
934
|
-
async stopTrace(kind, reason) {
|
|
935
|
-
const trace = this.activeTrace(kind);
|
|
936
|
-
if (!trace)
|
|
937
|
-
return { active: false, loaded: false, kind, cleanup: { scriptUnloaded: true, sessionReleased: true } };
|
|
938
|
-
this.setActiveTrace(kind, undefined);
|
|
939
|
-
let stopped;
|
|
940
|
-
const errors = [];
|
|
941
|
-
try {
|
|
942
|
-
stopped = (await this.callScript(trace.scriptId, this.rpcName(kind, "Stop"), [reason])).value;
|
|
943
|
-
}
|
|
944
|
-
catch (error) {
|
|
945
|
-
errors.push(`stop RPC: ${errorText(error)}`);
|
|
946
|
-
}
|
|
947
|
-
try {
|
|
948
|
-
await this.callScript(trace.scriptId, "dispose");
|
|
949
|
-
}
|
|
950
|
-
catch (error) {
|
|
951
|
-
errors.push(`dispose RPC: ${errorText(error)}`);
|
|
952
|
-
}
|
|
953
|
-
try {
|
|
954
|
-
await this.unloadScript(trace.scriptId);
|
|
955
|
-
}
|
|
956
|
-
catch (error) {
|
|
957
|
-
errors.push(`script unload: ${errorText(error)}`);
|
|
958
|
-
}
|
|
959
|
-
try {
|
|
960
|
-
await this.releaseTarget(trace.target);
|
|
961
|
-
}
|
|
962
|
-
catch (error) {
|
|
963
|
-
errors.push(`session release: ${errorText(error)}`);
|
|
964
|
-
}
|
|
965
|
-
return {
|
|
966
|
-
...(isRecord(stopped) ? stopped : { active: false }),
|
|
967
|
-
loaded: false,
|
|
968
|
-
kind,
|
|
969
|
-
reason,
|
|
970
|
-
cleanup: { scriptUnloaded: !errors.some(item => item.startsWith("script unload")), sessionReleased: !errors.some(item => item.startsWith("session release")), errors }
|
|
971
|
-
};
|
|
972
|
-
}
|
|
973
|
-
assertRuntimeBase(actual, expected, label) {
|
|
974
|
-
if (actual === undefined || actual === null)
|
|
975
|
-
return;
|
|
976
|
-
if (normalizeHex(actual, `${label}.moduleBase`) !== normalizeHex(expected, "discovered module base")) {
|
|
977
|
-
throw new Error(`${label} reported module base ${String(actual)}, expected dynamically discovered ${expected}`);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
artifactPath(buildKey, stem) {
|
|
981
|
-
if (!this.packagedProfiles)
|
|
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];
|
|
989
|
-
}
|
|
990
|
-
async readArtifact(buildKey, stem) {
|
|
991
|
-
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
|
-
let value;
|
|
995
|
-
try {
|
|
996
|
-
const text = this.packagedProfiles
|
|
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);
|
|
1002
|
-
}
|
|
1003
|
-
catch (error) {
|
|
1004
|
-
throw new Error(`failed to read ${basename(path)}: ${errorText(error)}`);
|
|
1005
|
-
}
|
|
1006
|
-
if (!isRecord(value))
|
|
1007
|
-
throw new Error(`${basename(path)} must contain a JSON object`);
|
|
1008
|
-
if (value.buildKey !== undefined && value.buildKey !== buildKey) {
|
|
1009
|
-
throw new Error(`${basename(path)} belongs to ${String(value.buildKey)}, not ${buildKey}`);
|
|
1010
|
-
}
|
|
1011
|
-
return value;
|
|
1012
|
-
}
|
|
1013
|
-
async readArtifactOptional(buildKey, stem) {
|
|
1014
|
-
if (this.packagedProfiles && !this.artifactPath(buildKey, stem))
|
|
1015
|
-
return undefined;
|
|
1016
|
-
try {
|
|
1017
|
-
return await this.readArtifact(buildKey, stem);
|
|
1018
|
-
}
|
|
1019
|
-
catch (error) {
|
|
1020
|
-
if (/failed to read .*ENOENT|no such file or directory/i.test(errorText(error)))
|
|
1021
|
-
return undefined;
|
|
1022
|
-
throw error;
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
async readBuildProfile(buildKey) {
|
|
1026
|
-
return this.readArtifact(buildKey, "build-profile");
|
|
1027
|
-
}
|
|
1028
|
-
async loadDataSources(buildKey) {
|
|
1029
|
-
const artifact = await this.readArtifact(buildKey, "cpp-data-sources");
|
|
1030
|
-
return recordsFrom(artifact, ["dataSources", "records"]);
|
|
1031
|
-
}
|
|
1032
|
-
async requireDataSource(buildKey, dataSourceId) {
|
|
1033
|
-
const records = await this.loadDataSources(buildKey);
|
|
1034
|
-
const source = records.find(record => stringValue(record.dataSourceId ?? record.id) === dataSourceId);
|
|
1035
|
-
if (!source)
|
|
1036
|
-
throw new Error(`data source ${dataSourceId} was not found for ${buildKey}`);
|
|
1037
|
-
if (source.buildKey !== undefined && source.buildKey !== buildKey)
|
|
1038
|
-
throw new Error(`data source ${dataSourceId} has a build mismatch`);
|
|
1039
|
-
return source;
|
|
1040
|
-
}
|
|
1041
|
-
profileDataSource(profile, dataSourceId) {
|
|
1042
|
-
const records = recordsFrom(profile, ["dataSources", "dataRoots", "readers"]);
|
|
1043
|
-
return records.find(record => stringValue(record.dataSourceId ?? record.id) === dataSourceId);
|
|
1044
|
-
}
|
|
1045
|
-
isReaderReady(source, profileEntry) {
|
|
1046
|
-
const statuses = [
|
|
1047
|
-
statusValue(source, "readerStatus", "reader_status"),
|
|
1048
|
-
isRecord(source.reader) ? statusValue(source.reader, "status", "readerStatus") : undefined,
|
|
1049
|
-
profileEntry ? statusValue(profileEntry, "readerStatus", "reader_status") : undefined,
|
|
1050
|
-
profileEntry && isRecord(profileEntry.reader) ? statusValue(profileEntry.reader, "status", "readerStatus") : undefined
|
|
1051
|
-
];
|
|
1052
|
-
return statuses.some(value => value === "reader_ready");
|
|
1053
|
-
}
|
|
1054
|
-
normalizeReaderPlan(buildKey, dataSourceId, source, profileEntry) {
|
|
1055
|
-
const candidates = [
|
|
1056
|
-
profileEntry?.readerDefinition,
|
|
1057
|
-
profileEntry?.reader,
|
|
1058
|
-
source.readerDefinition,
|
|
1059
|
-
source.reader,
|
|
1060
|
-
profileEntry,
|
|
1061
|
-
source
|
|
1062
|
-
];
|
|
1063
|
-
const reader = candidates.find(isRecord);
|
|
1064
|
-
if (!reader)
|
|
1065
|
-
throw new Error(`data source ${dataSourceId} has no reader definition`);
|
|
1066
|
-
const rootCandidate = isRecord(reader.root) ? reader.root : {};
|
|
1067
|
-
const rawKind = stringValue(rootCandidate.kind ?? reader.rootKind);
|
|
1068
|
-
const kindAliases = {
|
|
1069
|
-
module_rva: "module_rva",
|
|
1070
|
-
moduleRva: "module_rva",
|
|
1071
|
-
module_address: "module_rva",
|
|
1072
|
-
global_pointer_rva: "global_pointer_rva",
|
|
1073
|
-
globalPointerRva: "global_pointer_rva",
|
|
1074
|
-
global_pointer: "global_pointer_rva"
|
|
1075
|
-
};
|
|
1076
|
-
const kind = rawKind ? kindAliases[rawKind] : undefined;
|
|
1077
|
-
if (!kind)
|
|
1078
|
-
throw new Error(`data source ${dataSourceId} reader root kind is missing or unsupported`);
|
|
1079
|
-
const rawRva = rootCandidate.rva ?? reader.rootRva ?? (kind === "global_pointer_rva" ? source.globalPointerRva ?? profileEntry?.globalPointerRva : undefined);
|
|
1080
|
-
const rva = normalizeHex(rawRva, `${dataSourceId}.reader.root.rva`);
|
|
1081
|
-
const rawChain = arrayValue(reader.pointerChain ?? source.pointerChain ?? profileEntry?.pointerChain);
|
|
1082
|
-
if (rawChain.length > 16)
|
|
1083
|
-
throw new Error(`${dataSourceId}.pointerChain exceeds 16 steps`);
|
|
1084
|
-
const pointerChain = rawChain.map((step, index) => {
|
|
1085
|
-
if (typeof step === "number")
|
|
1086
|
-
return { offset: boundedInteger(step, 0, 0, 0x1000000, `pointerChain[${index}]`), dereference: true };
|
|
1087
|
-
if (!isRecord(step))
|
|
1088
|
-
throw new Error(`pointerChain[${index}] must be an offset or object`);
|
|
1089
|
-
return {
|
|
1090
|
-
offset: boundedInteger(step.offset, 0, 0, 0x1000000, `pointerChain[${index}].offset`),
|
|
1091
|
-
dereference: booleanValue(step.dereference) ?? true
|
|
1092
|
-
};
|
|
1093
|
-
});
|
|
1094
|
-
const rawFields = arrayValue(reader.fields ?? source.fields ?? profileEntry?.fields);
|
|
1095
|
-
if (rawFields.length > 256)
|
|
1096
|
-
throw new Error(`${dataSourceId}.fields exceeds 256 entries`);
|
|
1097
|
-
const supportedKinds = new Set(["bool", "u8", "s8", "u16", "s16", "u32", "s32", "u64", "s64", "float", "double", "pointer", "utf8", "bytes"]);
|
|
1098
|
-
const fields = rawFields.map((field, index) => {
|
|
1099
|
-
if (!isRecord(field))
|
|
1100
|
-
throw new Error(`fields[${index}] must be an object`);
|
|
1101
|
-
const name = requiredString(field.name, `fields[${index}].name`);
|
|
1102
|
-
const kindValue = requiredString(field.kind ?? field.type, `fields[${index}].kind`);
|
|
1103
|
-
if (!supportedKinds.has(kindValue))
|
|
1104
|
-
throw new Error(`fields[${index}].kind ${kindValue} is unsupported`);
|
|
1105
|
-
const normalized = {
|
|
1106
|
-
name,
|
|
1107
|
-
offset: boundedInteger(field.offset, 0, 0, 0x1000000, `fields[${index}].offset`),
|
|
1108
|
-
kind: kindValue
|
|
1109
|
-
};
|
|
1110
|
-
if (kindValue === "utf8")
|
|
1111
|
-
normalized.maxBytes = boundedInteger(field.maxBytes, 4096, 1, MAX_STRING_BYTES, `fields[${index}].maxBytes`);
|
|
1112
|
-
if (kindValue === "bytes")
|
|
1113
|
-
normalized.size = boundedInteger(field.size, 1, 1, 4096, `fields[${index}].size`);
|
|
1114
|
-
return normalized;
|
|
1115
|
-
});
|
|
1116
|
-
const rawInvariants = arrayValue(reader.validationRules ?? reader.invariants ?? source.validationRules ?? profileEntry?.validationRules);
|
|
1117
|
-
if (rawInvariants.length === 0)
|
|
1118
|
-
throw new Error(`data source ${dataSourceId} reader has no machine-checkable validation rules`);
|
|
1119
|
-
if (rawInvariants.length > 128)
|
|
1120
|
-
throw new Error(`${dataSourceId}.validationRules exceeds 128 entries`);
|
|
1121
|
-
const allowedInvariants = new Set(["non_null", "aligned", "field_range", "field_equals", "vtable_in_module"]);
|
|
1122
|
-
const invariants = rawInvariants.map((rule, index) => {
|
|
1123
|
-
if (!isRecord(rule))
|
|
1124
|
-
throw new Error(`validationRules[${index}] must be an object`);
|
|
1125
|
-
const kindValue = requiredString(rule.kind ?? rule.type, `validationRules[${index}].kind`);
|
|
1126
|
-
if (!allowedInvariants.has(kindValue))
|
|
1127
|
-
throw new Error(`validationRules[${index}].kind ${kindValue} is unsupported`);
|
|
1128
|
-
if (kindValue === "aligned")
|
|
1129
|
-
boundedInteger(rule.alignment, 8, 1, 4096, `validationRules[${index}].alignment`);
|
|
1130
|
-
if (kindValue === "field_range") {
|
|
1131
|
-
requiredString(rule.field, `validationRules[${index}].field`);
|
|
1132
|
-
if (typeof rule.minimum !== "number" || typeof rule.maximum !== "number")
|
|
1133
|
-
throw new Error(`validationRules[${index}] requires numeric minimum/maximum`);
|
|
1134
|
-
}
|
|
1135
|
-
return { ...rule, kind: kindValue };
|
|
1136
|
-
});
|
|
1137
|
-
let container;
|
|
1138
|
-
if (reader.container !== undefined) {
|
|
1139
|
-
if (!isRecord(reader.container) || reader.container.kind !== "vector")
|
|
1140
|
-
throw new Error(`${dataSourceId}.container currently supports only vector`);
|
|
1141
|
-
const containerFields = arrayValue(reader.container.fields).map((field, index) => {
|
|
1142
|
-
if (!isRecord(field))
|
|
1143
|
-
throw new Error(`container.fields[${index}] must be an object`);
|
|
1144
|
-
const kindValue = requiredString(field.kind ?? field.type, `container.fields[${index}].kind`);
|
|
1145
|
-
if (!supportedKinds.has(kindValue))
|
|
1146
|
-
throw new Error(`container.fields[${index}].kind ${kindValue} is unsupported`);
|
|
1147
|
-
return {
|
|
1148
|
-
name: requiredString(field.name, `container.fields[${index}].name`),
|
|
1149
|
-
offset: boundedInteger(field.offset, 0, 0, 0x1000000, `container.fields[${index}].offset`),
|
|
1150
|
-
kind: kindValue,
|
|
1151
|
-
...(kindValue === "utf8" ? { maxBytes: boundedInteger(field.maxBytes, 4096, 1, MAX_STRING_BYTES, `container.fields[${index}].maxBytes`) } : {}),
|
|
1152
|
-
...(kindValue === "bytes" ? { size: boundedInteger(field.size, 1, 1, 4096, `container.fields[${index}].size`) } : {})
|
|
1153
|
-
};
|
|
1154
|
-
});
|
|
1155
|
-
container = {
|
|
1156
|
-
kind: "vector",
|
|
1157
|
-
beginOffset: boundedInteger(reader.container.beginOffset, 0, 0, 0x1000000, "container.beginOffset"),
|
|
1158
|
-
endOffset: boundedInteger(reader.container.endOffset, 8, 0, 0x1000000, "container.endOffset"),
|
|
1159
|
-
elementSize: boundedInteger(reader.container.elementSize, 1, 1, 0x100000, "container.elementSize"),
|
|
1160
|
-
fields: containerFields
|
|
1161
|
-
};
|
|
1162
|
-
}
|
|
1163
|
-
return {
|
|
1164
|
-
dataSourceId,
|
|
1165
|
-
buildKey,
|
|
1166
|
-
moduleName: normalizeModuleName(reader.module ?? source.module ?? profileEntry?.module, this.moduleName),
|
|
1167
|
-
root: { kind, rva },
|
|
1168
|
-
pointerChain,
|
|
1169
|
-
fields,
|
|
1170
|
-
invariants,
|
|
1171
|
-
...(container ? { container } : {}),
|
|
1172
|
-
maxItems: boundedInteger(reader.maxItems, 1024, 1, MAX_READ_ITEMS, "reader.maxItems"),
|
|
1173
|
-
maxStringBytes: boundedInteger(reader.maxStringBytes, 4096, 1, MAX_STRING_BYTES, "reader.maxStringBytes")
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1176
|
-
async loadLuaCatalog(buildKey) {
|
|
1177
|
-
const manifest = await this.readArtifact(buildKey, "wow-lua-api-all-rva");
|
|
1178
|
-
const abi = await this.readArtifactOptional(buildKey, "lua-abi")
|
|
1179
|
-
?? await this.readArtifactOptional(buildKey, "lua-abi-static-evidence")
|
|
1180
|
-
?? {};
|
|
1181
|
-
const coverage = await this.readArtifactOptional(buildKey, "lua-wrapper-coverage") ?? {};
|
|
1182
|
-
const records = recordsFrom(manifest, ["records"]);
|
|
1183
|
-
if (records.length === 0)
|
|
1184
|
-
throw new Error("Lua registration manifest has no records");
|
|
1185
|
-
const profiles = this.luaAbiProfiles(abi, buildKey);
|
|
1186
|
-
const evidenceRecords = [
|
|
1187
|
-
...recordsFrom(abi, ["callbacks", "targets", "records"]),
|
|
1188
|
-
...recordsFrom(coverage, ["records", "wrappers"])
|
|
1189
|
-
];
|
|
1190
|
-
const evidenceByKey = new Map();
|
|
1191
|
-
for (const record of evidenceRecords)
|
|
1192
|
-
evidenceByKey.set(this.luaRecordKey(record), record);
|
|
1193
|
-
const defaultProfileId = profiles.length === 1 ? stringValue(profiles[0].id) : undefined;
|
|
1194
|
-
const targets = records.map((record, index) => {
|
|
1195
|
-
const namespace = stringValue(record.namespace) ?? "global";
|
|
1196
|
-
const name = requiredString(record.name, `records[${index}].name`);
|
|
1197
|
-
const rva = normalizeHex(record.rva ?? record.wrapperRva, `records[${index}].rva`);
|
|
1198
|
-
const evidence = evidenceByKey.get(this.luaRecordKey({ namespace, name, rva })) ?? {};
|
|
1199
|
-
const rawAbi = isRecord(record.abi) ? record.abi : isRecord(evidence.abi) ? evidence.abi : {};
|
|
1200
|
-
const callback = isRecord(evidence.standardLuaCallback) ? evidence.standardLuaCallback : {};
|
|
1201
|
-
const argument = isRecord(evidence.luaStateArgument) ? evidence.luaStateArgument : {};
|
|
1202
|
-
const evidenceStatus = stringValue(rawAbi.status ?? evidence.abiStatus ?? callback.status);
|
|
1203
|
-
const profileId = stringValue(rawAbi.profileId ?? evidence.profileId) ?? defaultProfileId ?? "unresolved";
|
|
1204
|
-
const hasProfile = profiles.some(profile => profile.id === profileId);
|
|
1205
|
-
const luaStateArgIndex = integerValue(rawAbi.luaStateArgIndex)
|
|
1206
|
-
?? integerValue(evidence.luaStateArgIndex)
|
|
1207
|
-
?? (stringValue(argument.register)?.toUpperCase() === "RCX" ? 0 : undefined);
|
|
1208
|
-
const confirmed = hasProfile && luaStateArgIndex !== undefined && ["confirmed", "dynamically_verified", "abi_resolved"].includes(evidenceStatus ?? "");
|
|
1209
|
-
return {
|
|
1210
|
-
id: stringValue(record.id) ?? `${buildKey}:${namespace}.${name}:${rva}`,
|
|
1211
|
-
buildKey,
|
|
1212
|
-
namespace,
|
|
1213
|
-
name,
|
|
1214
|
-
rva,
|
|
1215
|
-
registrationPrimitive: stringValue(record.registrationPrimitive) ?? "unresolved",
|
|
1216
|
-
sourceKind: stringValue(record.sourceKind) ?? "unresolved",
|
|
1217
|
-
abi: {
|
|
1218
|
-
status: confirmed ? (evidenceStatus === "dynamically_verified" ? "dynamically_verified" : "confirmed") : "unresolved",
|
|
1219
|
-
profileId,
|
|
1220
|
-
luaStateArgIndex: luaStateArgIndex ?? null,
|
|
1221
|
-
returnConvention: stringValue(rawAbi.returnConvention)
|
|
1222
|
-
?? (callback.prototype !== undefined ? "lua_return_count_int32" : "opaque"),
|
|
1223
|
-
evidence: arrayValue(rawAbi.evidence).filter(item => typeof item === "string")
|
|
1224
|
-
}
|
|
1225
|
-
};
|
|
1226
|
-
});
|
|
1227
|
-
return { buildKey, moduleName: normalizeModuleName(manifest.module, this.moduleName), targets, abiProfiles: profiles };
|
|
1228
|
-
}
|
|
1229
|
-
luaRecordKey(record) {
|
|
1230
|
-
const namespace = stringValue(record.namespace) ?? "global";
|
|
1231
|
-
const name = stringValue(record.name) ?? "";
|
|
1232
|
-
let rva = "";
|
|
1233
|
-
try {
|
|
1234
|
-
if (record.rva !== undefined || record.wrapperRva !== undefined)
|
|
1235
|
-
rva = normalizeHex(record.rva ?? record.wrapperRva, "rva");
|
|
1236
|
-
}
|
|
1237
|
-
catch { /* key can fall back to API */ }
|
|
1238
|
-
return `${namespace}\u0000${name}\u0000${rva}`;
|
|
1239
|
-
}
|
|
1240
|
-
luaAbiProfiles(artifact, buildKey) {
|
|
1241
|
-
const candidates = recordsFrom(artifact, ["abiProfiles", "profiles"]);
|
|
1242
|
-
if (candidates.length === 0) {
|
|
1243
|
-
const singleton = [artifact.abiProfile, artifact.profile, artifact.luaAbiProfile].find(isRecord);
|
|
1244
|
-
if (singleton)
|
|
1245
|
-
candidates.push(singleton);
|
|
1246
|
-
}
|
|
1247
|
-
const profiles = [];
|
|
1248
|
-
candidates.forEach((profile, index) => {
|
|
1249
|
-
const bindings = isRecord(profile.bindings) ? profile.bindings : {};
|
|
1250
|
-
const getTop = bindings.getTop ?? profile.getTopRva;
|
|
1251
|
-
const type = bindings.type ?? profile.typeRva;
|
|
1252
|
-
const rawLayout = isRecord(profile.stackLayout) ? profile.stackLayout : undefined;
|
|
1253
|
-
if (getTop === undefined || (type === undefined && rawLayout === undefined))
|
|
1254
|
-
return;
|
|
1255
|
-
const normalizedBindings = {
|
|
1256
|
-
getTop: normalizeHex(getTop, `abiProfiles[${index}].bindings.getTop`),
|
|
1257
|
-
...(type === undefined ? {} : { type: normalizeHex(type, `abiProfiles[${index}].bindings.type`) })
|
|
1258
|
-
};
|
|
1259
|
-
for (const key of ["toBoolean", "isInteger", "toInteger", "toNumber", "toLString", "toPointer", "toUserdata", "auxiliaryTypeName", "setTop", "pushNil", "next"]) {
|
|
1260
|
-
if (bindings[key] !== undefined)
|
|
1261
|
-
normalizedBindings[key] = normalizeHex(bindings[key], `abiProfiles[${index}].bindings.${key}`);
|
|
1262
|
-
}
|
|
1263
|
-
profiles.push({
|
|
1264
|
-
id: stringValue(profile.id) ?? `lua-abi-${index + 1}`,
|
|
1265
|
-
buildKey,
|
|
1266
|
-
status: ["confirmed", "dynamically_verified"].includes(stringValue(profile.status) ?? "") ? profile.status : "unresolved",
|
|
1267
|
-
bindings: normalizedBindings,
|
|
1268
|
-
...(rawLayout ? {
|
|
1269
|
-
stackLayout: {
|
|
1270
|
-
basePointerOffset: boundedInteger(rawLayout.basePointerOffset, 0, 0, 0x1000, `abiProfiles[${index}].stackLayout.basePointerOffset`),
|
|
1271
|
-
topPointerOffset: boundedInteger(rawLayout.topPointerOffset, 0, 0, 0x1000, `abiProfiles[${index}].stackLayout.topPointerOffset`),
|
|
1272
|
-
slotSize: boundedInteger(rawLayout.slotSize, 0, 1, 0x1000, `abiProfiles[${index}].stackLayout.slotSize`),
|
|
1273
|
-
payloadOffset: boundedInteger(rawLayout.payloadOffset, 0, 0, 0x1000, `abiProfiles[${index}].stackLayout.payloadOffset`),
|
|
1274
|
-
tagOffset: boundedInteger(rawLayout.tagOffset, 0, 0, 0x1000, `abiProfiles[${index}].stackLayout.tagOffset`),
|
|
1275
|
-
tagSize: boundedInteger(rawLayout.tagSize, 2, 1, 4, `abiProfiles[${index}].stackLayout.tagSize`),
|
|
1276
|
-
...(rawLayout.auxiliaryOffset === undefined ? {} : { auxiliaryOffset: boundedInteger(rawLayout.auxiliaryOffset, 0, 0, 0x1000, `abiProfiles[${index}].stackLayout.auxiliaryOffset`) }),
|
|
1277
|
-
maxSlots: boundedInteger(rawLayout.maxSlots, 8000, 1, 100000, `abiProfiles[${index}].stackLayout.maxSlots`)
|
|
1278
|
-
}
|
|
1279
|
-
} : {}),
|
|
1280
|
-
...(isRecord(profile.typeTags) ? { typeTags: profile.typeTags } : {}),
|
|
1281
|
-
...(Array.isArray(profile.userdataProfiles) ? { userdataProfiles: profile.userdataProfiles } : {}),
|
|
1282
|
-
...(Array.isArray(profile.evidence) ? { evidence: profile.evidence } : {})
|
|
1283
|
-
});
|
|
1284
|
-
});
|
|
1285
|
-
return profiles;
|
|
1286
|
-
}
|
|
1287
|
-
async loadCppCatalog(input, maxHooks) {
|
|
1288
|
-
const callgraph = await this.readArtifact(input.buildKey, "lua-wrapper-callgraph");
|
|
1289
|
-
const clusters = await this.readArtifactOptional(input.buildKey, "cpp-implementation-clusters") ?? {};
|
|
1290
|
-
const targets = this.cppTargets(callgraph, clusters, input.buildKey);
|
|
1291
|
-
if (targets.length === 0)
|
|
1292
|
-
throw new Error("C++ callgraph contains no resolved trace targets");
|
|
1293
|
-
const apis = new Set(normalizeApiList(input));
|
|
1294
|
-
const rvas = new Set(normalizeRvaSelection(input.rva));
|
|
1295
|
-
const clustersWanted = new Set((input.clusterIds ?? []).map((value, index) => requiredString(value, `clusterIds[${index}]`)));
|
|
1296
|
-
const expression = input.glob === undefined ? undefined : globExpression(requiredString(input.glob, "glob"));
|
|
1297
|
-
const selected = targets.filter(target => {
|
|
1298
|
-
const aliases = arrayValue(target.aliases).filter((item) => typeof item === "string");
|
|
1299
|
-
const namespaceMatch = input.namespace === undefined || aliases.some(api => api.startsWith(`${input.namespace}.`));
|
|
1300
|
-
const apiMatch = apis.size === 0 || aliases.some(api => apis.has(api));
|
|
1301
|
-
const globMatch = expression === undefined || aliases.some(api => expression.test(api));
|
|
1302
|
-
const rvaMatch = rvas.size === 0 || rvas.has(String(target.rva));
|
|
1303
|
-
const clusterMatch = clustersWanted.size === 0 || arrayValue(target.clusterIds).some(id => typeof id === "string" && clustersWanted.has(id));
|
|
1304
|
-
return namespaceMatch && apiMatch && globMatch && rvaMatch && clusterMatch;
|
|
1305
|
-
});
|
|
1306
|
-
if (selected.length === 0)
|
|
1307
|
-
throw new Error("C++ trace selection matched no indexed business targets");
|
|
1308
|
-
if (selected.length > maxHooks)
|
|
1309
|
-
throw new Error(`C++ trace selection resolved ${selected.length} targets, exceeding maxHooks=${maxHooks}`);
|
|
1310
|
-
return { buildKey: input.buildKey, moduleName: normalizeModuleName(callgraph.module, this.moduleName), targets: selected };
|
|
1311
|
-
}
|
|
1312
|
-
cppTargets(callgraph, clusters, buildKey) {
|
|
1313
|
-
const byRva = new Map();
|
|
1314
|
-
const add = (rvaValue, api, clusterId) => {
|
|
1315
|
-
if (rvaValue === undefined || rvaValue === null)
|
|
1316
|
-
return;
|
|
1317
|
-
let rva;
|
|
1318
|
-
try {
|
|
1319
|
-
rva = normalizeHex(rvaValue, "business callee RVA");
|
|
1320
|
-
}
|
|
1321
|
-
catch {
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
const rvaNumber = hexToNumber(rva, "business callee RVA");
|
|
1325
|
-
const existing = byRva.get(rva) ?? { id: `${buildKey}:cpp:${rva}`, rva, rvaNumber, aliases: [], clusterIds: [] };
|
|
1326
|
-
const aliases = existing.aliases;
|
|
1327
|
-
if (api && !aliases.includes(api))
|
|
1328
|
-
aliases.push(api);
|
|
1329
|
-
const clusterIds = existing.clusterIds;
|
|
1330
|
-
if (clusterId && !clusterIds.includes(clusterId))
|
|
1331
|
-
clusterIds.push(clusterId);
|
|
1332
|
-
byRva.set(rva, existing);
|
|
1333
|
-
};
|
|
1334
|
-
for (const record of recordsFrom(callgraph, ["records", "wrappers"])) {
|
|
1335
|
-
const namespace = stringValue(record.namespace) ?? "global";
|
|
1336
|
-
const name = stringValue(record.name ?? record.apiName);
|
|
1337
|
-
const api = stringValue(record.api) ?? (name ? `${namespace}.${name}` : undefined);
|
|
1338
|
-
add(pickPath(record, [
|
|
1339
|
-
["firstBusinessCalleeRva"], ["businessCalleeRva"], ["businessEntryRva"],
|
|
1340
|
-
["firstBusinessCallee", "rva"], ["firstNonLuaBusinessCallee", "rva"], ["businessEntry", "rva"]
|
|
1341
|
-
]), api, stringValue(record.implementationClusterId ?? record.clusterId));
|
|
1342
|
-
}
|
|
1343
|
-
for (const cluster of recordsFrom(clusters, ["clusters", "records"])) {
|
|
1344
|
-
const clusterId = stringValue(cluster.clusterId ?? cluster.id);
|
|
1345
|
-
const rva = pickPath(cluster, [["businessEntryRva"], ["entryRva"], ["businessEntry", "rva"]]);
|
|
1346
|
-
const wrappers = arrayValue(cluster.wrappers ?? cluster.apis ?? cluster.APIs);
|
|
1347
|
-
if (wrappers.length === 0)
|
|
1348
|
-
add(rva, undefined, clusterId);
|
|
1349
|
-
for (const wrapper of wrappers) {
|
|
1350
|
-
if (typeof wrapper === "string")
|
|
1351
|
-
add(rva, wrapper, clusterId);
|
|
1352
|
-
else if (isRecord(wrapper)) {
|
|
1353
|
-
const namespace = stringValue(wrapper.namespace) ?? "global";
|
|
1354
|
-
const name = stringValue(wrapper.name);
|
|
1355
|
-
add(rva, stringValue(wrapper.api) ?? (name ? `${namespace}.${name}` : undefined), clusterId);
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
return [...byRva.values()];
|
|
1360
|
-
}
|
|
1361
|
-
signatureRecords(profile) {
|
|
1362
|
-
const records = [];
|
|
1363
|
-
const visit = (value, depth) => {
|
|
1364
|
-
if (depth > 4)
|
|
1365
|
-
return;
|
|
1366
|
-
if (Array.isArray(value)) {
|
|
1367
|
-
for (const item of value)
|
|
1368
|
-
visit(item, depth + 1);
|
|
1369
|
-
return;
|
|
1370
|
-
}
|
|
1371
|
-
if (!isRecord(value))
|
|
1372
|
-
return;
|
|
1373
|
-
const hasRva = value.rva !== undefined || value.entryRva !== undefined;
|
|
1374
|
-
const hasPattern = value.entryBytesHex !== undefined || value.bytesHex !== undefined || value.signature !== undefined || value.aob !== undefined;
|
|
1375
|
-
if (hasRva && hasPattern)
|
|
1376
|
-
records.push(value);
|
|
1377
|
-
for (const [key, child] of Object.entries(value)) {
|
|
1378
|
-
if (["staticEvidence", "liveEvidence", "evidence", "inputs", "outputs"].includes(key))
|
|
1379
|
-
continue;
|
|
1380
|
-
if (typeof child === "object" && child !== null)
|
|
1381
|
-
visit(child, depth + 1);
|
|
1382
|
-
}
|
|
1383
|
-
};
|
|
1384
|
-
visit(profile.signatures ?? profile.functions ?? profile.buildSignatures ?? profile, 0);
|
|
1385
|
-
const seen = new Set();
|
|
1386
|
-
return records.filter(record => {
|
|
1387
|
-
const key = `${String(record.rva ?? record.entryRva)}:${String(record.entryBytesHex ?? record.bytesHex ?? record.signature ?? record.aob)}`;
|
|
1388
|
-
if (seen.has(key))
|
|
1389
|
-
return false;
|
|
1390
|
-
seen.add(key);
|
|
1391
|
-
return true;
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
1394
|
-
async verifySignature(target, signature, index) {
|
|
1395
|
-
try {
|
|
1396
|
-
const rva = normalizeHex(signature.rva ?? signature.entryRva, `signature[${index}].rva`);
|
|
1397
|
-
const offset = hexToNumber(rva, `signature[${index}].rva`);
|
|
1398
|
-
const patternValue = requiredString(signature.entryBytesHex ?? signature.bytesHex ?? signature.signature ?? signature.aob, `signature[${index}].pattern`);
|
|
1399
|
-
const tokens = patternValue.includes(" ") ? patternValue.trim().split(/\s+/) : patternValue.match(/.{1,2}/g) ?? [];
|
|
1400
|
-
if (tokens.length === 0 || tokens.length > 256 || tokens.some(token => !/^(?:[0-9a-f]{2}|\?\?)$/i.test(token))) {
|
|
1401
|
-
throw new Error("signature pattern must contain byte or ?? tokens and be at most 256 bytes");
|
|
1402
|
-
}
|
|
1403
|
-
if (offset + tokens.length > target.moduleSize)
|
|
1404
|
-
throw new Error("signature exceeds module range");
|
|
1405
|
-
const address = hexAdd(target.moduleBase, rva);
|
|
1406
|
-
const read = await this.executor.execute({ operation: "read_memory", sessionId: target.sessionId, address, size: tokens.length }, target.context);
|
|
1407
|
-
const actualHex = requiredString(read.bytesHex, "read_memory.bytesHex").toLowerCase();
|
|
1408
|
-
const actual = actualHex.match(/.{1,2}/g) ?? [];
|
|
1409
|
-
const passed = tokens.every((token, tokenIndex) => token === "??" || token.toLowerCase() === actual[tokenIndex]);
|
|
1410
|
-
return { name: stringValue(signature.name ?? signature.id) ?? `signature_${index}`, rva, runtimeAddress: address, expected: tokens.join(" "), actual: actual.join(" "), passed };
|
|
1411
|
-
}
|
|
1412
|
-
catch (error) {
|
|
1413
|
-
return { name: stringValue(signature.name ?? signature.id) ?? `signature_${index}`, passed: false, error: errorText(error) };
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
statusCounts(records, preferredKey = "status") {
|
|
1417
|
-
const counts = {};
|
|
1418
|
-
for (const record of records) {
|
|
1419
|
-
const status = statusValue(record, preferredKey, "status", "resolutionStatus", "readerStatus") ?? "unresolved";
|
|
1420
|
-
counts[status] = (counts[status] ?? 0) + 1;
|
|
1421
|
-
}
|
|
1422
|
-
return { total: records.length, statuses: counts };
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
function errorText(error) {
|
|
1426
|
-
return error instanceof Error ? error.message : String(error);
|
|
1427
|
-
}
|
|
1428
|
-
export function createWowAnalysisService(options) {
|
|
1429
|
-
return new WowAnalysisService(options);
|
|
1430
|
-
}
|