wowdump 0.3.9 → 0.3.11
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 +30 -8
- package/dist/adapters/{reader.js → memory.js} +2 -2
- package/dist/analysis/disassemble.js +45 -0
- package/dist/analysis/pe.js +49 -0
- package/dist/cli.js +38 -99
- package/dist/core/build-store.js +1 -21
- package/dist/{reader → memory}/broker.js +51 -44
- package/dist/memory/client.js +1 -0
- package/dist/memory/dump.js +142 -0
- package/dist/{reader → memory}/launcher.js +40 -13
- package/dist/{reader → memory}/main.js +7 -22
- package/dist/{reader → memory}/windows.js +3 -3
- package/dist/memory-main.js +2 -0
- package/dist/toolchain.js +15 -0
- package/package.json +3 -3
- package/skills/wowdump/SKILL.md +13 -13
- package/skills/wowdump/references/character-stats-case.md +7 -9
- package/skills/wowdump/references/commands.md +22 -27
- package/skills/wowdump/references/disassemble.md +14 -33
- package/skills/wowdump/references/evidence-workflow.md +2 -2
- package/skills/wowdump/references/memory.md +23 -0
- package/skills/wowdump/references/profiles.md +9 -5
- package/skills/wowdump/references/task-scope.md +16 -0
- package/skills/wowdump/references/workflow.md +22 -3
- package/dist/analysis/runtime-dump.js +0 -93
- package/dist/debug/cdb.js +0 -412
- package/dist/reader/client.js +0 -1
- package/dist/reader-main.js +0 -2
- package/skills/wowdump/references/request-schema.md +0 -12
- package/skills/wowdump/references/windbg.md +0 -63
- /package/dist/{reader → memory}/protocol.js +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# wowdump
|
|
2
2
|
|
|
3
|
-
Windows 下的 WoW 原生内存分析 CLI
|
|
3
|
+
Windows 下的 WoW 原生内存分析 CLI。原生内存引擎负责读取、页查询和段导出;IDA Pro MCP 优先,iced-x86 提供无需额外工具的局部 64 位反汇编。最终生成按 build 保存的 profile。
|
|
4
4
|
|
|
5
5
|
## 安装与更新
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ wowdump init
|
|
|
10
10
|
wowdump --version
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
安装会初始化 `~/.wowdump`,把包内完整 Skill 安装到 `~/.agents/skills/wowdump`。升级或 init 会同步 Skill,差异文件先备份到 `~/.wowdump/skill-backups/`;个人新增文件和 config.json 保留。iced-x86 随 npm
|
|
13
|
+
安装会初始化 `~/.wowdump`,把包内完整 Skill 安装到 `~/.agents/skills/wowdump`。升级或 init 会同步 Skill,差异文件先备份到 `~/.wowdump/skill-backups/`;个人新增文件和 config.json 保留。iced-x86 随 npm 安装,无需安装调试器或 CE。
|
|
14
14
|
|
|
15
15
|
## 使用流程
|
|
16
16
|
|
|
@@ -33,9 +33,9 @@ wowdump memory read --pid <pid> --build "<buildKey>" --profile "<character-stats
|
|
|
33
33
|
wowdump memory regions --pid <pid> --start <startAddress> --end <endAddress> --max-regions 20000
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
尖括号是待替换参数。已有匹配且适用的 profile 时直接
|
|
36
|
+
尖括号是待替换参数。已有匹配且适用的 profile 时直接 内存引擎 读取,无需重新 dump。不再提供实时调试、断点或任意调试命令。
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
内存读取与导出复用同一个管理员 broker:首次请求 UAC,空闲 20 分钟退出。运行时证据采集需要 `--confirm`。
|
|
39
39
|
|
|
40
40
|
## 数据放在哪里
|
|
41
41
|
|
|
@@ -44,18 +44,20 @@ wowdump memory regions --pid <pid> --start <startAddress> --end <endAddress> --m
|
|
|
44
44
|
- `build.json`:目标身份。
|
|
45
45
|
- `database/`:静态分析数据库位置和状态,不因任务重复创建。
|
|
46
46
|
- `runtime/<session>/`:本轮证据、dump 和候选。
|
|
47
|
-
- `profile/`:用户确认的
|
|
47
|
+
- `profile/`:用户确认的 内存引擎 接口。
|
|
48
48
|
|
|
49
|
-
Dump 默认导出 `.text/.rdata/.pdata`,`.data`
|
|
49
|
+
Dump 默认导出 `.text/.rdata/.pdata`,`.data` 按需加入。内存引擎 默认选择当前 build 的 profile,也支持显式路径;它不读取 IDA 数据库或 dump 二进制。
|
|
50
50
|
|
|
51
51
|
## 0.3.9 的读取与验证
|
|
52
52
|
|
|
53
|
+
0.3.10 另修复请求超时计时器提前释放活动引用的问题,确保没有其他活动句柄时仍返回 REQUEST_TIMEOUT;broker 空闲退出策略不变。
|
|
54
|
+
|
|
53
55
|
- verify 接受 candidate 或 reader_ready;普通 memory read 要求 reader_ready。
|
|
54
56
|
- 输出 `validation.read: "passed"`,替代旧版 `verified:true`;fieldSemantics、rootSemantics、crossProcess 为 not_checked,需要独立证据。命令不自动认证语义、提升状态或保存 profile。
|
|
55
|
-
- 新增 `root.lookup`:按动态键选择有界表项,核对对象键并检查读取期间变化,避免固化本轮哈希槽位。含 lookup 的 profile 需要 0.3.9 或更新版本;旧
|
|
57
|
+
- 新增 `root.lookup`:按动态键选择有界表项,核对对象键并检查读取期间变化,避免固化本轮哈希槽位。含 lookup 的 profile 需要 0.3.9 或更新版本;旧 内存引擎 可能忽略未知参数。
|
|
56
58
|
- 校验目标 build 时,缺少 executableSha256 会报错。模块外根 RVA、空根、短读和不一致查表结果均返回结构化错误。
|
|
57
59
|
|
|
58
|
-
详细用法由安装的 Skill 按需加载:[命令](skills/wowdump/references/commands.md)、[Profile 格式](skills/wowdump/references/profiles.md)、[字段挖掘方法与成功案例](skills/wowdump/references/character-stats-case.md)、[
|
|
60
|
+
详细用法由安装的 Skill 按需加载:[命令](skills/wowdump/references/commands.md)、[Profile 格式](skills/wowdump/references/profiles.md)、[字段挖掘方法与成功案例](skills/wowdump/references/character-stats-case.md)、[内存引擎](skills/wowdump/references/memory.md)。
|
|
59
61
|
|
|
60
62
|
## 开发验证
|
|
61
63
|
|
|
@@ -66,3 +68,23 @@ npm test
|
|
|
66
68
|
npm run package:verify
|
|
67
69
|
npm run release:verify
|
|
68
70
|
```
|
|
71
|
+
|
|
72
|
+
## 原生导出与离线分析
|
|
73
|
+
|
|
74
|
+
`analyze runtime --kind dump` 使用 Windows API 分块写入 `.bin`,manifest v4 保留 RVA、读取缺口和哈希。每次创建新 session,超过限额报错;缺页不伪装成完整读取。`progress.json` 提供进度。运行时导出不是原子快照。
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
wowdump analyze disassemble --file <section.bin> --rva <startRva> --offset <fileOffset> --size 4096 --max-instructions 256
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
该命令仅局部解码,不替代 IDA 的反编译与交叉引用。IDA 不可用时由 Agent 基于指令和内存证据分析。
|
|
81
|
+
|
|
82
|
+
## 模块与命名
|
|
83
|
+
|
|
84
|
+
- `src/memory/`:Memory Engine,Windows 内存访问、broker、导出与生命周期。
|
|
85
|
+
- `src/analysis/`:PE 解析与 iced-x86 离线反汇编。
|
|
86
|
+
- `src/core/profile-engine.ts`:profile 身份校验、RVA/指针链与字段解析。
|
|
87
|
+
- `src/adapters/memory.ts`:profile 引擎到 broker 的适配。
|
|
88
|
+
- `src/memory-main.ts`:管理员 broker 入口;不再发布旧 reader 入口。
|
|
89
|
+
|
|
90
|
+
开发测试的自定义入口变量为 `WOWDUMP_MEMORY_COMMAND`,测试后端变量为 `WOWDUMP_USE_FAKE_MEMORY`。旧变量不再使用。升级后首次原生请求会认证关闭旧版 broker,然后启动新引擎;profile v1 的 `readerStatus` / `reader_ready` 保持兼容。
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
function record(value) {
|
|
2
2
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3
3
|
}
|
|
4
|
-
/** Adapts the elevated
|
|
5
|
-
export class
|
|
4
|
+
/** Adapts the elevated memory broker to ProfileEngine's minimal memory API. */
|
|
5
|
+
export class MemoryProfileAdapter {
|
|
6
6
|
invoke;
|
|
7
7
|
constructor(invoke) {
|
|
8
8
|
this.invoke = invoke;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { Decoder, DecoderOptions, Formatter, FormatterSyntax } from "iced-x86";
|
|
4
|
+
export async function disassembleFile(input) {
|
|
5
|
+
if (!/^0x[0-9a-f]+$/i.test(input.rva) || BigInt(input.rva) > 0xffffffffn ||
|
|
6
|
+
!Number.isSafeInteger(input.offset) || input.offset < 0 || !Number.isSafeInteger(input.size) || input.size < 1 || input.size > 1024 * 1024 ||
|
|
7
|
+
!Number.isSafeInteger(input.maxInstructions) || input.maxInstructions < 1 || input.maxInstructions > 10000) {
|
|
8
|
+
throw Object.assign(new Error("invalid bounded disassembly range"), { code: "DISASSEMBLY_ARGUMENT_INVALID" });
|
|
9
|
+
}
|
|
10
|
+
const file = await open(resolve(input.file), "r");
|
|
11
|
+
let bytes;
|
|
12
|
+
try {
|
|
13
|
+
const buffer = Buffer.alloc(input.size);
|
|
14
|
+
const result = await file.read(buffer, 0, input.size, input.offset);
|
|
15
|
+
bytes = buffer.subarray(0, result.bytesRead);
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
await file.close();
|
|
19
|
+
}
|
|
20
|
+
if (!bytes.length)
|
|
21
|
+
throw Object.assign(new Error("no bytes in selected range"), { code: "DISASSEMBLY_EMPTY" });
|
|
22
|
+
const decoder = new Decoder(64, bytes, DecoderOptions.None);
|
|
23
|
+
decoder.ip = BigInt(input.rva);
|
|
24
|
+
const formatter = new Formatter(FormatterSyntax.Intel);
|
|
25
|
+
const instructions = [];
|
|
26
|
+
let decodedBytes = 0;
|
|
27
|
+
try {
|
|
28
|
+
while (decoder.canDecode && instructions.length < input.maxInstructions) {
|
|
29
|
+
const instruction = decoder.decode();
|
|
30
|
+
decodedBytes += instruction.length;
|
|
31
|
+
try {
|
|
32
|
+
instructions.push({ rva: `0x${instruction.ip.toString(16)}`, length: instruction.length, text: formatter.format(instruction), invalid: instruction.isInvalid,
|
|
33
|
+
...(instruction.isIpRelMemoryOperand ? { memoryRva: `0x${instruction.ipRelMemoryAddress.toString(16)}` } : {}) });
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
instruction.free();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { ok: true, command: "analyze.disassemble", engine: "iced-x86", bitness: 64, file: resolve(input.file), offset: input.offset, rva: input.rva, bytesRead: bytes.length, decodedBytes, sourceShortRead: bytes.length < input.size, truncated: decoder.canDecode, instructions };
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
decoder.free();
|
|
43
|
+
formatter.free();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
class PeError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function hex(value) { return `0x${value.toString(16)}`; }
|
|
11
|
+
function readU16(buffer, offset) { return buffer.readUInt16LE(offset); }
|
|
12
|
+
function readU32(buffer, offset) { return buffer.readUInt32LE(offset); }
|
|
13
|
+
export async function readPeLayout(file) {
|
|
14
|
+
const input = resolve(file);
|
|
15
|
+
const buffer = await readFile(input);
|
|
16
|
+
if (buffer.length < 0x100 || buffer.toString("ascii", 0, 2) !== "MZ")
|
|
17
|
+
throw new PeError("PE_INVALID", "Wow.exe has no DOS header");
|
|
18
|
+
const peOffset = readU32(buffer, 0x3c);
|
|
19
|
+
if (peOffset + 24 > buffer.length || buffer.toString("ascii", peOffset, peOffset + 4) !== "PE\0\0")
|
|
20
|
+
throw new PeError("PE_INVALID", "Wow.exe has no PE header");
|
|
21
|
+
const sections = readU16(buffer, peOffset + 6);
|
|
22
|
+
const optionalSize = readU16(buffer, peOffset + 20);
|
|
23
|
+
const optional = peOffset + 24;
|
|
24
|
+
if (optionalSize < 64 || optional + optionalSize > buffer.length)
|
|
25
|
+
throw new PeError("PE_INVALID", "truncated optional header");
|
|
26
|
+
const magic = readU16(buffer, optional);
|
|
27
|
+
if (![0x20b, 0x10b].includes(magic))
|
|
28
|
+
throw new PeError("PE_INVALID", "unsupported PE format");
|
|
29
|
+
const imageBase = magic === 0x20b ? buffer.readBigUInt64LE(optional + 24) : BigInt(readU32(buffer, optional + 28));
|
|
30
|
+
const sizeOfImage = readU32(buffer, optional + 56);
|
|
31
|
+
const table = optional + optionalSize;
|
|
32
|
+
if (sections > 96 || table + sections * 40 > buffer.length)
|
|
33
|
+
throw new PeError("PE_INVALID", "truncated section table");
|
|
34
|
+
const values = [];
|
|
35
|
+
for (let index = 0; index < sections; index += 1) {
|
|
36
|
+
const offset = table + index * 40;
|
|
37
|
+
if (offset + 40 > buffer.length)
|
|
38
|
+
break;
|
|
39
|
+
const name = buffer.subarray(offset, offset + 8).toString("ascii").replace(/\0+$/u, "");
|
|
40
|
+
const virtualSize = readU32(buffer, offset + 8);
|
|
41
|
+
const rva = readU32(buffer, offset + 12);
|
|
42
|
+
const rawSize = readU32(buffer, offset + 16);
|
|
43
|
+
const characteristics = readU32(buffer, offset + 36);
|
|
44
|
+
const protection = `${characteristics & 0x40000000 ? "r" : ""}${characteristics & 0x80000000 ? "w" : ""}${characteristics & 0x20000000 ? "x" : ""}`;
|
|
45
|
+
if (name && (virtualSize || rawSize))
|
|
46
|
+
values.push({ name, rva: hex(BigInt(rva)), virtualSize, rawSize, characteristics: hex(BigInt(characteristics)), protection });
|
|
47
|
+
}
|
|
48
|
+
return { imageBase: hex(imageBase), moduleSize: sizeOfImage, sections: values };
|
|
49
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { disassembleFile } from "./analysis/disassemble.js";
|
|
2
3
|
import { spawn } from "node:child_process";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { existsSync, realpathSync, readFileSync } from "node:fs";
|
|
@@ -6,13 +7,12 @@ import { mkdir, open, readFile, readdir, writeFile } from "node:fs/promises";
|
|
|
6
7
|
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
7
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
9
|
import { Command } from "commander";
|
|
9
|
-
import { WindowsBrokerManager } from "./
|
|
10
|
+
import { WindowsBrokerManager } from "./memory/launcher.js";
|
|
10
11
|
import { initializeWowdumpHome, bootstrapToolchain, resolveToolchain } from "./toolchain.js";
|
|
11
|
-
import { buildPaths, databaseStatus, ensureBuild,
|
|
12
|
-
import { readPeLayout } from "./debug/cdb.js";
|
|
12
|
+
import { buildPaths, databaseStatus, ensureBuild, readBuild, resolveWowdumpHome, safeBuildKey, sha256File } from "./core/build-store.js";
|
|
13
13
|
import { ProfileEngine } from "./core/profile-engine.js";
|
|
14
|
-
import {
|
|
15
|
-
import { enumerateWindowsProcesses } from "./
|
|
14
|
+
import { MemoryProfileAdapter } from "./adapters/memory.js";
|
|
15
|
+
import { enumerateWindowsProcesses } from "./memory/windows.js";
|
|
16
16
|
export function parseBuildInfo(content, file) {
|
|
17
17
|
const lines = content.split(/\r?\n/).filter(line => line.trim());
|
|
18
18
|
const headers = lines[0]?.split("|") ?? [];
|
|
@@ -51,7 +51,7 @@ async function discoverWowTargets(selectedPid, elevatedModules) {
|
|
|
51
51
|
const modules = response && typeof response === "object" && Array.isArray(response.modules)
|
|
52
52
|
? response.modules
|
|
53
53
|
: [];
|
|
54
|
-
const main = modules.find(module => /^Wow\.exe$/i.test(String(module.name ?? "")))
|
|
54
|
+
const main = modules.find(module => /^Wow\.exe$/i.test(String(module.name ?? "")));
|
|
55
55
|
if (!path && typeof main?.path === "string" && main.path)
|
|
56
56
|
path = main.path;
|
|
57
57
|
if (!moduleBase && typeof main?.base === "string")
|
|
@@ -199,16 +199,16 @@ function writeJson(io, value) {
|
|
|
199
199
|
io.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
200
200
|
}
|
|
201
201
|
function brokerCommandLine(env, packageDist) {
|
|
202
|
-
const configured = env.
|
|
202
|
+
const configured = env.WOWDUMP_MEMORY_COMMAND?.trim();
|
|
203
203
|
if (configured)
|
|
204
204
|
return { file: configured, args: [] };
|
|
205
|
-
const candidate = join(packageDist, "
|
|
205
|
+
const candidate = join(packageDist, "memory-main.js");
|
|
206
206
|
return existsSync(candidate) ? { file: process.execPath, args: [candidate, "--request-stdio"] } : null;
|
|
207
207
|
}
|
|
208
208
|
async function defaultBroker(invocation, env, packageDist) {
|
|
209
209
|
const command = brokerCommandLine(env, packageDist);
|
|
210
210
|
if (!command) {
|
|
211
|
-
throw new CliError("BROKER_NOT_CONFIGURED", "
|
|
211
|
+
throw new CliError("BROKER_NOT_CONFIGURED", "memory broker entry was not found", { expected: join(packageDist, "memory-main.js"), environment: "WOWDUMP_MEMORY_COMMAND" });
|
|
212
212
|
}
|
|
213
213
|
const result = await new Promise(resolveResult => {
|
|
214
214
|
const child = spawn(command.file, command.args, { shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -223,15 +223,15 @@ async function defaultBroker(invocation, env, packageDist) {
|
|
|
223
223
|
child.stdin?.end(`${JSON.stringify(invocation)}\n`);
|
|
224
224
|
});
|
|
225
225
|
if (result.exitCode !== 0)
|
|
226
|
-
throw new CliError("BROKER_FAILED", result.stderr.trim() || `
|
|
226
|
+
throw new CliError("BROKER_FAILED", result.stderr.trim() || `memory broker exited with ${result.exitCode}`);
|
|
227
227
|
const line = result.stdout.split(/\r?\n/).find(value => value.trim());
|
|
228
228
|
if (!line)
|
|
229
|
-
throw new CliError("BROKER_PROTOCOL_ERROR", "
|
|
229
|
+
throw new CliError("BROKER_PROTOCOL_ERROR", "memory broker returned no JSON response");
|
|
230
230
|
try {
|
|
231
231
|
return JSON.parse(line);
|
|
232
232
|
}
|
|
233
233
|
catch {
|
|
234
|
-
throw new CliError("BROKER_PROTOCOL_ERROR", "
|
|
234
|
+
throw new CliError("BROKER_PROTOCOL_ERROR", "memory broker returned invalid JSON", { stdout: result.stdout });
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
const windowsBrokerManagers = new Map();
|
|
@@ -239,7 +239,7 @@ function persistentWindowsBroker(home, packageDist) {
|
|
|
239
239
|
const key = resolve(home).toLowerCase();
|
|
240
240
|
let manager = windowsBrokerManagers.get(key);
|
|
241
241
|
if (!manager) {
|
|
242
|
-
manager = new WindowsBrokerManager({ home,
|
|
242
|
+
manager = new WindowsBrokerManager({ home, memoryEntry: join(packageDist, "memory-main.js") });
|
|
243
243
|
windowsBrokerManagers.set(key, manager);
|
|
244
244
|
}
|
|
245
245
|
return manager;
|
|
@@ -285,7 +285,7 @@ function selected(source, keys) {
|
|
|
285
285
|
function requireRuntimeConfirmation(options) {
|
|
286
286
|
if (options.confirm === true)
|
|
287
287
|
return;
|
|
288
|
-
throw new CliError("CONFIRMATION_REQUIRED", "
|
|
288
|
+
throw new CliError("CONFIRMATION_REQUIRED", "runtime evidence collection requires --confirm", {
|
|
289
289
|
pid: options.pid,
|
|
290
290
|
buildKey: options.build,
|
|
291
291
|
operation: "runtime",
|
|
@@ -337,7 +337,7 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
337
337
|
? (invocation => persistentWindowsBroker(home, packageDist).request(invocation))
|
|
338
338
|
: (invocation => defaultBroker(invocation, env, packageDist)));
|
|
339
339
|
const profileEngine = new ProfileEngine();
|
|
340
|
-
const profileAdapter = new
|
|
340
|
+
const profileAdapter = new MemoryProfileAdapter(broker);
|
|
341
341
|
const program = new Command()
|
|
342
342
|
.name("wowdump")
|
|
343
343
|
.description("Build-aware WoW native memory analysis CLI")
|
|
@@ -369,7 +369,7 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
369
369
|
writeJson(io, { ...await databaseStatus(home, options.build), command: "database.status" });
|
|
370
370
|
});
|
|
371
371
|
program.command("target")
|
|
372
|
-
.description("Show target,
|
|
372
|
+
.description("Show target, memory broker, profile and monitor state")
|
|
373
373
|
.option("--pid <pid>", "target process ID")
|
|
374
374
|
.option("--build <buildKey>", "expected build key")
|
|
375
375
|
.action(async (options) => {
|
|
@@ -499,22 +499,21 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
499
499
|
.description("Stop a monitor and release its resources")
|
|
500
500
|
.requiredOption("--id <watchId>", "monitor ID")
|
|
501
501
|
.action(async (options) => writeJson(io, await broker({ command: "watch.stop", payload: { watchId: options.id } })));
|
|
502
|
-
const analyze = program.command("analyze").description("
|
|
502
|
+
const analyze = program.command("analyze").description("Collect memory evidence and disassemble local code");
|
|
503
503
|
analyze.command("runtime")
|
|
504
|
-
.description("Dump PE sections
|
|
504
|
+
.description("Dump PE sections using native memory reads or verify a profile")
|
|
505
505
|
.requiredOption("--pid <pid>", "target process ID")
|
|
506
506
|
.requiredOption("--build <buildKey>", "build key")
|
|
507
507
|
.option("--build-key <buildKey>", "alias for --build")
|
|
508
508
|
.option("--kind <kind>", "dump or verify", "dump")
|
|
509
|
-
.option("--profile <path>", "
|
|
509
|
+
.option("--profile <path>", "memory profile containing candidate RVAs (verify only)")
|
|
510
510
|
.option("--output-dir <path>", "directory for dump manifest and section binaries")
|
|
511
511
|
.option("--sections <names...>", "sections to dump (default: .text .rdata .pdata)")
|
|
512
512
|
.option("--max-section-bytes <bytes>", "per-section dump limit", "134217728")
|
|
513
513
|
.option("--max-total-bytes <bytes>", "total dump limit", "268435456")
|
|
514
|
-
.option("--duration-ms <ms>", "
|
|
515
|
-
.option("--
|
|
516
|
-
.option("--
|
|
517
|
-
.option("--confirm", "confirm the bounded debugger operation")
|
|
514
|
+
.option("--duration-ms <ms>", "native dump deadline", "30000")
|
|
515
|
+
.option("--chunk-bytes <bytes>", "native read block size (maximum 1 MiB)", "65536")
|
|
516
|
+
.option("--confirm", "confirm the bounded memory operation")
|
|
518
517
|
.action(async (options) => {
|
|
519
518
|
const normalized = {
|
|
520
519
|
pid: positiveInteger(options.pid, "pid"),
|
|
@@ -544,96 +543,36 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
544
543
|
const requestedSections = Array.isArray(options.sections) && options.sections.length > 0
|
|
545
544
|
? options.sections.map((value) => String(value).toLowerCase())
|
|
546
545
|
: [".text", ".rdata", ".pdata"];
|
|
547
|
-
const cdbArgs = JSON.parse(options.cdbArgs);
|
|
548
|
-
const cdbCommands = JSON.parse(options.cdbCommands);
|
|
549
|
-
if (!Array.isArray(cdbArgs) || !Array.isArray(cdbCommands))
|
|
550
|
-
throw new CliError("ARGUMENT_INVALID", "cdb-args and cdb-commands must be JSON arrays");
|
|
551
|
-
const reusable = requestedSections.includes(".data")
|
|
552
|
-
? null
|
|
553
|
-
: await findReusableDump(home, normalized.build, { executableSha256: buildRecord.executableSha256, sections: requestedSections, maxSectionBytes: normalized.maxSectionBytes, maxTotalBytes: normalized.maxTotalBytes, cdbArgs, commands: cdbCommands });
|
|
554
|
-
if (reusable) {
|
|
555
|
-
writeJson(io, { ok: true, command: "analyze.runtime.dump", reused: true, manifestFile: String(reusable.manifestFile ?? ""), manifest: reusable });
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
546
|
const moduleResponse = await broker({ command: "modules", payload: { pid: normalized.pid } });
|
|
559
547
|
const modules = moduleResponse && typeof moduleResponse === "object" && Array.isArray(moduleResponse.modules) ? moduleResponse.modules : [];
|
|
560
|
-
const module = modules.find(item => /^Wow\.exe$/i.test(String(item.name ?? "")))
|
|
548
|
+
const module = modules.find(item => /^Wow\.exe$/i.test(String(item.name ?? "")));
|
|
561
549
|
if (!module || typeof module.base !== "string")
|
|
562
550
|
throw new CliError("MODULE_NOT_FOUND", "Wow.exe module was not found", { modules });
|
|
563
|
-
const moduleBase = module.base;
|
|
564
|
-
const layout = await readPeLayout(buildRecord.executablePath);
|
|
565
551
|
const sessionId = randomUUID();
|
|
566
552
|
const sessionDirectory = join(buildPaths(home, normalized.build).runtime, sessionId);
|
|
567
553
|
const outputDirectory = resolve(options.outputDir ?? join(sessionDirectory, "dump"));
|
|
568
554
|
await mkdir(sessionDirectory, { recursive: true });
|
|
569
555
|
await writeFile(join(sessionDirectory, "target.json"), `${JSON.stringify({ schema: "wowdump.runtime-target.v2", pid: normalized.pid, buildKey: normalized.build, executableSha256: buildRecord.executableSha256, sessionId, createdAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
total += requested;
|
|
575
|
-
return { ...section, runtimeAddress: `0x${(BigInt(moduleBase) + BigInt(section.rva)).toString(16)}`, readSize: requested, file: join(outputDirectory, `${section.name}.bin`) };
|
|
576
|
-
});
|
|
577
|
-
if (sections.length === 0)
|
|
578
|
-
throw new CliError("SECTION_NOT_FOUND", "no requested PE sections were found", { requestedSections, available: layout.sections.map(section => section.name) });
|
|
579
|
-
const raw = await broker({ command: "debug", payload: {
|
|
580
|
-
kind: "dump", pid: normalized.pid, buildKey: normalized.build, executablePath: buildRecord.executablePath,
|
|
581
|
-
executableSha256: buildRecord.executableSha256, moduleBase, preferredImageBase: buildRecord.preferredImageBase,
|
|
582
|
-
moduleSize: Number(module.size ?? buildRecord.moduleSize ?? layout.moduleSize), outputDir: outputDirectory, sections,
|
|
583
|
-
maxSectionBytes: normalized.maxSectionBytes, maxTotalBytes: normalized.maxTotalBytes, timeoutMs: normalized.durationMs,
|
|
584
|
-
cdbArgs, commands: cdbCommands
|
|
556
|
+
const raw = await broker({ command: "dump", payload: {
|
|
557
|
+
pid: normalized.pid, buildKey: normalized.build,
|
|
558
|
+
executableSha256: buildRecord.executableSha256, outputDir: outputDirectory, sections: requestedSections,
|
|
559
|
+
maxSectionBytes: normalized.maxSectionBytes, maxTotalBytes: normalized.maxTotalBytes, timeoutMs: normalized.durationMs, chunkBytes: positiveInteger(options.chunkBytes, "chunk-bytes")
|
|
585
560
|
} });
|
|
586
561
|
const value = raw && typeof raw === "object" ? raw : {};
|
|
587
562
|
if (value.ok !== true)
|
|
588
|
-
throw new CliError(String(value.error?.code ?? "
|
|
563
|
+
throw new CliError(String(value.error?.code ?? "DUMP_FAILED"), String(value.error?.message ?? "native dump failed"), value);
|
|
589
564
|
writeJson(io, { ...value, command: "analyze.runtime.dump", sessionDirectory });
|
|
590
565
|
});
|
|
591
|
-
analyze.command("
|
|
592
|
-
.description("
|
|
593
|
-
.requiredOption("--
|
|
594
|
-
.requiredOption("--
|
|
595
|
-
.option("--
|
|
596
|
-
.option("--
|
|
597
|
-
.option("--max-
|
|
598
|
-
.
|
|
599
|
-
.
|
|
600
|
-
.
|
|
601
|
-
.option("--output <path>", "debug evidence JSON path")
|
|
602
|
-
.option("--confirm", "confirm the bounded debugger operation")
|
|
603
|
-
.action(async (options) => {
|
|
604
|
-
const pid = positiveInteger(options.pid, "pid");
|
|
605
|
-
const build = String(options.build);
|
|
606
|
-
const durationMs = positiveInteger(options.durationMs, "duration-ms");
|
|
607
|
-
const kind = String(options.kind);
|
|
608
|
-
if (!["breakpoint", "command"].includes(kind))
|
|
609
|
-
throw new CliError("ARGUMENT_INVALID", "kind must be breakpoint or command");
|
|
610
|
-
requireRuntimeConfirmation({ pid, build, kind, durationMs, maxHits: options.maxHits, confirm: options.confirm === true });
|
|
611
|
-
const buildRecord = await readBuild(home, build);
|
|
612
|
-
if (!buildRecord)
|
|
613
|
-
throw new CliError("BUILD_NOT_FOUND", `build ${build} has not been discovered`);
|
|
614
|
-
const moduleResponse = await broker({ command: "modules", payload: { pid } });
|
|
615
|
-
const modules = moduleResponse && typeof moduleResponse === "object" && Array.isArray(moduleResponse.modules) ? moduleResponse.modules : [];
|
|
616
|
-
const module = modules.find(item => /^Wow\.exe$/i.test(String(item.name ?? ""))) ?? modules[0];
|
|
617
|
-
if (!module || typeof module.base !== "string")
|
|
618
|
-
throw new CliError("MODULE_NOT_FOUND", "Wow.exe module was not found");
|
|
619
|
-
const sessionDirectory = join(buildPaths(home, build).runtime, randomUUID());
|
|
620
|
-
await mkdir(sessionDirectory, { recursive: true });
|
|
621
|
-
const output = resolve(options.output ?? join(sessionDirectory, "debug.json"));
|
|
622
|
-
const parsedArgs = JSON.parse(options.cdbArgs);
|
|
623
|
-
const parsedCommands = JSON.parse(options.cdbCommands);
|
|
624
|
-
if (!Array.isArray(parsedArgs) || !Array.isArray(parsedCommands))
|
|
625
|
-
throw new CliError("ARGUMENT_INVALID", "cdb-args and cdb-commands must be JSON arrays");
|
|
626
|
-
const payload = kind === "command"
|
|
627
|
-
? { kind, pid, buildKey: build, durationMs, outputFile: output, cdbArgs: parsedArgs, commands: parsedCommands }
|
|
628
|
-
: { kind: "breakpoint", pid, buildKey: build, rva: options.rva, moduleBase: module.base, maxHits: positiveInteger(options.maxHits, "max-hits"), durationMs, outputFile: output, cdbArgs: parsedArgs, commands: parsedCommands };
|
|
629
|
-
if (kind !== "command" && !options.rva)
|
|
630
|
-
throw new CliError("RVA_REQUIRED", "breakpoint requires --rva");
|
|
631
|
-
const raw = await broker({ command: "debug", payload });
|
|
632
|
-
const value = raw && typeof raw === "object" ? raw : {};
|
|
633
|
-
if (value.ok !== true)
|
|
634
|
-
throw new CliError(String(value.error?.code ?? "CDB_FAILED"), String(value.error?.message ?? "CDB debug failed"), value.error);
|
|
635
|
-
writeJson(io, { ...value, command: "analyze.debug", sessionDirectory });
|
|
636
|
-
});
|
|
566
|
+
analyze.command("disassemble")
|
|
567
|
+
.description("Decode a bounded local binary range with iced-x86 (64-bit)")
|
|
568
|
+
.requiredOption("--file <path>", "raw section binary")
|
|
569
|
+
.requiredOption("--rva <hex>", "RVA of the first decoded byte")
|
|
570
|
+
.option("--offset <bytes>", "file offset", "0")
|
|
571
|
+
.option("--size <bytes>", "maximum bytes", "4096")
|
|
572
|
+
.option("--max-instructions <count>", "instruction limit", "256")
|
|
573
|
+
.action(async (options) => writeJson(io, await disassembleFile({ file: options.file, rva: options.rva,
|
|
574
|
+
offset: nonNegativeInteger(options.offset, "offset"), size: positiveInteger(options.size, "size"),
|
|
575
|
+
maxInstructions: positiveInteger(options.maxInstructions, "max-instructions") })));
|
|
637
576
|
program.command("init")
|
|
638
577
|
.description("Initialize WOWDUMP_HOME without overwriting existing files")
|
|
639
578
|
.option("--skip-toolchain-download", "kept for compatibility; no external toolchain is installed")
|
package/dist/core/build-store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
|
-
import { mkdir, open, readFile,
|
|
3
|
+
import { mkdir, open, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join, normalize, resolve } from "node:path";
|
|
6
6
|
export function resolveWowdumpHome(env = process.env, userHome = homedir()) {
|
|
@@ -144,23 +144,3 @@ export async function databaseStatus(home, buildKey) {
|
|
|
144
144
|
}
|
|
145
145
|
return { ok: true, buildKey: safeBuildKey(buildKey), buildDirectory: paths.build, databasePath: paths.databaseFile, exists: Boolean(database), locked: lock, build, database };
|
|
146
146
|
}
|
|
147
|
-
export async function findReusableDump(home, buildKey, criteria) {
|
|
148
|
-
const runtime = buildPaths(home, buildKey).runtime;
|
|
149
|
-
let sessions;
|
|
150
|
-
try {
|
|
151
|
-
sessions = await readdir(runtime, { withFileTypes: true });
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
if (error.code === "ENOENT")
|
|
155
|
-
return null;
|
|
156
|
-
throw error;
|
|
157
|
-
}
|
|
158
|
-
for (const session of sessions.filter(item => item.isDirectory())) {
|
|
159
|
-
const manifestFile = join(runtime, session.name, "dump", "manifest.json");
|
|
160
|
-
const manifest = await readJson(manifestFile);
|
|
161
|
-
if (!manifest || typeof manifest.moduleBase !== "string" || JSON.stringify(manifest.dumpParameters ?? null) !== JSON.stringify(criteria))
|
|
162
|
-
continue;
|
|
163
|
-
return { ...manifest, manifestFile };
|
|
164
|
-
}
|
|
165
|
-
return null;
|
|
166
|
-
}
|