wowdump 0.3.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 +21 -21
- package/README.md +21 -53
- package/dist/adapters/reader.js +33 -0
- package/dist/analysis/disassemble.js +77 -0
- package/dist/{frida-runtime.js → analysis/frida-runtime.js} +48 -48
- package/dist/analysis/runtime-script.js +36 -0
- package/dist/cli.js +255 -189
- package/dist/core/profile-engine.js +238 -0
- package/dist/frida-worker.js +54 -55
- package/dist/{reader-broker.js → reader/broker.js} +15 -0
- package/dist/{reader-client.js → reader/client.js} +1 -1
- package/dist/{windows-launcher.js → reader/launcher.js} +16 -4
- 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 +1 -66
- package/dist/toolchain.js +102 -573
- package/package.json +9 -10
- package/skills/wowdump/SKILL.md +22 -15
- 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 -1332
- package/dist/discovery.js +0 -48
- package/dist/dry-run.js +0 -36
- package/dist/error-log.js +0 -71
- package/dist/focused-session.js +0 -89
- package/dist/ghidra.js +0 -769
- package/dist/main.js +0 -66
- package/dist/observability.js +0 -41
- package/dist/processes.js +0 -44
- package/dist/session.js +0 -42
- package/dist/storage.js +0 -12
- package/dist/windows-reader.js +0 -102
- package/dist/wow-analysis.js +0 -1405
- package/skills/wowdump/commands.md +0 -44
- /package/dist/{adapters.js → core/build-adapters.js} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Follen
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Follen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,70 +1,38 @@
|
|
|
1
1
|
# wowdump
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
the `wowdump` CLI; MCP and Frida remain optional integration paths.
|
|
3
|
+
WoW 原生内存分析 CLI。用 Frida 运行时证据和 IDA Pro/iced-x86 局部反汇编定位字段,生成可复核的 Reader profile。
|
|
5
4
|
|
|
6
|
-
##
|
|
5
|
+
## 安装
|
|
7
6
|
|
|
8
7
|
```powershell
|
|
9
|
-
npm install
|
|
10
|
-
npm run build
|
|
11
|
-
$env:WOWDUMP_HOME='C:\Users\follen\.wowdump'
|
|
12
|
-
wowdump --help
|
|
8
|
+
npm install --global wowdump
|
|
13
9
|
wowdump init
|
|
14
|
-
$env:WOW_ROOT='D:\Game\World of Warcraft'
|
|
15
|
-
npm run discover
|
|
16
|
-
npm run dry-run
|
|
17
|
-
npm start
|
|
18
10
|
```
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
`commands.md` reference under `%USERPROFILE%\.agents\skills\wowdump`, and detects
|
|
22
|
-
an existing Java/Ghidra installation. If either tool is missing, the postinstall
|
|
23
|
-
bootstrap prepares a pinned copy under `%WOWDUMP_HOME%\toolchains` (set
|
|
24
|
-
`WOWDUMP_SKIP_TOOLCHAIN_DOWNLOAD=1` to skip downloads and configure paths
|
|
25
|
-
manually). Use `WOWDUMP_SKILL_HOME` to choose another Skill root.
|
|
26
|
-
|
|
27
|
-
## Analysis CLI
|
|
12
|
+
安装脚本会初始化 `~/.wowdump`,并把 Skill 与 references 参考文档安装到 `~/.agents/skills/wowdump`。反汇编保底使用 npm 内置的 `iced-x86`。
|
|
28
13
|
|
|
29
|
-
|
|
30
|
-
disk executable. It does not attach to a running game or require elevation:
|
|
14
|
+
## 基本流程
|
|
31
15
|
|
|
32
16
|
```powershell
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```
|
|
17
|
+
# 1. 导出运行时模块或 provider
|
|
18
|
+
wowdump analyze runtime --pid 1234 --build "retail@VERSION" --kind text --duration-ms 5000 --max-events 100 --confirm > runtime.json
|
|
36
19
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
```powershell
|
|
40
|
-
wowdump analyze runtime-export --pid 1234 --build "retail@VERSION" `
|
|
41
|
-
--static-profile .wowdump\profiles\static.json --kind providers --confirm
|
|
42
|
-
wowdump analyze verify --static-profile .wowdump\profiles\static.json `
|
|
43
|
-
--runtime-export .wowdump\runtime\export.json
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
The static profile supplies candidate RVAs. The runtime export records the live
|
|
47
|
-
module base and `runtimeAddress = moduleBase + rva`; after verification, persist
|
|
48
|
-
only the passing records in a `reader_ready` profile and use the reader for
|
|
49
|
-
subsequent polling.
|
|
20
|
+
# 2. 通过 IDA Pro MCP(若可用)或 iced-x86 局部反汇编生成 profile
|
|
21
|
+
wowdump analyze disassemble --exe "C:\Games\World of Warcraft\_retail_\Wow.exe" --build "retail@VERSION" --runtime-export "$HOME\.wowdump\runtime\<session-id>\text.json" --output "$HOME\.wowdump\runtime\<session-id>\candidate.profile.json"
|
|
50
22
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
`127.0.0.1` behind a random 256-bit token. Later CLI processes reuse the same
|
|
55
|
-
broker and monitor state until 20 minutes of idle time elapse. No PowerShell
|
|
56
|
-
script or native reader command is a user entry point.
|
|
23
|
+
# 3. 用户确认后,把 profile 保存到当前 build 的 profile 目录,再读取
|
|
24
|
+
wowdump memory read --pid 1234 --profile "$HOME\.wowdump\retail@VERSION\profile\player-state.json" --field playerAuras
|
|
25
|
+
```
|
|
57
26
|
|
|
58
|
-
|
|
59
|
-
each `_retail_`, `_beta_`, `_classic_`, and other directory containing `Wow.exe`.
|
|
60
|
-
The collector polls `Wow.exe` processes, matches each executable path to its
|
|
61
|
-
install, and creates one Frida session per PID.
|
|
27
|
+
Windows reader 首次使用时由 Node 请求 UAC 启动 broker,后续调用复用同一 broker,空闲 20 分钟后退出。Frida 运行时导出需要显式 `--confirm`;高开销 Stalker 默认关闭。
|
|
62
28
|
|
|
63
|
-
|
|
64
|
-
loading the Frida session or writing `errors.jsonl`. Each discovered install and
|
|
65
|
-
live process includes its `flavor`, `buildKey`, and `adapterStatus`. Builds with
|
|
66
|
-
no verified adapter are reported as `unsupported` with an `adapterReason`.
|
|
29
|
+
当前 build 的持久化 profile 放在 `~/.wowdump/<buildKey>/profile/`;动态导出、静态候选和验证报告放在 runtime 会话临时目录。完整字段和 JSON 约定见 `~/.agents/skills/wowdump/references/commands.md`、`references/request-schema.md` 与 `references/profiles.md`;短时 Hook 示例见 `~/.agents/skills/wowdump/scripts/dynamic-session.js`。
|
|
67
30
|
|
|
68
|
-
|
|
69
|
-
adapter lookup, dry-run formatting, and CIM process-row parsing.
|
|
31
|
+
## 开发验证
|
|
70
32
|
|
|
33
|
+
```powershell
|
|
34
|
+
npm run typecheck
|
|
35
|
+
npm run build
|
|
36
|
+
npm test
|
|
37
|
+
npm pack --dry-run
|
|
38
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function record(value) {
|
|
2
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3
|
+
}
|
|
4
|
+
/** Adapts the elevated reader broker to ProfileEngine's minimal memory API. */
|
|
5
|
+
export class ReaderProfileAdapter {
|
|
6
|
+
invoke;
|
|
7
|
+
constructor(invoke) {
|
|
8
|
+
this.invoke = invoke;
|
|
9
|
+
}
|
|
10
|
+
async modules(pid) {
|
|
11
|
+
const response = record(await this.invoke({ command: "modules", payload: { pid } }));
|
|
12
|
+
if (response.ok !== true)
|
|
13
|
+
throw new Error(String(record(response.error).message ?? "module enumeration failed"));
|
|
14
|
+
const values = Array.isArray(response.modules) ? response.modules : [];
|
|
15
|
+
return values.filter(value => value && typeof value === "object").map(value => {
|
|
16
|
+
const item = value;
|
|
17
|
+
return { name: String(item.name ?? ""), base: String(item.base ?? "0x0"), ...(item.size !== undefined ? { size: Number(item.size) } : {}), ...(item.path !== undefined ? { path: String(item.path) } : {}) };
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async read(pid, address, size) {
|
|
21
|
+
const response = record(await this.invoke({ command: "read", payload: { pid, address, size } }));
|
|
22
|
+
const error = response.error;
|
|
23
|
+
return {
|
|
24
|
+
ok: response.ok === true,
|
|
25
|
+
address: typeof response.address === "string" ? response.address : address,
|
|
26
|
+
requestedSize: Number(response.requestedSize ?? size),
|
|
27
|
+
bytesRead: Number(response.bytesRead ?? 0),
|
|
28
|
+
dataHex: typeof response.dataHex === "string" ? response.dataHex : undefined,
|
|
29
|
+
complete: response.complete === true,
|
|
30
|
+
...(error !== undefined ? { error } : {})
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { Decoder, DecoderOptions, Formatter, FormatterSyntax } from "iced-x86";
|
|
6
|
+
import { inspectPeSections, sha256File } from "../toolchain.js";
|
|
7
|
+
function record(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : {}; }
|
|
8
|
+
function array(value) { return Array.isArray(value) ? value.filter(item => item && typeof item === "object" && !Array.isArray(item)) : []; }
|
|
9
|
+
function hex(value) { if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0)
|
|
10
|
+
return BigInt(value); if (typeof value === "string" && /^0x[0-9a-f]+$/i.test(value.trim()))
|
|
11
|
+
return BigInt(value); return undefined; }
|
|
12
|
+
function hexText(value) { return `0x${value.toString(16)}`; }
|
|
13
|
+
function moduleInfo(runtime) { const module = record(runtime.module); const base = hex(module.moduleBase ?? module.base ?? runtime.moduleBase) ?? 0n; const size = Number(module.moduleSize ?? module.size ?? runtime.moduleSize ?? 0); return { base, size: Number.isFinite(size) ? size : 0, name: String(module.name ?? "Wow.exe") }; }
|
|
14
|
+
function sampleBytes(item) { const value = item.bytesHex ?? item.dataHex; if (typeof value !== "string" || !/^[0-9a-f]*$/i.test(value) || value.length % 2 !== 0)
|
|
15
|
+
return undefined; return Buffer.from(value, "hex"); }
|
|
16
|
+
function sampleRva(item) { return hex(item.rva) ?? (hex(item.address) !== undefined ? hex(item.address) : undefined); }
|
|
17
|
+
function decodeSamples(runtime, maxInstructions) {
|
|
18
|
+
const info = moduleInfo(runtime);
|
|
19
|
+
const samples = [...array(runtime.samples), ...array(runtime.records)];
|
|
20
|
+
const formatter = new Formatter(FormatterSyntax.Masm);
|
|
21
|
+
const output = [];
|
|
22
|
+
let remaining = maxInstructions;
|
|
23
|
+
for (const sample of samples) {
|
|
24
|
+
if (remaining <= 0)
|
|
25
|
+
break;
|
|
26
|
+
const bytes = sampleBytes(sample);
|
|
27
|
+
const raw = sampleRva(sample);
|
|
28
|
+
if (!bytes || raw === undefined)
|
|
29
|
+
continue;
|
|
30
|
+
const rva = raw >= info.base && info.base > 0n ? raw - info.base : raw;
|
|
31
|
+
const ip = info.base + rva;
|
|
32
|
+
const decoder = new Decoder(64, bytes, DecoderOptions.None);
|
|
33
|
+
decoder.ip = ip;
|
|
34
|
+
const instructions = [];
|
|
35
|
+
while (remaining > 0 && decoder.canDecode) {
|
|
36
|
+
const instruction = decoder.decode();
|
|
37
|
+
if (instruction.code === 0)
|
|
38
|
+
break;
|
|
39
|
+
instructions.push(formatter.format(instruction));
|
|
40
|
+
remaining--;
|
|
41
|
+
}
|
|
42
|
+
output.push({ rva: hexText(rva), address: hexText(ip), bytesHex: bytes.toString("hex"), instructions, source: "frida-text" });
|
|
43
|
+
}
|
|
44
|
+
return output;
|
|
45
|
+
}
|
|
46
|
+
function fieldCandidates(runtime, ida) {
|
|
47
|
+
const candidates = {};
|
|
48
|
+
const source = { ...record(runtime.fields), ...record(ida.fields) };
|
|
49
|
+
for (const [name, value] of Object.entries(source)) {
|
|
50
|
+
const item = record(value);
|
|
51
|
+
const rva = item.rva ?? record(item.root).rva;
|
|
52
|
+
const type = item.type ?? (record(item.layout).type);
|
|
53
|
+
const layout = item.layout;
|
|
54
|
+
if (rva !== undefined && (typeof type === "string" || (layout && typeof layout === "object" && !Array.isArray(layout))))
|
|
55
|
+
candidates[name] = { ...item, root: item.root ?? { rva }, ...(type ? { type } : {}), evidence: [...array(item.evidence), { source: ida && Object.keys(ida).length ? "ida-pro-mcp" : "iced-x86", kind: "runtime-guided" }] };
|
|
56
|
+
}
|
|
57
|
+
return candidates;
|
|
58
|
+
}
|
|
59
|
+
export async function runDisassembly(options) {
|
|
60
|
+
const executable = resolve(options.exe);
|
|
61
|
+
await access(executable, constants.R_OK);
|
|
62
|
+
if (!options.buildKey?.trim())
|
|
63
|
+
throw new Error("buildKey is required");
|
|
64
|
+
const runtime = record(JSON.parse(await readFile(resolve(options.runtimeExport), "utf8")));
|
|
65
|
+
const ida = options.idaEvidence ? record(JSON.parse(await readFile(resolve(options.idaEvidence), "utf8"))) : {};
|
|
66
|
+
const info = moduleInfo(runtime);
|
|
67
|
+
const decoded = decodeSamples(runtime, Math.min(Math.max(options.maxInstructions ?? 256, 1), 10_000));
|
|
68
|
+
const sections = inspectPeSections(await readFile(executable));
|
|
69
|
+
const fields = fieldCandidates(runtime, ida);
|
|
70
|
+
const ready = Object.keys(fields).length > 0 && Object.values(fields).every(field => { const value = record(field); return value.root !== undefined && (value.type !== undefined || value.layout !== undefined); });
|
|
71
|
+
const outputFile = resolve(options.output ?? `${executable}.${options.buildKey.replace(/[^A-Za-z0-9_.@-]+/g, "-")}.profile.json`);
|
|
72
|
+
const profile = { schema: "wowdump.profile.v1", id: `${options.buildKey}-runtime`, buildKey: options.buildKey, readerStatus: ready ? "reader_ready" : "candidate", confidence: ready ? "confirmed" : "candidate", module: { name: info.name, rvaBase: "0x0" }, executable: { path: executable, sha256: await sha256File(executable) }, sections, fields, evidence: [{ source: "frida", kind: "runtime-text", samples: decoded.length }, { source: options.idaEvidence ? "ida-pro-mcp" : "iced-x86", kind: "disassembly", instructions: decoded.length }], analysis: { engine: options.idaEvidence ? "ida-pro-mcp+iced-x86" : "iced-x86", generatedAt: new Date().toISOString() } };
|
|
73
|
+
const disassembly = { module: { name: info.name, moduleBase: hexText(info.base), moduleSize: info.size }, sections, instructions: decoded, idaEvidence: options.idaEvidence ?? null };
|
|
74
|
+
if (!options.dryRun)
|
|
75
|
+
await writeFile(outputFile, `${JSON.stringify(profile, null, 2)}\n`, "utf8");
|
|
76
|
+
return { ok: true, command: "analyze.disassemble", engine: options.idaEvidence ? "ida-pro-mcp" : "iced-x86", buildKey: options.buildKey, executable, outputFile, profile, disassembly };
|
|
77
|
+
}
|
|
@@ -9,54 +9,54 @@ const DEFAULT_SCRIPT_TIMEOUT_MS = 150;
|
|
|
9
9
|
* `script_load` still accept native GumJS, so a new WoW build can use any
|
|
10
10
|
* Frida API without requiring a host release for every new command.
|
|
11
11
|
*/
|
|
12
|
-
const BRIDGE_SOURCE = String.raw `
|
|
13
|
-
function __wowBytes(value) {
|
|
14
|
-
return value == null ? [] : Array.from(new Uint8Array(value));
|
|
15
|
-
}
|
|
16
|
-
function __wowModule(value) {
|
|
17
|
-
return { name: value.name, base: value.base.toString(), size: value.size, path: value.path };
|
|
18
|
-
}
|
|
19
|
-
function __wowExport(value) {
|
|
20
|
-
return { type: value.type, name: value.name, address: value.address.toString(), moduleName: value.moduleName };
|
|
21
|
-
}
|
|
22
|
-
function __wowRange(value) {
|
|
23
|
-
return { base: value.base.toString(), size: value.size, protection: value.protection, file: value.file ?? null };
|
|
24
|
-
}
|
|
25
|
-
rpc.exports = {
|
|
26
|
-
enumerateModules() {
|
|
27
|
-
return Process.enumerateModules().map(__wowModule);
|
|
28
|
-
},
|
|
29
|
-
enumerateExports(moduleName) {
|
|
30
|
-
const module = Process.getModuleByName(moduleName);
|
|
31
|
-
return module.enumerateExports().map(__wowExport);
|
|
32
|
-
},
|
|
33
|
-
enumerateRanges(protection, coalesce) {
|
|
34
|
-
try {
|
|
35
|
-
return Process.enumerateRanges({ protection: protection || "r--", coalesce: Boolean(coalesce) }).map(__wowRange);
|
|
36
|
-
} catch (_) {
|
|
37
|
-
return Process.enumerateRanges(protection || "r--").map(__wowRange);
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
readMemory(address, size) {
|
|
41
|
-
return __wowBytes(ptr(address).readByteArray(size));
|
|
42
|
-
},
|
|
43
|
-
writeMemory(address, bytes) {
|
|
44
|
-
const target = ptr(address);
|
|
45
|
-
const input = Uint8Array.from(bytes || []);
|
|
46
|
-
const before = __wowBytes(target.readByteArray(input.byteLength));
|
|
47
|
-
target.writeByteArray(input.buffer);
|
|
48
|
-
return { before, after: Array.from(input), address: target.toString(), size: input.byteLength };
|
|
49
|
-
},
|
|
50
|
-
protectMemory(address, size, protection) {
|
|
51
|
-
const changed = Memory.protect(ptr(address), size, protection);
|
|
52
|
-
return { address: ptr(address).toString(), size, protection, changed };
|
|
53
|
-
},
|
|
54
|
-
scanMemory(address, size, pattern) {
|
|
55
|
-
return Memory.scanSync(ptr(address), size, pattern).map(match => ({
|
|
56
|
-
address: match.address.toString(), size: match.size
|
|
57
|
-
}));
|
|
58
|
-
}
|
|
59
|
-
};
|
|
12
|
+
const BRIDGE_SOURCE = String.raw `
|
|
13
|
+
function __wowBytes(value) {
|
|
14
|
+
return value == null ? [] : Array.from(new Uint8Array(value));
|
|
15
|
+
}
|
|
16
|
+
function __wowModule(value) {
|
|
17
|
+
return { name: value.name, base: value.base.toString(), size: value.size, path: value.path };
|
|
18
|
+
}
|
|
19
|
+
function __wowExport(value) {
|
|
20
|
+
return { type: value.type, name: value.name, address: value.address.toString(), moduleName: value.moduleName };
|
|
21
|
+
}
|
|
22
|
+
function __wowRange(value) {
|
|
23
|
+
return { base: value.base.toString(), size: value.size, protection: value.protection, file: value.file ?? null };
|
|
24
|
+
}
|
|
25
|
+
rpc.exports = {
|
|
26
|
+
enumerateModules() {
|
|
27
|
+
return Process.enumerateModules().map(__wowModule);
|
|
28
|
+
},
|
|
29
|
+
enumerateExports(moduleName) {
|
|
30
|
+
const module = Process.getModuleByName(moduleName);
|
|
31
|
+
return module.enumerateExports().map(__wowExport);
|
|
32
|
+
},
|
|
33
|
+
enumerateRanges(protection, coalesce) {
|
|
34
|
+
try {
|
|
35
|
+
return Process.enumerateRanges({ protection: protection || "r--", coalesce: Boolean(coalesce) }).map(__wowRange);
|
|
36
|
+
} catch (_) {
|
|
37
|
+
return Process.enumerateRanges(protection || "r--").map(__wowRange);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
readMemory(address, size) {
|
|
41
|
+
return __wowBytes(ptr(address).readByteArray(size));
|
|
42
|
+
},
|
|
43
|
+
writeMemory(address, bytes) {
|
|
44
|
+
const target = ptr(address);
|
|
45
|
+
const input = Uint8Array.from(bytes || []);
|
|
46
|
+
const before = __wowBytes(target.readByteArray(input.byteLength));
|
|
47
|
+
target.writeByteArray(input.buffer);
|
|
48
|
+
return { before, after: Array.from(input), address: target.toString(), size: input.byteLength };
|
|
49
|
+
},
|
|
50
|
+
protectMemory(address, size, protection) {
|
|
51
|
+
const changed = Memory.protect(ptr(address), size, protection);
|
|
52
|
+
return { address: ptr(address).toString(), size, protection, changed };
|
|
53
|
+
},
|
|
54
|
+
scanMemory(address, size, pattern) {
|
|
55
|
+
return Memory.scanSync(ptr(address), size, pattern).map(match => ({
|
|
56
|
+
address: match.address.toString(), size: match.size
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
60
|
`;
|
|
61
61
|
function asRecord(value) {
|
|
62
62
|
return value && typeof value === "object" ? value : {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Built-in bounded sampler used by `analyze runtime`; dynamic stays caller-owned. */
|
|
2
|
+
export const RUNTIME_EXPORT_SCRIPT = String.raw `
|
|
3
|
+
"use strict";
|
|
4
|
+
const input = globalThis.__WOWDUMP_INPUT__ || {};
|
|
5
|
+
const moduleName = typeof input.module === "string" && input.module ? input.module : "Wow.exe";
|
|
6
|
+
const module = Process.findModuleByName(moduleName);
|
|
7
|
+
const limit = Math.min(Math.max(Number(input.maxEvents || 16), 1), 256);
|
|
8
|
+
const sampleBytes = Math.min(Math.max(Number(input.sampleBytes || 64), 1), 256);
|
|
9
|
+
function bytes(address, size) { const value = address.readByteArray(size); return value ? Array.from(new Uint8Array(value), byte => byte.toString(16).padStart(2, "0")).join("") : ""; }
|
|
10
|
+
function collect() {
|
|
11
|
+
if (!module) return { ok: false, module: { name: moduleName }, records: [], evidence: [{ source: "frida", kind: "module-missing" }] };
|
|
12
|
+
if (input.kind === "text") {
|
|
13
|
+
const ranges = Process.enumerateRanges("r-x").filter(range => range.base.compare(module.base) >= 0 && range.base.compare(module.base.add(module.size)) < 0);
|
|
14
|
+
const textRanges = ranges.map(range => ({ base: range.base.toString(), rva: range.base.sub(module.base).toString(), size: range.size, protection: range.protection }));
|
|
15
|
+
const records = [];
|
|
16
|
+
for (const range of ranges) {
|
|
17
|
+
if (records.length >= limit) break;
|
|
18
|
+
const rva = range.base.sub(module.base);
|
|
19
|
+
const offset = rva.isNull() && range.size > 0x1000 ? 0x1000 : 0;
|
|
20
|
+
const address = range.base.add(offset);
|
|
21
|
+
records.push({ address: address.toString(), rva: address.sub(module.base).toString(), size: Math.min(sampleBytes, range.size - offset), bytesHex: bytes(address, Math.min(sampleBytes, range.size - offset)) });
|
|
22
|
+
}
|
|
23
|
+
return { ok: true, kind: "text", module: { name: module.name, moduleBase: module.base.toString(), moduleSize: module.size }, textRanges, samples: records, records, evidence: [{ source: "frida", kind: "frida-runtime-text", samples: records.length }] };
|
|
24
|
+
}
|
|
25
|
+
const profile = input.profile && typeof input.profile === "object" ? input.profile : {};
|
|
26
|
+
const candidates = Array.isArray(profile.providers) ? profile.providers : [];
|
|
27
|
+
const records = [];
|
|
28
|
+
for (const candidate of candidates.slice(0, limit)) {
|
|
29
|
+
if (!candidate || typeof candidate.rva !== "string") continue;
|
|
30
|
+
const address = module.base.add(ptr(candidate.rva));
|
|
31
|
+
records.push({ name: candidate.name || "candidate", rva: candidate.rva, runtimeAddress: address.toString(), bytesHex: bytes(address, Math.min(Number(candidate.size || 16), 256)), source: "frida" });
|
|
32
|
+
}
|
|
33
|
+
return { ok: true, module: { name: module.name, moduleBase: module.base.toString(), moduleSize: module.size }, records, evidence: [{ source: "frida", kind: "frida-runtime-read", count: records.length }] };
|
|
34
|
+
}
|
|
35
|
+
rpc.exports = { collect };
|
|
36
|
+
`;
|