wowdump 0.3.4 → 0.3.7

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.
@@ -1,715 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { appendFile, mkdir } from "node:fs/promises";
3
- import { join } from "node:path";
4
- const DEFAULT_MAX_SOURCE_BYTES = 4 * 1024 * 1024;
5
- const DEFAULT_MAX_MEMORY_BYTES = 16 * 1024 * 1024;
6
- const DEFAULT_SCRIPT_TIMEOUT_MS = 150;
7
- /*
8
- * This is deliberately a small, stable RPC facade over GumJS. `script` and
9
- * `script_load` still accept native GumJS, so a new WoW build can use any
10
- * Frida API without requiring a host release for every new command.
11
- */
12
- const BRIDGE_SOURCE = String.raw `
13
- function __wowBytes(value) {
14
- return value == null ? [] : Array.from(new Uint8Array(value));
15
- }
16
- function __wowModule(value) {
17
- return { name: value.name, base: value.base.toString(), size: value.size, path: value.path };
18
- }
19
- function __wowExport(value) {
20
- return { type: value.type, name: value.name, address: value.address.toString(), moduleName: value.moduleName };
21
- }
22
- function __wowRange(value) {
23
- return { base: value.base.toString(), size: value.size, protection: value.protection, file: value.file ?? null };
24
- }
25
- rpc.exports = {
26
- enumerateModules() {
27
- return Process.enumerateModules().map(__wowModule);
28
- },
29
- enumerateExports(moduleName) {
30
- const module = Process.getModuleByName(moduleName);
31
- return module.enumerateExports().map(__wowExport);
32
- },
33
- enumerateRanges(protection, coalesce) {
34
- try {
35
- return Process.enumerateRanges({ protection: protection || "r--", coalesce: Boolean(coalesce) }).map(__wowRange);
36
- } catch (_) {
37
- return Process.enumerateRanges(protection || "r--").map(__wowRange);
38
- }
39
- },
40
- readMemory(address, size) {
41
- return __wowBytes(ptr(address).readByteArray(size));
42
- },
43
- writeMemory(address, bytes) {
44
- const target = ptr(address);
45
- const input = Uint8Array.from(bytes || []);
46
- const before = __wowBytes(target.readByteArray(input.byteLength));
47
- target.writeByteArray(input.buffer);
48
- return { before, after: Array.from(input), address: target.toString(), size: input.byteLength };
49
- },
50
- protectMemory(address, size, protection) {
51
- const changed = Memory.protect(ptr(address), size, protection);
52
- return { address: ptr(address).toString(), size, protection, changed };
53
- },
54
- scanMemory(address, size, pattern) {
55
- return Memory.scanSync(ptr(address), size, pattern).map(match => ({
56
- address: match.address.toString(), size: match.size
57
- }));
58
- }
59
- };
60
- `;
61
- function asRecord(value) {
62
- return value && typeof value === "object" ? value : {};
63
- }
64
- function normalize(value, seen = new WeakSet(), depth = 0) {
65
- if (value === undefined || value === null)
66
- return value;
67
- if (typeof value === "bigint")
68
- return value.toString();
69
- if (Buffer.isBuffer(value))
70
- return value.toString("hex");
71
- if (value instanceof ArrayBuffer)
72
- return Buffer.from(new Uint8Array(value)).toString("hex");
73
- if (ArrayBuffer.isView(value)) {
74
- return Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("hex");
75
- }
76
- if (depth > 8)
77
- return "[MaxDepth]";
78
- if (typeof value === "object") {
79
- if (seen.has(value))
80
- return "[Circular]";
81
- seen.add(value);
82
- }
83
- if (Array.isArray(value))
84
- return value.map(item => normalize(item, seen, depth + 1));
85
- if (typeof value === "object") {
86
- const output = {};
87
- for (const [key, item] of Object.entries(value))
88
- output[key] = normalize(item, seen, depth + 1);
89
- return output;
90
- }
91
- return value;
92
- }
93
- function redactArtifact(value, key = "") {
94
- if (typeof value === "string") {
95
- const looksLikePath = /^(?:[a-zA-Z]:[\\/]|\\\\|\/)/.test(value);
96
- const looksLikeBytes = /(?:hex|bytes)$/i.test(key) && value.length > 32;
97
- const looksLikeHexBlob = value.length > 256 && value.length % 2 === 0 && /^[0-9a-f]+$/i.test(value);
98
- if (looksLikeHexBlob)
99
- return { sha256: hash(value), bytes: value.length / 2, encoding: "hex" };
100
- if (looksLikePath || looksLikeBytes)
101
- return { sha256: hash(value), bytes: Buffer.byteLength(value, "utf8") };
102
- return value;
103
- }
104
- if (Array.isArray(value))
105
- return value.map(item => redactArtifact(item, key));
106
- if (value && typeof value === "object") {
107
- const output = {};
108
- for (const [childKey, childValue] of Object.entries(value))
109
- output[childKey] = redactArtifact(childValue, childKey);
110
- return output;
111
- }
112
- return value;
113
- }
114
- function decodeHostArg(value) {
115
- if (Array.isArray(value))
116
- return value.map(item => decodeHostArg(item));
117
- if (value && typeof value === "object") {
118
- const record = value;
119
- if (typeof record.__bufferHex === "string")
120
- return Buffer.from(parseHex(record.__bufferHex, "__bufferHex", DEFAULT_MAX_MEMORY_BYTES));
121
- const output = {};
122
- for (const [key, item] of Object.entries(record))
123
- output[key] = decodeHostArg(item);
124
- return output;
125
- }
126
- return value;
127
- }
128
- function requirePositiveInteger(value, field) {
129
- if (!Number.isSafeInteger(value) || value === undefined || value < 1) {
130
- throw new Error(`${field} must be a positive integer`);
131
- }
132
- return value;
133
- }
134
- function requireString(value, field) {
135
- if (typeof value !== "string" || value.trim() === "")
136
- throw new Error(`${field} is required`);
137
- return value;
138
- }
139
- function parseHex(value, field, maxBytes) {
140
- const text = requireString(value, field).replace(/^0x/i, "");
141
- if (!text || !/^[0-9a-f]+$/i.test(text) || text.length % 2 !== 0) {
142
- throw new Error(`${field} must be an even-length hexadecimal string`);
143
- }
144
- if (text.length / 2 > maxBytes)
145
- throw new Error(`${field} exceeds the memory limit`);
146
- const bytes = [];
147
- for (let index = 0; index < text.length; index += 2)
148
- bytes.push(Number.parseInt(text.slice(index, index + 2), 16));
149
- return bytes;
150
- }
151
- function pointer(value) {
152
- if (typeof value === "number") {
153
- if (!Number.isSafeInteger(value) || value < 0)
154
- throw new Error("address must be a non-negative hexadecimal pointer");
155
- return `0x${value.toString(16)}`;
156
- }
157
- if (typeof value === "bigint") {
158
- if (value < 0n)
159
- throw new Error("address must be a non-negative hexadecimal pointer");
160
- return `0x${value.toString(16)}`;
161
- }
162
- const text = requireString(value, "address").trim();
163
- if (!/^(?:0x)?[0-9a-f]+$/i.test(text))
164
- throw new Error("address must be a hexadecimal pointer");
165
- return text;
166
- }
167
- function protection(value) {
168
- const text = requireString(value, "protection").trim();
169
- if (!/^[rwx-]{3}$/.test(text))
170
- throw new Error("protection must contain three characters from r, w, x, or -");
171
- return text;
172
- }
173
- function hash(value) {
174
- return createHash("sha256").update(value).digest("hex").slice(0, 16);
175
- }
176
- function exportFunction(script, name) {
177
- const exports = script.exports ?? {};
178
- const candidate = exports[name] ?? exports[name.toLowerCase()];
179
- if (typeof candidate !== "function")
180
- throw new Error(`script export ${name} was not found`);
181
- return candidate;
182
- }
183
- function buildPrelude(context) {
184
- const payload = JSON.stringify({
185
- pid: context?.pid,
186
- buildKey: context?.buildKey,
187
- flavor: context?.flavor,
188
- executable: context?.executable,
189
- adapter: context?.adapter
190
- });
191
- const verifiedPrelude = context?.adapter?.adapter?.fridaPrelude ?? "";
192
- return `${verifiedPrelude}\nglobalThis.__WOW_BUILD_CONTEXT__ = Object.freeze(${payload});`;
193
- }
194
- /** Merge caller-supplied request fields with the resolver context. */
195
- function requestContext(request, context, existing) {
196
- return {
197
- ...(existing ?? {}),
198
- ...(context ?? {}),
199
- ...(request.pid === undefined ? {} : { pid: request.pid }),
200
- ...(request.buildKey === undefined ? {} : { buildKey: request.buildKey }),
201
- ...(request.flavor === undefined ? {} : { flavor: request.flavor }),
202
- ...(request.adapter === undefined ? {} : { adapter: request.adapter }),
203
- ...(request.executable === undefined ? {} : { executable: request.executable })
204
- };
205
- }
206
- function sessionContext(record) {
207
- const context = normalize(record.context);
208
- return {
209
- pid: record.pid,
210
- ...(context.buildKey === undefined ? {} : { buildKey: context.buildKey }),
211
- ...(context.flavor === undefined ? {} : { flavor: context.flavor }),
212
- ...(context.adapter === undefined ? {} : { adapter: context.adapter }),
213
- ...(context.executable === undefined ? {} : { executable: context.executable })
214
- };
215
- }
216
- function serializeDevice(device) {
217
- return { id: device.id, name: device.name, type: normalize(device.type) };
218
- }
219
- function serializeProcess(process) {
220
- return { pid: process.pid, name: process.name, parameters: normalize(process.parameters) };
221
- }
222
- function serializeApplication(application) {
223
- return {
224
- identifier: application.identifier,
225
- name: application.name,
226
- pid: application.pid,
227
- parameters: normalize(application.parameters)
228
- };
229
- }
230
- export class FridaCommandRuntime {
231
- sessions = new Map();
232
- scripts = new Map();
233
- apiOverride;
234
- loaderOverride;
235
- artifactDir;
236
- maxSourceBytes;
237
- maxMemoryBytes;
238
- apiPromise;
239
- sessionCounter = 0;
240
- scriptCounter = 0;
241
- constructor(optionsOrApi = {}) {
242
- const candidate = optionsOrApi;
243
- const looksLikeApi = [candidate.enumerateDevices, candidate.enumerateProcesses, candidate.getLocalDevice, candidate.getDevice, candidate.attach]
244
- .some(value => typeof value === "function")
245
- && !("api" in optionsOrApi);
246
- const options = looksLikeApi ? {} : optionsOrApi;
247
- this.apiOverride = looksLikeApi ? optionsOrApi : options.api ?? options.fridaApi ?? options.frida;
248
- this.loaderOverride = options.loader ?? options.loadFrida;
249
- this.artifactDir = options.artifactDir;
250
- this.maxSourceBytes = options.maxSourceBytes ?? DEFAULT_MAX_SOURCE_BYTES;
251
- this.maxMemoryBytes = options.maxMemoryBytes ?? DEFAULT_MAX_MEMORY_BYTES;
252
- if (!Number.isSafeInteger(this.maxSourceBytes) || this.maxSourceBytes < 1) {
253
- throw new TypeError("maxSourceBytes must be a positive safe integer");
254
- }
255
- if (!Number.isSafeInteger(this.maxMemoryBytes) || this.maxMemoryBytes < 1) {
256
- throw new TypeError("maxMemoryBytes must be a positive safe integer");
257
- }
258
- }
259
- async execute(request, context) {
260
- const operation = request?.operation ?? request?.op ?? request?.command;
261
- if (typeof operation !== "string" || !operation.trim())
262
- throw new Error("operation is required");
263
- if (request.pid !== undefined)
264
- requirePositiveInteger(request.pid, "pid");
265
- const effectiveContext = requestContext(request, context);
266
- const hasContext = Object.keys(effectiveContext).length > 0;
267
- const result = await this.dispatch({ ...request, operation: operation }, hasContext ? effectiveContext : undefined);
268
- const enriched = {
269
- operation,
270
- ...(hasContext ? { target: normalize(effectiveContext) } : {}),
271
- ...result
272
- };
273
- await this.record(request, hasContext ? effectiveContext : undefined, enriched);
274
- return enriched;
275
- }
276
- async close() {
277
- for (const scriptId of [...this.scripts.keys()]) {
278
- await this.unloadScript(scriptId).catch(() => undefined);
279
- }
280
- for (const sessionId of [...this.sessions.keys()]) {
281
- await this.detachSession(sessionId).catch(() => undefined);
282
- }
283
- }
284
- /**
285
- * Run a caller-supplied export while forwarding Frida binary messages to a
286
- * consumer. Runtime dump uses this path so ArrayBuffers never become JSON.
287
- */
288
- async streamScriptCall(request, context, handler, callArgs = []) {
289
- const record = await this.session(request, context);
290
- const source = this.source(request.source);
291
- const script = await record.session.createScript(buildPrelude(context ?? record.context) + "\n" + source, request.options);
292
- let pending = Promise.resolve();
293
- script.message?.connect((message, data) => {
294
- pending = pending.then(() => handler(message, data));
295
- });
296
- await script.load();
297
- try {
298
- const name = requireString(request.exportName ?? request.method, "exportName");
299
- const value = await exportFunction(script, name)(...callArgs.map(item => decodeHostArg(item)));
300
- await pending;
301
- return { sessionId: record.id, value: normalize(value), ...sessionContext(record) };
302
- }
303
- finally {
304
- try {
305
- await script.unload();
306
- }
307
- catch { /* cleanup is best effort */ }
308
- }
309
- }
310
- async api() {
311
- if (this.apiOverride)
312
- return this.apiOverride;
313
- this.apiPromise ??= Promise.resolve(this.loaderOverride ? this.loaderOverride() : import("frida")).then(module => {
314
- const candidate = module.default;
315
- return candidate ?? module;
316
- });
317
- return this.apiPromise;
318
- }
319
- async device(id) {
320
- const api = await this.api();
321
- const getDevice = api.getDevice;
322
- const getLocalDevice = api.getLocalDevice;
323
- if (id && getDevice) {
324
- const device = await getDevice(id);
325
- if (!device)
326
- throw new Error(`Frida device ${id} was not found`);
327
- return device;
328
- }
329
- if (!id && getLocalDevice) {
330
- const device = await getLocalDevice();
331
- if (!device)
332
- throw new Error("Frida local device was not found");
333
- return device;
334
- }
335
- if (typeof api.enumerateDevices !== "function")
336
- throw new Error("Frida device enumeration is not available");
337
- const devices = await api.enumerateDevices();
338
- const wanted = id ?? "local";
339
- const device = devices.find(item => item.id === wanted) ?? (id ? undefined : devices[0]);
340
- if (!device)
341
- throw new Error(`Frida device ${wanted} was not found`);
342
- return device;
343
- }
344
- async dispatch(request, context) {
345
- switch (request.operation) {
346
- case "devices": {
347
- const enumerateDevices = (await this.api()).enumerateDevices;
348
- if (!enumerateDevices)
349
- throw new Error("Frida device enumeration is not available");
350
- const devices = await enumerateDevices();
351
- return { devices: devices.map(serializeDevice) };
352
- }
353
- case "host_call":
354
- return this.hostCall(request, context);
355
- case "processes": {
356
- const device = await this.device(request.deviceId);
357
- const api = await this.api();
358
- const processes = device.enumerateProcesses
359
- ? await device.enumerateProcesses(request.options)
360
- : api.enumerateProcesses
361
- ? await api.enumerateProcesses(request.options)
362
- : (() => { throw new Error("Frida process enumeration is not available"); })();
363
- return { device: serializeDevice(device), processes: processes.map(serializeProcess) };
364
- }
365
- case "applications": {
366
- const device = await this.device(request.deviceId);
367
- if (!device.enumerateApplications)
368
- throw new Error("this Frida device does not support application enumeration");
369
- const applications = await device.enumerateApplications(request.options);
370
- return { device: serializeDevice(device), applications: applications.map(serializeApplication) };
371
- }
372
- case "spawn": {
373
- const device = await this.device(request.deviceId);
374
- const program = request.argv && request.argv.length > 0 ? request.argv : requireString(request.program, "program");
375
- if (!device.spawn)
376
- throw new Error("this Frida device does not support spawn");
377
- const pid = await device.spawn(program, request.options);
378
- return { device: serializeDevice(device), pid };
379
- }
380
- case "resume":
381
- return { pid: await this.requirePid(request, context), resumed: await this.resume(request, context) };
382
- case "kill":
383
- return { pid: await this.requirePid(request, context), killed: await this.kill(request, context) };
384
- case "attach":
385
- return this.attach(request, context);
386
- case "detach":
387
- return this.detach(request, context);
388
- case "session_status":
389
- return { sessions: [...this.sessions.values()].map(item => ({ id: item.id, pid: item.pid, device: serializeDevice(item.device), scripts: [...item.scripts.keys()], ...sessionContext(item) })) };
390
- case "modules":
391
- return this.namedBridgeCall(request, context, "enumerateModules", [], "modules");
392
- case "exports":
393
- return this.namedBridgeCall(request, context, "enumerateExports", [requireString(request.module, "module")], "exports");
394
- case "ranges":
395
- return this.namedBridgeCall(request, context, "enumerateRanges", [request.protection ?? "r--", request.coalesce ?? false], "ranges");
396
- case "read_memory": {
397
- const size = requirePositiveInteger(request.size, "size");
398
- if (size > this.maxMemoryBytes)
399
- throw new Error("size exceeds the memory limit");
400
- const response = await this.bridgeCall(request, context, "readMemory", [pointer(request.address), size]);
401
- const value = response.value;
402
- const bytes = Array.isArray(value) ? value : asRecord(value).bytes instanceof Array ? asRecord(value).bytes : [];
403
- const bytesHex = Buffer.from(bytes).toString("hex");
404
- return { ...response, address: pointer(request.address), size: bytes.length, bytesHex, hex: bytesHex };
405
- }
406
- case "write_memory": {
407
- const bytes = parseHex(request.bytesHex ?? request.hex, "bytesHex", this.maxMemoryBytes);
408
- const response = await this.bridgeCall(request, context, "writeMemory", [pointer(request.address), bytes]);
409
- const record = asRecord(response.value);
410
- const beforeHex = Array.isArray(record.before) ? Buffer.from(record.before).toString("hex") : undefined;
411
- const afterHex = Array.isArray(record.after) ? Buffer.from(record.after).toString("hex") : Buffer.from(bytes).toString("hex");
412
- return {
413
- ...response,
414
- address: record.address ?? pointer(request.address),
415
- size: bytes.length,
416
- beforeHex,
417
- afterHex,
418
- hex: afterHex,
419
- rollback: beforeHex
420
- ? { operation: "write_memory", address: record.address ?? request.address, bytesHex: beforeHex }
421
- : undefined
422
- };
423
- }
424
- case "protect_memory": {
425
- const size = requirePositiveInteger(request.size, "size");
426
- if (size > this.maxMemoryBytes)
427
- throw new Error("size exceeds the memory limit");
428
- return this.bridgeCall(request, context, "protectMemory", [pointer(request.address), size, protection(request.protection)]);
429
- }
430
- case "scan_memory": {
431
- const size = requirePositiveInteger(request.size, "size");
432
- if (size > this.maxMemoryBytes)
433
- throw new Error("size exceeds the memory limit");
434
- return this.bridgeCall(request, context, "scanMemory", [pointer(request.address), size, requireString(request.pattern, "pattern")]);
435
- }
436
- case "script_load":
437
- return this.loadScript(request, context);
438
- case "script_call":
439
- return this.callScript(request);
440
- case "script_unload":
441
- return this.unloadScriptResult(request, context);
442
- case "script":
443
- return request.persist ? this.loadScript(request, context) : this.runTransientScript(request, context);
444
- case "compile_script": {
445
- const source = this.source(request.source);
446
- const record = await this.session(request, context);
447
- if (!record.session.compileScript)
448
- throw new Error("this Frida session does not support compileScript");
449
- const bytes = await record.session.compileScript(buildPrelude(context ?? record.context) + "\n" + source, request.options);
450
- return { sessionId: record.id, bytesHex: Buffer.from(bytes).toString("hex"), size: bytes.length };
451
- }
452
- default:
453
- throw new Error(`unsupported Frida operation: ${String(request.operation)}`);
454
- }
455
- }
456
- async attach(request, context) {
457
- const pid = await this.requirePid(request, context);
458
- const device = await this.device(request.deviceId);
459
- const existing = [...this.sessions.values()].find(item => item.pid === pid && item.device.id === device.id);
460
- const targetContext = requestContext(request, context, existing?.context);
461
- if (existing) {
462
- existing.context = targetContext;
463
- return {
464
- sessionId: existing.id,
465
- pid,
466
- device: serializeDevice(device),
467
- reused: true,
468
- ...sessionContext(existing)
469
- };
470
- }
471
- const api = await this.api();
472
- const session = device.attach
473
- ? await device.attach(pid)
474
- : api.attach
475
- ? await api.attach(pid)
476
- : (() => { throw new Error("Frida attach is not available"); })();
477
- const record = {
478
- id: `session-${++this.sessionCounter}`,
479
- device,
480
- session,
481
- pid,
482
- context: targetContext,
483
- scripts: new Map(),
484
- messages: new Map()
485
- };
486
- this.sessions.set(record.id, record);
487
- return { sessionId: record.id, pid, device: serializeDevice(device), ...sessionContext(record) };
488
- }
489
- async hostCall(request, context) {
490
- const scope = requireString(request.scope, "scope");
491
- const method = requireString(request.method, "method");
492
- let receiver;
493
- if (scope === "api") {
494
- receiver = await this.api();
495
- }
496
- else if (scope === "device") {
497
- receiver = await this.device(request.deviceId);
498
- }
499
- else if (scope === "session") {
500
- const record = await this.session(request, context);
501
- receiver = record.session;
502
- }
503
- else {
504
- throw new Error("scope must be api, device, or session");
505
- }
506
- const callable = receiver[method];
507
- if (typeof callable !== "function")
508
- throw new Error(`${scope}.${method} is not a callable Frida method`);
509
- const value = await callable.apply(receiver, (request.args ?? []).map(decodeHostArg));
510
- return { scope, method, value: normalize(value) };
511
- }
512
- async detach(request, context) {
513
- const id = request.sessionId ?? [...this.sessions.values()].find(item => item.pid === (request.pid ?? context?.pid))?.id;
514
- if (!id)
515
- throw new Error("sessionId or pid is required");
516
- const record = this.sessions.get(id);
517
- await this.detachSession(id);
518
- return { sessionId: id, detached: true, ...(record ? sessionContext(record) : {}) };
519
- }
520
- async detachSession(id) {
521
- const record = this.sessions.get(id);
522
- if (!record)
523
- throw new Error(`session ${id} was not found`);
524
- for (const scriptId of [...record.scripts.keys()])
525
- await this.unloadScript(scriptId).catch(() => undefined);
526
- try {
527
- await record.bridge?.unload();
528
- }
529
- catch { /* bridge cleanup is best effort */ }
530
- await record.session.detach();
531
- this.sessions.delete(id);
532
- }
533
- async resume(request, context) {
534
- const pid = this.requirePid(request, context);
535
- const api = await this.api();
536
- if (api.resume) {
537
- await api.resume(pid);
538
- return true;
539
- }
540
- const device = await this.device(request.deviceId);
541
- if (!device.resume)
542
- throw new Error("Frida resume is not available");
543
- await device.resume(pid);
544
- return true;
545
- }
546
- async kill(request, context) {
547
- const pid = this.requirePid(request, context);
548
- const api = await this.api();
549
- if (api.kill) {
550
- await api.kill(pid);
551
- return true;
552
- }
553
- const device = await this.device(request.deviceId);
554
- if (!device.kill)
555
- throw new Error("Frida kill is not available");
556
- await device.kill(pid);
557
- return true;
558
- }
559
- requirePid(request, context) {
560
- return requirePositiveInteger(request.pid ?? context?.pid, "pid");
561
- }
562
- source(value) {
563
- const source = requireString(value, "source");
564
- if (Buffer.byteLength(source, "utf8") > this.maxSourceBytes)
565
- throw new Error("source exceeds the script size limit");
566
- return source;
567
- }
568
- async session(request, context) {
569
- if (request.sessionId) {
570
- const record = this.sessions.get(request.sessionId);
571
- if (!record)
572
- throw new Error(`session ${request.sessionId} was not found`);
573
- record.context = requestContext(request, context, record.context);
574
- return record;
575
- }
576
- const pid = this.requirePid(request, context);
577
- const device = await this.device(request.deviceId);
578
- const existing = [...this.sessions.values()].find(item => item.pid === pid && item.device.id === device.id);
579
- if (existing) {
580
- existing.context = requestContext(request, context, existing.context);
581
- return existing;
582
- }
583
- const attached = await this.attach({ ...request, operation: "attach", pid }, context);
584
- const id = String(attached.sessionId);
585
- const record = this.sessions.get(id);
586
- if (!record)
587
- throw new Error("Frida session was not created");
588
- return record;
589
- }
590
- async bridge(record, context) {
591
- if (!record.bridge) {
592
- record.bridge = await record.session.createScript(buildPrelude(context ?? record.context) + "\n" + BRIDGE_SOURCE);
593
- await record.bridge.load();
594
- }
595
- return record.bridge;
596
- }
597
- async bridgeCall(request, context, name, args) {
598
- const record = await this.session(request, context);
599
- const script = await this.bridge(record, context);
600
- const fn = exportFunction(script, name);
601
- const value = await fn(...args);
602
- return { sessionId: record.id, value: normalize(value), ...sessionContext(record) };
603
- }
604
- async namedBridgeCall(request, context, name, args, resultKey) {
605
- const response = await this.bridgeCall(request, context, name, args);
606
- return { ...response, [resultKey]: response.value };
607
- }
608
- async loadScript(request, context) {
609
- const record = await this.session(request, context);
610
- const source = this.source(request.source);
611
- const script = await record.session.createScript(buildPrelude(context ?? record.context) + "\n" + source, request.options);
612
- const requestedId = request.scriptId === undefined ? undefined : requireString(request.scriptId, "scriptId");
613
- const id = requestedId ?? `script-${++this.scriptCounter}`;
614
- if (this.scripts.has(id))
615
- throw new Error(`script ${id} already exists`);
616
- const messages = [];
617
- script.message?.connect((message, data) => {
618
- messages.push({ message: normalize(message), dataHex: data ? data.toString("hex") : undefined });
619
- });
620
- await script.load();
621
- record.scripts.set(id, script);
622
- record.messages.set(id, messages);
623
- this.scripts.set(id, { session: record, script });
624
- return { scriptId: id, sessionId: record.id, loaded: true, ...sessionContext(record) };
625
- }
626
- async callScript(request) {
627
- const id = requireString(request.scriptId, "scriptId");
628
- const entry = this.scripts.get(id);
629
- if (!entry)
630
- throw new Error(`script ${id} was not found`);
631
- const name = requireString(request.exportName ?? request.method, "exportName");
632
- const value = await exportFunction(entry.script, name)(...(request.args ?? []).map(item => decodeHostArg(item)));
633
- return { scriptId: id, value: normalize(value), messages: normalize(entry.session.messages.get(id) ?? []), ...sessionContext(entry.session) };
634
- }
635
- async unloadScriptResult(request, _context) {
636
- const id = requireString(request.scriptId, "scriptId");
637
- const entry = this.scripts.get(id);
638
- if (!entry)
639
- throw new Error(`script ${id} was not found`);
640
- const context = sessionContext(entry.session);
641
- const unloaded = await this.unloadScript(id);
642
- return { scriptId: id, unloaded, ...context };
643
- }
644
- async unloadScript(id) {
645
- const entry = this.scripts.get(id);
646
- if (!entry)
647
- throw new Error(`script ${id} was not found`);
648
- await entry.script.unload();
649
- entry.session.scripts.delete(id);
650
- entry.session.messages.delete(id);
651
- this.scripts.delete(id);
652
- return true;
653
- }
654
- async runTransientScript(request, context) {
655
- const record = await this.session(request, context);
656
- const source = this.source(request.source);
657
- const script = await record.session.createScript(buildPrelude(context ?? record.context) + "\n" + source, request.options);
658
- const messages = [];
659
- script.message?.connect((message, data) => {
660
- messages.push({ message: normalize(message), dataHex: data ? data.toString("hex") : undefined });
661
- });
662
- await script.load();
663
- const timeoutMs = Math.max(1, Math.min(request.timeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS, 120000));
664
- await new Promise(resolve => setTimeout(resolve, timeoutMs));
665
- try {
666
- await script.unload();
667
- }
668
- catch { /* transient cleanup is best effort */ }
669
- return { sessionId: record.id, messages: normalize(messages), ...sessionContext(record) };
670
- }
671
- async record(request, context, result) {
672
- if (!this.artifactDir)
673
- return;
674
- const redactedRequest = { ...request };
675
- if (typeof redactedRequest.source === "string") {
676
- redactedRequest.source = { sha256: hash(redactedRequest.source), bytes: Buffer.byteLength(redactedRequest.source, "utf8") };
677
- }
678
- if (typeof redactedRequest.bytesHex === "string") {
679
- redactedRequest.bytesHex = { sha256: hash(redactedRequest.bytesHex), bytes: redactedRequest.bytesHex.length / 2 };
680
- }
681
- if (typeof redactedRequest.hex === "string") {
682
- redactedRequest.hex = { sha256: hash(redactedRequest.hex), bytes: redactedRequest.hex.length / 2 };
683
- }
684
- if (typeof redactedRequest.program === "string") {
685
- redactedRequest.program = { sha256: hash(redactedRequest.program), bytes: Buffer.byteLength(redactedRequest.program, "utf8") };
686
- }
687
- if (Array.isArray(redactedRequest.argv)) {
688
- redactedRequest.argv = redactedRequest.argv.map(value => typeof value === "string"
689
- ? { sha256: hash(value), bytes: Buffer.byteLength(value, "utf8") }
690
- : value);
691
- }
692
- if (Array.isArray(redactedRequest.args)) {
693
- redactedRequest.args = { count: redactedRequest.args.length };
694
- }
695
- if (redactedRequest.options && typeof redactedRequest.options === "object") {
696
- redactedRequest.options = { keys: Object.keys(redactedRequest.options) };
697
- }
698
- const redactedContext = context
699
- ? {
700
- ...context,
701
- executable: context.executable
702
- ? { sha256: hash(context.executable), bytes: Buffer.byteLength(context.executable, "utf8") }
703
- : undefined
704
- }
705
- : undefined;
706
- const record = {
707
- timestamp: new Date().toISOString(),
708
- request: redactedRequest,
709
- target: redactedContext ? normalize(redactedContext) : undefined,
710
- result: redactArtifact(normalize(result))
711
- };
712
- await mkdir(this.artifactDir, { recursive: true });
713
- await appendFile(join(this.artifactDir, "frida-commands.jsonl"), JSON.stringify(record) + "\n", "utf8");
714
- }
715
- }