wowdump 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +56 -118
- package/dist/agent.js +5 -8
- package/dist/cli.js +497 -0
- package/dist/discovery.js +1 -12
- package/dist/dry-run.js +0 -2
- package/dist/focused-session.js +61 -1329
- package/dist/frida-runtime.js +51 -73
- package/dist/frida-worker.js +100 -0
- package/dist/ghidra.js +769 -0
- package/dist/main.js +66 -0
- package/dist/processes.js +2 -5
- package/dist/reader-broker.js +460 -0
- package/dist/reader-client.js +1 -0
- package/dist/reader-main.js +67 -0
- package/dist/session.js +17 -120
- package/dist/toolchain.js +594 -0
- package/dist/windows-launcher.js +207 -0
- package/dist/windows-reader.js +102 -0
- package/dist/wow-analysis.js +211 -236
- package/package.json +18 -35
- package/skills/wowdump/SKILL.md +15 -0
- package/skills/wowdump/commands.md +44 -0
- package/dist/analysis-path.js +0 -38
- package/dist/analysis-process-log.js +0 -146
- package/dist/broker-client.js +0 -411
- package/dist/broker-codec.js +0 -148
- package/dist/broker-core.js +0 -1045
- package/dist/broker-gateway.js +0 -447
- package/dist/broker-ledger.js +0 -196
- package/dist/broker-main.js +0 -291
- package/dist/broker-protocol.js +0 -119
- package/dist/broker-runtime.js +0 -1283
- package/dist/broker-server.js +0 -466
- package/dist/build-bundle-loader.js +0 -183
- package/dist/build-bundle.js +0 -11
- package/dist/focus-errors.js +0 -63
- package/dist/focus-service.js +0 -1855
- package/dist/mcp-main.js +0 -51
- package/dist/mcp.js +0 -924
- package/dist/process-log-lock.js +0 -181
- package/dist/runtime-config.js +0 -399
- package/resources/builds/retail/12.0.7.68974/build-profile.json +0 -290
- package/resources/builds/retail/12.0.7.68974/data-sources.json +0 -1633
- package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +0 -5130
- package/resources/builds/retail/12.0.7.68974/manifest.json +0 -63
- package/resources/builds/retail/12.0.7.68974/signatures.json +0 -260
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createConnection } from "node:net";
|
|
3
|
+
import { mkdir, open, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import koffi from "koffi";
|
|
6
|
+
import { ReaderProtocolError } from "./reader-broker.js";
|
|
7
|
+
function quoteWindowsArgument(value) {
|
|
8
|
+
if (value.length > 0 && !/[\s"]/u.test(value))
|
|
9
|
+
return value;
|
|
10
|
+
let result = '"';
|
|
11
|
+
let slashes = 0;
|
|
12
|
+
for (const character of value) {
|
|
13
|
+
if (character === "\\") {
|
|
14
|
+
slashes += 1;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (character === '"') {
|
|
18
|
+
result += "\\".repeat(slashes * 2 + 1) + '"';
|
|
19
|
+
slashes = 0;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
result += "\\".repeat(slashes) + character;
|
|
23
|
+
slashes = 0;
|
|
24
|
+
}
|
|
25
|
+
return result + "\\".repeat(slashes * 2) + '"';
|
|
26
|
+
}
|
|
27
|
+
export function buildWindowsCommandLine(args) {
|
|
28
|
+
return args.map(quoteWindowsArgument).join(" ");
|
|
29
|
+
}
|
|
30
|
+
export function shellExecuteRunAs(file, args, cwd) {
|
|
31
|
+
if (process.platform !== "win32")
|
|
32
|
+
throw new Error("ShellExecuteEx runas is only available on Windows");
|
|
33
|
+
const shell32 = koffi.load("shell32.dll");
|
|
34
|
+
const kernel32 = koffi.load("kernel32.dll");
|
|
35
|
+
const SHELLEXECUTEINFOW = koffi.struct("WOWDUMP_SHELLEXECUTEINFOW", {
|
|
36
|
+
cbSize: "uint32_t",
|
|
37
|
+
fMask: "uint32_t",
|
|
38
|
+
hwnd: "void *",
|
|
39
|
+
lpVerb: "str16",
|
|
40
|
+
lpFile: "str16",
|
|
41
|
+
lpParameters: "str16",
|
|
42
|
+
lpDirectory: "str16",
|
|
43
|
+
nShow: "int32_t",
|
|
44
|
+
hInstApp: "void *",
|
|
45
|
+
lpIDList: "void *",
|
|
46
|
+
lpClass: "str16",
|
|
47
|
+
hkeyClass: "void *",
|
|
48
|
+
dwHotKey: "uint32_t",
|
|
49
|
+
hIcon: "void *",
|
|
50
|
+
hProcess: "void *"
|
|
51
|
+
});
|
|
52
|
+
const ShellExecuteExW = shell32.func("bool __stdcall ShellExecuteExW(_Inout_ WOWDUMP_SHELLEXECUTEINFOW *info)");
|
|
53
|
+
const GetLastError = kernel32.func("uint32_t __stdcall GetLastError()");
|
|
54
|
+
const info = {
|
|
55
|
+
cbSize: koffi.sizeof(SHELLEXECUTEINFOW),
|
|
56
|
+
fMask: 0,
|
|
57
|
+
hwnd: null,
|
|
58
|
+
lpVerb: "runas",
|
|
59
|
+
lpFile: resolve(file),
|
|
60
|
+
lpParameters: buildWindowsCommandLine(args),
|
|
61
|
+
lpDirectory: resolve(cwd),
|
|
62
|
+
nShow: 0,
|
|
63
|
+
hInstApp: null,
|
|
64
|
+
lpIDList: null,
|
|
65
|
+
lpClass: null,
|
|
66
|
+
hkeyClass: null,
|
|
67
|
+
dwHotKey: 0,
|
|
68
|
+
hIcon: null,
|
|
69
|
+
hProcess: null
|
|
70
|
+
};
|
|
71
|
+
if (!ShellExecuteExW(info)) {
|
|
72
|
+
const win32 = Number(GetLastError());
|
|
73
|
+
const error = new Error(`ShellExecuteExW(runas) failed with Win32 error ${win32}`);
|
|
74
|
+
error.win32 = win32;
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function invocationRequest(invocation) {
|
|
79
|
+
return { command: invocation.command, ...invocation.payload };
|
|
80
|
+
}
|
|
81
|
+
function requestPipe(metadata, request, timeoutMs) {
|
|
82
|
+
return new Promise((resolveResult, reject) => {
|
|
83
|
+
const socket = createConnection({ host: metadata.host, port: metadata.port });
|
|
84
|
+
let buffer = "";
|
|
85
|
+
let settled = false;
|
|
86
|
+
const finish = (error, value) => {
|
|
87
|
+
if (settled)
|
|
88
|
+
return;
|
|
89
|
+
settled = true;
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
socket.destroy();
|
|
92
|
+
if (error)
|
|
93
|
+
reject(error);
|
|
94
|
+
else
|
|
95
|
+
resolveResult(value);
|
|
96
|
+
};
|
|
97
|
+
const timer = setTimeout(() => finish(new Error(`reader broker request timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
98
|
+
socket.setEncoding("utf8");
|
|
99
|
+
socket.once("connect", () => socket.write(`${JSON.stringify({ token: metadata.token, request })}\n`));
|
|
100
|
+
socket.on("data", chunk => {
|
|
101
|
+
buffer += chunk;
|
|
102
|
+
const newline = buffer.indexOf("\n");
|
|
103
|
+
if (newline < 0)
|
|
104
|
+
return;
|
|
105
|
+
try {
|
|
106
|
+
finish(undefined, JSON.parse(buffer.slice(0, newline)));
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
socket.once("error", error => finish(error));
|
|
113
|
+
socket.once("end", () => { if (!settled)
|
|
114
|
+
finish(new Error("reader broker closed without a response")); });
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async function pause(milliseconds) {
|
|
118
|
+
await new Promise(resolvePause => setTimeout(resolvePause, milliseconds));
|
|
119
|
+
}
|
|
120
|
+
export class WindowsBrokerManager {
|
|
121
|
+
metadataFile;
|
|
122
|
+
options;
|
|
123
|
+
constructor(options) {
|
|
124
|
+
this.options = { ...options, home: resolve(options.home), readerEntry: resolve(options.readerEntry) };
|
|
125
|
+
this.metadataFile = join(this.options.home, "runtime", "reader-broker.json");
|
|
126
|
+
}
|
|
127
|
+
async request(invocation) {
|
|
128
|
+
const request = invocationRequest(invocation);
|
|
129
|
+
const existing = await this.readMetadata();
|
|
130
|
+
if (existing) {
|
|
131
|
+
try {
|
|
132
|
+
return await requestPipe(existing, request, this.options.requestTimeoutMs ?? 30_000);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
await rm(this.metadataFile, { force: true }).catch(() => undefined);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const metadata = await this.launchBroker();
|
|
139
|
+
return requestPipe(metadata, request, this.options.requestTimeoutMs ?? 30_000);
|
|
140
|
+
}
|
|
141
|
+
async readMetadata() {
|
|
142
|
+
try {
|
|
143
|
+
const value = JSON.parse(await readFile(this.metadataFile, "utf8"));
|
|
144
|
+
return value?.schema === "wowdump.reader-broker.v1"
|
|
145
|
+
&& value.host === "127.0.0.1"
|
|
146
|
+
&& Number.isSafeInteger(value.port)
|
|
147
|
+
&& value.port > 0
|
|
148
|
+
&& value.port <= 65535
|
|
149
|
+
&& typeof value.token === "string"
|
|
150
|
+
? value
|
|
151
|
+
: undefined;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async launchBroker() {
|
|
158
|
+
await mkdir(dirname(this.metadataFile), { recursive: true });
|
|
159
|
+
const lockFile = `${this.metadataFile}.lock`;
|
|
160
|
+
let lock;
|
|
161
|
+
try {
|
|
162
|
+
lock = await open(lockFile, "wx", 0o600);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error.code !== "EEXIST")
|
|
166
|
+
throw error;
|
|
167
|
+
const deadline = Date.now() + (this.options.launchTimeoutMs ?? 30_000);
|
|
168
|
+
while (Date.now() < deadline) {
|
|
169
|
+
const metadata = await this.readMetadata();
|
|
170
|
+
if (metadata)
|
|
171
|
+
return metadata;
|
|
172
|
+
await pause(100);
|
|
173
|
+
}
|
|
174
|
+
await rm(lockFile, { force: true }).catch(() => undefined);
|
|
175
|
+
return this.launchBroker();
|
|
176
|
+
}
|
|
177
|
+
const token = randomBytes(32).toString("hex");
|
|
178
|
+
const args = [this.options.readerEntry, "--listen", "127.0.0.1", "--port", "0", "--token", token, "--metadata", this.metadataFile];
|
|
179
|
+
const launch = this.options.launch ?? shellExecuteRunAs;
|
|
180
|
+
try {
|
|
181
|
+
try {
|
|
182
|
+
launch(this.options.nodeExecutable ?? process.execPath, args, dirname(this.options.readerEntry));
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
const native = error;
|
|
186
|
+
throw new ReaderProtocolError("ELEVATION_FAILED", native.message, {
|
|
187
|
+
verb: "runas",
|
|
188
|
+
win32: native.win32 ?? null,
|
|
189
|
+
requestedRights: ["PROCESS_QUERY_INFORMATION", "PROCESS_VM_READ"],
|
|
190
|
+
readerEntry: this.options.readerEntry
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const deadline = Date.now() + (this.options.launchTimeoutMs ?? 30_000);
|
|
194
|
+
while (Date.now() < deadline) {
|
|
195
|
+
const metadata = await this.readMetadata();
|
|
196
|
+
if (metadata?.token === token)
|
|
197
|
+
return metadata;
|
|
198
|
+
await pause(100);
|
|
199
|
+
}
|
|
200
|
+
throw new Error(`elevated reader broker did not become ready within ${this.options.launchTimeoutMs ?? 30_000}ms`);
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
await lock.close().catch(() => undefined);
|
|
204
|
+
await rm(lockFile, { force: true }).catch(() => undefined);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import koffi from "koffi";
|
|
2
|
+
const PROCESS_QUERY_INFORMATION = 0x0400;
|
|
3
|
+
const PROCESS_VM_READ = 0x0010;
|
|
4
|
+
const HANDLE = koffi.pointer("WOWDUMP_HANDLE", koffi.opaque());
|
|
5
|
+
const SIZE_T = process.arch === "x64" ? "uint64_t" : "uint32_t";
|
|
6
|
+
const kernel32 = koffi.load("kernel32.dll");
|
|
7
|
+
const shell32 = koffi.load("shell32.dll");
|
|
8
|
+
const OpenProcess = kernel32.func("OpenProcess", HANDLE, ["uint32_t", "bool", "uint32_t"]);
|
|
9
|
+
const ReadProcessMemory = kernel32.func("ReadProcessMemory", "bool", [HANDLE, "uint64_t", koffi.pointer("uint8_t"), SIZE_T, koffi.out(koffi.pointer(SIZE_T))]);
|
|
10
|
+
const VirtualQueryEx = kernel32.func("VirtualQueryEx", SIZE_T, [HANDLE, "uint64_t", koffi.pointer("uint8_t"), SIZE_T]);
|
|
11
|
+
const CloseHandle = kernel32.func("CloseHandle", "bool", [HANDLE]);
|
|
12
|
+
const GetLastError = kernel32.func("GetLastError", "uint32_t", []);
|
|
13
|
+
const IsUserAnAdmin = shell32.func("IsUserAnAdmin", "bool", []);
|
|
14
|
+
function addressOf(handle) {
|
|
15
|
+
return `0x${koffi.address(handle).toString(16)}`;
|
|
16
|
+
}
|
|
17
|
+
function win32Error(operation) {
|
|
18
|
+
const win32 = Number(GetLastError());
|
|
19
|
+
const error = new Error(`${operation} failed with Win32 error ${win32}`);
|
|
20
|
+
error.code = "WIN32_ERROR";
|
|
21
|
+
error.win32 = win32;
|
|
22
|
+
return error;
|
|
23
|
+
}
|
|
24
|
+
class WindowsHandle {
|
|
25
|
+
raw;
|
|
26
|
+
pid;
|
|
27
|
+
id;
|
|
28
|
+
constructor(raw, pid) {
|
|
29
|
+
this.raw = raw;
|
|
30
|
+
this.pid = pid;
|
|
31
|
+
this.id = addressOf(raw);
|
|
32
|
+
}
|
|
33
|
+
close() {
|
|
34
|
+
if (this.raw !== null) {
|
|
35
|
+
CloseHandle(this.raw);
|
|
36
|
+
this.raw = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class WindowsNativeReader {
|
|
41
|
+
async openProcess(pid, rights) {
|
|
42
|
+
const requested = rights.reduce((mask, right) => {
|
|
43
|
+
if (right === "PROCESS_QUERY_INFORMATION")
|
|
44
|
+
return mask | PROCESS_QUERY_INFORMATION;
|
|
45
|
+
if (right === "PROCESS_VM_READ")
|
|
46
|
+
return mask | PROCESS_VM_READ;
|
|
47
|
+
return mask;
|
|
48
|
+
}, 0);
|
|
49
|
+
const handle = OpenProcess(requested || PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid);
|
|
50
|
+
if (!handle)
|
|
51
|
+
throw win32Error(`OpenProcess(${pid})`);
|
|
52
|
+
return new WindowsHandle(handle, pid);
|
|
53
|
+
}
|
|
54
|
+
async readProcessMemory(handle, address, size) {
|
|
55
|
+
const native = handle;
|
|
56
|
+
const bytes = Buffer.allocUnsafe(size);
|
|
57
|
+
const read = [0n];
|
|
58
|
+
const ok = ReadProcessMemory(native.raw, address, bytes, size, read);
|
|
59
|
+
const bytesRead = Number(read[0]);
|
|
60
|
+
if (!ok && bytesRead === 0)
|
|
61
|
+
throw win32Error(`ReadProcessMemory(${native.id}, ${`0x${address.toString(16)}`})`);
|
|
62
|
+
return { bytes: bytes.subarray(0, bytesRead), bytesRead };
|
|
63
|
+
}
|
|
64
|
+
async virtualQueryEx(handle, address) {
|
|
65
|
+
const native = handle;
|
|
66
|
+
const buffer = Buffer.alloc(48);
|
|
67
|
+
const result = Number(VirtualQueryEx(native.raw, address, buffer, buffer.length));
|
|
68
|
+
if (result === 0)
|
|
69
|
+
throw win32Error(`VirtualQueryEx(${native.id})`);
|
|
70
|
+
return {
|
|
71
|
+
baseAddress: `0x${buffer.readBigUInt64LE(0).toString(16)}`,
|
|
72
|
+
allocationBase: `0x${buffer.readBigUInt64LE(8).toString(16)}`,
|
|
73
|
+
allocationProtect: buffer.readUInt32LE(16),
|
|
74
|
+
regionSize: buffer.readBigUInt64LE(24).toString(),
|
|
75
|
+
state: buffer.readUInt32LE(32),
|
|
76
|
+
protect: buffer.readUInt32LE(36),
|
|
77
|
+
type: buffer.readUInt32LE(40),
|
|
78
|
+
bytesReturned: result
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async isProcessAlive(pid) {
|
|
82
|
+
try {
|
|
83
|
+
const handle = await this.openProcess(pid, ["PROCESS_QUERY_INFORMATION"]);
|
|
84
|
+
handle.close();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
elevationStatus() {
|
|
92
|
+
return {
|
|
93
|
+
elevated: Boolean(IsUserAnAdmin()),
|
|
94
|
+
processArchitecture: process.arch,
|
|
95
|
+
backend: "koffi/kernel32",
|
|
96
|
+
requestedRights: ["PROCESS_QUERY_INFORMATION", "PROCESS_VM_READ"]
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function createWindowsNativeReader() {
|
|
101
|
+
return new WindowsNativeReader();
|
|
102
|
+
}
|