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.
Files changed (63) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +17 -111
  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} +3 -25
  6. package/dist/analysis/runtime-script.js +36 -0
  7. package/dist/cli.js +563 -0
  8. package/dist/core/profile-engine.js +238 -0
  9. package/dist/frida-worker.js +99 -0
  10. package/dist/reader/broker.js +475 -0
  11. package/dist/reader/client.js +1 -0
  12. package/dist/reader/launcher.js +219 -0
  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 +2 -0
  17. package/dist/toolchain.js +123 -0
  18. package/package.json +19 -37
  19. package/skills/wowdump/SKILL.md +22 -0
  20. package/skills/wowdump/references/commands.md +63 -0
  21. package/skills/wowdump/references/disassemble.md +18 -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 +44 -0
  27. package/skills/wowdump/scripts/dynamic-session.js +133 -0
  28. package/dist/agent.js +0 -1335
  29. package/dist/analysis-path.js +0 -38
  30. package/dist/analysis-process-log.js +0 -146
  31. package/dist/broker-client.js +0 -411
  32. package/dist/broker-codec.js +0 -148
  33. package/dist/broker-core.js +0 -1045
  34. package/dist/broker-gateway.js +0 -447
  35. package/dist/broker-ledger.js +0 -196
  36. package/dist/broker-main.js +0 -291
  37. package/dist/broker-protocol.js +0 -119
  38. package/dist/broker-runtime.js +0 -1283
  39. package/dist/broker-server.js +0 -466
  40. package/dist/build-bundle-loader.js +0 -183
  41. package/dist/build-bundle.js +0 -11
  42. package/dist/discovery.js +0 -59
  43. package/dist/dry-run.js +0 -38
  44. package/dist/error-log.js +0 -71
  45. package/dist/focus-errors.js +0 -63
  46. package/dist/focus-service.js +0 -1855
  47. package/dist/focused-session.js +0 -1357
  48. package/dist/mcp-main.js +0 -51
  49. package/dist/mcp.js +0 -924
  50. package/dist/observability.js +0 -41
  51. package/dist/process-log-lock.js +0 -195
  52. package/dist/processes.js +0 -47
  53. package/dist/runtime-config.js +0 -399
  54. package/dist/session.js +0 -145
  55. package/dist/storage.js +0 -12
  56. package/dist/wow-analysis.js +0 -1430
  57. package/resources/builds/retail/12.0.7.68974/build-profile.json +0 -290
  58. package/resources/builds/retail/12.0.7.68974/data-sources.json +0 -1633
  59. package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +0 -5130
  60. package/resources/builds/retail/12.0.7.68974/manifest.json +0 -63
  61. package/resources/builds/retail/12.0.7.68974/signatures.json +0 -260
  62. /package/dist/{adapters.js → core/build-adapters.js} +0 -0
  63. /package/dist/{types.js → core/types.js} +0 -0
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Follenfang
3
+ Copyright (c) 2026 Follen
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,132 +1,38 @@
1
1
  # wowdump
2
2
 
3
- `wowdump` is a build-aware World of Warcraft runtime analysis MCP server. It exposes Frida process control, focused Lua/C++ tracing, bounded data watches, build profile validation, and verified data readers over the MCP `2026-07-28` protocol.
3
+ WoW 原生内存分析 CLI。用 Frida 运行时证据和 IDA Pro/iced-x86 局部反汇编定位字段,生成可复核的 Reader profile
4
4
 
5
- The package supports multiple WoW flavors and builds. `retail` is a flavor in a build key such as `retail@12.0.7.68974`; it is not part of the product name.
6
-
7
- ## Install
8
-
9
- Run directly from an MCP client:
10
-
11
- ```json
12
- {
13
- "mcpServers": {
14
- "wowdump": {
15
- "command": "npx",
16
- "args": [
17
- "-y",
18
- "wowdump",
19
- "--game-root",
20
- "D:\\Game\\World of Warcraft"
21
- ]
22
- }
23
- }
24
- }
25
- ```
26
-
27
- Or install it globally:
5
+ ## 安装
28
6
 
29
7
  ```powershell
30
8
  npm install --global wowdump
31
- wowdump --game-root "D:\Game\World of Warcraft"
32
- ```
33
-
34
- The `wowdump` executable only starts the stdio MCP server. It is not a separate interactive CLI.
35
-
36
- ## Game Directories
37
-
38
- Game directories are resolved once when the MCP server starts. The first non-empty source wins:
39
-
40
- 1. Repeatable `--game-root <path>` arguments.
41
- 2. `WOWDUMP_GAME_ROOTS` as a semicolon-separated list on Windows.
42
- 3. `game_roots` in the TOML file selected by `--config <path>`.
43
- 4. `%LOCALAPPDATA%\wowdump\config.toml`.
44
- 5. Automatic detection from verified Battle.net Agent installation metadata, followed by common Windows installation locations.
45
-
46
- Example persistent configuration:
47
-
48
- ```toml
49
- game_roots = [
50
- "D:/Game/World of Warcraft",
51
- "E:/World of Warcraft"
52
- ]
53
-
54
- broker_idle_ms = 1200000
9
+ wowdump init
55
10
  ```
56
11
 
57
- `WOW_ROOT` remains a deprecated compatibility fallback. Invalid selected paths are reported on stderr and are not silently replaced by a lower-priority source.
58
-
59
- PID, build, module base, Frida sessions, Hook IDs, and runtime addresses are never persisted in TOML. They are discovered and validated for the current process.
60
-
61
- ## Selecting A Target
62
-
63
- Use these read-only tools before starting a dynamic session:
64
-
65
- - `wow_install_list`: installed game directories and flavors.
66
- - `wow_build_list`: detected build keys and versions.
67
- - `wow_target_list`: running Wow.exe PIDs matched to an installation and build.
68
-
69
- Focused and trace operations accept an explicit `pid` and `buildKey`. The server rejects PID/build mismatches and does not reuse a historical PID or module base.
12
+ 安装脚本会初始化 `~/.wowdump`,并把 Skill references 参考文档安装到 `~/.agents/skills/wowdump`。反汇编保底使用 npm 内置的 `iced-x86`。
70
13
 
71
- ## MCP Tools
14
+ ## 基本流程
72
15
 
73
- The primary tool groups are:
74
-
75
- - `frida_command`: generic Frida read, write, script, Hook, process, and session operations.
76
- - `wow_lua_trace_*` and `wow_cpp_trace_*`: build-aware bounded trace sessions.
77
- - `wow_focus_*`, `wow_watch_*`, and `wow_session_checkpoint`: explicit before/during/after analysis windows.
78
- - `wow_data_source_*`, `wow_data_read`, and `wow_build_profile_validate`: verified build data and readers.
79
- - `wow_broker_*`: Broker status, start, reconnect, and stop.
80
- - `wow_analysis_coverage` and `wow_analysis_checkpoint`: static profile coverage and checkpoints.
81
-
82
- The server rejects legacy MCP handshakes. Protocol output is written only to stdout; diagnostics are written to stderr.
83
-
84
- ## Runtime Layout
85
-
86
- Production uses three distinct directory roles:
87
-
88
- - Package `resources/builds`: read-only, build-specific profiles and signatures.
89
- - `%LOCALAPPDATA%\wowdump`: writable Broker ledgers, focused sessions, logs, and cache.
90
- - Repository `analyze/vm`: offline analysis inputs and intermediate evidence; it is not required by the installed package.
16
+ ```powershell
17
+ # 1. 导出运行时模块或 provider
18
+ wowdump analyze runtime --pid 1234 --build "retail@VERSION" --kind text --duration-ms 5000 --max-events 100 --confirm > runtime.json
91
19
 
92
- Stable overrides can be placed in TOML:
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"
93
22
 
94
- ```toml
95
- profile_root = "D:/wowdump-profiles"
96
- runtime_root = "C:/Users/ACCOUNT/AppData/Local/wowdump"
97
- log_level = "info"
98
- broker_idle_ms = 1200000
23
+ # 3. 用户确认后,把 profile 保存到当前 build 的 profile 目录,再读取
24
+ wowdump memory read --pid 1234 --profile "$HOME\.wowdump\retail@VERSION\profile\player-state.json" --field playerAuras
99
25
  ```
100
26
 
101
- The MCP process does not own Frida sessions. A single per-user `wowdump-broker` process owns attach/detach, scripts, Hooks, Interceptors, request forwarding, and cleanup. Requests refresh its idle timer; the default idle timeout is 20 minutes.
102
-
103
- ## Packaged Builds
104
-
105
- This release includes a verified profile for:
106
-
107
- - `retail@12.0.7.68974`
27
+ Windows reader 首次使用时由 Node 请求 UAC 启动 broker,后续调用复用同一 broker,空闲 20 分钟后退出。Frida 运行时导出需要显式 `--confirm`;高开销 Stalker 默认关闭。
108
28
 
109
- Every build has its own RVA profile, entry signatures, data sources, and manifest hashes. A new build must be relocated and validated; old RVAs are not automatically reused.
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`。
110
30
 
111
- ## Development
31
+ ## 开发验证
112
32
 
113
33
  ```powershell
114
- npm install
115
34
  npm run typecheck
116
- npm test
117
35
  npm run build
118
- npm run bundle:verify
119
- npm run package:verify
120
- ```
121
-
122
- To regenerate the packaged bundle from the repository-only analysis workspace:
123
-
124
- ```powershell
125
- npm run bundle:build
36
+ npm test
37
+ npm pack --dry-run
126
38
  ```
127
-
128
- `npm run package:verify` enforces the npm file whitelist, creates a real tarball, installs it in a temporary directory, starts the installed `wowdump` bin, and performs an MCP `2026-07-28` handshake.
129
-
130
- ## License
131
-
132
- MIT
@@ -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
+ }
@@ -1,16 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { appendFile, mkdir } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- export const BROKER_MANAGED_COMMAND = Symbol("wowdump.broker-managed-command");
5
- export function brokerManagedCommand(request, purpose) {
6
- Object.defineProperty(request, BROKER_MANAGED_COMMAND, {
7
- value: { purpose, irreversibleReason: "Broker-managed observation instrumentation is cleaned through the owned resource graph." },
8
- enumerable: false,
9
- configurable: false,
10
- writable: false
11
- });
12
- return request;
13
- }
14
4
  const DEFAULT_MAX_SOURCE_BYTES = 4 * 1024 * 1024;
15
5
  const DEFAULT_MAX_MEMORY_BYTES = 16 * 1024 * 1024;
16
6
  const DEFAULT_SCRIPT_TIMEOUT_MS = 150;
@@ -196,9 +186,6 @@ function buildPrelude(context) {
196
186
  buildKey: context?.buildKey,
197
187
  flavor: context?.flavor,
198
188
  executable: context?.executable,
199
- processStartTime: context?.processStartTime,
200
- moduleBase: context?.moduleBase,
201
- moduleIdentity: context?.moduleIdentity,
202
189
  adapter: context?.adapter
203
190
  });
204
191
  const verifiedPrelude = context?.adapter?.adapter?.fridaPrelude ?? "";
@@ -213,10 +200,7 @@ function requestContext(request, context, existing) {
213
200
  ...(request.buildKey === undefined ? {} : { buildKey: request.buildKey }),
214
201
  ...(request.flavor === undefined ? {} : { flavor: request.flavor }),
215
202
  ...(request.adapter === undefined ? {} : { adapter: request.adapter }),
216
- ...(request.executable === undefined ? {} : { executable: request.executable }),
217
- ...(request.processStartTime === undefined ? {} : { processStartTime: request.processStartTime }),
218
- ...(request.moduleBase === undefined ? {} : { moduleBase: request.moduleBase }),
219
- ...(request.moduleIdentity === undefined ? {} : { moduleIdentity: request.moduleIdentity })
203
+ ...(request.executable === undefined ? {} : { executable: request.executable })
220
204
  };
221
205
  }
222
206
  function sessionContext(record) {
@@ -226,10 +210,7 @@ function sessionContext(record) {
226
210
  ...(context.buildKey === undefined ? {} : { buildKey: context.buildKey }),
227
211
  ...(context.flavor === undefined ? {} : { flavor: context.flavor }),
228
212
  ...(context.adapter === undefined ? {} : { adapter: context.adapter }),
229
- ...(context.executable === undefined ? {} : { executable: context.executable }),
230
- ...(context.processStartTime === undefined ? {} : { processStartTime: context.processStartTime }),
231
- ...(context.moduleBase === undefined ? {} : { moduleBase: context.moduleBase }),
232
- ...(context.moduleIdentity === undefined ? {} : { moduleIdentity: context.moduleIdentity })
213
+ ...(context.executable === undefined ? {} : { executable: context.executable })
233
214
  };
234
215
  }
235
216
  function serializeDevice(device) {
@@ -514,10 +495,6 @@ export class FridaCommandRuntime {
514
495
  const record = this.sessions.get(id);
515
496
  if (!record)
516
497
  throw new Error(`session ${id} was not found`);
517
- // Detach is an at-most-once terminal transition. Frida may report an
518
- // error after teardown has already started, so retaining the record would
519
- // let Broker cleanup call detach again on a half-destroyed agent.
520
- this.sessions.delete(id);
521
498
  for (const scriptId of [...record.scripts.keys()])
522
499
  await this.unloadScript(scriptId).catch(() => undefined);
523
500
  try {
@@ -525,6 +502,7 @@ export class FridaCommandRuntime {
525
502
  }
526
503
  catch { /* bridge cleanup is best effort */ }
527
504
  await record.session.detach();
505
+ this.sessions.delete(id);
528
506
  }
529
507
  async resume(request, context) {
530
508
  const pid = this.requirePid(request, context);
@@ -0,0 +1,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
+ `;