wowdump 0.3.7 → 0.3.9
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 +40 -19
- package/dist/cli.js +5 -3
- package/dist/core/profile-engine.js +112 -3
- package/dist/toolchain.js +72 -97
- package/package.json +2 -2
- package/skills/wowdump/SKILL.md +14 -21
- package/skills/wowdump/references/character-stats-case.md +130 -0
- package/skills/wowdump/references/commands.md +17 -15
- package/skills/wowdump/references/disassemble.md +45 -35
- package/skills/wowdump/references/evidence-workflow.md +8 -36
- package/skills/wowdump/references/profiles.md +62 -34
- package/skills/wowdump/references/request-schema.md +9 -25
- package/skills/wowdump/references/windbg.md +9 -1
- package/skills/wowdump/references/workflow.md +15 -22
package/README.md
CHANGED
|
@@ -1,41 +1,61 @@
|
|
|
1
1
|
# wowdump
|
|
2
2
|
|
|
3
|
-
WoW 原生内存分析 CLI。用
|
|
3
|
+
Windows 下的 WoW 原生内存分析 CLI。用 CDB 运行时证据和 IDA Pro MCP / iced-x86 分析字段,生成 Reader 可读取的 build profile。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 安装与更新
|
|
6
6
|
|
|
7
7
|
```powershell
|
|
8
8
|
npm install --global wowdump
|
|
9
9
|
wowdump init
|
|
10
|
+
wowdump --version
|
|
10
11
|
```
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
安装会初始化 `~/.wowdump`,把包内完整 Skill 安装到 `~/.agents/skills/wowdump`。升级或 init 会同步 Skill,差异文件先备份到 `~/.wowdump/skill-backups/`;个人新增文件和 config.json 保留。iced-x86 随 npm 安装,CDB 从本机探测,不自动下载或修改环境变量。
|
|
13
14
|
|
|
14
|
-
##
|
|
15
|
+
## 使用流程
|
|
15
16
|
|
|
16
17
|
```powershell
|
|
17
|
-
# 1.
|
|
18
|
+
# 1. 发现目标,取得真实 PID、buildKey、路径与哈希
|
|
18
19
|
wowdump targets
|
|
19
|
-
wowdump database status --build "
|
|
20
|
+
wowdump database status --build "<buildKey>"
|
|
20
21
|
|
|
21
|
-
# 2.
|
|
22
|
-
wowdump analyze runtime --pid
|
|
22
|
+
# 2. 仅在需要新的静态证据时导出相关段
|
|
23
|
+
wowdump analyze runtime --pid <pid> --build "<buildKey>" --kind dump --confirm
|
|
23
24
|
|
|
24
|
-
# 3.
|
|
25
|
-
# 4.
|
|
26
|
-
wowdump analyze
|
|
27
|
-
# 5. 将候选证据交给 verify,再用 Reader 验证
|
|
25
|
+
# 3. Agent 使用 IDA MCP 或 iced-x86 分析,生成候选 profile
|
|
26
|
+
# 4. 验证实际读取;不会修改候选状态
|
|
27
|
+
wowdump analyze runtime --pid <pid> --build "<buildKey>" --kind verify --profile "<candidate.profile.json>" --confirm
|
|
28
28
|
|
|
29
|
-
#
|
|
30
|
-
wowdump memory read --pid
|
|
29
|
+
# 5. 补齐字段和对象身份证据,用户确认后保存至当前 build 的 profile 目录
|
|
30
|
+
wowdump memory read --pid <pid> --build "<buildKey>" --profile "<character-stats.json>" --field critPercent
|
|
31
31
|
|
|
32
|
-
#
|
|
33
|
-
wowdump memory regions --pid
|
|
32
|
+
# 有界页面查询
|
|
33
|
+
wowdump memory regions --pid <pid> --start <startAddress> --end <endAddress> --max-regions 20000
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
尖括号是待替换参数。已有匹配且适用的 profile 时直接 Reader 读取,无需重新 dump。CDB 诊断与断点不是必经步骤,执行前先查 Skill 的退出和超时限制。
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
内存和 CDB 请求复用同一个管理员 broker:首次请求 UAC,空闲 20 分钟退出。所有调试请求需要 `--confirm`。
|
|
39
|
+
|
|
40
|
+
## 数据放在哪里
|
|
41
|
+
|
|
42
|
+
统一使用 `~/.wowdump/<buildKey>/`:
|
|
43
|
+
|
|
44
|
+
- `build.json`:目标身份。
|
|
45
|
+
- `database/`:静态分析数据库位置和状态,不因任务重复创建。
|
|
46
|
+
- `runtime/<session>/`:本轮证据、dump 和候选。
|
|
47
|
+
- `profile/`:用户确认的 Reader 接口。
|
|
48
|
+
|
|
49
|
+
Dump 默认导出 `.text/.rdata/.pdata`,`.data` 按需加入。Reader 默认选择当前 build 的 profile,也支持显式路径;它不读取 IDA 数据库或 dump 二进制。
|
|
50
|
+
|
|
51
|
+
## 0.3.9 的读取与验证
|
|
52
|
+
|
|
53
|
+
- verify 接受 candidate 或 reader_ready;普通 memory read 要求 reader_ready。
|
|
54
|
+
- 输出 `validation.read: "passed"`,替代旧版 `verified:true`;fieldSemantics、rootSemantics、crossProcess 为 not_checked,需要独立证据。命令不自动认证语义、提升状态或保存 profile。
|
|
55
|
+
- 新增 `root.lookup`:按动态键选择有界表项,核对对象键并检查读取期间变化,避免固化本轮哈希槽位。含 lookup 的 profile 需要 0.3.9 或更新版本;旧 Reader 可能忽略未知参数。
|
|
56
|
+
- 校验目标 build 时,缺少 executableSha256 会报错。模块外根 RVA、空根、短读和不一致查表结果均返回结构化错误。
|
|
57
|
+
|
|
58
|
+
详细用法由安装的 Skill 按需加载:[命令](skills/wowdump/references/commands.md)、[Profile 格式](skills/wowdump/references/profiles.md)、[字段挖掘方法与成功案例](skills/wowdump/references/character-stats-case.md)、[CDB 限制](skills/wowdump/references/windbg.md)。
|
|
39
59
|
|
|
40
60
|
## 开发验证
|
|
41
61
|
|
|
@@ -43,5 +63,6 @@ Windows reader 首次使用时由 Node 请求 UAC 启动 broker,后续内存
|
|
|
43
63
|
npm run typecheck
|
|
44
64
|
npm run build
|
|
45
65
|
npm test
|
|
46
|
-
npm
|
|
66
|
+
npm run package:verify
|
|
67
|
+
npm run release:verify
|
|
47
68
|
```
|
package/dist/cli.js
CHANGED
|
@@ -313,7 +313,9 @@ async function validateProfileIdentity(home, buildKey, profile, file) {
|
|
|
313
313
|
: profile.executable && typeof profile.executable === "object" && !Array.isArray(profile.executable)
|
|
314
314
|
? String(profile.executable.sha256 ?? "")
|
|
315
315
|
: "";
|
|
316
|
-
if (profileHash
|
|
316
|
+
if (!profileHash)
|
|
317
|
+
throw new CliError("PROFILE_IDENTITY_REQUIRED", "profile executableSha256 is required", { profileFile: file });
|
|
318
|
+
if (profileHash !== build.executableSha256) {
|
|
317
319
|
throw new CliError("EXECUTABLE_MISMATCH", "profile executable hash does not match the selected build", { expected: build.executableSha256, actual: profileHash, profileFile: file });
|
|
318
320
|
}
|
|
319
321
|
}
|
|
@@ -535,8 +537,8 @@ export function createWowdumpCli(dependencies = {}) {
|
|
|
535
537
|
if (normalized.kind === "verify") {
|
|
536
538
|
if (!profile)
|
|
537
539
|
throw new CliError("PROFILE_REQUIRED", "verify requires --profile <file>");
|
|
538
|
-
const result = await profileEngine.
|
|
539
|
-
writeJson(io, { ...result, command: "analyze.runtime.verify"
|
|
540
|
+
const result = await profileEngine.verify({ pid: normalized.pid, buildKey: normalized.build, executableSha256: buildRecord.executableSha256, profile, fields: undefined }, profileAdapter);
|
|
541
|
+
writeJson(io, { ...result, command: "analyze.runtime.verify" });
|
|
540
542
|
return;
|
|
541
543
|
}
|
|
542
544
|
const requestedSections = Array.isArray(options.sections) && options.sections.length > 0
|
|
@@ -132,10 +132,47 @@ function rootSpec(value, name) {
|
|
|
132
132
|
const root = record(value, `${name}.root`);
|
|
133
133
|
if (root.rva === undefined && root.address === undefined)
|
|
134
134
|
throw new ProfileEngineError("PROFILE_INVALID", `${name}.root requires rva or address`);
|
|
135
|
+
if (root.rva !== undefined && root.address !== undefined)
|
|
136
|
+
throw new ProfileEngineError("PROFILE_INVALID", `${name}.root must use rva or address, not both`);
|
|
135
137
|
return root;
|
|
136
138
|
}
|
|
139
|
+
function validateRootRange(value, module) {
|
|
140
|
+
const root = rootSpec(value, "field");
|
|
141
|
+
if (root.rva !== undefined && module.size !== undefined) {
|
|
142
|
+
const rva = hex(root.rva, "root.rva");
|
|
143
|
+
const span = root.dereference === true ? 8n : 1n;
|
|
144
|
+
if (!Number.isSafeInteger(module.size) || module.size < 1 || rva + span > BigInt(module.size)) {
|
|
145
|
+
throw new ProfileEngineError("RVA_OUT_OF_MODULE", "root RVA falls outside the selected module", { rva: String(root.rva), moduleSize: module.size });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (root.lookup !== undefined) {
|
|
149
|
+
const lookup = record(root.lookup, "root.lookup");
|
|
150
|
+
for (const key of ["countRoot", "keyRoot"]) {
|
|
151
|
+
const child = rootSpec(lookup[key], key);
|
|
152
|
+
if (child.lookup !== undefined)
|
|
153
|
+
throw new ProfileEngineError("PROFILE_INVALID", "nested lookups are not supported");
|
|
154
|
+
validateRootRange(child, module);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
137
158
|
export class ProfileEngine {
|
|
138
159
|
async read(request, adapter) {
|
|
160
|
+
return this.readInternal(request, adapter, false);
|
|
161
|
+
}
|
|
162
|
+
async verify(request, adapter) {
|
|
163
|
+
const result = await this.readInternal(request, adapter, true);
|
|
164
|
+
return {
|
|
165
|
+
...result,
|
|
166
|
+
command: "profile.verify",
|
|
167
|
+
validation: {
|
|
168
|
+
read: "passed",
|
|
169
|
+
fieldSemantics: "not_checked",
|
|
170
|
+
rootSemantics: "not_checked",
|
|
171
|
+
crossProcess: "not_checked"
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
async readInternal(request, adapter, allowCandidate) {
|
|
139
176
|
const profile = record(request.profile, "profile");
|
|
140
177
|
const buildKey = String(profile.buildKey ?? "");
|
|
141
178
|
if (!buildKey)
|
|
@@ -148,13 +185,16 @@ export class ProfileEngine {
|
|
|
148
185
|
? profile.executable
|
|
149
186
|
: undefined;
|
|
150
187
|
const profileHash = typeof profile.executableSha256 === "string" ? profile.executableSha256 : String(executable?.sha256 ?? "");
|
|
188
|
+
if (!profileHash)
|
|
189
|
+
throw new ProfileEngineError("PROFILE_IDENTITY_REQUIRED", "profile executableSha256 is required when checking a target build");
|
|
151
190
|
if (profileHash && profileHash !== request.executableSha256) {
|
|
152
191
|
throw new ProfileEngineError("EXECUTABLE_MISMATCH", "profile executable hash does not match the target build", { expected: request.executableSha256, actual: profileHash });
|
|
153
192
|
}
|
|
154
193
|
}
|
|
155
194
|
const status = String(profile.readerStatus ?? profile.status ?? (profile.confidence === "reader_ready" ? "reader_ready" : ""));
|
|
156
|
-
if (status !== "reader_ready")
|
|
157
|
-
throw new ProfileEngineError("PROFILE_NOT_READY", "profile is not reader_ready", { status });
|
|
195
|
+
if (status !== "reader_ready" && !(allowCandidate && status === "candidate")) {
|
|
196
|
+
throw new ProfileEngineError("PROFILE_NOT_READY", allowCandidate ? "verify requires candidate or reader_ready" : "profile is not reader_ready", { status });
|
|
197
|
+
}
|
|
158
198
|
const modules = await adapter.modules(request.pid);
|
|
159
199
|
const moduleSpec = profile.module && typeof profile.module === "object" ? record(profile.module, "profile.module") : {};
|
|
160
200
|
const moduleName = String(moduleSpec.name ?? profile.moduleName ?? "Wow.exe");
|
|
@@ -163,10 +203,14 @@ export class ProfileEngine {
|
|
|
163
203
|
throw new ProfileEngineError("MODULE_NOT_FOUND", `${moduleName} was not found`, { modules: modules.map(item => item.name) });
|
|
164
204
|
const definitions = record(profile.fields ?? profile.readers, "profile.fields");
|
|
165
205
|
const names = request.fields?.length ? [...request.fields] : Object.keys(definitions);
|
|
206
|
+
if (names.length === 0)
|
|
207
|
+
throw new ProfileEngineError("PROFILE_INVALID", "profile must define at least one field");
|
|
166
208
|
const fields = {};
|
|
167
209
|
const reads = [];
|
|
168
210
|
for (const name of names) {
|
|
169
211
|
const definition = record(definitions[name], `field ${name}`);
|
|
212
|
+
if (definition.root !== undefined)
|
|
213
|
+
validateRootRange(definition.root, module);
|
|
170
214
|
fields[name] = await this.readField(request.pid, hex(module.base, "module.base"), name, definition, adapter, reads);
|
|
171
215
|
}
|
|
172
216
|
return { ok: true, command: "profile.read", pid: request.pid, buildKey, profileId: profileId(profile), module, fields, reads };
|
|
@@ -195,9 +239,74 @@ export class ProfileEngine {
|
|
|
195
239
|
if (root.dereference === true) {
|
|
196
240
|
const bytes = await this.readBytes(pid, address, 8, field, adapter, reads);
|
|
197
241
|
address = BigInt(`0x${bytes.toString("hex").match(/../g).reverse().join("")}`);
|
|
242
|
+
if (address === 0n)
|
|
243
|
+
throw new ProfileEngineError("NULL_POINTER", `${field} root is null`);
|
|
198
244
|
}
|
|
245
|
+
if (root.lookup !== undefined)
|
|
246
|
+
return this.resolveLookup(pid, moduleBase, address, root, adapter, field, reads);
|
|
199
247
|
return address;
|
|
200
248
|
}
|
|
249
|
+
async resolveLookup(pid, moduleBase, table, root, adapter, field, reads) {
|
|
250
|
+
const spec = record(root.lookup, `${field}.root.lookup`);
|
|
251
|
+
const countRoot = rootSpec(spec.countRoot, "lookup.countRoot");
|
|
252
|
+
const keyRoot = rootSpec(spec.keyRoot, "lookup.keyRoot");
|
|
253
|
+
if (countRoot.lookup !== undefined || keyRoot.lookup !== undefined)
|
|
254
|
+
throw new ProfileEngineError("PROFILE_INVALID", "nested lookups are not supported");
|
|
255
|
+
const stride = integer(spec.stride, "lookup.stride", 4096);
|
|
256
|
+
const maximum = integer(spec.maxItems, "lookup.maxItems", 65536);
|
|
257
|
+
const keySize = integer(spec.keySize, "lookup.keySize", 64);
|
|
258
|
+
const entryKeyOffset = integer(hex(spec.entryKeyOffset ?? "0x0", "lookup.entryKeyOffset"), "lookup.entryKeyOffset", 4096);
|
|
259
|
+
const valueOffset = integer(hex(spec.valueOffset, "lookup.valueOffset"), "lookup.valueOffset", 4096);
|
|
260
|
+
const keyOffset = hex(spec.keyOffset ?? "0x0", "lookup.keyOffset");
|
|
261
|
+
const objectKeyOffset = hex(spec.objectKeyOffset, "lookup.objectKeyOffset");
|
|
262
|
+
if (!stride || !maximum || !keySize || entryKeyOffset + keySize > stride || valueOffset + 8 > stride || stride * maximum > 1024 * 1024) {
|
|
263
|
+
throw new ProfileEngineError("PROFILE_INVALID", "lookup bounds must fit within the stride and a 1 MiB maximum table");
|
|
264
|
+
}
|
|
265
|
+
const countType = spec.countType ?? "u32";
|
|
266
|
+
if (countType !== "u32" && countType !== "u64")
|
|
267
|
+
throw new ProfileEngineError("PROFILE_INVALID", "lookup.countType must be u32 or u64");
|
|
268
|
+
const countAddress = await this.resolveRoot(pid, moduleBase, countRoot, adapter, field, reads);
|
|
269
|
+
const countBytes = await this.readBytes(pid, countAddress, countType === "u64" ? 8 : 4, field, adapter, reads);
|
|
270
|
+
const countBig = countType === "u64" ? countBytes.readBigUInt64LE() : BigInt(countBytes.readUInt32LE());
|
|
271
|
+
if (countBig > BigInt(maximum))
|
|
272
|
+
throw new ProfileEngineError("LOOKUP_LIMIT", "table count exceeds maxItems", { count: countBig.toString(), maximum });
|
|
273
|
+
const keyAddress = await this.resolveRoot(pid, moduleBase, keyRoot, adapter, field, reads) + keyOffset;
|
|
274
|
+
const key = await this.readBytes(pid, keyAddress, keySize, field, adapter, reads);
|
|
275
|
+
if (key.every(byte => byte === 0))
|
|
276
|
+
throw new ProfileEngineError("LOOKUP_KEY_EMPTY", "lookup key is empty");
|
|
277
|
+
const count = Number(countBig);
|
|
278
|
+
if (!count)
|
|
279
|
+
throw new ProfileEngineError("LOOKUP_NOT_FOUND", "lookup table is empty");
|
|
280
|
+
const bytes = await this.readBytes(pid, table, count * stride, field, adapter, reads);
|
|
281
|
+
let match = -1;
|
|
282
|
+
for (let index = 0; index < count; index++) {
|
|
283
|
+
const offset = index * stride;
|
|
284
|
+
if (!bytes.subarray(offset + entryKeyOffset, offset + entryKeyOffset + keySize).equals(key))
|
|
285
|
+
continue;
|
|
286
|
+
if (match !== -1)
|
|
287
|
+
throw new ProfileEngineError("LOOKUP_AMBIGUOUS", "multiple table entries match the key");
|
|
288
|
+
match = offset;
|
|
289
|
+
}
|
|
290
|
+
if (match === -1)
|
|
291
|
+
throw new ProfileEngineError("LOOKUP_NOT_FOUND", "no table entry matches the key");
|
|
292
|
+
const object = bytes.readBigUInt64LE(match + valueOffset);
|
|
293
|
+
if (object === 0n)
|
|
294
|
+
throw new ProfileEngineError("NULL_POINTER", "matching table entry contains a null pointer");
|
|
295
|
+
const objectKey = await this.readBytes(pid, object + objectKeyOffset, keySize, field, adapter, reads);
|
|
296
|
+
if (!objectKey.equals(key))
|
|
297
|
+
throw new ProfileEngineError("LOOKUP_OBJECT_MISMATCH", "object key does not match the selected table entry");
|
|
298
|
+
const { lookup: _lookup, ...tableRoot } = root;
|
|
299
|
+
const tableAfter = await this.resolveRoot(pid, moduleBase, tableRoot, adapter, field, reads);
|
|
300
|
+
const countAddressAfter = await this.resolveRoot(pid, moduleBase, countRoot, adapter, field, reads);
|
|
301
|
+
const keyAddressAfter = await this.resolveRoot(pid, moduleBase, keyRoot, adapter, field, reads) + keyOffset;
|
|
302
|
+
const countAfter = await this.readBytes(pid, countAddressAfter, countBytes.length, field, adapter, reads);
|
|
303
|
+
const keyAfter = await this.readBytes(pid, keyAddressAfter, keySize, field, adapter, reads);
|
|
304
|
+
const entryAfter = await this.readBytes(pid, table + BigInt(match), stride, field, adapter, reads);
|
|
305
|
+
if (tableAfter !== table || countAddressAfter !== countAddress || keyAddressAfter !== keyAddress || !countAfter.equals(countBytes) || !keyAfter.equals(key) || !entryAfter.subarray(entryKeyOffset, entryKeyOffset + keySize).equals(key) || entryAfter.readBigUInt64LE(valueOffset) !== object) {
|
|
306
|
+
throw new ProfileEngineError("LOOKUP_CHANGED", "lookup identity or table changed during the read; retry with a fresh snapshot");
|
|
307
|
+
}
|
|
308
|
+
return object;
|
|
309
|
+
}
|
|
201
310
|
async readLayout(pid, address, layout, field, adapter, reads) {
|
|
202
311
|
const kind = String(layout.kind ?? "scalar");
|
|
203
312
|
if (kind === "linked_list") {
|
|
@@ -240,7 +349,7 @@ export class ProfileEngine {
|
|
|
240
349
|
const bytesRead = Number(result.bytesRead ?? result.dataHex.length / 2);
|
|
241
350
|
const bytes = Buffer.from(result.dataHex, "hex");
|
|
242
351
|
reads.push({ field, address: hexText(address), size, bytesRead, dataHex: result.dataHex, complete: result.complete === true && bytesRead === size });
|
|
243
|
-
if (bytesRead !== size || bytes.length
|
|
352
|
+
if (result.complete !== true || bytesRead !== size || bytes.length !== size || !/^(?:[0-9a-f]{2})*$/i.test(result.dataHex))
|
|
244
353
|
throw new ProfileEngineError("PARTIAL_READ", `${field} read was incomplete`, { address: hexText(address), size, bytesRead });
|
|
245
354
|
return bytes.subarray(0, size);
|
|
246
355
|
}
|
package/dist/toolchain.js
CHANGED
|
@@ -1,112 +1,87 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname,
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { resolveWowdumpHome } from "./core/build-store.js";
|
|
7
|
-
export const DEFAULT_WOWDUMP_SKILL = `---
|
|
8
|
-
name: wowdump
|
|
9
|
-
description: 用 WinDbg CDB 运行时证据和 IDA Pro MCP 或 iced-x86 定位 WoW 字段,生成并验证 Reader profile。
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# wowdump
|
|
13
|
-
|
|
14
|
-
先运行 \`wowdump targets\`,再复用 \`~/.wowdump/<buildKey>/database/\`。runtime dump 保存当前 session 的二进制证据,debug 命令保存 CDB 证据,Reader 只读取 build profile。详情见 references/workflow.md、references/commands.md、references/windbg.md、references/disassemble.md 和 references/profiles.md。
|
|
15
|
-
`;
|
|
16
|
-
export const DEFAULT_WOWDUMP_COMMANDS = `# wowdump 命令参考
|
|
17
|
-
|
|
18
|
-
1. \`wowdump targets\`
|
|
19
|
-
2. \`wowdump database status --build <buildKey>\`
|
|
20
|
-
3. \`wowdump analyze runtime --pid <pid> --build <buildKey> --kind dump --confirm\`
|
|
21
|
-
4. \`wowdump analyze debug --pid <pid> --build <buildKey> --rva <rva> --kind breakpoint --confirm\`
|
|
22
|
-
5. \`wowdump analyze runtime --pid <pid> --build <buildKey> --kind verify --profile <profile.json> --confirm\`
|
|
23
|
-
6. \`wowdump memory read --pid <pid> --build <buildKey> --profile <profile.json> --field <name>\`
|
|
24
|
-
|
|
25
|
-
\`dump\` 默认流式导出供 IDA/iced-x86 分析的 PE 段并生成 manifest;\`verify\` 只验证已有候选 RVA。未确认的候选 profile 放在 runtime session 目录;确认后才复制到当前 build 的 profile 目录。
|
|
26
|
-
`;
|
|
27
7
|
function normalizePath(value) { return resolve(value.trim().replace(/^"|"$/g, "")); }
|
|
28
|
-
function directoryExists(value) { try {
|
|
29
|
-
return statSync(value).isDirectory();
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return false;
|
|
33
|
-
} }
|
|
34
|
-
function fileExists(value) { try {
|
|
35
|
-
return statSync(value).isFile();
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return false;
|
|
39
|
-
} }
|
|
40
|
-
async function createFileIfMissing(file, contents, created, preserved) { try {
|
|
41
|
-
await writeFile(file, contents, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
42
|
-
created.push(file);
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
if (error.code === "EEXIST")
|
|
46
|
-
preserved.push(file);
|
|
47
|
-
else
|
|
48
|
-
throw error;
|
|
49
|
-
} }
|
|
50
8
|
export { resolveWowdumpHome, sha256File } from "./core/build-store.js";
|
|
51
|
-
export function resolveWowdumpSkillHome(env = process.env, userHome = homedir()) {
|
|
9
|
+
export function resolveWowdumpSkillHome(env = process.env, userHome = homedir()) {
|
|
10
|
+
return normalizePath(env.WOWDUMP_SKILL_HOME || join(userHome, ".agents", "skills"));
|
|
11
|
+
}
|
|
52
12
|
export async function initializeWowdumpHome(options = {}) {
|
|
53
13
|
const env = options.env ?? process.env;
|
|
54
14
|
const home = normalizePath(options.home ?? resolveWowdumpHome(env, options.userHome));
|
|
55
|
-
const created = [];
|
|
56
|
-
const preserved = [];
|
|
57
|
-
await mkdir(home, { recursive: true });
|
|
58
|
-
const configFile = join(home, "config.json");
|
|
59
|
-
await createFileIfMissing(configFile, `${JSON.stringify({ schema: "wowdump.config.v1" }, null, 2)}\n`, created, preserved);
|
|
60
15
|
const skillDirectory = join(normalizePath(options.skillHome ?? resolveWowdumpSkillHome(env, options.userHome)), "wowdump");
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
16
|
+
const bundled = resolve(dirname(fileURLToPath(import.meta.url)), "..", "skills", "wowdump");
|
|
17
|
+
const result = {
|
|
18
|
+
home, created: [], preserved: [], updated: [], backups: [],
|
|
19
|
+
configFile: join(home, "config.json"), skillFile: join(skillDirectory, "SKILL.md"),
|
|
20
|
+
commandsFile: join(skillDirectory, "references", "commands.md")
|
|
21
|
+
};
|
|
22
|
+
// Read the complete source before making changes; an incomplete package must not install a fallback skill.
|
|
23
|
+
const files = new Map();
|
|
24
|
+
async function collect(directory, prefix = "") {
|
|
25
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
26
|
+
const name = join(prefix, entry.name);
|
|
27
|
+
if (entry.isDirectory())
|
|
28
|
+
await collect(join(directory, entry.name), name);
|
|
29
|
+
else if (entry.isFile())
|
|
30
|
+
files.set(name, await readFile(join(directory, entry.name)));
|
|
31
|
+
}
|
|
66
32
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
33
|
+
await collect(bundled);
|
|
34
|
+
if (!files.has("SKILL.md") || !files.has(join("references", "commands.md")))
|
|
35
|
+
throw new Error("SKILL_BUNDLE_INVALID: required skill files are missing");
|
|
36
|
+
if (options.skillSource)
|
|
37
|
+
files.set("SKILL.md", await readFile(normalizePath(options.skillSource)));
|
|
38
|
+
if (options.commandsSource)
|
|
39
|
+
files.set(join("references", "commands.md"), await readFile(normalizePath(options.commandsSource)));
|
|
40
|
+
await mkdir(home, { recursive: true });
|
|
41
|
+
try {
|
|
42
|
+
await writeFile(result.configFile, JSON.stringify({ schema: "wowdump.config.v1" }, null, 2) + "\n", { flag: "wx", mode: 0o600 });
|
|
43
|
+
result.created.push(result.configFile);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.code !== "EEXIST")
|
|
47
|
+
throw error;
|
|
48
|
+
result.preserved.push(result.configFile);
|
|
77
49
|
}
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
50
|
+
const backupDirectory = join(home, "skill-backups", randomUUID());
|
|
51
|
+
for (const [name, contents] of files) {
|
|
52
|
+
const destination = join(skillDirectory, name);
|
|
53
|
+
let existing;
|
|
54
|
+
try {
|
|
55
|
+
existing = await readFile(destination);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error.code !== "ENOENT")
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
if (existing?.equals(contents)) {
|
|
62
|
+
result.preserved.push(destination);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (existing) {
|
|
66
|
+
const backup = join(backupDirectory, name);
|
|
67
|
+
await mkdir(dirname(backup), { recursive: true });
|
|
68
|
+
await writeFile(backup, existing, { flag: "wx", mode: 0o600 });
|
|
69
|
+
result.backups.push(backup);
|
|
70
|
+
}
|
|
71
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
72
|
+
const temporary = `${destination}.${randomUUID()}.tmp`;
|
|
73
|
+
await writeFile(temporary, contents, { flag: "wx", mode: 0o600 });
|
|
74
|
+
await rename(temporary, destination);
|
|
75
|
+
(existing ? result.updated : result.created).push(destination);
|
|
91
76
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
export async function bootstrapToolchain(options = {}) {
|
|
80
|
+
return { ...await initializeWowdumpHome(options), toolchain: { ready: true, disassembler: "iced-x86" }, installed: [] };
|
|
81
|
+
}
|
|
82
|
+
export function resolveToolchain(options = {}) {
|
|
83
|
+
return { home: normalizePath(options.home ?? resolveWowdumpHome(options.env ?? process.env, options.userHome)), ready: true, disassembler: "iced-x86" };
|
|
98
84
|
}
|
|
99
|
-
export async function bootstrapToolchain(options = {}) { const home = await initializeWowdumpHome(options); return { ...home, toolchain: { ready: true, disassembler: "iced-x86" }, installed: [] }; }
|
|
100
|
-
export function resolveToolchain(options = {}) { return { home: normalizePath(options.home ?? resolveWowdumpHome(options.env ?? process.env, options.userHome)), ready: true, disassembler: "iced-x86" }; }
|
|
101
|
-
export function inspectPeSections(buffer) { if (buffer.length < 0x40 || buffer.toString("ascii", 0, 2) !== "MZ")
|
|
102
|
-
return []; const pe = buffer.readUInt32LE(0x3c); if (pe + 24 > buffer.length || buffer.toString("ascii", pe, pe + 4) !== "PE\0\0")
|
|
103
|
-
return []; const count = buffer.readUInt16LE(pe + 6); const optional = buffer.readUInt16LE(pe + 20); const table = pe + 24 + optional; const out = []; for (let i = 0; i < count; i++) {
|
|
104
|
-
const off = table + i * 40;
|
|
105
|
-
if (off + 40 > buffer.length)
|
|
106
|
-
break;
|
|
107
|
-
out.push({ name: buffer.subarray(off, off + 8).toString("ascii").replace(/\0+$/, ""), virtualSize: `0x${buffer.readUInt32LE(off + 8).toString(16)}`, rva: `0x${buffer.readUInt32LE(off + 12).toString(16)}`, rawSize: `0x${buffer.readUInt32LE(off + 16).toString(16)}`, rawOffset: `0x${buffer.readUInt32LE(off + 20).toString(16)}` });
|
|
108
|
-
} return out; }
|
|
109
85
|
const invokedFile = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : "";
|
|
110
|
-
if (invokedFile === import.meta.url && process.argv.includes("--postinstall"))
|
|
86
|
+
if (invokedFile === import.meta.url && process.argv.includes("--postinstall"))
|
|
111
87
|
await bootstrapToolchain();
|
|
112
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wowdump",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "WoW native memory analysis CLI with WinDbg CDB evidence, IDA/iced-x86 disassembly, and an elevated reader broker",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -44,6 +44,6 @@
|
|
|
44
44
|
},
|
|
45
45
|
"allowScripts": {
|
|
46
46
|
"koffi@2.16.3": true,
|
|
47
|
-
"wowdump@0.3.
|
|
47
|
+
"wowdump@0.3.9": true
|
|
48
48
|
}
|
|
49
49
|
}
|
package/skills/wowdump/SKILL.md
CHANGED
|
@@ -1,31 +1,24 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wowdump
|
|
3
|
-
description: 使用
|
|
3
|
+
description: 使用 wowdump CLI 定位和读取 WoW 原生内存字段,结合 CDB、IDA Pro MCP 或 iced-x86 证据生成并验证 Reader profile。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# wowdump
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
先明确要读的对象、字段和单位。例如“角色属性”中的暴击等级与暴击百分比是不同字段;不要用 Aura、资源值等其他结果替代。
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
1. 运行 `wowdump targets`,使用返回的 PID、路径、buildKey 和哈希;多个目标且用户尚未指定时请用户选择。
|
|
11
|
+
2. 有匹配 build/hash 的已验证 profile 时直接 Reader 读取。用户要求全新分析时,创建新 session,不使用旧候选作为本轮发现。
|
|
12
|
+
3. 未知字段按 [定位流程](references/workflow.md) 收集证据;需要静态分析时先读 [IDA 与局部反汇编](references/disassemble.md)。
|
|
13
|
+
4. 按 [Profile 格式与生命周期](references/profiles.md) 构建候选,按 [验证方法](references/evidence-workflow.md) 分别验证读取、字段含义和复用能力。
|
|
14
|
+
5. 返回字段、单位、观测时间和证据路径。用户确认持久化后保存 profile;已有保存授权时沿用。
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
6. 需要 CDB 的其他诊断能力时使用 `wowdump analyze debug --kind command --cdb-commands <JSON数组> --confirm`。参数和命令都是数组,不经过 shell。
|
|
18
|
-
7. 由 IDA Pro MCP 优先分析原始 Wow.exe;没有 IDA MCP 时用 iced-x86 做局部反汇编。输出 RVA、调用关系、结构偏移和字段类型。
|
|
19
|
-
8. 将证据整理为 candidate profile,再运行 `wowdump analyze runtime --kind verify --profile <profile> --confirm`。
|
|
20
|
-
9. Reader 实读成功且用户确认后,才把 profile 放到 `~/.wowdump/<buildKey>/profile/`。
|
|
16
|
+
按需查阅:
|
|
17
|
+
- [命令参数](references/commands.md):CLI 用法。
|
|
18
|
+
- [字段定义](references/request-schema.md):目标含义不明确时。
|
|
19
|
+
- [CDB 使用与退出限制](references/windbg.md):执行 dump 或 debug 前。
|
|
20
|
+
- [字段挖掘方法与角色属性案例](references/character-stats-case.md):未知字段、根指针不稳定或静态分析卡住时参考;学习证据链,不照抄地址。
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
目录统一为 `~/.wowdump/<buildKey>/`:`database/` 保存静态数据库,`runtime/<session>/` 保存本轮证据与候选,`profile/` 保存已确认的持久化接口。Reader 读取 profile。broker 首次 UAC 后复用,空闲 20 分钟退出。
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
- Reader broker:一次 UAC 后复用 20 分钟,负责 `ReadProcessMemory`、`VirtualQueryEx`、模块枚举和 CDB 子进程。
|
|
26
|
-
- IDA/iced-x86:静态分析,不读取运行时堆状态。
|
|
27
|
-
- Reader:根据 profile 的 RVA、指针链、类型和边界做有界读取;不猜地址。
|
|
28
|
-
|
|
29
|
-
详细命令、调试器参数、证据门槛和 profile 格式见 references 目录。
|
|
30
|
-
|
|
31
|
-
IDA MCP 使用长 TTL 会话(默认 `idle_ttl_sec: 3600`)。每次查询前先做 health probe;出现 `worker not reachable` 时按 `references/disassemble.md` 重新打开并只重试一次。IDA worker 与 Windows broker 独立,broker 的 UAC 复用不会保持 IDA worker 存活。
|
|
24
|
+
脚本和任务证据放在 runtime session,具体地址由本轮分析确定。保留禁用 Frida Hook 和 Stalker 的约束。
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# 字段挖掘参考:从角色属性案例提炼方法
|
|
2
|
+
|
|
3
|
+
查询暴击、急速、精通等角色属性时参考。这里记录一次真实调查的方法与反例,不把旧 build 的偏移当成新任务的答案。用户要求全新分析时重新建立证据。
|
|
4
|
+
|
|
5
|
+
## 这次真正奏效的思路
|
|
6
|
+
|
|
7
|
+
**从取值代码确认字段,从对象身份确认根,最后用 Reader 验证整条路径。** 找到一个数值或一条当前能读的指针链,只完成了其中一部分。
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
目标字段、类型和单位
|
|
11
|
+
→ API 名称 / 注册关系 / getter 的真实取值指令
|
|
12
|
+
→ 对象相对字段偏移 + 转换公式
|
|
13
|
+
→ 可读页内的对象候选 + 多字段独立对照
|
|
14
|
+
→ 对象根的引用、初始化、更新与失效路径
|
|
15
|
+
→ 稳定身份来源 + 固定指针链或动态容器选择规则
|
|
16
|
+
→ candidate profile → 实时 verify → 用户已授权的正式 profile
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
这不是要求所有任务扫描整个流程。已有匹配 build/hash 且适用的正式 profile 时直接读取;已有哪一层证据,就从缺失的一层继续。具体命令见 [commands.md](commands.md),静态工具选择见 [disassemble.md](disassemble.md)。
|
|
20
|
+
|
|
21
|
+
### 给下一次调查的操作要点
|
|
22
|
+
|
|
23
|
+
1. **字段语义先收敛。** “暴击等级”和“暴击百分比”分开;记录 getter 是直接读取还是有公式转换。没有证明的职业修正系数不擅自补上。
|
|
24
|
+
2. **静态分析提供结构,不代替实时值。** 使用原始 PE 和同 build/hash 的运行时段;优先 IDA MCP,缺少时用 iced-x86 做局部分析。按原 RVA 映射,结合 `.rdata` 名称、类型和 `.pdata` 边界,而不是只看少量 `.text` 字节。函数边界是线索,重叠代码仍需按控制流确认。
|
|
25
|
+
3. **工具的空结果也要验证。** 用一条已知 RIP 相对指令检查解码器输出,再信任批量检索。先证明脚本能找到已知样本,别把接口用错造成的零结果解释成程序没有结构。
|
|
26
|
+
4. **按真实路径解码。** 遇到重叠字节、异常指令密集区,回到有证据的入口和跳转目标。只有证明条件互补且中间不改变相关标志,才把两条同目标条件跳转折叠。中间值不是返回值;指针计算失败时保留失败假设,不凭低位相似宣布成功。
|
|
27
|
+
5. **把值与对象身份绑在一起。** 页可读、数值合理、几个字段相等,支持对象候选;还要找 GUID 或其他稳定身份来源,区分自己、目标、附近同类型对象和旧缓存。CDB 与 Reader 一致证明读取实现一致,角色面板或独立状态变化才补充字段语义证据。
|
|
28
|
+
6. **查根的生命周期,而不是挑最短链。** 查看初始化、查找、插入、删除、扩容和清理路径。遇到容器时,确认容量、步长、键、值及空/删除槽含义;固定结构偏移可以存,哈希槽位、列表序号和本轮对象地址不应伪装成固定结构。
|
|
29
|
+
7. **优先找稳定身份路径。** getter 的快速缓存路径难还原时,继续看缓存未命中分支:它可能显式取得身份并查对象。本次正是从回退分支找到玩家 GUID 来源,避开了未完整还原的缓存指针算法;这是一种可选策略,不代表所有 getter 都有这样的分支。
|
|
30
|
+
8. **验证完整的读取规则。** 用实际 profile 读取,而不只运行临时脚本;保留原始字节、解析值、命令和时间。查表要有上限、唯一命中、对象身份复核和变化检测。普通 Reader 对落盘文件再读一次,确认正式接口确实可用。
|
|
31
|
+
|
|
32
|
+
### 卡住时如何选下一步
|
|
33
|
+
|
|
34
|
+
| 现象 | 下一项有效动作 |
|
|
35
|
+
|---|---|
|
|
36
|
+
| API 字符串存在但没有直接 XREF | 检查注册表、间接引用、地址映射及检索器已知样本;名字地址不当函数入口。 |
|
|
37
|
+
| 线性反汇编出现大量异常指令 | 回到调用点/分支目标,分析相关标志位,再解码可达块;不扩大同一错误起点的输出。 |
|
|
38
|
+
| 当前读数正确,隔一会变成零或异常值 | 核对对象身份、根槽和容器变化;不把错误结果解释成属性真的为零。 |
|
|
39
|
+
| 多条链都能到同一个对象 | 查它们所属结构和生命周期;短链、重复命中次数都不等于当前玩家语义。 |
|
|
40
|
+
| 固定槽位失效 | 找键及键的动态来源,按容器规则查找;不只换成新的槽位偏移。 |
|
|
41
|
+
| getter 的缓存解码复杂 | 查缓存未命中/重建路径及其返回的标识,再寻找可读的对象索引。 |
|
|
42
|
+
| verify 成功但含义仍不明 | 分开报告读取、字段语义、根语义和跨进程验证;不自动提升状态。 |
|
|
43
|
+
| 容器中找不到玩家 | 返回未找到;检查索引是否覆盖该状态,必要时继续找更完整索引,不选第一项兜底。 |
|
|
44
|
+
|
|
45
|
+
每轮调查保留“当前假设 → 使用证据 → 结果 → 下一项能区分假设的检查”。失败后改假设或检查工具,不重复同一无效扫描;需要换装、换角色或重启才能区分时,具体说明要用户配合什么,不把缺少这项验证说成永远找不到。
|
|
46
|
+
|
|
47
|
+
### 什么进 CLI,什么留在任务里
|
|
48
|
+
|
|
49
|
+
- CLI 负责通用读取、页查询、有界遍历、类型解析及结构化错误;本次新增的是通用 `root.lookup`,不是写死某个游戏表。
|
|
50
|
+
- 本 build 的 RVA、字段偏移、键布局和证据进 profile;临时检索脚本、dump、失败假设进 runtime session。
|
|
51
|
+
- Skill 负责何时选哪条分析路径及如何判断证据。禁止 Hook/Stalker 的现有约束保持不变;本次成功不依赖它们。
|
|
52
|
+
- 只有发现通用工具缺陷或缺少通用读取表达能力才改框架,不为每次查询改源码。新增能力需测试失败路径,并确认实际使用的 CLI 支持它。
|
|
53
|
+
|
|
54
|
+
下面保留本次具体过程,便于对照上述判断方法。历史读数和地址仅适用于标注的 build 与证据范围。
|
|
55
|
+
|
|
56
|
+
## 先找字段含义,再找对象
|
|
57
|
+
|
|
58
|
+
API 名称所在的只读数据不是 getter 函数。需要沿注册关系、代码引用和真实控制流,确定取值指令、对象来源与字段类型。getter 的函数 RVA 与存放对象指针的全局槽位 RVA 也不是同一个地址;Reader 读取后者,不执行 getter。
|
|
59
|
+
|
|
60
|
+
线性反汇编遇到重叠指令或混淆分支时,按有证据的跳转目标重新解码。不要把错误路径的算式当成指针算法。本次一条重建算法生成了无效高位、看似正确的低 32 位;低位只能缩小候选范围,页检查和多字段匹配才支持进一步调查。
|
|
61
|
+
|
|
62
|
+
本次 build `retail@12.1.0.69587` 的静态/运行时证据支持以下对象相对位置:
|
|
63
|
+
|
|
64
|
+
| 字段 | 对象偏移 | 类型及解释 |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| 智力 | 0x1f70 | u32,属性点 |
|
|
67
|
+
| 耐力 | 0x1f6c | u32,属性点 |
|
|
68
|
+
| 生命值 | 0x19b8 | u64 |
|
|
69
|
+
| 暴击 | 0x723c | f32,百分比 |
|
|
70
|
+
| 急速倍率 | 0x1ccc | f32;getter 转换为 `100 × (1 / value − 1)` |
|
|
71
|
+
| 精通原始值 | 0x7254 | f32,不等于职业专属精通效果百分比 |
|
|
72
|
+
|
|
73
|
+
这些不是模块 RVA。全能候选尚未完成语义验证,所以没有混入已核对的字段。
|
|
74
|
+
|
|
75
|
+
## 有界读取与独立对照
|
|
76
|
+
|
|
77
|
+
用 VirtualQueryEx 结果限定可读区间,每次读取截断到当前 region 末尾;短读保留错误,不用零填补。多个候选要对照一组字段,而不是只匹配一个常见数值。角色切换可能改变数值,也可能更换对象,需记录观测时刻。
|
|
78
|
+
|
|
79
|
+
本次 Reader 与一次 CDB 非侵入读取分别得到:智力 2669、耐力 31204、生命值 624080、暴击约 17.391304%、急速倍率约 0.818332613(换算约 22.199700%)、精通原始值约 31.326086。这是历史观测,不是下一次查询的预期常量。
|
|
80
|
+
|
|
81
|
+
CDB 使用方式和退出限制见 [windbg.md](windbg.md)。本次 `-pd` 曾出现退出码为零但命令未执行;改用 `-pvr` 并以 `qd` 结束后才取得实际读数。必须核对输出标记、读数、超时和目标状态,单凭 exitCode:0 不足以认定成功。此案例没有 Hook,也没有调用未知原生函数。
|
|
82
|
+
|
|
83
|
+
## 从对象反查根:一次命中不代表稳定
|
|
84
|
+
|
|
85
|
+
在有界模块数据和候选指针范围内查找引用,保存每个候选槽位及间接偏移。读取较多小块时复用 broker 连接,完成后关闭;本次逐请求建连接曾耗尽临时端口,改为单连接解决。
|
|
86
|
+
|
|
87
|
+
本次发现的链采用记号 `*[地址]` 表示读取 8 字节指针:
|
|
88
|
+
|
|
89
|
+
| 候选对象链 | 后续实际复核 |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `*[*[base + 0x61f0110] + 0x28]` | 最初命中玩家,后续指向其他地址;智力、耐力、暴击变成零。排除为已验证玩家根。 |
|
|
92
|
+
| `*[*[base + 0x6288450] + 0x3b8]` | 后续读到零和异常浮点数,排除为已验证玩家根。 |
|
|
93
|
+
| `*[*[base + 0x59c5ec8] + 0x1c8]` | 当时仍匹配六项读数;后续证实是容器槽位,正式方案已改为按 GUID 查表。 |
|
|
94
|
+
| `*[*[base + 0x6202890] + 0x980]` | 后续仍匹配上述六项读数;根语义和跨重启表现待确认。 |
|
|
95
|
+
|
|
96
|
+
最短链失败时,CLI 仍返回 `verified:true`,因为内存读取完整。这是**读取成功不等于对象正确**的实测反例。不要为了落盘而挑一条偶然匹配的链。
|
|
97
|
+
|
|
98
|
+
## 如何收敛成 profile
|
|
99
|
+
|
|
100
|
+
继续分析候选根的读写引用、所属对象和列表选择条件,确认它为何指向当前玩家。把得到的模块 RVA、解引用步骤与字段偏移写入候选,使用同一份 profile 反复读取并对照;允许用户配合换装、换角色或重启验证,但分别记录覆盖范围。
|
|
101
|
+
|
|
102
|
+
按 [profiles.md](profiles.md) 保存:身份必须匹配,根语义成立,字段经过验证,用户已授权。跨重启尚未验证时明确注明;根语义未成立则保留候选,不放进持久化 profile 目录。固定槽位方案后来被排除,最终采用下节的按玩家 GUID 选对象方案。
|
|
103
|
+
|
|
104
|
+
## 最终确认:玩家 GUID + 动态查表
|
|
105
|
+
|
|
106
|
+
后续修正临时检索脚本:iced-x86 的接口是 isIpRelMemoryOperand / ipRelMemoryAddress,先前使用不存在的属性导致空结果;旧 root-static-xrefs.json 因此作废。正确检索找到根的读写引用。
|
|
107
|
+
|
|
108
|
+
初始化函数 RVA 0x258570、查找函数 0x25e57b0 与类型字符串证明 0x59c5ec8 是 GUID → AIObstacle 表,容量在 0x59c5ec0,步长 0x28,键位于条目 +0,关联对象指针位于 +0x10。旧 +0x1c8 是槽位 11 的值,不是结构常量;表扩容或条目位置变化后不应继续使用。
|
|
109
|
+
|
|
110
|
+
玩家 getter 的缓存回退路径:0x251dc76 调用 0x251e020,正常分支 0x251e373 跳到 0x1f9dfb0;后者正常分支 0x1f9e1a2 返回 `*[base + 0x7dec228] + 0x10`。此处是当前玩家 16 字节 GUID 的来源,而不是猜测选中目标的全局变量。当前进程四处关键代码字节与静态段相同;该 GUID 和已核对属性对象的 +0x10 GUID 完全一致。
|
|
111
|
+
|
|
112
|
+
正式 profile 使用 root.lookup:动态读取玩家 GUID,在容量上限内寻找唯一相同键,读取对象指针,再比较对象 GUID;最后复读表根、容量、键和命中条目。找不到玩家或发生变化时返回错误,不退回旧槽位。AIObstacle 表并非已证明覆盖所有游戏状态的完整对象索引,玩家条目缺失应明确报告 LOOKUP_NOT_FOUND。
|
|
113
|
+
|
|
114
|
+
三轮真实 verify 与落盘后的普通 memory read 均成功,且属性随游戏状态变化。最终文件位于 `~/.wowdump/retail@12.1.0.69587/profile/character-stats.json`,只存模块 RVA 和相对布局。未测试真实重启,因此 restartVerified 保持 false;不要把合成重定位测试描述为真实跨进程测试。
|
|
115
|
+
|
|
116
|
+
## 本次本机证据索引
|
|
117
|
+
|
|
118
|
+
以下文件位于 `~/.wowdump/retail@12.1.0.69587/runtime/character-stats-11628/`,不是 npm 包内容。其他机器缺少这些文件时,不将案例当作可执行 profile。
|
|
119
|
+
|
|
120
|
+
- `candidate-comparison.json`:多候选、面板字段对照。
|
|
121
|
+
- `cdb-current-stats-pvr.json`、`cdb-stats-summary.json`:独立读取结果及限制。
|
|
122
|
+
- `character-stats-profile-verification.json`:会话绝对地址 profile 的 Reader 验证。
|
|
123
|
+
- `root-indirect-scan.json`:模块根候选与间接引用。
|
|
124
|
+
- `root-static-xrefs.json`:已作废的错误检索结果;使用 root-xrefs-corrected.json 和 root-functions-corrected.txt。
|
|
125
|
+
- `candidate.profile.json`、`rva-candidate-verify.json`:最短 RVA 候选及其失败的语义复核。
|
|
126
|
+
- `other-roots-verify.json`:其余三条链的 CLI 参数、原始输出和退出码。
|
|
127
|
+
- `root-59c5ec8.candidate.profile.json`、`local-cli-candidate-verify.json`:修订后的本地 CLI 直接验证 candidate,仍得到六项匹配读数;validation 区分读取与未检查项目,候选状态不变。
|
|
128
|
+
- `player-guid-source-verify.json`:玩家 GUID 与对象 GUID 的实时对照。
|
|
129
|
+
- `current-code-check.json`:当前进程关键代码与静态段的字节比对。
|
|
130
|
+
- `guid-lookup-live-verify.json`、`formal-profile-read.json`:按键查表的三轮 verify,以及正式 profile 的普通 Reader 读取结果。
|
|
@@ -6,7 +6,7 @@ Windows 下内存和 CDB 请求都连接同一个管理员 broker。首次连接
|
|
|
6
6
|
|
|
7
7
|
```powershell
|
|
8
8
|
wowdump targets
|
|
9
|
-
wowdump targets --pid
|
|
9
|
+
wowdump targets --pid <pid>
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
使用输出中的 `pid`、`path`、`buildKey` 和哈希。多个目标时先让用户选择。
|
|
@@ -15,8 +15,8 @@ wowdump targets --pid 33976
|
|
|
15
15
|
|
|
16
16
|
```powershell
|
|
17
17
|
wowdump analyze runtime `
|
|
18
|
-
--pid
|
|
19
|
-
--build "
|
|
18
|
+
--pid <pid> `
|
|
19
|
+
--build "<buildKey>" `
|
|
20
20
|
--kind dump `
|
|
21
21
|
--sections .text .rdata .pdata `
|
|
22
22
|
--confirm
|
|
@@ -28,17 +28,17 @@ wowdump analyze runtime `
|
|
|
28
28
|
|
|
29
29
|
```powershell
|
|
30
30
|
wowdump analyze debug `
|
|
31
|
-
--pid
|
|
32
|
-
--build "
|
|
33
|
-
--rva
|
|
31
|
+
--pid <pid> `
|
|
32
|
+
--build "<buildKey>" `
|
|
33
|
+
--rva <confirmedRva> `
|
|
34
34
|
--kind breakpoint `
|
|
35
35
|
--max-hits 3 `
|
|
36
36
|
--duration-ms 3000 `
|
|
37
37
|
--confirm
|
|
38
38
|
|
|
39
39
|
wowdump analyze debug `
|
|
40
|
-
--pid
|
|
41
|
-
--build "
|
|
40
|
+
--pid <pid> `
|
|
41
|
+
--build "<buildKey>" `
|
|
42
42
|
--kind command `
|
|
43
43
|
--cdb-commands '["lm","r","~"]' `
|
|
44
44
|
--cdb-args '["-lines"]' `
|
|
@@ -50,21 +50,23 @@ wowdump analyze debug `
|
|
|
50
50
|
## 数据库和验证
|
|
51
51
|
|
|
52
52
|
```powershell
|
|
53
|
-
wowdump database status --build "
|
|
54
|
-
wowdump analyze runtime --pid
|
|
53
|
+
wowdump database status --build "<buildKey>"
|
|
54
|
+
wowdump analyze runtime --pid <pid> --build "<buildKey>" --kind verify --profile <profile.json> --confirm
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
`verify`
|
|
57
|
+
`verify` 接受 candidate 和 reader_ready,执行 profile 的 RVA、指针链、lookup 和类型读取,不读取 IDA 数据库或 dump 文件,也不修改候选状态。validation.read 为 passed 仅表示读取成功;字段语义、根语义和跨进程复用不由该命令认证。补齐相关证据并得到用户确认后,才保存到 `~/.wowdump/<buildKey>/profile/`。
|
|
58
58
|
|
|
59
59
|
## Reader
|
|
60
60
|
|
|
61
61
|
```powershell
|
|
62
62
|
wowdump memory read `
|
|
63
|
-
--pid
|
|
64
|
-
--build "
|
|
65
|
-
--profile "$HOME\.wowdump
|
|
63
|
+
--pid <pid> `
|
|
64
|
+
--build "<buildKey>" `
|
|
65
|
+
--profile "$HOME\.wowdump\<buildKey>\profile\player-state.json" `
|
|
66
66
|
--field playerStats
|
|
67
|
-
wowdump memory regions --pid
|
|
67
|
+
wowdump memory regions --pid <pid> --start <startAddress> --end <endAddress> --max-regions 20000
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
Reader 使用 `ReadProcessMemory` 和 `VirtualQueryEx` 做有界读取,返回实际地址、读取长度、解析值和证据。
|
|
71
|
+
|
|
72
|
+
断点退出限制见 [windbg.md](windbg.md);candidate 验证步骤见 [profiles.md](profiles.md)。尖括号参数需替换后运行。
|
|
@@ -1,52 +1,62 @@
|
|
|
1
|
-
#
|
|
1
|
+
# IDA 与局部反汇编
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 选择与复用
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
先发现当前可调用的 IDA Pro MCP 工具及参数 schema,不假定存在特定工具名或 TTL 参数。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
用 database status 获取数据库路径和状态。身份匹配且 ready 时优先打开已有数据库;不存在或 stale 时以原始 Wow.exe 创建。写库前使用环境提供的数据库锁;工具缺少锁能力时保持单一写入者并记录限制。保存后检查文件与状态,不能把打开 session 当作完成建库,也不手工改成 ready 冒充完成。
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- Hex-Rays 插件页:https://plugins.hex-rays.com/mrexodia/ida-pro-mcp
|
|
9
|
+
原始 PE 是主输入,保留节区、导入和异常信息。需要运行时证据时先读 [windbg.md](windbg.md),用 dump 导出 .text/.rdata/.pdata,按需加 .data。段 .bin 是 overlay,不作为完整程序打开。检查 manifest 身份、大小、哈希及 shortRead。
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
## 地址映射
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
codex plugin add ida-pro-mcp@mrexodia
|
|
17
|
-
```
|
|
13
|
+
段内偏移 = 目标 RVA - sectionRva,读取窗口必须落在实际文件范围内。
|
|
14
|
+
IDA 地址 = 数据库 image base + sectionRva,不硬编码 image base。
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
runtime dump 的绝对指针包含 ASLR。改变段加载地址不等于修正指针;分析表项时先减 manifest.moduleBase 得到 RVA,再映射到 IDA 地址。保留原始指针作证据。.data 只代表当前 session;写入持久库前保留 overlay 来源和恢复方式,避免污染原始文件视图。
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
只输出请求字段相关的函数/字符串 RVA、引用、结构偏移、类型和调用关系。查找方法见 [workflow.md](workflow.md),全量函数/字符串列表不是 profile。
|
|
22
19
|
|
|
23
|
-
##
|
|
20
|
+
## 会话失联
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
用实际支持的健康查询或轻量读取检查 session;支持 TTL 时可延长空闲时间。失联后保存证据,重新打开同一数据库并重试一次。仍失败则转 iced-x86 或报告环境问题。broker 与 IDA worker 生命周期独立。
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
## iced-x86 局部解码示例
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
{
|
|
31
|
-
"input_path": "C:\\Games\\World of Warcraft\\_retail_\\Wow.exe",
|
|
32
|
-
"mode": "prefer_headless",
|
|
33
|
-
"run_auto_analysis": true,
|
|
34
|
-
"build_caches": true,
|
|
35
|
-
"init_hexrays": true,
|
|
36
|
-
"idle_ttl_sec": 3600
|
|
37
|
-
}
|
|
38
|
-
```
|
|
26
|
+
在任务 session 创建 .cjs 文件。传入已安装 wowdump 的 package.json 绝对路径以解析随包依赖;全局安装根可通过 npm root -g 获取。
|
|
39
27
|
|
|
40
|
-
|
|
28
|
+
参数依次为 package.json、段 .bin、sectionRva、起始 RVA、字节数、manifest.moduleBase。示例有界解码一个窗口,输出指令 RVA,不提供自动反编译或全局 XREF。
|
|
41
29
|
|
|
42
|
-
|
|
30
|
+
~~~javascript
|
|
31
|
+
const fs = require("node:fs");
|
|
32
|
+
const { createRequire } = require("node:module");
|
|
33
|
+
const [pkg, file, section, start, size, base] = process.argv.slice(2);
|
|
34
|
+
const { Decoder, DecoderOptions, Formatter, FormatterSyntax } = createRequire(pkg)("iced-x86");
|
|
35
|
+
const rva = BigInt(start), sectionRva = BigInt(section), moduleBase = BigInt(base);
|
|
36
|
+
const offset = rva - sectionRva, length = Number(size);
|
|
37
|
+
const bytes = fs.readFileSync(file);
|
|
38
|
+
if (offset < 0n || !Number.isSafeInteger(length) || length < 1 || length > 65536 ||
|
|
39
|
+
offset + BigInt(length) > BigInt(bytes.length)) throw new Error("Invalid byte window");
|
|
40
|
+
const decoder = new Decoder(64, bytes.subarray(Number(offset), Number(offset) + length), DecoderOptions.None);
|
|
41
|
+
const formatter = new Formatter(FormatterSyntax.Intel);
|
|
42
|
+
decoder.ip = moduleBase + rva;
|
|
43
|
+
try {
|
|
44
|
+
while (decoder.canDecode) {
|
|
45
|
+
const instruction = decoder.decode();
|
|
46
|
+
try {
|
|
47
|
+
console.log(JSON.stringify({ rva: "0x" + (instruction.ip - moduleBase).toString(16),
|
|
48
|
+
text: formatter.format(instruction), invalid: instruction.isInvalid }));
|
|
49
|
+
if (instruction.isInvalid) break;
|
|
50
|
+
} finally { instruction.free(); }
|
|
51
|
+
}
|
|
52
|
+
} finally { decoder.free(); formatter.free(); }
|
|
53
|
+
~~~
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
2. 用相同 `input_path` 和 `idle_ttl_sec: 3600` 重新调用 `idb_open`。
|
|
46
|
-
3. 对新 session 做 `server_health`,成功后重试原查询一次。
|
|
47
|
-
4. 第二次仍失联时停止扩大分析范围,保存已取得的 JSON 证据并转用 iced-x86 或请求用户稍后重试。
|
|
55
|
+
起点应来自函数边界或已确认控制流,任意字节可解码不代表代码。RIP 相对目标由下一条指令地址加有符号位移得到,地址运算使用 BigInt。窗口不足时按控制流扩展并记录范围;解码成功不证明字段语义。
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
- IDA 不可用时,Agent 使用 npm 内置的 `iced-x86` 对 manifest 指向的局部 RVA 反汇编,输出模块 RVA、指令文本、访问偏移和证据。Agent 负责把这些线索与运行时字段对应起来,再用 CDB 断点或 Reader 验证 `root`、`pointerChain`、`layout/type`、计数上限和停止条件。不要把一次运行的绝对地址写进持久化 profile。
|
|
57
|
+
## IDA MCP 来源
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
- [项目仓库](https://github.com/mrexodia/ida-pro-mcp)
|
|
60
|
+
- [Hex-Rays 插件目录](https://plugins.hex-rays.com/mrexodia/ida-pro-mcp)
|
|
61
|
+
|
|
62
|
+
按安装版本 README 发现工具;已有可用 MCP 时直接使用。
|
|
@@ -1,41 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 验证字段,而不只验证读取
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
每个字段分别记录三层结论:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
1. **读取有效**:build/hash 匹配,指针链有来源,读取完整,类型和边界正确。保留地址、dataHex、bytesRead、complete、解析值及命令结果。短读或错误不转换为零值。
|
|
6
|
+
2. **语义有效**:与同一时刻角色面板或其他独立观测对照。百分比、等级、基础值和有效值分开;记录允许的舍入误差。请用户配合一次可控变化(例如换装),读取变化前后值,核对变化方向和幅度。一次相等仅是候选支持证据。
|
|
7
|
+
3. **复用有效**:新进程或重启后的 moduleBase 变化时,同一 profile 仍解析到正确对象。未做这一步就标记“跨进程未验证”,不要报告长期稳定。
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
不要求用户每次都重新配合已有验证;新字段首次建立语义时才补相应证据。输出 evidence 文件包含字段名、来源文件/函数 RVA、假设、原始读数、观测时间、对照值、转换公式及验证状态。多个字段逐个记录,局部成功不代表全部成功。
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
`analyze runtime --kind verify` 返回分项 validation:read 为 passed 表示读取成功,fieldSemantics、rootSemantics、crossProcess 为 not_checked。它不做角色面板对照,不自动证明语义或重启复用,也不修改候选状态。旧版本的 verified:true 同样只代表读取调用成功。候选验证见 [profiles.md](profiles.md)。
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
- 候选地址或 `rva`、字节样本、来源和采样时间
|
|
13
|
-
- 每个请求字段对应的线索
|
|
14
|
-
|
|
15
|
-
不要导出整段进程内存。把 `durationMs`、命中次数、命令数量和读取范围限制在请求所需范围内。
|
|
16
|
-
|
|
17
|
-
## Disassemble(按需使用)
|
|
18
|
-
|
|
19
|
-
`analyze runtime --kind dump` 的 `manifest.json` 和段文件是静态分析输入。只有 CDB 证据不足以确认函数、布局或类型时才调用 IDA Pro MCP;没有时再用 iced-x86 解码。分析后回到 `analyze debug` 或 `analyze runtime --kind verify --profile <file>` 验证。输出 profile 时,每个字段至少给出:
|
|
20
|
-
|
|
21
|
-
```json
|
|
22
|
-
{
|
|
23
|
-
"name": "playerAuras",
|
|
24
|
-
"module": "Wow.exe",
|
|
25
|
-
"rva": "0x123456",
|
|
26
|
-
"pointerChain": [{ "offset": "0x20", "type": "pointer" }],
|
|
27
|
-
"type": "array",
|
|
28
|
-
"elementSize": 64,
|
|
29
|
-
"evidence": [{ "source": "ida-pro-mcp", "functionRva": "0x123000" }]
|
|
30
|
-
}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
没有可靠定位时输出 `confidence: "candidate"`,不要填充猜测地址。
|
|
34
|
-
|
|
35
|
-
## Reader
|
|
36
|
-
|
|
37
|
-
Reader 根据模块名、RVA、指针链和类型执行原子读取;它不负责猜测结构,也不执行 CDB/IDA。一次读取结果应保留实际地址、请求大小、已读字节、解析值和错误状态。
|
|
38
|
-
|
|
39
|
-
需要连续数据时使用 `memory watch` 或重复 `memory read`。运行时模块基址可能变化,profile 保存 RVA,不保存本次运行的绝对地址。
|
|
40
|
-
|
|
41
|
-
## Verify
|
|
13
|
+
dump 的日志、manifest 和段字节属于取证材料;检查每段大小、哈希和 shortRead 后再引用。分析输出中的 RVA、结构偏移、类型和调用关系必须能追溯到这些材料或原始 PE。
|
|
@@ -1,45 +1,73 @@
|
|
|
1
|
-
# Profile
|
|
1
|
+
# Profile 格式与生命周期
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 地址规则
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- runtime VA = 本轮 moduleBase + RVA;IDA VA = 数据库 image base + RVA。
|
|
6
|
+
- 对象偏移以对象指针为基准,不直接加到模块基址。
|
|
7
|
+
- root.rva 默认只加模块基址;root.dereference:true 才读取根处的 8 字节指针。
|
|
8
|
+
- pointerChain 每步先加 offset,默认再读取指针;dereference:false 仅加偏移。
|
|
9
|
+
- fields 是字段名到定义的对象;复杂布局使用 layout。
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
~/.wowdump/<buildKey>/
|
|
9
|
-
├── build.json
|
|
10
|
-
├── database/
|
|
11
|
-
│ ├── database.json
|
|
12
|
-
│ └── ida/Wow.i64
|
|
13
|
-
├── profile/ # 只放用户确认保存的 profile
|
|
14
|
-
│ ├── player-state.json
|
|
15
|
-
│ └── combat-state.json
|
|
16
|
-
└── runtime/<session-id>/ # 当前任务证据
|
|
17
|
-
├── target.json
|
|
18
|
-
├── debug.json
|
|
19
|
-
├── dump/manifest.json
|
|
20
|
-
├── ida-evidence.json
|
|
21
|
-
├── candidate.profile.json
|
|
22
|
-
└── verify.json
|
|
23
|
-
```
|
|
11
|
+
## 最小示例
|
|
24
12
|
|
|
25
|
-
|
|
13
|
+
下例全部地址、build 和哈希都是合成测试数据,不能用于游戏。它读取全局指针,再从对象 +0x20 读取 f32。
|
|
26
14
|
|
|
27
|
-
|
|
15
|
+
~~~json
|
|
16
|
+
{
|
|
17
|
+
"schema": "wowdump.profile.v1",
|
|
18
|
+
"id": "example-stat",
|
|
19
|
+
"buildKey": "fixture@1",
|
|
20
|
+
"executableSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
21
|
+
"readerStatus": "candidate",
|
|
22
|
+
"confidence": "candidate",
|
|
23
|
+
"module": { "name": "Fixture.exe" },
|
|
24
|
+
"fields": {
|
|
25
|
+
"exampleValue": {
|
|
26
|
+
"root": { "rva": "0x100", "dereference": true },
|
|
27
|
+
"pointerChain": [{ "offset": "0x20", "dereference": false }],
|
|
28
|
+
"type": "f32"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"evidence": [{ "source": "synthetic-example", "semanticVerified": false, "restartVerified": false }]
|
|
32
|
+
}
|
|
33
|
+
~~~
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
替换为本轮证实的身份、RVA、偏移和类型。直接模块标量无需解引用。数组使用 layout.kind:"array"、item、stride、count;动态计数格式先核对当前 Reader 并设置 maxItems。结构使用 layout.kind:"struct" 和 fields。复杂布局先核对当前实现或验证用例,不自创 schema。
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
~/.wowdump/<buildKey>/runtime/<session-id>/
|
|
33
|
-
```
|
|
37
|
+
## 验证候选
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
`analyze runtime --kind verify --profile <file> --confirm` 接受 readerStatus 为 candidate 或 reader_ready;普通 memory read 仍要求 reader_ready。
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
直接验证 candidate.profile.json,结果另存 verify.json。命令不修改候选、不提升状态、不自动持久化。validation.read 为 passed 只表示此次读取完成;fieldSemantics、rootSemantics、crossProcess 为 not_checked,需另外补证据。不要为执行验证而先将候选改成 reader_ready。
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
2. 向用户展示候选 profile 的路径、字段和验证结果,询问是否保存为该 build 的持久化 profile。
|
|
41
|
-
3. 用户确认后,将候选复制到 `~/.wowdump/<buildKey>/profile/<profile-id>.json`,保留 `buildKey`、来源和验证证据。
|
|
42
|
-
4. 目标文件已存在时先询问覆盖,或使用新的 profile-id;不要静默覆盖。
|
|
43
|
-
5. 后续读取优先使用这个绝对路径,或用 `wowdump profiles --build <buildKey>` 列出它。Reader 只读取 profile,不读取 `database/ida` 和 `runtime/dump/*.bin`。
|
|
43
|
+
语义验证见 [evidence-workflow.md](evidence-workflow.md)。验证失败时保留错误和候选状态,不复制到持久化目录。
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
## 保存与复用
|
|
46
|
+
|
|
47
|
+
统一在 ~/.wowdump/<buildKey>/:
|
|
48
|
+
- database/ 保存每个 build 的静态数据库。
|
|
49
|
+
- runtime/<session>/ 保存 target、dump、静态证据、候选、验证副本和 verify.json。
|
|
50
|
+
- profile/ 保存用户确认的已验证 profile。
|
|
51
|
+
|
|
52
|
+
buildKey 使用 targets 返回的安全单层目录名。profile 必须带真实 executableSha256;build/hash 不匹配重新分析,缺少哈希不视作匹配。
|
|
53
|
+
|
|
54
|
+
展示字段、单位、验证范围和绝对路径。用户确认后复制到 profile/<id>.json;已有保存授权直接沿用。覆盖现有文件前确认覆盖范围。未做重启测试时保留限制。profile 不保存 moduleBase、堆绝对地址或 IDA session ID。
|
|
55
|
+
|
|
56
|
+
### 持久化前检查根指针
|
|
57
|
+
|
|
58
|
+
- 持久化字段从当前 build 的模块 RVA 出发,随后使用有证据支持的相对偏移和解引用;根 RVA 必须落在对应模块范围内。堆地址减 moduleBase 不会变成模块 RVA。
|
|
59
|
+
- 指针扫描命中只说明某个槽位当时引用该对象。先确认槽位的含义、所属结构和生命周期,排除队列、缓存、目标单位或任意列表项。重复读取与当前对象对照;指针改变后仍可读不代表仍是玩家。
|
|
60
|
+
- 单独记录根语义、字段语义、同会话重读、换角色和跨进程验证。允许明确披露尚未进行的重启测试;根语义尚未确认的扫描命中继续留在 runtime,不凭 reader_ready 或 verified:true 提升为持久化接口。
|
|
61
|
+
- 绝对地址 profile 只适合本轮调查。证据里的历史地址可留在 runtime 文件,持久化 profile 引用该证据路径,不携带其本轮对象地址作为读取根。
|
|
62
|
+
|
|
63
|
+
具体反例见 [角色属性案例](character-stats-case.md)。这些是保存决策规则,不表示当前 CLI 已自动执行全部检查。
|
|
64
|
+
|
|
65
|
+
## 按键选择动态表项:root.lookup
|
|
66
|
+
|
|
67
|
+
根处是表而不是唯一对象时,使用有界键查找,不把本轮槽位编号写进 pointerChain。root.rva/dereference 先解析表地址,lookup 返回匹配表项中的对象指针,随后执行字段的 pointerChain。
|
|
68
|
+
|
|
69
|
+
lookup 参数:countRoot 和 keyRoot 是普通根定义(rva 或会话 address,可选 dereference),不嵌套 lookup;countType 为 u32(默认)或 u64;keyOffset 是键相对 keyRoot 的偏移;keySize 是比较的原始字节数;stride 是表项步长;entryKeyOffset 是表项内的键偏移(默认 0);valueOffset 是对象指针偏移;objectKeyOffset 用于核对对象本身的键;maxItems 必须显式提供。偏移使用 0x 字符串,大小和数量使用整数。
|
|
70
|
+
|
|
71
|
+
单次表读取上限 1 MiB,keySize 上限 64,stride 上限 4096,maxItems 上限 65536;实际容量超限返回 LOOKUP_LIMIT。键为空、没有命中、重复命中、对象键不符分别返回 LOOKUP_KEY_EMPTY、LOOKUP_NOT_FOUND、LOOKUP_AMBIGUOUS、LOOKUP_OBJECT_MISMATCH。选择后复读根、容量、键和命中条目;发现变化返回 LOOKUP_CHANGED,不沿用旧槽位。
|
|
72
|
+
|
|
73
|
+
这些复核不是进程冻结或原子快照保证;目标持续变化时保留失败证据,有限重试。lookup profile 必须使用 0.3.9 或更新版本 Reader;旧版可能忽略未知参数,不要交给旧 Reader 执行。
|
|
@@ -1,28 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 确定字段含义
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
通常在任务记录中列明目标即可;无需为每次查询创建额外请求 JSON。这些描述不是 CLI 接受的 schema。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
{ "name": "nearbyEnemies", "kind": "unit-list", "scope": "around-player" },
|
|
12
|
-
{ "name": "cooldowns", "kind": "cooldown", "scope": "player" }
|
|
13
|
-
],
|
|
14
|
-
"limits": { "maxUnits": 64, "maxAuras": 64, "maxEvents": 100, "durationMs": 5000 },
|
|
15
|
-
"debug": { "kind": "breakpoint", "rva": "0x...", "maxHits": 3, "durationMs": 3000 }
|
|
16
|
-
}
|
|
17
|
-
```
|
|
5
|
+
| 用户目标 | 需要确定 |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| 角色属性 | 玩家;暴击/急速/精通/全能;等级数值还是百分比;当前有效值还是基础值 |
|
|
8
|
+
| Buff/Debuff | 玩家还是目标;有益/有害;持续时间单位和层数 |
|
|
9
|
+
| 技能冷却 | 技能 ID;剩余时间、完整冷却还是充能;区分 GCD |
|
|
10
|
+
| 附近敌人 | 范围、单位类型、敌对/存活条件;姓名板集合不等于附近全部敌人 |
|
|
18
11
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
常见字段映射:
|
|
22
|
-
|
|
23
|
-
- 我方 Buff:`kind: "aura", scope: "player", direction: "helpful"`
|
|
24
|
-
- 敌方 Debuff:`kind: "aura", scope: "target", direction: "harmful"`
|
|
25
|
-
- 技能冷却:`kind: "cooldown", scope: "player"`
|
|
26
|
-
- 周围敌人:`kind: "unit-list", scope: "around-player"`
|
|
27
|
-
|
|
28
|
-
这些是查询意图。没有 reader-ready profile 定义前,不要把 CDB 一次性证据中的绝对地址交给长期 reader。
|
|
12
|
+
用户已说“暴击全能那些”时,先按面板当前二级属性组织字段;等级与百分比分开命名。只在含义影响分析且上下文不足时询问。每个字段记录原始类型、单位、转换公式和观测条件;原始数值不带单位时不要猜换算系数。
|
|
@@ -36,7 +36,7 @@ wowdump analyze debug `
|
|
|
36
36
|
--confirm
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
默认脚本使用硬件执行断点,只记录 RIP、RCX、RDX、R8、R9、RSP、返回地址和线程 ID
|
|
39
|
+
默认脚本使用硬件执行断点,只记录 RIP、RCX、RDX、R8、R9、RSP、返回地址和线程 ID。当前脚本在下一次命中时才检查计数上限,达到记录上限不保证立即退出。broker 用 `VirtualQueryEx` 检查入口页和返回地址页,结果写入 session 的 `debug.json`。
|
|
40
40
|
|
|
41
41
|
## 其他调试命令
|
|
42
42
|
|
|
@@ -53,3 +53,11 @@ wowdump analyze debug `
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
命令和参数通过 `spawn(file, args, {shell:false})` 传递;不要把它们拼成 shell 字符串。调试命令必须有时限,输出会截断到证据上限。
|
|
56
|
+
|
|
57
|
+
## 当前生命周期限制
|
|
58
|
+
|
|
59
|
+
当前 CLI 使用 -p 附加、-cf 命令文件,默认末尾追加 q,没有默认选择非侵入式附加。shell:false 只描述启动方式,不限制 CDB 命令本身的作用。
|
|
60
|
+
|
|
61
|
+
超时会终止 CDB,并在 Windows 使用 taskkill /T /F 清理进程树;这不能证明正常 detach。detached:true 当前是固定输出,也不是证据。不要把 q 当成经过实测的正常脱离保证。
|
|
62
|
+
|
|
63
|
+
执行前核对当前版本的附加方式、退出命令和时限。断点退出与真实目标存活尚未完成端到端验证;字段定位先使用静态证据和 Reader,调试退出验证先在可控测试进程完成。超时后记录目标是否存活和可响应,不仅看退出码。透传参数会改变附加模式,避免重复提供冲突的目标参数。
|
|
@@ -1,30 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 从目标字段到地址
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 建立本轮证据
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
2. `wowdump database status --build <buildKey>` 确认静态数据库是否可复用。
|
|
7
|
-
3. 运行时证据都放在当前 build 的 runtime session;profile 只保存稳定的 RVA、相对偏移、类型和边界。
|
|
5
|
+
运行 targets 后确认身份,检查 database status。为当前任务保留 runtime session,记录目标字段、已证实事实、候选、已排除假设和下一项证据缺口。继续任务时读取这份记录,避免重复全量扫描。
|
|
8
6
|
|
|
9
|
-
##
|
|
7
|
+
## 定位字段
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
| --- | --- | --- |
|
|
13
|
-
| 不知道进程和模块 | `targets`、broker `modules` | PID、路径、模块基址明确 |
|
|
14
|
-
| 需要 PE 样本 | `analyze runtime --kind dump` | manifest 和段二进制已落盘 |
|
|
15
|
-
| 需要确认函数入口 | IDA/iced 局部分析 | 得到单个函数 RVA 和调用关系 |
|
|
16
|
-
| 需要观察调用参数 | `analyze debug --kind breakpoint` | 获得有限次数寄存器、返回地址和页证据 |
|
|
17
|
-
| 需要其他诊断 | `analyze debug --kind command` | CDB 命令输出带时限且有证据文件 |
|
|
18
|
-
| 需要稳定字段 | `analyze runtime --kind verify` | broker 实读成功,类型和边界一致 |
|
|
9
|
+
从与目标相关的字符串、符号、调用点或已有本轮证据开始,按需使用 IDA;具体输入和地址映射见 [disassemble.md](disassemble.md)。
|
|
19
10
|
|
|
20
|
-
|
|
11
|
+
- 名称字符串只说明存在该名称。检查其引用、邻近表项、注册调用和重定位线索,确定哪里存放函数指针。没有直接 XREF 时,检查 RIP 相对引用和表间接访问;不要把字符串 RVA 当作函数入口。
|
|
12
|
+
- 沿已确认函数分析参数来源、调用者、返回路径和实际内存访问。区分 getter、包装函数和格式化函数;看见名称或一条 load 指令不足以证明字段含义。
|
|
13
|
+
- 沿对象来源回溯到可解释的根:模块全局指针、索引表或对象容器。记录每次“加偏移”及“解引用”。函数 RVA 与堆对象字段偏移分开记录。
|
|
14
|
+
- 区分读取原始字段和计算结果。若返回值由多个量计算,记录各依赖及公式;Reader 没有相应表达能力时明确返回原始量,不把它标成最终面板值。
|
|
15
|
+
- 用页面查询和小范围读取检验指针链、类型、容器边界。需要调用证据时先读 [windbg.md](windbg.md) 中的当前限制。
|
|
21
16
|
|
|
22
|
-
|
|
17
|
+
一次只扩大足以检验假设的范围,但没有找到候选时继续换证据路径:字符串引用失败可检查注册表和调用者;入口存在但对象不明可回溯根;值不匹配可检查类型、派生公式和状态条件。每次记录排除理由。工具失败不等于字段不存在。
|
|
23
18
|
|
|
24
|
-
##
|
|
19
|
+
## 转交 Reader
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
- `memory regions` 先用 `VirtualQueryEx` 确认页面,再进行有界 `memory read`。
|
|
30
|
-
- 只有用户确认后的 profile 才复制到 `~/.wowdump/<buildKey>/profile/`。
|
|
21
|
+
根据证据填写 [profiles.md](profiles.md) 的真实字段结构,执行 [evidence-workflow.md](evidence-workflow.md) 的验证。页面可读、数值合理和名称相似都不能单独证明语义正确。
|
|
22
|
+
|
|
23
|
+
真正缺少目标进程、工具入口或可观察状态时,明确缺少的条件并保留进度;不要以“少量扫描没结果”代替结论。
|