wowdump 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +21 -53
  3. package/dist/adapters/reader.js +33 -0
  4. package/dist/analysis/disassemble.js +77 -0
  5. package/dist/{frida-runtime.js → analysis/frida-runtime.js} +48 -48
  6. package/dist/analysis/runtime-script.js +170 -0
  7. package/dist/cli.js +337 -192
  8. package/dist/core/profile-engine.js +238 -0
  9. package/dist/frida-worker.js +54 -55
  10. package/dist/{reader-broker.js → reader/broker.js} +15 -0
  11. package/dist/{reader-client.js → reader/client.js} +1 -1
  12. package/dist/{windows-launcher.js → reader/launcher.js} +16 -4
  13. package/dist/reader/main.js +100 -0
  14. package/dist/reader/protocol.js +1 -0
  15. package/dist/reader/windows.js +242 -0
  16. package/dist/reader-main.js +1 -66
  17. package/dist/toolchain.js +102 -573
  18. package/package.json +13 -10
  19. package/skills/wowdump/SKILL.md +24 -15
  20. package/skills/wowdump/references/commands.md +77 -0
  21. package/skills/wowdump/references/disassemble.md +62 -0
  22. package/skills/wowdump/references/dynamic.md +54 -0
  23. package/skills/wowdump/references/evidence-workflow.md +41 -0
  24. package/skills/wowdump/references/profiles.md +34 -0
  25. package/skills/wowdump/references/request-schema.md +28 -0
  26. package/skills/wowdump/references/workflow.md +45 -0
  27. package/skills/wowdump/scripts/dynamic-session.js +133 -0
  28. package/dist/agent.js +0 -1332
  29. package/dist/discovery.js +0 -48
  30. package/dist/dry-run.js +0 -36
  31. package/dist/error-log.js +0 -71
  32. package/dist/focused-session.js +0 -89
  33. package/dist/ghidra.js +0 -769
  34. package/dist/main.js +0 -66
  35. package/dist/observability.js +0 -41
  36. package/dist/processes.js +0 -44
  37. package/dist/session.js +0 -42
  38. package/dist/storage.js +0 -12
  39. package/dist/windows-reader.js +0 -102
  40. package/dist/wow-analysis.js +0 -1405
  41. package/skills/wowdump/commands.md +0 -44
  42. /package/dist/{adapters.js → core/build-adapters.js} +0 -0
  43. /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
- Node/TypeScript WoW native-memory analysis framework. The normal entry point is
4
- the `wowdump` CLI; MCP and Frida remain optional integration paths.
3
+ WoW 原生内存分析 CLI。用 Frida 运行时证据和 IDA Pro/iced-x86 局部反汇编定位字段,生成可复核的 Reader profile。
5
4
 
6
- ## Run
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
- `npm install` initializes `WOWDUMP_HOME`, installs the `wowdump` Skill and its
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
- Static analysis is the default analysis path and runs Ghidra Headless against a
30
- disk executable. It does not attach to a running game or require elevation:
14
+ ## 基本流程
31
15
 
32
16
  ```powershell
33
- wowdump analyze static --exe "D:\Game\World of Warcraft\_retail_\Wow.exe" `
34
- --build "retail@12.1.0.69587" --output "$env:USERPROFILE\.wowdump\profiles\69587\profile.json"
35
- ```
17
+ # 1. 导出运行时模块段
18
+ wowdump analyze runtime --pid 1234 --build "retail@VERSION" --kind dump --confirm
36
19
 
37
- Runtime export is a separate, explicitly confirmed Frida-worker operation:
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 分析 dump-<id>\manifest.json,保存字段候选和证据
21
+ # 3. 将候选证据交给 profile 生成步骤,再用 reader 验证
50
22
 
51
- The CLI also exposes `target`, `profiles`, `memory read`, and `memory watch`.
52
- Those commands use an isolated reader broker. The broker performs one elevated
53
- launch through Node's `ShellExecuteExW(..., "runas", ...)`, then listens only on
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
+ # 4. 用户确认后,把 profile 保存到当前 build profile 目录,再读取
24
+ wowdump memory read --pid 1234 --profile "$HOME\.wowdump\retail@VERSION\profile\player-state.json" --field playerAuras
25
+ ```
57
26
 
58
- The root `.build.info` is read from the parent game directory and associated with
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
- `npm run dry-run` performs one process scan and prints a JSON report without
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
+ `analyze runtime --kind dump` 会把 `.text`、`.rdata`、`.data` `.pdata` 导出为二进制文件,并写入 `manifest.json`;manifest IDA Pro MCP 或 iced-x86 使用。`analyze runtime --kind verify --profile <file>` 只验证已有 profile 的 RVA。当前 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
- Run the regression suite with `npm test`. It covers build-info discovery,
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,170 @@
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
+ const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
20
+
21
+ function readBytes(address, size) {
22
+ const value = address.readByteArray(size);
23
+ return value ? new Uint8Array(value) : null;
24
+ }
25
+
26
+ function bytesHex(address, size) {
27
+ const bytes = readBytes(address, size);
28
+ if (!bytes) return "";
29
+ let output = "";
30
+ for (let index = 0; index < bytes.length; index++) output += bytes[index].toString(16).padStart(2, "0");
31
+ return output;
32
+ }
33
+
34
+ function base64(bytes) {
35
+ let output = "";
36
+ for (let index = 0; index < bytes.length; index += 3) {
37
+ const first = bytes[index];
38
+ const second = index + 1 < bytes.length ? bytes[index + 1] : 0;
39
+ const third = index + 2 < bytes.length ? bytes[index + 2] : 0;
40
+ const value = (first << 16) | (second << 8) | third;
41
+ output += BASE64[(value >>> 18) & 63];
42
+ output += BASE64[(value >>> 12) & 63];
43
+ output += index + 1 < bytes.length ? BASE64[(value >>> 6) & 63] : "=";
44
+ output += index + 2 < bytes.length ? BASE64[value & 63] : "=";
45
+ }
46
+ return output;
47
+ }
48
+
49
+ function ascii(address, size) {
50
+ const bytes = readBytes(address, size);
51
+ if (!bytes) return "";
52
+ let output = "";
53
+ for (let index = 0; index < bytes.length; index++) {
54
+ const value = bytes[index];
55
+ output += value >= 0x20 && value <= 0x7e ? String.fromCharCode(value) : "";
56
+ }
57
+ return output;
58
+ }
59
+
60
+ function u16(address) { return address.readU16(); }
61
+ function u32(address) { return address.readU32(); }
62
+ function sectionName(address) {
63
+ return ascii(address, 8).replace(/\0/g, "").trim();
64
+ }
65
+
66
+ function parseSections() {
67
+ if (!module) return { error: "module-missing", sections: [] };
68
+ try {
69
+ const base = module.base;
70
+ if (u16(base) !== 0x5a4d) return { error: "invalid-dos-signature", sections: [] };
71
+ const peOffset = u32(base.add(0x3c));
72
+ if (peOffset < 0x40 || peOffset > module.size - 24) return { error: "invalid-pe-offset", sections: [] };
73
+ const pe = base.add(peOffset);
74
+ if (u32(pe) !== 0x00004550) return { error: "invalid-pe-signature", sections: [] };
75
+ const count = u16(pe.add(6));
76
+ const optionalSize = u16(pe.add(20));
77
+ if (count < 1 || count > 96 || optionalSize < 0x60) return { error: "invalid-section-table", sections: [] };
78
+ const table = pe.add(24 + optionalSize);
79
+ const sections = [];
80
+ for (let index = 0; index < count; index++) {
81
+ const header = table.add(index * 40);
82
+ const name = sectionName(header);
83
+ const virtualSize = u32(header.add(8));
84
+ const rva = u32(header.add(12));
85
+ const rawSize = u32(header.add(16));
86
+ const characteristics = u32(header.add(36));
87
+ const size = Math.max(virtualSize, rawSize);
88
+ if (!name || !size || rva >= module.size) continue;
89
+ const boundedSize = Math.min(size, module.size - rva);
90
+ const executable = (characteristics & 0x20000000) !== 0;
91
+ const writable = (characteristics & 0x80000000) !== 0;
92
+ const readable = (characteristics & 0x40000000) !== 0;
93
+ sections.push({
94
+ name,
95
+ rva: "0x" + rva.toString(16),
96
+ runtimeAddress: base.add(rva).toString(),
97
+ virtualSize,
98
+ rawSize,
99
+ characteristics: "0x" + characteristics.toString(16),
100
+ protection: (readable ? "r" : "") + (writable ? "w" : "") + (executable ? "x" : ""),
101
+ size: boundedSize
102
+ });
103
+ }
104
+ return { error: null, sections };
105
+ } catch (error) {
106
+ return { error: String(error), sections: [] };
107
+ }
108
+ }
109
+
110
+ function dumpSection(section, totalState) {
111
+ const requestedSize = Math.min(section.size, maxSectionBytes);
112
+ const available = Math.max(0, maxTotalBytes - totalState.bytes);
113
+ const readSize = Math.min(requestedSize, available);
114
+ const chunks = [];
115
+ let offset = 0;
116
+ while (offset < readSize) {
117
+ const size = Math.min(chunkSize, readSize - offset);
118
+ const bytes = readBytes(module.base.add(parseInt(section.rva, 16)).add(offset), size);
119
+ if (!bytes) break;
120
+ chunks.push({ offset, size: bytes.length, dataBase64: base64(bytes) });
121
+ offset += bytes.length;
122
+ if (bytes.length !== size) break;
123
+ }
124
+ totalState.bytes += offset;
125
+ return { ...section, requestedSize, readSize: offset, truncated: offset < section.size, chunks };
126
+ }
127
+
128
+ function candidateEntries(profile) {
129
+ const entries = [];
130
+ const fields = profile && typeof profile.fields === "object" && !Array.isArray(profile.fields) ? profile.fields : {};
131
+ for (const [name, value] of Object.entries(fields)) entries.push({ name, value });
132
+ return entries.slice(0, maxCandidates);
133
+ }
134
+
135
+ function candidateRva(value) {
136
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
137
+ const item = value;
138
+ const root = item.root && typeof item.root === "object" && !Array.isArray(item.root) ? item.root : {};
139
+ const rva = item.rva ?? root.rva;
140
+ if (typeof rva !== "string" && typeof rva !== "number") return null;
141
+ try { return ptr(String(rva)).toString(); } catch (_) { return null; }
142
+ }
143
+
144
+ function collect() {
145
+ if (!module) return { ok: false, kind: input.kind || "dump", module: { name: moduleName }, sections: [], records: [], evidence: [{ source: "frida", kind: "module-missing" }] };
146
+ if (input.kind === "dump") {
147
+ const parsed = parseSections();
148
+ 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 }] };
149
+ const selected = parsed.sections.filter(section => [".text", ".rdata", ".data", ".pdata"].includes(section.name.toLowerCase()));
150
+ const totalState = { bytes: 0 };
151
+ const sections = selected.map(section => dumpSection(section, totalState));
152
+ 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 }] };
153
+ }
154
+ if (input.kind !== "verify") return { ok: false, kind: input.kind || null, module: { name: module.name }, records: [], evidence: [{ source: "frida", kind: "unsupported-runtime-kind" }] };
155
+ const profile = input.profile && typeof input.profile === "object" ? input.profile : null;
156
+ if (!profile) return { ok: false, kind: "verify", module: { name: module.name, moduleBase: module.base.toString() }, records: [], evidence: [{ source: "frida", kind: "profile-required" }] };
157
+ const records = [];
158
+ for (const entry of candidateEntries(profile)) {
159
+ const rva = candidateRva(entry.value);
160
+ if (rva === null) { records.push({ name: entry.name, ok: false, reason: "missing-rva", source: "frida" }); continue; }
161
+ const address = module.base.add(ptr(rva));
162
+ try {
163
+ const bytes = bytesHex(address, Math.min(Number(entry.value.size || entry.value.byteSize || maxVerifyBytes), maxVerifyBytes));
164
+ 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" });
165
+ } catch (error) { records.push({ name: entry.name, rva, runtimeAddress: address.toString(), ok: false, reason: String(error), source: "frida" }); }
166
+ }
167
+ 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 }] };
168
+ }
169
+ rpc.exports = { collect };
170
+ `;