wowdump 0.3.4 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -6
- package/dist/analysis/runtime-dump.js +2 -2
- package/dist/cli.js +133 -175
- package/dist/core/build-adapters.js +0 -8
- package/dist/debug/cdb.js +412 -0
- package/dist/reader/broker.js +88 -7
- package/dist/reader/launcher.js +5 -5
- package/dist/reader/main.js +18 -35
- package/dist/reader/windows.js +2 -2
- package/dist/toolchain.js +3 -3
- package/package.json +4 -5
- package/skills/wowdump/SKILL.md +18 -12
- package/skills/wowdump/references/commands.md +27 -38
- package/skills/wowdump/references/disassemble.md +3 -3
- package/skills/wowdump/references/evidence-workflow.md +6 -6
- package/skills/wowdump/references/profiles.md +2 -2
- package/skills/wowdump/references/request-schema.md +2 -2
- package/skills/wowdump/references/windbg.md +55 -0
- package/skills/wowdump/references/workflow.md +17 -32
- package/dist/analysis/frida-runtime.js +0 -715
- package/dist/analysis/runtime-script.js +0 -156
- package/dist/frida-worker.js +0 -153
- package/skills/wowdump/references/dynamic.md +0 -54
- package/skills/wowdump/scripts/dynamic-session.js +0 -133
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Built-in runtime evidence operations used by `analyze runtime`.
|
|
3
|
-
*
|
|
4
|
-
* The caller-supplied `analyze dynamic` path remains the escape hatch for
|
|
5
|
-
* field-specific GumJS. This script only provides two bounded primitives:
|
|
6
|
-
* `dump` exports PE sections for static analysis and `verify` reads profile
|
|
7
|
-
* candidates at runtime.
|
|
8
|
-
*/
|
|
9
|
-
export const RUNTIME_EXPORT_SCRIPT = String.raw `
|
|
10
|
-
"use strict";
|
|
11
|
-
const input = globalThis.__WOWDUMP_INPUT__ || {};
|
|
12
|
-
const moduleName = typeof input.module === "string" && input.module ? input.module : "Wow.exe";
|
|
13
|
-
const module = Process.findModuleByName(moduleName);
|
|
14
|
-
const maxCandidates = Math.min(Math.max(Number(input.maxCandidates || input.maxEvents || 256), 1), 4096);
|
|
15
|
-
const maxSectionBytes = Math.min(Math.max(Number(input.maxSectionBytes || 128 * 1024 * 1024), 1), 512 * 1024 * 1024);
|
|
16
|
-
const maxTotalBytes = Math.min(Math.max(Number(input.maxTotalBytes || 256 * 1024 * 1024), 1), 768 * 1024 * 1024);
|
|
17
|
-
const chunkSize = Math.min(Math.max(Number(input.chunkSize || 1024 * 1024), 4096), 4 * 1024 * 1024);
|
|
18
|
-
const maxVerifyBytes = Math.min(Math.max(Number(input.maxVerifyBytes || 256), 1), 4096);
|
|
19
|
-
|
|
20
|
-
function readBytes(address, size) {
|
|
21
|
-
const value = address.readByteArray(size);
|
|
22
|
-
return value ? new Uint8Array(value) : null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function bytesHex(address, size) {
|
|
26
|
-
const bytes = readBytes(address, size);
|
|
27
|
-
if (!bytes) return "";
|
|
28
|
-
let output = "";
|
|
29
|
-
for (let index = 0; index < bytes.length; index++) output += bytes[index].toString(16).padStart(2, "0");
|
|
30
|
-
return output;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function ascii(address, size) {
|
|
34
|
-
const bytes = readBytes(address, size);
|
|
35
|
-
if (!bytes) return "";
|
|
36
|
-
let output = "";
|
|
37
|
-
for (let index = 0; index < bytes.length; index++) {
|
|
38
|
-
const value = bytes[index];
|
|
39
|
-
output += value >= 0x20 && value <= 0x7e ? String.fromCharCode(value) : "";
|
|
40
|
-
}
|
|
41
|
-
return output;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function u16(address) { return address.readU16(); }
|
|
45
|
-
function u32(address) { return address.readU32(); }
|
|
46
|
-
function sectionName(address) {
|
|
47
|
-
return ascii(address, 8).replace(/\0/g, "").trim();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function parseSections() {
|
|
51
|
-
if (!module) return { error: "module-missing", sections: [] };
|
|
52
|
-
try {
|
|
53
|
-
const base = module.base;
|
|
54
|
-
if (u16(base) !== 0x5a4d) return { error: "invalid-dos-signature", sections: [] };
|
|
55
|
-
const peOffset = u32(base.add(0x3c));
|
|
56
|
-
if (peOffset < 0x40 || peOffset > module.size - 24) return { error: "invalid-pe-offset", sections: [] };
|
|
57
|
-
const pe = base.add(peOffset);
|
|
58
|
-
if (u32(pe) !== 0x00004550) return { error: "invalid-pe-signature", sections: [] };
|
|
59
|
-
const count = u16(pe.add(6));
|
|
60
|
-
const optionalSize = u16(pe.add(20));
|
|
61
|
-
if (count < 1 || count > 96 || optionalSize < 0x60) return { error: "invalid-section-table", sections: [] };
|
|
62
|
-
const table = pe.add(24 + optionalSize);
|
|
63
|
-
const sections = [];
|
|
64
|
-
for (let index = 0; index < count; index++) {
|
|
65
|
-
const header = table.add(index * 40);
|
|
66
|
-
const name = sectionName(header);
|
|
67
|
-
const virtualSize = u32(header.add(8));
|
|
68
|
-
const rva = u32(header.add(12));
|
|
69
|
-
const rawSize = u32(header.add(16));
|
|
70
|
-
const characteristics = u32(header.add(36));
|
|
71
|
-
const size = Math.max(virtualSize, rawSize);
|
|
72
|
-
if (!name || !size || rva >= module.size) continue;
|
|
73
|
-
const boundedSize = Math.min(size, module.size - rva);
|
|
74
|
-
const executable = (characteristics & 0x20000000) !== 0;
|
|
75
|
-
const writable = (characteristics & 0x80000000) !== 0;
|
|
76
|
-
const readable = (characteristics & 0x40000000) !== 0;
|
|
77
|
-
sections.push({
|
|
78
|
-
name,
|
|
79
|
-
rva: "0x" + rva.toString(16),
|
|
80
|
-
runtimeAddress: base.add(rva).toString(),
|
|
81
|
-
virtualSize,
|
|
82
|
-
rawSize,
|
|
83
|
-
characteristics: "0x" + characteristics.toString(16),
|
|
84
|
-
protection: (readable ? "r" : "") + (writable ? "w" : "") + (executable ? "x" : ""),
|
|
85
|
-
size: boundedSize
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
return { error: null, sections };
|
|
89
|
-
} catch (error) {
|
|
90
|
-
return { error: String(error), sections: [] };
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function dumpSection(section, totalState) {
|
|
95
|
-
const requestedSize = Math.min(section.size, maxSectionBytes);
|
|
96
|
-
const available = Math.max(0, maxTotalBytes - totalState.bytes);
|
|
97
|
-
const readSize = Math.min(requestedSize, available);
|
|
98
|
-
let offset = 0;
|
|
99
|
-
while (offset < readSize) {
|
|
100
|
-
const size = Math.min(chunkSize, readSize - offset);
|
|
101
|
-
const bytes = readBytes(module.base.add(parseInt(section.rva, 16)).add(offset), size);
|
|
102
|
-
if (!bytes) break;
|
|
103
|
-
send({ type: "wowdump.dump.chunk", section: section.name, offset, size: bytes.length }, bytes.buffer);
|
|
104
|
-
offset += bytes.length;
|
|
105
|
-
if (bytes.length !== size) break;
|
|
106
|
-
}
|
|
107
|
-
totalState.bytes += offset;
|
|
108
|
-
return { ...section, requestedSize, readSize: offset, truncated: offset < section.size };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function candidateEntries(profile) {
|
|
112
|
-
const entries = [];
|
|
113
|
-
const fields = profile && typeof profile.fields === "object" && !Array.isArray(profile.fields) ? profile.fields : {};
|
|
114
|
-
for (const [name, value] of Object.entries(fields)) entries.push({ name, value });
|
|
115
|
-
return entries.slice(0, maxCandidates);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function candidateRva(value) {
|
|
119
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
120
|
-
const item = value;
|
|
121
|
-
const root = item.root && typeof item.root === "object" && !Array.isArray(item.root) ? item.root : {};
|
|
122
|
-
const rva = item.rva ?? root.rva;
|
|
123
|
-
if (typeof rva !== "string" && typeof rva !== "number") return null;
|
|
124
|
-
try { return ptr(String(rva)).toString(); } catch (_) { return null; }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function collect() {
|
|
128
|
-
if (!module) return { ok: false, kind: input.kind || "dump", module: { name: moduleName }, sections: [], records: [], evidence: [{ source: "frida", kind: "module-missing" }] };
|
|
129
|
-
if (input.kind === "dump") {
|
|
130
|
-
const parsed = parseSections();
|
|
131
|
-
if (parsed.error) return { ok: false, kind: "dump", module: { name: module.name, moduleBase: module.base.toString(), moduleSize: module.size }, sections: [], records: [], evidence: [{ source: "frida", kind: "pe-parse-failed", error: parsed.error }] };
|
|
132
|
-
const requestedSections = Array.isArray(input.sections) && input.sections.length > 0
|
|
133
|
-
? input.sections.map(value => String(value).toLowerCase())
|
|
134
|
-
: [".text", ".rdata", ".pdata"];
|
|
135
|
-
const selected = parsed.sections.filter(section => requestedSections.includes(section.name.toLowerCase()));
|
|
136
|
-
const totalState = { bytes: 0 };
|
|
137
|
-
const sections = selected.map(section => dumpSection(section, totalState));
|
|
138
|
-
return { ok: true, kind: "dump", module: { name: module.name, moduleBase: module.base.toString(), moduleSize: module.size, path: module.path }, sections, totalBytes: totalState.bytes, limits: { maxSectionBytes, maxTotalBytes, chunkSize }, truncated: sections.some(section => section.truncated), evidence: [{ source: "frida", kind: "frida-runtime-pe-sections", sections: sections.map(section => section.name), totalBytes: totalState.bytes }] };
|
|
139
|
-
}
|
|
140
|
-
if (input.kind !== "verify") return { ok: false, kind: input.kind || null, module: { name: module.name }, records: [], evidence: [{ source: "frida", kind: "unsupported-runtime-kind" }] };
|
|
141
|
-
const profile = input.profile && typeof input.profile === "object" ? input.profile : null;
|
|
142
|
-
if (!profile) return { ok: false, kind: "verify", module: { name: module.name, moduleBase: module.base.toString() }, records: [], evidence: [{ source: "frida", kind: "profile-required" }] };
|
|
143
|
-
const records = [];
|
|
144
|
-
for (const entry of candidateEntries(profile)) {
|
|
145
|
-
const rva = candidateRva(entry.value);
|
|
146
|
-
if (rva === null) { records.push({ name: entry.name, ok: false, reason: "missing-rva", source: "frida" }); continue; }
|
|
147
|
-
const address = module.base.add(ptr(rva));
|
|
148
|
-
try {
|
|
149
|
-
const bytes = bytesHex(address, Math.min(Number(entry.value.size || entry.value.byteSize || maxVerifyBytes), maxVerifyBytes));
|
|
150
|
-
records.push({ name: entry.name, rva, runtimeAddress: address.toString(), bytesHex: bytes, bytesRead: bytes.length / 2, complete: bytes.length > 0, ok: bytes.length > 0, source: "frida" });
|
|
151
|
-
} catch (error) { records.push({ name: entry.name, rva, runtimeAddress: address.toString(), ok: false, reason: String(error), source: "frida" }); }
|
|
152
|
-
}
|
|
153
|
-
return { ok: true, kind: "verify", module: { name: module.name, moduleBase: module.base.toString(), moduleSize: module.size, path: module.path }, records, evidence: [{ source: "frida", kind: "frida-runtime-verify", candidates: records.length, passed: records.filter(item => item.ok).length }] };
|
|
154
|
-
}
|
|
155
|
-
rpc.exports = { collect };
|
|
156
|
-
`;
|
package/dist/frida-worker.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { createInterface } from "node:readline";
|
|
3
|
-
import { FridaCommandRuntime } from "./analysis/frida-runtime.js";
|
|
4
|
-
import { RuntimeDumpWriter } from "./analysis/runtime-dump.js";
|
|
5
|
-
import { isAbsolute, resolve } from "node:path";
|
|
6
|
-
function record(value) {
|
|
7
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8
|
-
}
|
|
9
|
-
function positive(value, name) {
|
|
10
|
-
const number = Number(value);
|
|
11
|
-
if (!Number.isSafeInteger(number) || number < 1)
|
|
12
|
-
throw new Error(`${name} must be a positive integer`);
|
|
13
|
-
return number;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Elevated Frida execution boundary. Analysis belongs to the caller-supplied
|
|
17
|
-
* GumJS source; this process only attaches, loads, calls, and cleans it up.
|
|
18
|
-
*/
|
|
19
|
-
async function run(input) {
|
|
20
|
-
if (input.command !== "dynamic-script" && input.command !== "runtime-dump")
|
|
21
|
-
throw new Error("unsupported worker command");
|
|
22
|
-
const pid = positive(input.pid, "pid");
|
|
23
|
-
const buildKey = String(input.build ?? input.buildKey ?? "").trim();
|
|
24
|
-
if (!buildKey)
|
|
25
|
-
throw new Error("build is required");
|
|
26
|
-
const source = typeof input.source === "string"
|
|
27
|
-
? input.source
|
|
28
|
-
: typeof input.script === "string" ? await readFile(input.script, "utf8") : "";
|
|
29
|
-
if (!source.trim())
|
|
30
|
-
throw new Error("script or source is required");
|
|
31
|
-
const runtime = new FridaCommandRuntime({ artifactDir: process.env.WOW_ANALYZE_DIR });
|
|
32
|
-
let sessionId;
|
|
33
|
-
let loaded = false;
|
|
34
|
-
const isDump = input.command === "runtime-dump";
|
|
35
|
-
const outputDirectory = isDump ? resolve(String(input.outputDir ?? "")) : undefined;
|
|
36
|
-
const outputDirectoryInput = isDump ? String(input.outputDir ?? "") : "";
|
|
37
|
-
const writer = isDump ? new RuntimeDumpWriter({
|
|
38
|
-
outputDirectory: outputDirectory,
|
|
39
|
-
buildKey,
|
|
40
|
-
pid,
|
|
41
|
-
executableSha256: typeof input.executableSha256 === "string" ? input.executableSha256 : null,
|
|
42
|
-
preferredImageBase: typeof input.preferredImageBase === "string" ? input.preferredImageBase : null,
|
|
43
|
-
moduleSize: Number.isSafeInteger(Number(input.moduleSize)) ? Number(input.moduleSize) : null,
|
|
44
|
-
dumpParameters: input.dumpParameters && typeof input.dumpParameters === "object" ? input.dumpParameters : undefined
|
|
45
|
-
}) : undefined;
|
|
46
|
-
let finalized = false;
|
|
47
|
-
try {
|
|
48
|
-
if (isDump) {
|
|
49
|
-
if (!outputDirectoryInput || !isAbsolute(outputDirectoryInput))
|
|
50
|
-
throw new Error("runtime dump outputDir must be an absolute path");
|
|
51
|
-
await writer.initialize();
|
|
52
|
-
}
|
|
53
|
-
const attached = await runtime.execute({ operation: "attach", pid, buildKey, allowUnmatched: true });
|
|
54
|
-
sessionId = String(attached.sessionId);
|
|
55
|
-
const modules = await runtime.execute({ operation: "modules", sessionId, pid, buildKey });
|
|
56
|
-
const module = Array.isArray(modules.modules)
|
|
57
|
-
? modules.modules.find(item => String(item.name ?? "").toLowerCase() === "wow.exe")
|
|
58
|
-
: undefined;
|
|
59
|
-
const scriptSource = `globalThis.__WOWDUMP_INPUT__ = Object.freeze(${JSON.stringify(input.args ?? {})});\n${source}`;
|
|
60
|
-
let value;
|
|
61
|
-
let messages = [];
|
|
62
|
-
if (isDump) {
|
|
63
|
-
await writeFile(resolve(outputDirectory, "..", "target.json"), `${JSON.stringify({ schema: "wowdump.runtime-target.v1", pid, buildKey, module, executableSha256: input.executableSha256 ?? null, createdAt: new Date().toISOString() }, null, 2)}\n`, "utf8").catch(() => undefined);
|
|
64
|
-
let progressBytes = 0;
|
|
65
|
-
const streamed = await runtime.streamScriptCall({
|
|
66
|
-
operation: "script_load",
|
|
67
|
-
sessionId,
|
|
68
|
-
pid,
|
|
69
|
-
buildKey,
|
|
70
|
-
source: scriptSource,
|
|
71
|
-
exportName: typeof input.exportName === "string" ? input.exportName : "collect"
|
|
72
|
-
}, undefined, async (message, data) => {
|
|
73
|
-
const envelope = message && typeof message === "object" ? message : {};
|
|
74
|
-
if (envelope.type !== "send" || !data)
|
|
75
|
-
return;
|
|
76
|
-
const payload = envelope.payload && typeof envelope.payload === "object" ? envelope.payload : {};
|
|
77
|
-
if (payload.type !== "wowdump.dump.chunk")
|
|
78
|
-
return;
|
|
79
|
-
await writer.writeChunk(payload, data);
|
|
80
|
-
progressBytes += data.byteLength;
|
|
81
|
-
if (input.progress !== false)
|
|
82
|
-
process.stderr.write(`wowdump dump ${progressBytes} bytes\\n`);
|
|
83
|
-
});
|
|
84
|
-
value = streamed.value;
|
|
85
|
-
const result = value && typeof value === "object" ? value : {};
|
|
86
|
-
if (result.ok !== true)
|
|
87
|
-
throw new Error(String(result.error ?? "runtime dump failed"));
|
|
88
|
-
const persisted = await writer.finalize({ ...result, module: module ?? result.module });
|
|
89
|
-
finalized = true;
|
|
90
|
-
await writeFile(resolve(outputDirectory, "..", "dynamic.json"), `${JSON.stringify({ ...result, manifestFile: persisted.manifestFile, generatedAt: new Date().toISOString() }, null, 2)}\n`, "utf8").catch(() => undefined);
|
|
91
|
-
return { ok: true, command: "analyze.runtime.dump", pid, buildKey, manifestFile: persisted.manifestFile, outputDirectory, manifest: persisted.manifest, sections: persisted.sections };
|
|
92
|
-
}
|
|
93
|
-
await runtime.execute({
|
|
94
|
-
operation: "script_load",
|
|
95
|
-
sessionId,
|
|
96
|
-
pid,
|
|
97
|
-
buildKey,
|
|
98
|
-
source: scriptSource,
|
|
99
|
-
scriptId: "dynamic"
|
|
100
|
-
});
|
|
101
|
-
loaded = true;
|
|
102
|
-
if (typeof input.exportName === "string" && input.exportName.trim()) {
|
|
103
|
-
const called = await runtime.execute({
|
|
104
|
-
operation: "script_call",
|
|
105
|
-
sessionId,
|
|
106
|
-
scriptId: "dynamic",
|
|
107
|
-
exportName: input.exportName,
|
|
108
|
-
args: Array.isArray(input.callArgs) ? input.callArgs : []
|
|
109
|
-
});
|
|
110
|
-
value = called.value;
|
|
111
|
-
messages = Array.isArray(called.messages) ? called.messages : [];
|
|
112
|
-
}
|
|
113
|
-
const durationMs = Math.min(120_000, positive(input.durationMs ?? 100, "duration-ms"));
|
|
114
|
-
if (value === undefined)
|
|
115
|
-
await new Promise(resolve => setTimeout(resolve, durationMs));
|
|
116
|
-
return {
|
|
117
|
-
ok: true,
|
|
118
|
-
command: "analyze.dynamic",
|
|
119
|
-
pid,
|
|
120
|
-
buildKey,
|
|
121
|
-
module: module && typeof module.base === "string"
|
|
122
|
-
? { name: module.name, moduleBase: module.base, moduleSize: module.size ?? null }
|
|
123
|
-
: null,
|
|
124
|
-
exportName: typeof input.exportName === "string" ? input.exportName : null,
|
|
125
|
-
value,
|
|
126
|
-
messages,
|
|
127
|
-
script: typeof input.script === "string" ? input.script : "<inline>",
|
|
128
|
-
evidence: [{ kind: "frida-gumjs", source: "frida", moduleBase: module?.base ?? null }]
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
finally {
|
|
132
|
-
if (sessionId && loaded)
|
|
133
|
-
await runtime.execute({ operation: "script_unload", sessionId, scriptId: "dynamic" }).catch(() => undefined);
|
|
134
|
-
if (isDump && writer && !finalized)
|
|
135
|
-
await writer.abort().catch(() => undefined);
|
|
136
|
-
await runtime.close();
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
140
|
-
let failed = false;
|
|
141
|
-
for await (const line of input) {
|
|
142
|
-
if (!line.trim())
|
|
143
|
-
continue;
|
|
144
|
-
try {
|
|
145
|
-
process.stdout.write(`${JSON.stringify(await run(record(JSON.parse(line))))}\n`);
|
|
146
|
-
}
|
|
147
|
-
catch (error) {
|
|
148
|
-
failed = true;
|
|
149
|
-
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
if (failed)
|
|
153
|
-
process.exitCode = 1;
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# Dynamic GumJS 编写指南
|
|
2
|
-
|
|
3
|
-
## 目标与优先级
|
|
4
|
-
|
|
5
|
-
dynamic 是默认主路径,负责从当前运行进程取得真实值和线索:模块基址、对象地址、函数地址、字节样本、字符串、返回值或事件证据。脚本可以使用静态阶段给出的候选 RVA,也可以自己做有界签名扫描;不要把未经验证的绝对地址当成长期 profile。
|
|
6
|
-
|
|
7
|
-
## 脚本入口
|
|
8
|
-
|
|
9
|
-
通过 `rpc.exports` 暴露一个小而明确的函数,例如 `collect`。CLI 的 `--args` 会放在 `globalThis.__WOWDUMP_INPUT__`,字段列表和读取上限从这里取得。
|
|
10
|
-
|
|
11
|
-
API 参考: [Frida JavaScript API](https://frida.re/docs/javascript-api/)。
|
|
12
|
-
|
|
13
|
-
一次普通 run 由 CLI 完成 attach 并加载脚本,脚本在同一次执行中完成 Hook、采集、限时和 cleanup。需要持续观察时,由脚本内部维护事件队列、采样计时器和清理逻辑,在一次 run 结束时返回结果;不增加单独的 `analyze focus` 命令,也暂不实现跨命令的 status、pause 或 checkpoint。
|
|
14
|
-
|
|
15
|
-
轻量模式保持不变:
|
|
16
|
-
|
|
17
|
-
```text
|
|
18
|
-
wowdump analyze dynamic --script <GumJS> --confirm
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
`--script` 必须由 Agent 显式提供。仓库中的 `scripts/dynamic-session.js` 只是可复制和修改的示例,不是 CLI 默认脚本,也不会被自动执行。
|
|
22
|
-
|
|
23
|
-
## 允许的操作
|
|
24
|
-
|
|
25
|
-
- 枚举模块和导出
|
|
26
|
-
- 在指定模块或范围内扫描模式
|
|
27
|
-
- 读取有界内存并返回十六进制样本
|
|
28
|
-
- 短时 Hook、回调或事件监听
|
|
29
|
-
- 返回候选地址、RVA、对象关系和置信度
|
|
30
|
-
|
|
31
|
-
每次脚本都要设置读取范围、最大 Hook 数、最大事件数和结束时间。默认禁止 `Stalker`;除非用户明确要求并给出极小范围、时长和事件上限,否则不要启用它。Hook、事件和内存读取都必须有上限。
|
|
32
|
-
|
|
33
|
-
## 输出要求
|
|
34
|
-
|
|
35
|
-
```json
|
|
36
|
-
{
|
|
37
|
-
"buildKey": "retail@12.1.0.69587",
|
|
38
|
-
"pid": 33976,
|
|
39
|
-
"module": { "name": "Wow.exe", "moduleBase": "0x7ff7b5040000" },
|
|
40
|
-
"fields": {
|
|
41
|
-
"playerAuras": [{ "address": "0x...", "bytesHex": "...", "source": "frida" }]
|
|
42
|
-
},
|
|
43
|
-
"evidence": [{ "source": "frida", "kind": "runtime-sample" }]
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
输出应足够让 IDA Pro MCP 或 iced-x86 在样本附近定位函数、字符串和 XREF,也应保留 Hook 的 `this`、参数、返回值和访问到的对象地址。拿不到字段时返回空数组和原因,不填充猜测地址。
|
|
48
|
-
|
|
49
|
-
## 与静态分析交替
|
|
50
|
-
|
|
51
|
-
- 已有候选 RVA:优先直接 Hook,观察真实调用和返回值。
|
|
52
|
-
- 没有候选 RVA:先做小范围模块/字节扫描;仍不明确时导出最小 text 样本交给 IDA 或 iced。
|
|
53
|
-
- 静态给出候选后回到 dynamic 验证,不要求一次静态分析解决全部字段。
|
|
54
|
-
- 运行时值、用户触发动作和短时事件优先由 Frida 完成;长期稳定的 RVA、指针链和类型才需要静态证据。
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copy-and-edit example for `wowdump analyze dynamic`.
|
|
3
|
-
* It is intentionally not selected or executed by the CLI automatically.
|
|
4
|
-
*/
|
|
5
|
-
"use strict";
|
|
6
|
-
|
|
7
|
-
const input = globalThis.__WOWDUMP_INPUT__ && typeof globalThis.__WOWDUMP_INPUT__ === "object"
|
|
8
|
-
? globalThis.__WOWDUMP_INPUT__
|
|
9
|
-
: {};
|
|
10
|
-
|
|
11
|
-
const limit = (value, fallback, maximum) => {
|
|
12
|
-
const number = Number(value);
|
|
13
|
-
if (!Number.isFinite(number) || number < 1) return fallback;
|
|
14
|
-
return Math.min(Math.floor(number), maximum);
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const durationMs = limit(input.durationMs, 5000, 120000);
|
|
18
|
-
const maxEvents = limit(input.maxEvents, 100, 10000);
|
|
19
|
-
const maxHooks = limit(input.maxHooks, 1, 32);
|
|
20
|
-
const maxReadBytes = limit(input.maxReadBytes, 64, 256);
|
|
21
|
-
const events = [];
|
|
22
|
-
const candidates = [];
|
|
23
|
-
const errors = [];
|
|
24
|
-
const hooks = [];
|
|
25
|
-
let stopWaiting;
|
|
26
|
-
|
|
27
|
-
function record(event) {
|
|
28
|
-
if (events.length >= maxEvents) return false;
|
|
29
|
-
events.push({ timestamp: Date.now(), ...event });
|
|
30
|
-
if (events.length >= maxEvents && stopWaiting) stopWaiting();
|
|
31
|
-
return events.length < maxEvents;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function candidateAddress(candidate, module) {
|
|
35
|
-
if (!candidate || typeof candidate !== "object") return null;
|
|
36
|
-
if (candidate.address !== undefined) return ptr(String(candidate.address));
|
|
37
|
-
if (candidate.rva !== undefined) return module.base.add(ptr(String(candidate.rva)));
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function inModule(address, module) {
|
|
42
|
-
const end = module.base.add(module.size);
|
|
43
|
-
return address.compare(module.base) >= 0 && address.compare(end) < 0;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function sampleBytes(address) {
|
|
47
|
-
try {
|
|
48
|
-
const range = Process.findRangeByAddress(address, "r--");
|
|
49
|
-
if (!range) return null;
|
|
50
|
-
const bytes = Memory.readByteArray(address, Math.min(maxReadBytes, range.size));
|
|
51
|
-
return bytes ? Array.from(new Uint8Array(bytes), value => value.toString(16).padStart(2, "0")).join("") : null;
|
|
52
|
-
} catch (error) {
|
|
53
|
-
errors.push(`read ${address}: ${error}`);
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function installHook(address, name) {
|
|
59
|
-
if (hooks.length >= maxHooks) return;
|
|
60
|
-
try {
|
|
61
|
-
const listener = Interceptor.attach(address, {
|
|
62
|
-
onEnter(args) {
|
|
63
|
-
record({ kind: "enter", name, address: address.toString(), firstArg: args[0] ? args[0].toString() : null });
|
|
64
|
-
},
|
|
65
|
-
onLeave(retval) {
|
|
66
|
-
record({ kind: "leave", name, address: address.toString(), returnValue: retval.toString() });
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
hooks.push(listener);
|
|
70
|
-
} catch (error) {
|
|
71
|
-
errors.push(`hook ${name}: ${error}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
rpc.exports = {
|
|
76
|
-
async collect() {
|
|
77
|
-
const moduleName = typeof input.module === "string" && input.module.trim() ? input.module : "Wow.exe";
|
|
78
|
-
const module = Process.findModuleByName(moduleName);
|
|
79
|
-
if (!module) {
|
|
80
|
-
return { ok: false, module: { name: moduleName }, candidates: [], events: [], evidence: [], errors: [`module ${moduleName} was not found`] };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const requested = Array.isArray(input.candidates) ? input.candidates.slice(0, maxHooks) : [];
|
|
84
|
-
const resolved = [];
|
|
85
|
-
for (const [index, value] of requested.entries()) {
|
|
86
|
-
try {
|
|
87
|
-
const address = candidateAddress(value, module);
|
|
88
|
-
if (!address || !inModule(address, module)) {
|
|
89
|
-
errors.push(`candidate ${index} is outside ${moduleName}`);
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
const item = {
|
|
93
|
-
name: value && typeof value.name === "string" ? value.name : `candidate_${index}`,
|
|
94
|
-
address: address.toString(),
|
|
95
|
-
rva: address.sub(module.base).toString(),
|
|
96
|
-
bytesHex: sampleBytes(address),
|
|
97
|
-
source: "frida"
|
|
98
|
-
};
|
|
99
|
-
candidates.push(item);
|
|
100
|
-
resolved.push({ address, name: item.name });
|
|
101
|
-
} catch (error) {
|
|
102
|
-
errors.push(`candidate ${index}: ${error}`);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
for (const item of resolved) installHook(item.address, item.name);
|
|
107
|
-
let timer;
|
|
108
|
-
try {
|
|
109
|
-
await new Promise(resolve => {
|
|
110
|
-
stopWaiting = resolve;
|
|
111
|
-
timer = setTimeout(resolve, durationMs);
|
|
112
|
-
});
|
|
113
|
-
} finally {
|
|
114
|
-
stopWaiting = undefined;
|
|
115
|
-
if (timer) clearTimeout(timer);
|
|
116
|
-
for (const listener of hooks.splice(0)) {
|
|
117
|
-
try { listener.detach(); } catch (error) { errors.push(`cleanup: ${error}`); }
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return {
|
|
122
|
-
ok: errors.length === 0,
|
|
123
|
-
module: { name: module.name, moduleBase: module.base.toString(), moduleSize: module.size },
|
|
124
|
-
moduleBase: module.base.toString(),
|
|
125
|
-
candidates,
|
|
126
|
-
events,
|
|
127
|
-
truncated: events.length >= maxEvents,
|
|
128
|
-
limits: { durationMs, maxEvents, maxHooks, maxReadBytes },
|
|
129
|
-
evidence: [{ source: "frida", kind: "dynamic-session", moduleBase: module.base.toString(), eventCount: events.length, hookCount: candidates.length }],
|
|
130
|
-
errors
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
};
|