@jshookmcp/jshook 0.2.8 → 0.3.0
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 +36 -5
- package/README.zh.md +36 -5
- package/dist/{AntiCheatDetector-S8VRj-dD.mjs → AntiCheatDetector-CqGDXmfc.mjs} +160 -54
- package/dist/{CodeInjector-4Z3ngPoX.mjs → CodeInjector-BdjRfNx7.mjs} +5 -5
- package/dist/ConsoleMonitor-DykL3IAw.mjs +2269 -0
- package/dist/{DarwinAPI-B8hg_yhz.mjs → DarwinAPI-ETyy0xyo.mjs} +1 -1
- package/dist/DetailedDataManager-HT49OrvF.mjs +217 -0
- package/dist/EventBus-DFKvADm3.mjs +141 -0
- package/dist/EvidenceGraphBridge-318Oi0Lf.mjs +153 -0
- package/dist/{ExtensionManager-D5-bO9D8.mjs → ExtensionManager-BDMsY2Dz.mjs} +27 -13
- package/dist/{FingerprintManager-BVxFJL2-.mjs → FingerprintManager-BN4UQWnX.mjs} +1 -1
- package/dist/{HardwareBreakpoint-DK1yjWkV.mjs → HardwareBreakpoint-Cc2AFq1Y.mjs} +3 -3
- package/dist/{HeapAnalyzer-CEbo10xU.mjs → HeapAnalyzer-DruMgsgj.mjs} +21 -21
- package/dist/HookGeneratorBuilders.core.generators.storage-CTbB4Lcx.mjs +566 -0
- package/dist/InstrumentationSession-DLH0vd-z.mjs +244 -0
- package/dist/{MemoryController-DdtnBdD4.mjs → MemoryController-CMtviNW_.mjs} +3 -3
- package/dist/{MemoryScanSession-RMixN3bX.mjs → MemoryScanSession-ITgb_NMi.mjs} +81 -78
- package/dist/{MemoryScanner-QjK4ld0B.mjs → MemoryScanner-CiL7Z3ey.mjs} +50 -21
- package/dist/{NativeMemoryManager.impl-CB6gJ0NM.mjs → NativeMemoryManager.impl-D9Lkovvn.mjs} +20 -56
- package/dist/{NativeMemoryManager.utils-BML4q1ry.mjs → NativeMemoryManager.utils-BBlAixF5.mjs} +1 -1
- package/dist/{PEAnalyzer-CK0xe0Fs.mjs → PEAnalyzer-DMQ44gen.mjs} +16 -16
- package/dist/PageController-BPJNqqBN.mjs +431 -0
- package/dist/{PointerChainEngine-Cd73qu5b.mjs → PointerChainEngine-K7wN8Z-w.mjs} +10 -7
- package/dist/PrerequisiteError-TuyZIs6n.mjs +20 -0
- package/dist/ProcessRegistry-zGg12QbE.mjs +74 -0
- package/dist/ResponseBuilder-CJXWmWNw.mjs +143 -0
- package/dist/ReverseEvidenceGraph-C02-gXOh.mjs +269 -0
- package/dist/ScriptManager-ZuWD-0Jg.mjs +3003 -0
- package/dist/{Speedhack-CeF0XmEz.mjs → Speedhack-D-z0umeT.mjs} +2 -2
- package/dist/{StructureAnalyzer-D4GkMduU.mjs → StructureAnalyzer-Cav5AVSL.mjs} +9 -6
- package/dist/ToolCatalog-5OJdMiF0.mjs +582 -0
- package/dist/ToolError-jh9whhMd.mjs +15 -0
- package/dist/ToolProbe-DbCFGyrg.mjs +45 -0
- package/dist/ToolRegistry-B9krbTtI.mjs +180 -0
- package/dist/ToolRouter.policy-BGDAGyeH.mjs +344 -0
- package/dist/TraceRecorder-B41Z5XBj.mjs +1286 -0
- package/dist/{Win32API-Bc0QnQsN.mjs → Win32API-C2kjj0ze.mjs} +19 -13
- package/dist/{Win32Debug-DUHt9XUn.mjs → Win32Debug-CKrGOTpo.mjs} +3 -3
- package/dist/WorkflowEngine-DJ6M4opp.mjs +569 -0
- package/dist/analysis-BHeJW2Nb.mjs +1234 -0
- package/dist/antidebug-BRKeyt27.mjs +1081 -0
- package/dist/artifactRetention-CPXkUJXp.mjs +598 -0
- package/dist/artifacts-DkfosXH3.mjs +59 -0
- package/dist/authorization-schema-DRqyJMSk.mjs +31 -0
- package/dist/betterSqlite3-DLSBZodi.mjs +74 -0
- package/dist/binary-instrument--V3MAhJ4.mjs +971 -0
- package/dist/bind-helpers-ClV34xdn.mjs +42 -0
- package/dist/boringssl-inspector-Bo_LOLaS.mjs +180 -0
- package/dist/browser-Dx3_S2cG.mjs +4369 -0
- package/dist/capabilities-CcHlvWgK.mjs +33 -0
- package/dist/concurrency-Drev_Vz9.mjs +41 -0
- package/dist/{constants-CCvsN80K.mjs → constants-CDZLOoVv.mjs} +105 -48
- package/dist/coordination-DgItD9DL.mjs +259 -0
- package/dist/debugger-RS3RSAqs.mjs +1288 -0
- package/dist/definitions-BEoYofW5.mjs +47 -0
- package/dist/definitions-BRaefg3u.mjs +365 -0
- package/dist/definitions-BbkvZkiv.mjs +96 -0
- package/dist/definitions-BtWSHJ3o.mjs +17 -0
- package/dist/definitions-C1gCHO0i.mjs +43 -0
- package/dist/definitions-CDOg_b-l.mjs +138 -0
- package/dist/definitions-CVPD9hzZ.mjs +54 -0
- package/dist/definitions-Cea8Lgl7.mjs +94 -0
- package/dist/definitions-DAgIyjxM.mjs +10 -0
- package/dist/definitions-DJA27nsL.mjs +66 -0
- package/dist/definitions-DKPFU3LW.mjs +25 -0
- package/dist/definitions-DPRpZQ96.mjs +47 -0
- package/dist/definitions-DUE5gmdn.mjs +18 -0
- package/dist/definitions-DYVjOtxa.mjs +26 -0
- package/dist/definitions-DcYLVLCo.mjs +37 -0
- package/dist/definitions-Pp5LI2H4.mjs +27 -0
- package/dist/definitions-j9KdHVNR.mjs +14 -0
- package/dist/definitions-uzkjBwa7.mjs +258 -0
- package/dist/definitions-va-AnLuQ.mjs +28 -0
- package/dist/encoding-DJeqHmpd.mjs +1079 -0
- package/dist/evidence-graph-bridge-DcYizFk2.mjs +136 -0
- package/dist/{factory-CibqTNC8.mjs → factory-C90tBff6.mjs} +41 -56
- package/dist/flat-target-session-Dgax2Cy3.mjs +29 -0
- package/dist/graphql-CoHrhweh.mjs +1197 -0
- package/dist/handlers-4jmR0nMs.mjs +898 -0
- package/dist/handlers-BAHPxcch.mjs +789 -0
- package/dist/handlers-BOs9b907.mjs +2600 -0
- package/dist/handlers-BWXEy6ef.mjs +917 -0
- package/dist/handlers-Bndn6QvE.mjs +111 -0
- package/dist/handlers-BqC4bD4s.mjs +681 -0
- package/dist/handlers-BtYq60bM2.mjs +276 -0
- package/dist/handlers-BzgcB4iv.mjs +799 -0
- package/dist/handlers-CRyRWj2b.mjs +859 -0
- package/dist/handlers-CVv2H1uq.mjs +592 -0
- package/dist/handlers-Dl5a7JS4.mjs +572 -0
- package/dist/handlers-Dx2d7jt7.mjs +2537 -0
- package/dist/handlers-Dz9PYsCa.mjs +2805 -0
- package/dist/handlers-HujRKC3b.mjs +661 -0
- package/dist/handlers.impl-XWXkQfyi.mjs +807 -0
- package/dist/hooks-B1B8NRHL.mjs +898 -0
- package/dist/index.mjs +491 -259
- package/dist/{logger-BmWzC2lM.mjs → logger-Dh_xb7_2.mjs} +14 -6
- package/dist/maintenance-PRMkLVRW.mjs +835 -0
- package/dist/manifest-67Bok-Si.mjs +58 -0
- package/dist/manifest-6lNTMZAB2.mjs +87 -0
- package/dist/manifest-B2duEHiH.mjs +90 -0
- package/dist/manifest-B6EY9Vm8.mjs +57 -0
- package/dist/manifest-B6nKSbyY.mjs +95 -0
- package/dist/manifest-BL8AQNPF.mjs +106 -0
- package/dist/manifest-BSZvJJmV.mjs +47 -0
- package/dist/manifest-BU7qzUyX.mjs +418 -0
- package/dist/manifest-Bl62e8WK.mjs +49 -0
- package/dist/manifest-Bo5cXjdt.mjs +82 -0
- package/dist/manifest-BpS4gtUK.mjs +1347 -0
- package/dist/manifest-Bv65_e2W.mjs +101 -0
- package/dist/manifest-BytNIF4Z.mjs +117 -0
- package/dist/manifest-C-xtsjS3.mjs +81 -0
- package/dist/manifest-CDYl7OhA.mjs +66 -0
- package/dist/manifest-CRZ3xmkD.mjs +61 -0
- package/dist/manifest-CoW6u4Tp.mjs +132 -0
- package/dist/manifest-Cq5zN_8A.mjs +50 -0
- package/dist/manifest-D7YZM_2e.mjs +194 -0
- package/dist/manifest-DE_VrAeQ.mjs +314 -0
- package/dist/manifest-DGsXSCpT.mjs +39 -0
- package/dist/manifest-DJ2vfEuW.mjs +156 -0
- package/dist/manifest-DPXDYhEu.mjs +80 -0
- package/dist/manifest-Dd4fQb0a.mjs +322 -0
- package/dist/manifest-Deq6opGg.mjs +223 -0
- package/dist/manifest-DfJTafJK.mjs +37 -0
- package/dist/manifest-DgOdgN_j.mjs +50 -0
- package/dist/manifest-DlbMW4v4.mjs +47 -0
- package/dist/manifest-DmVfbH0w.mjs +374 -0
- package/dist/manifest-Dog6Ddjr.mjs +109 -0
- package/dist/manifest-DvgU5FWb.mjs +58 -0
- package/dist/manifest-HsfDBs7j.mjs +50 -0
- package/dist/manifest-I8oQHvCG.mjs +186 -0
- package/dist/manifest-NvH_a-av.mjs +786 -0
- package/dist/manifest-cEJU1v0Z.mjs +129 -0
- package/dist/manifest-wOl5XLB12.mjs +112 -0
- package/dist/modules-tZozf0LQ.mjs +10635 -0
- package/dist/mojo-ipc-DXNEXEqb.mjs +640 -0
- package/dist/network-CPVvwvFg.mjs +3852 -0
- package/dist/{artifacts-BbdOMET5.mjs → outputPaths-um7lCRY3.mjs} +219 -216
- package/dist/parse-args-B4cY5Vx5.mjs +39 -0
- package/dist/platform-CYeFoTWp.mjs +2161 -0
- package/dist/process-BTbgcVc6.mjs +1306 -0
- package/dist/proxy-r8YN6nP1.mjs +192 -0
- package/dist/registry-Bl8ZQW61.mjs +34 -0
- package/dist/response-CWhh2aLo.mjs +34 -0
- package/dist/server/plugin-api.mjs +2 -2
- package/dist/shared-state-board-BoZnSoj-.mjs +586 -0
- package/dist/sourcemap-BIDHUVXy.mjs +934 -0
- package/dist/ssrf-policy-Dsqd-DTX.mjs +166 -0
- package/dist/streaming-Dal6utPp.mjs +725 -0
- package/dist/tool-builder-BHJp32mV.mjs +186 -0
- package/dist/transform-DRVgGG90.mjs +1011 -0
- package/dist/types-Bx92KJfT.mjs +4 -0
- package/dist/wasm-BYx5UOeG.mjs +1044 -0
- package/dist/webcrack-Be0_FccV.mjs +747 -0
- package/dist/workflow-BpuKEtvn.mjs +725 -0
- package/package.json +82 -49
- package/dist/ExtensionManager-CPTJhHFg.mjs +0 -2
- package/dist/ToolCatalog-Bq4V2sbJ.mjs +0 -67201
- package/dist/{CacheAdapters-CzFNpD9a.mjs → CacheAdapters-jJFy20G-.mjs} +0 -0
- package/dist/{StealthVerifier-BzBCFiwx.mjs → StealthVerifier-BWmPgQsv.mjs} +0 -0
- package/dist/{VersionDetector-CNXcvD46.mjs → VersionDetector-K3V4vGsw.mjs} +0 -0
- package/dist/{formatAddress-ChCSIRWT.mjs → formatAddress-nnMvEohD.mjs} +0 -0
- package/dist/{types-BBjOqye-.mjs → types-DDBWs9UP.mjs} +1 -1
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-CjcI7cDX.mjs";
|
|
2
|
+
import { t as logger } from "./logger-Dh_xb7_2.mjs";
|
|
3
|
+
import { Q as FRIDA_TIMEOUT_MS, et as GHIDRA_TIMEOUT_MS, xr as UNIDBG_TIMEOUT_MS } from "./constants-CDZLOoVv.mjs";
|
|
4
|
+
import { t as probeCommand } from "./ToolProbe-DbCFGyrg.mjs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { basename, dirname, join } from "node:path";
|
|
8
|
+
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
//#region src/modules/binary-instrument/FridaSession.ts
|
|
11
|
+
const FRIDA_MAX_BUFFER_BYTES = 5 * 1024 * 1024;
|
|
12
|
+
var FridaSession = class {
|
|
13
|
+
sessions = /* @__PURE__ */ new Map();
|
|
14
|
+
activeSessionId;
|
|
15
|
+
fridaProbe;
|
|
16
|
+
probePromise;
|
|
17
|
+
async attach(target) {
|
|
18
|
+
const availability = await this.getAvailability();
|
|
19
|
+
if (!availability.available) throw new Error(availability.reason ?? "Frida CLI is not available");
|
|
20
|
+
const probe = await this.runFridaCommand(target, "console.log(\"__frida_attach_ok__\");");
|
|
21
|
+
if (probe.error) throw new Error(probe.error);
|
|
22
|
+
const sessionId = randomUUID();
|
|
23
|
+
const record = {
|
|
24
|
+
id: sessionId,
|
|
25
|
+
target,
|
|
26
|
+
pid: this.resolvePid(target),
|
|
27
|
+
status: "attached",
|
|
28
|
+
attachedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
29
|
+
};
|
|
30
|
+
this.sessions.set(sessionId, record);
|
|
31
|
+
this.activeSessionId = sessionId;
|
|
32
|
+
return sessionId;
|
|
33
|
+
}
|
|
34
|
+
async detach() {
|
|
35
|
+
const active = this.getActiveSessionRecord();
|
|
36
|
+
if (!active) return;
|
|
37
|
+
active.status = "detached";
|
|
38
|
+
this.activeSessionId = void 0;
|
|
39
|
+
}
|
|
40
|
+
async executeScript(script) {
|
|
41
|
+
const session = this.requireActiveSession();
|
|
42
|
+
const result = await this.runFridaCommand(session.target, script);
|
|
43
|
+
if (result.error) {
|
|
44
|
+
session.status = "error";
|
|
45
|
+
session.lastError = result.error;
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
async enumerateModules() {
|
|
50
|
+
const session = this.requireActiveSession();
|
|
51
|
+
const result = await this.runFridaCommand(session.target, "console.log(JSON.stringify(Process.enumerateModules()));");
|
|
52
|
+
const parsed = this.parseModuleList(result.output);
|
|
53
|
+
if (parsed.length > 0) return parsed;
|
|
54
|
+
if (result.error) {
|
|
55
|
+
session.status = "error";
|
|
56
|
+
session.lastError = result.error;
|
|
57
|
+
}
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
async enumerateFunctions(moduleName) {
|
|
61
|
+
const session = this.requireActiveSession();
|
|
62
|
+
const safeModuleName = JSON.stringify(moduleName);
|
|
63
|
+
const result = await this.runFridaCommand(session.target, [
|
|
64
|
+
`const entries = Process.getModuleByName(${safeModuleName}).enumerateExports()`,
|
|
65
|
+
".filter(function (entry) { return entry.type === \"function\"; })",
|
|
66
|
+
".map(function (entry) {",
|
|
67
|
+
" return { name: entry.name, address: String(entry.address), size: 0 };",
|
|
68
|
+
"});",
|
|
69
|
+
"console.log(JSON.stringify(entries));"
|
|
70
|
+
].join(""));
|
|
71
|
+
const parsed = this.parseFunctionList(result.output);
|
|
72
|
+
if (parsed.length > 0) return parsed;
|
|
73
|
+
if (result.error) {
|
|
74
|
+
session.status = "error";
|
|
75
|
+
session.lastError = result.error;
|
|
76
|
+
}
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
async findSymbols(pattern) {
|
|
80
|
+
const session = this.requireActiveSession();
|
|
81
|
+
const trimmedPattern = pattern.trim();
|
|
82
|
+
const resolvedPattern = trimmedPattern.includes(":") ? trimmedPattern : trimmedPattern.includes("!") ? `exports:${trimmedPattern}` : `exports:*!${trimmedPattern}*`;
|
|
83
|
+
const matchPattern = JSON.stringify(resolvedPattern);
|
|
84
|
+
const result = await this.runFridaCommand(session.target, [
|
|
85
|
+
"const resolver = new ApiResolver(\"module\");",
|
|
86
|
+
`const matches = resolver.enumerateMatches(${matchPattern});`,
|
|
87
|
+
"const mapped = matches.map(function (entry) {",
|
|
88
|
+
" const resolvedName = typeof entry.name === \"string\" ? entry.name : \"unknown\";",
|
|
89
|
+
" const resolvedAddress = entry.address ? String(entry.address) : \"0x0\";",
|
|
90
|
+
" return { name: resolvedName, address: resolvedAddress, demangled: resolvedName };",
|
|
91
|
+
"});",
|
|
92
|
+
"console.log(JSON.stringify(mapped));"
|
|
93
|
+
].join(""));
|
|
94
|
+
const parsed = this.parseSymbolList(result.output);
|
|
95
|
+
if (parsed.length > 0) return parsed;
|
|
96
|
+
if (result.error) {
|
|
97
|
+
session.status = "error";
|
|
98
|
+
session.lastError = result.error;
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
listSessions() {
|
|
103
|
+
return Array.from(this.sessions.values()).map((session) => ({
|
|
104
|
+
id: session.id,
|
|
105
|
+
target: session.target,
|
|
106
|
+
pid: session.pid,
|
|
107
|
+
status: session.status
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
async isAvailable() {
|
|
111
|
+
return (await this.getAvailability()).available;
|
|
112
|
+
}
|
|
113
|
+
async getAvailability() {
|
|
114
|
+
if (this.fridaProbe) return this.fridaProbe;
|
|
115
|
+
if (!this.probePromise) this.probePromise = probeCommand("frida");
|
|
116
|
+
const resolved = await this.probePromise;
|
|
117
|
+
this.fridaProbe = resolved;
|
|
118
|
+
this.probePromise = void 0;
|
|
119
|
+
return resolved;
|
|
120
|
+
}
|
|
121
|
+
useSession(sessionId) {
|
|
122
|
+
if (!this.sessions.has(sessionId)) return false;
|
|
123
|
+
this.activeSessionId = sessionId;
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
hasSession(sessionId) {
|
|
127
|
+
return this.sessions.has(sessionId);
|
|
128
|
+
}
|
|
129
|
+
getSessionDiagnostics(sessionId) {
|
|
130
|
+
const session = this.sessions.get(sessionId);
|
|
131
|
+
if (!session) return;
|
|
132
|
+
return {
|
|
133
|
+
status: session.status,
|
|
134
|
+
lastError: session.lastError
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
getActiveSessionRecord() {
|
|
138
|
+
if (!this.activeSessionId) return;
|
|
139
|
+
return this.sessions.get(this.activeSessionId);
|
|
140
|
+
}
|
|
141
|
+
requireActiveSession() {
|
|
142
|
+
const session = this.getActiveSessionRecord();
|
|
143
|
+
if (!session) throw new Error("No active Frida session. Call attach() first.");
|
|
144
|
+
return session;
|
|
145
|
+
}
|
|
146
|
+
resolvePid(target) {
|
|
147
|
+
if (!/^\d+$/.test(target)) return null;
|
|
148
|
+
const parsed = Number.parseInt(target, 10);
|
|
149
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
150
|
+
}
|
|
151
|
+
async runFridaCommand(target, script) {
|
|
152
|
+
const availability = await this.getAvailability();
|
|
153
|
+
if (!availability.available) return {
|
|
154
|
+
output: "",
|
|
155
|
+
error: availability.reason ?? "Frida CLI is not available"
|
|
156
|
+
};
|
|
157
|
+
const command = availability.path ?? "frida";
|
|
158
|
+
const args = [
|
|
159
|
+
...this.buildTargetArgs(target),
|
|
160
|
+
"--runtime=v8",
|
|
161
|
+
"-q",
|
|
162
|
+
"-e",
|
|
163
|
+
script
|
|
164
|
+
];
|
|
165
|
+
try {
|
|
166
|
+
const result = await this.execFileUtf8(command, args, FRIDA_TIMEOUT_MS);
|
|
167
|
+
const output = result.stdout.trim();
|
|
168
|
+
const error = result.stderr.trim();
|
|
169
|
+
return error ? {
|
|
170
|
+
output,
|
|
171
|
+
error
|
|
172
|
+
} : { output };
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
175
|
+
logger.warn("[binary-instrument] Frida command failed", {
|
|
176
|
+
target,
|
|
177
|
+
message
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
output: "",
|
|
181
|
+
error: message
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
buildTargetArgs(target) {
|
|
186
|
+
if (/^\d+$/.test(target)) return ["-p", target];
|
|
187
|
+
if (target.includes("/") || target.includes("\\")) return ["-f", target];
|
|
188
|
+
return ["-n", target];
|
|
189
|
+
}
|
|
190
|
+
parseModuleList(output) {
|
|
191
|
+
const payload = this.extractJsonPayload(output);
|
|
192
|
+
if (!Array.isArray(payload)) return [];
|
|
193
|
+
const modules = [];
|
|
194
|
+
for (const entry of payload) {
|
|
195
|
+
if (!this.isRecord(entry)) continue;
|
|
196
|
+
const name = this.readStringField(entry, "name");
|
|
197
|
+
const path = this.readStringField(entry, "path");
|
|
198
|
+
const base = this.normalizeHex(entry["base"]);
|
|
199
|
+
const size = this.readNumberField(entry, "size");
|
|
200
|
+
if (!name || !path || !base || size === void 0) continue;
|
|
201
|
+
modules.push({
|
|
202
|
+
name,
|
|
203
|
+
base,
|
|
204
|
+
size,
|
|
205
|
+
path
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return modules;
|
|
209
|
+
}
|
|
210
|
+
parseFunctionList(output) {
|
|
211
|
+
const payload = this.extractJsonPayload(output);
|
|
212
|
+
if (!Array.isArray(payload)) return [];
|
|
213
|
+
const functions = [];
|
|
214
|
+
for (const entry of payload) {
|
|
215
|
+
if (!this.isRecord(entry)) continue;
|
|
216
|
+
const name = this.readStringField(entry, "name");
|
|
217
|
+
const address = this.normalizeHex(entry["address"]);
|
|
218
|
+
const size = this.readNumberField(entry, "size") ?? 0;
|
|
219
|
+
if (!name || !address) continue;
|
|
220
|
+
functions.push({
|
|
221
|
+
name,
|
|
222
|
+
address,
|
|
223
|
+
size
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return functions;
|
|
227
|
+
}
|
|
228
|
+
parseSymbolList(output) {
|
|
229
|
+
const payload = this.extractJsonPayload(output);
|
|
230
|
+
if (!Array.isArray(payload)) return [];
|
|
231
|
+
const symbols = [];
|
|
232
|
+
for (const entry of payload) {
|
|
233
|
+
if (!this.isRecord(entry)) continue;
|
|
234
|
+
const name = this.readStringField(entry, "name");
|
|
235
|
+
const address = this.normalizeHex(entry["address"]);
|
|
236
|
+
const demangled = this.readStringField(entry, "demangled");
|
|
237
|
+
if (!name || !address) continue;
|
|
238
|
+
if (demangled) symbols.push({
|
|
239
|
+
name,
|
|
240
|
+
address,
|
|
241
|
+
demangled
|
|
242
|
+
});
|
|
243
|
+
else symbols.push({
|
|
244
|
+
name,
|
|
245
|
+
address
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return symbols;
|
|
249
|
+
}
|
|
250
|
+
extractJsonPayload(output) {
|
|
251
|
+
const candidates = output.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("{") || line.startsWith("[")).toReversed();
|
|
252
|
+
for (const line of candidates) try {
|
|
253
|
+
return JSON.parse(line);
|
|
254
|
+
} catch {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
readStringField(record, key) {
|
|
259
|
+
const value = record[key];
|
|
260
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
261
|
+
}
|
|
262
|
+
readNumberField(record, key) {
|
|
263
|
+
const value = record[key];
|
|
264
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
265
|
+
}
|
|
266
|
+
normalizeHex(value) {
|
|
267
|
+
if (typeof value === "number" && Number.isFinite(value)) return `0x${value.toString(16)}`;
|
|
268
|
+
if (typeof value === "string" && value.length > 0) return value.startsWith("0x") ? value : `0x${value}`;
|
|
269
|
+
}
|
|
270
|
+
isRecord(value) {
|
|
271
|
+
return typeof value === "object" && value !== null;
|
|
272
|
+
}
|
|
273
|
+
execFileUtf8(file, args, timeoutMs) {
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
275
|
+
execFile(file, args, {
|
|
276
|
+
timeout: timeoutMs,
|
|
277
|
+
windowsHide: true,
|
|
278
|
+
maxBuffer: FRIDA_MAX_BUFFER_BYTES,
|
|
279
|
+
encoding: "utf8"
|
|
280
|
+
}, (error, stdout, stderr) => {
|
|
281
|
+
if (error) {
|
|
282
|
+
reject(error);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
resolve({
|
|
286
|
+
stdout: typeof stdout === "string" ? stdout : "",
|
|
287
|
+
stderr: typeof stderr === "string" ? stderr : ""
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/modules/binary-instrument/GhidraAnalyzer.ts
|
|
295
|
+
const GHIDRA_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
296
|
+
var GhidraAnalyzer = class {
|
|
297
|
+
ghidraProbe;
|
|
298
|
+
probePromise;
|
|
299
|
+
async analyze(binaryPath, options) {
|
|
300
|
+
await access(binaryPath);
|
|
301
|
+
const fileBuffer = await readFile(binaryPath);
|
|
302
|
+
const strings = this.extractPrintableStrings(fileBuffer);
|
|
303
|
+
const imports = this.deriveImports(strings);
|
|
304
|
+
const exports = this.deriveExports(strings);
|
|
305
|
+
if (!(await this.getAvailability()).available) return {
|
|
306
|
+
functions: [],
|
|
307
|
+
imports,
|
|
308
|
+
exports,
|
|
309
|
+
strings
|
|
310
|
+
};
|
|
311
|
+
const timeoutMs = typeof options?.timeout === "number" && Number.isFinite(options.timeout) ? options.timeout : GHIDRA_TIMEOUT_MS;
|
|
312
|
+
const scriptDirectory = await mkdtemp(join(tmpdir(), "jshook-ghidra-script-"));
|
|
313
|
+
const scriptPath = join(scriptDirectory, "BinaryInstrumentDump.py");
|
|
314
|
+
try {
|
|
315
|
+
await writeFile(scriptPath, this.buildDefaultScript(), "utf8");
|
|
316
|
+
const output = await this.headlessAnalyze(scriptPath, binaryPath, timeoutMs);
|
|
317
|
+
return {
|
|
318
|
+
functions: this.parseDecompiledOutput(output),
|
|
319
|
+
imports,
|
|
320
|
+
exports,
|
|
321
|
+
strings
|
|
322
|
+
};
|
|
323
|
+
} catch (error) {
|
|
324
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
325
|
+
logger.warn("[binary-instrument] Ghidra analyze fallback", {
|
|
326
|
+
binaryPath,
|
|
327
|
+
message
|
|
328
|
+
});
|
|
329
|
+
return {
|
|
330
|
+
functions: [],
|
|
331
|
+
imports,
|
|
332
|
+
exports,
|
|
333
|
+
strings
|
|
334
|
+
};
|
|
335
|
+
} finally {
|
|
336
|
+
await rm(scriptDirectory, {
|
|
337
|
+
recursive: true,
|
|
338
|
+
force: true
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async headlessAnalyze(scriptPath, binaryPath, timeoutMs = GHIDRA_TIMEOUT_MS) {
|
|
343
|
+
const availability = await this.getAvailability();
|
|
344
|
+
if (!availability.available) throw new Error(availability.reason ?? "Ghidra analyzeHeadless is not available");
|
|
345
|
+
await access(binaryPath);
|
|
346
|
+
await access(scriptPath);
|
|
347
|
+
const command = availability.path ?? "analyzeHeadless";
|
|
348
|
+
const projectDirectory = await mkdtemp(join(tmpdir(), "jshook-ghidra-project-"));
|
|
349
|
+
const projectName = "binary-instrument";
|
|
350
|
+
try {
|
|
351
|
+
const result = await this.execFileUtf8(command, [
|
|
352
|
+
projectDirectory,
|
|
353
|
+
projectName,
|
|
354
|
+
"-import",
|
|
355
|
+
binaryPath,
|
|
356
|
+
"-scriptPath",
|
|
357
|
+
dirname(scriptPath),
|
|
358
|
+
"-postScript",
|
|
359
|
+
basename(scriptPath)
|
|
360
|
+
], timeoutMs);
|
|
361
|
+
return [result.stdout.trim(), result.stderr.trim()].filter((entry) => entry.length > 0).join("\n");
|
|
362
|
+
} finally {
|
|
363
|
+
await rm(projectDirectory, {
|
|
364
|
+
recursive: true,
|
|
365
|
+
force: true
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
parseDecompiledOutput(output) {
|
|
370
|
+
const functions = [];
|
|
371
|
+
const blockPattern = /FUNCTION_START\s*[\r\n]+NAME:(.+?)\s*[\r\n]+ADDRESS:(.+?)\s*[\r\n]+SIGNATURE:(.+?)\s*[\r\n]+DECOMPILED_START\s*[\r\n]+([\s\S]*?)\s*[\r\n]+DECOMPILED_END\s*[\r\n]+FUNCTION_END/g;
|
|
372
|
+
let match = blockPattern.exec(output);
|
|
373
|
+
while (match) {
|
|
374
|
+
const rawName = match[1] ?? "";
|
|
375
|
+
const rawAddress = match[2] ?? "";
|
|
376
|
+
const rawSignature = match[3] ?? "";
|
|
377
|
+
const rawBody = match[4] ?? "";
|
|
378
|
+
const name = rawName.trim();
|
|
379
|
+
const address = this.normalizeHex(rawAddress.trim());
|
|
380
|
+
const signature = rawSignature.trim();
|
|
381
|
+
const decompiled = rawBody.trim();
|
|
382
|
+
if (name.length > 0 && address.length > 0 && signature.length > 0) functions.push({
|
|
383
|
+
name,
|
|
384
|
+
address,
|
|
385
|
+
signature,
|
|
386
|
+
decompiled
|
|
387
|
+
});
|
|
388
|
+
match = blockPattern.exec(output);
|
|
389
|
+
}
|
|
390
|
+
return functions;
|
|
391
|
+
}
|
|
392
|
+
async isAvailable() {
|
|
393
|
+
return (await this.getAvailability()).available;
|
|
394
|
+
}
|
|
395
|
+
async getAvailability() {
|
|
396
|
+
if (this.ghidraProbe) return this.ghidraProbe;
|
|
397
|
+
if (!this.probePromise) this.probePromise = probeCommand("analyzeHeadless", ["-help"]);
|
|
398
|
+
const resolved = await this.probePromise;
|
|
399
|
+
this.ghidraProbe = resolved;
|
|
400
|
+
this.probePromise = void 0;
|
|
401
|
+
return resolved;
|
|
402
|
+
}
|
|
403
|
+
buildDefaultScript() {
|
|
404
|
+
return [
|
|
405
|
+
"# @category BinaryInstrument",
|
|
406
|
+
"from ghidra.app.decompiler import DecompInterface",
|
|
407
|
+
"",
|
|
408
|
+
"program = currentProgram",
|
|
409
|
+
"interface = DecompInterface()",
|
|
410
|
+
"interface.openProgram(program)",
|
|
411
|
+
"function_manager = program.getFunctionManager()",
|
|
412
|
+
"functions = function_manager.getFunctions(True)",
|
|
413
|
+
"",
|
|
414
|
+
"for function in functions:",
|
|
415
|
+
" print(\"FUNCTION_START\")",
|
|
416
|
+
" print(\"NAME:\" + str(function.getName()))",
|
|
417
|
+
" print(\"ADDRESS:\" + str(function.getEntryPoint()))",
|
|
418
|
+
" try:",
|
|
419
|
+
" signature = str(function.getSignature())",
|
|
420
|
+
" except:",
|
|
421
|
+
" signature = str(function.getName()) + \"()\"",
|
|
422
|
+
" print(\"SIGNATURE:\" + signature)",
|
|
423
|
+
" print(\"DECOMPILED_START\")",
|
|
424
|
+
" try:",
|
|
425
|
+
" decompiled = interface.decompileFunction(function, 30, monitor).getDecompiledFunction()",
|
|
426
|
+
" if decompiled:",
|
|
427
|
+
" print(str(decompiled.getC()))",
|
|
428
|
+
" else:",
|
|
429
|
+
" print(\"// no decompiled output\")",
|
|
430
|
+
" except:",
|
|
431
|
+
" print(\"// decompile failed\")",
|
|
432
|
+
" print(\"DECOMPILED_END\")",
|
|
433
|
+
" print(\"FUNCTION_END\")"
|
|
434
|
+
].join("\n");
|
|
435
|
+
}
|
|
436
|
+
extractPrintableStrings(buffer) {
|
|
437
|
+
const results = [];
|
|
438
|
+
let current = "";
|
|
439
|
+
for (const byte of buffer.values()) {
|
|
440
|
+
if (byte >= 32 && byte <= 126) {
|
|
441
|
+
current += String.fromCharCode(byte);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (current.length >= 4) results.push(current);
|
|
445
|
+
current = "";
|
|
446
|
+
}
|
|
447
|
+
if (current.length >= 4) results.push(current);
|
|
448
|
+
return Array.from(new Set(results)).slice(0, 1e3);
|
|
449
|
+
}
|
|
450
|
+
deriveImports(strings) {
|
|
451
|
+
return strings.filter((entry) => /(?:\.dll|\.so|\.dylib|kernel32|user32|libc|printf|malloc|LoadLibrary)/i.test(entry)).slice(0, 100);
|
|
452
|
+
}
|
|
453
|
+
deriveExports(strings) {
|
|
454
|
+
return strings.filter((entry) => /^[A-Za-z_][A-Za-z0-9_@?$]{2,}$/.test(entry)).slice(0, 100);
|
|
455
|
+
}
|
|
456
|
+
normalizeHex(value) {
|
|
457
|
+
return value.startsWith("0x") ? value : `0x${value}`;
|
|
458
|
+
}
|
|
459
|
+
execFileUtf8(file, args, timeoutMs) {
|
|
460
|
+
return new Promise((resolve, reject) => {
|
|
461
|
+
execFile(file, args, {
|
|
462
|
+
timeout: timeoutMs,
|
|
463
|
+
windowsHide: true,
|
|
464
|
+
maxBuffer: GHIDRA_MAX_BUFFER_BYTES,
|
|
465
|
+
encoding: "utf8"
|
|
466
|
+
}, (error, stdout, stderr) => {
|
|
467
|
+
if (error) {
|
|
468
|
+
reject(error);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
resolve({
|
|
472
|
+
stdout: typeof stdout === "string" ? stdout : "",
|
|
473
|
+
stderr: typeof stderr === "string" ? stderr : ""
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
//#endregion
|
|
480
|
+
//#region src/modules/binary-instrument/HookCodeGenerator.ts
|
|
481
|
+
var HookCodeGenerator = class {
|
|
482
|
+
generateHooks(input) {
|
|
483
|
+
const templates = [];
|
|
484
|
+
for (const fn of input.functions) {
|
|
485
|
+
const category = this.classifyFunction(fn, input);
|
|
486
|
+
if (category === "unknown") continue;
|
|
487
|
+
templates.push({
|
|
488
|
+
functionName: fn.name,
|
|
489
|
+
description: this.describeCategory(category, input),
|
|
490
|
+
hookCode: this.buildHookCode(fn, category),
|
|
491
|
+
parameters: this.buildParameters(fn)
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return templates;
|
|
495
|
+
}
|
|
496
|
+
exportScript(templates, format) {
|
|
497
|
+
if (format !== "frida") throw new Error("Unsupported export format");
|
|
498
|
+
const lines = [
|
|
499
|
+
"// Frida hook script",
|
|
500
|
+
"// auto-generated by HookCodeGenerator",
|
|
501
|
+
`// Hook count: ${templates.length}`,
|
|
502
|
+
""
|
|
503
|
+
];
|
|
504
|
+
for (const template of templates) {
|
|
505
|
+
lines.push(`// ${template.functionName}: ${template.description}`);
|
|
506
|
+
lines.push(template.hookCode);
|
|
507
|
+
lines.push("");
|
|
508
|
+
}
|
|
509
|
+
return lines.join("\n");
|
|
510
|
+
}
|
|
511
|
+
classifyFunction(fn, input) {
|
|
512
|
+
const name = fn.name.toLowerCase();
|
|
513
|
+
const imports = input.imports.map((entry) => entry.toLowerCase());
|
|
514
|
+
const strings = input.strings.map((entry) => entry.toLowerCase());
|
|
515
|
+
if (fn.name.startsWith("Java_")) return "jni";
|
|
516
|
+
if (name.includes("aes")) return "aes";
|
|
517
|
+
if (name.includes("md5")) return "md5";
|
|
518
|
+
if (name.includes("sha")) return "sha";
|
|
519
|
+
if (name.includes("rsa")) return "rsa";
|
|
520
|
+
if (name.includes("base64")) return "base64";
|
|
521
|
+
if (name.includes("send") || name.includes("recv") || name.includes("socket") || name.includes("http")) return "network";
|
|
522
|
+
if (name.includes("open") || name.includes("read") || name.includes("write") || name.includes("fopen")) return "file-io";
|
|
523
|
+
if ((name.includes("decrypt") || name.includes("encrypt")) && strings.some((entry) => entry.includes("obfuscation") || entry.includes("encryption"))) return "obfuscation";
|
|
524
|
+
if (name.includes("memcpy") || name.includes("strcpy") || name.includes("memmove") || name.includes("string")) return "string";
|
|
525
|
+
if (imports.some((entry) => entry.includes("aes"))) return "aes";
|
|
526
|
+
return "unknown";
|
|
527
|
+
}
|
|
528
|
+
describeCategory(category, input) {
|
|
529
|
+
switch (category) {
|
|
530
|
+
case "jni": return "JNI bridge hook for Java/native boundary inspection";
|
|
531
|
+
case "aes": return "AES crypto hook for key/plaintext capture";
|
|
532
|
+
case "md5": return "MD5 crypto hook for digest inspection";
|
|
533
|
+
case "sha": return "SHA crypto hook for digest inspection";
|
|
534
|
+
case "rsa": return "RSA crypto hook for key operation tracing";
|
|
535
|
+
case "base64": return "Base64 transform hook for encoded payload tracing";
|
|
536
|
+
case "network": return "Network hook for request and payload tracing";
|
|
537
|
+
case "file-io": return "File I/O hook for filesystem access tracing";
|
|
538
|
+
case "string": return "String operation hook for buffer tracing";
|
|
539
|
+
case "obfuscation": return input.strings.some((entry) => entry.toLowerCase().includes("obfuscation")) ? "Potential obfuscation hook for runtime string decryption tracing" : "Potential obfuscation hook";
|
|
540
|
+
case "unknown": return "Unknown hook";
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
buildHookCode(fn, category) {
|
|
544
|
+
if (category === "jni") return [
|
|
545
|
+
"Java.perform(function () {",
|
|
546
|
+
` const target = ptr("${fn.address}");`,
|
|
547
|
+
" Interceptor.attach(target, {",
|
|
548
|
+
" onEnter(args) {",
|
|
549
|
+
` console.log("[jni] ${fn.name}");`,
|
|
550
|
+
" },",
|
|
551
|
+
" });",
|
|
552
|
+
"});"
|
|
553
|
+
].join("\n");
|
|
554
|
+
const extraLine = category === "aes" || category === "md5" || category === "sha" || category === "rsa" || category === "base64" ? " console.log(hexdump(args[0]));" : " console.log(\"[trace] entering\");";
|
|
555
|
+
const targetName = fn.name.replace(/[^A-Za-z0-9_]/g, "_");
|
|
556
|
+
return [
|
|
557
|
+
`const target_${targetName} = ptr("${fn.address}");`,
|
|
558
|
+
`Interceptor.attach(target_${targetName}, {`,
|
|
559
|
+
" onEnter(args) {",
|
|
560
|
+
` console.log("[hook] ${fn.name}");`,
|
|
561
|
+
extraLine,
|
|
562
|
+
" },",
|
|
563
|
+
"});"
|
|
564
|
+
].join("\n");
|
|
565
|
+
}
|
|
566
|
+
buildParameters(fn) {
|
|
567
|
+
if (fn.parameters.length > 0) return fn.parameters.map((parameter, index) => ({
|
|
568
|
+
name: parameter.name || `arg${index}`,
|
|
569
|
+
type: parameter.type || "pointer",
|
|
570
|
+
description: `Captured parameter ${index}`
|
|
571
|
+
}));
|
|
572
|
+
return [{
|
|
573
|
+
name: "arg0",
|
|
574
|
+
type: "pointer",
|
|
575
|
+
description: "Captured parameter 0"
|
|
576
|
+
}];
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
//#endregion
|
|
580
|
+
//#region src/modules/binary-instrument/ExtensionBridge.ts
|
|
581
|
+
async function invokePlugin(ctx, config) {
|
|
582
|
+
const available = getAvailablePlugins(ctx);
|
|
583
|
+
const normalizedRequested = normalizePluginId(config.pluginId);
|
|
584
|
+
const actualPluginId = resolvePluginId(normalizedRequested, available);
|
|
585
|
+
if (!actualPluginId) return {
|
|
586
|
+
success: false,
|
|
587
|
+
tool: "binary-instrument",
|
|
588
|
+
action: config.toolName,
|
|
589
|
+
error: `Plugin ${normalizedRequested} is not installed`
|
|
590
|
+
};
|
|
591
|
+
const runtime = findRuntime(ctx, actualPluginId);
|
|
592
|
+
if (!runtime?.lifecycleContext) return {
|
|
593
|
+
success: false,
|
|
594
|
+
tool: "binary-instrument",
|
|
595
|
+
action: config.toolName,
|
|
596
|
+
error: `Plugin ${actualPluginId} is installed but has no runtime`
|
|
597
|
+
};
|
|
598
|
+
try {
|
|
599
|
+
const firstText = (await runtime.lifecycleContext.invokeTool(config.toolName, config.args)).content?.find((item) => item.type === "text")?.text;
|
|
600
|
+
if (!firstText) return {
|
|
601
|
+
success: false,
|
|
602
|
+
tool: "binary-instrument",
|
|
603
|
+
action: config.toolName,
|
|
604
|
+
error: "Plugin returned no text content"
|
|
605
|
+
};
|
|
606
|
+
try {
|
|
607
|
+
const parsed = JSON.parse(firstText);
|
|
608
|
+
if (isResultRecord(parsed)) return {
|
|
609
|
+
tool: "binary-instrument",
|
|
610
|
+
action: config.toolName,
|
|
611
|
+
success: readBoolean(parsed, "success") ?? true,
|
|
612
|
+
data: parsed["data"],
|
|
613
|
+
error: readString(parsed, "error")
|
|
614
|
+
};
|
|
615
|
+
} catch {
|
|
616
|
+
return {
|
|
617
|
+
success: true,
|
|
618
|
+
tool: "binary-instrument",
|
|
619
|
+
action: config.toolName,
|
|
620
|
+
data: firstText
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
success: true,
|
|
625
|
+
tool: "binary-instrument",
|
|
626
|
+
action: config.toolName,
|
|
627
|
+
data: firstText
|
|
628
|
+
};
|
|
629
|
+
} catch (error) {
|
|
630
|
+
return {
|
|
631
|
+
success: false,
|
|
632
|
+
tool: "binary-instrument",
|
|
633
|
+
action: config.toolName,
|
|
634
|
+
error: error instanceof Error ? error.message : String(error)
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function getAvailablePlugins(ctx) {
|
|
639
|
+
return Array.from(ctx.extensionPluginsById.keys()).map(normalizePluginId);
|
|
640
|
+
}
|
|
641
|
+
function normalizePluginId(pluginId) {
|
|
642
|
+
return pluginId.replaceAll("_", "-");
|
|
643
|
+
}
|
|
644
|
+
function resolvePluginId(requested, installed) {
|
|
645
|
+
const requestedWithoutPrefix = requested.replace(/^plugin-/, "");
|
|
646
|
+
for (const installedId of installed) {
|
|
647
|
+
const installedWithoutPrefix = installedId.replace(/^plugin-/, "");
|
|
648
|
+
if (installedId === requested || installedWithoutPrefix === requestedWithoutPrefix) return installedId;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function findRuntime(ctx, normalizedPluginId) {
|
|
652
|
+
for (const [pluginId, runtime] of ctx.extensionPluginRuntimeById.entries()) if (normalizePluginId(pluginId) === normalizedPluginId) return isPluginRuntime(runtime) ? runtime : void 0;
|
|
653
|
+
}
|
|
654
|
+
function isPluginRuntime(value) {
|
|
655
|
+
return typeof value === "object" && value !== null;
|
|
656
|
+
}
|
|
657
|
+
function isResultRecord(value) {
|
|
658
|
+
return typeof value === "object" && value !== null;
|
|
659
|
+
}
|
|
660
|
+
function readBoolean(record, key) {
|
|
661
|
+
const value = record[key];
|
|
662
|
+
return typeof value === "boolean" ? value : void 0;
|
|
663
|
+
}
|
|
664
|
+
function readString(record, key) {
|
|
665
|
+
const value = record[key];
|
|
666
|
+
return typeof value === "string" ? value : void 0;
|
|
667
|
+
}
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region src/modules/binary-instrument/HookGenerator.ts
|
|
670
|
+
var HookGenerator = class {
|
|
671
|
+
generateFridaHookScript(symbols, options) {
|
|
672
|
+
const includeArgs = options?.includeArgs ?? true;
|
|
673
|
+
const includeRetAddr = options?.includeRetAddr ?? false;
|
|
674
|
+
const lines = [
|
|
675
|
+
"'use strict';",
|
|
676
|
+
"",
|
|
677
|
+
"function resolveTarget(name) {",
|
|
678
|
+
" try {",
|
|
679
|
+
" const exported = Module.findExportByName(null, name);",
|
|
680
|
+
" if (exported) {",
|
|
681
|
+
" return exported;",
|
|
682
|
+
" }",
|
|
683
|
+
" } catch (error) {}",
|
|
684
|
+
" try {",
|
|
685
|
+
" const symbol = DebugSymbol.fromName(name);",
|
|
686
|
+
" if (symbol && symbol.address) {",
|
|
687
|
+
" return symbol.address;",
|
|
688
|
+
" }",
|
|
689
|
+
" } catch (error) {}",
|
|
690
|
+
" return null;",
|
|
691
|
+
"}",
|
|
692
|
+
"",
|
|
693
|
+
"const installedHooks = [];"
|
|
694
|
+
];
|
|
695
|
+
for (let index = 0; index < symbols.length; index += 1) {
|
|
696
|
+
const descriptor = this.toDescriptor(symbols[index]);
|
|
697
|
+
const varName = `target_${index}`;
|
|
698
|
+
const label = descriptor.demangled ?? descriptor.name;
|
|
699
|
+
const addressLine = descriptor.address ? `const ${varName} = ptr("${this.escapeForDoubleQuotes(descriptor.address)}");` : `const ${varName} = resolveTarget("${this.escapeForDoubleQuotes(descriptor.name)}");`;
|
|
700
|
+
lines.push(addressLine);
|
|
701
|
+
lines.push(`if (${varName}) {`);
|
|
702
|
+
lines.push(` Interceptor.attach(${varName}, {`);
|
|
703
|
+
lines.push(" onEnter(args) {");
|
|
704
|
+
if (includeRetAddr) lines.push(` console.log("[binary-instrument] enter ${this.escapeForDoubleQuotes(label)} ret=" + this.returnAddress);`);
|
|
705
|
+
else lines.push(` console.log("[binary-instrument] enter ${this.escapeForDoubleQuotes(label)}");`);
|
|
706
|
+
if (includeArgs) {
|
|
707
|
+
lines.push(" const renderedArgs = [];");
|
|
708
|
+
lines.push(" for (let i = 0; i < 6; i += 1) {");
|
|
709
|
+
lines.push(" try {");
|
|
710
|
+
lines.push(" renderedArgs.push(String(args[i]));");
|
|
711
|
+
lines.push(" } catch (error) {");
|
|
712
|
+
lines.push(" renderedArgs.push(\"<unreadable>\");");
|
|
713
|
+
lines.push(" }");
|
|
714
|
+
lines.push(" }");
|
|
715
|
+
lines.push(" console.log(\"[binary-instrument] args \" + JSON.stringify(renderedArgs));");
|
|
716
|
+
}
|
|
717
|
+
lines.push(" },");
|
|
718
|
+
lines.push(" onLeave(retval) {");
|
|
719
|
+
lines.push(` console.log("[binary-instrument] leave ${this.escapeForDoubleQuotes(label)} retval=" + retval);`);
|
|
720
|
+
lines.push(" },");
|
|
721
|
+
lines.push(" });");
|
|
722
|
+
lines.push(` installedHooks.push({ name: "${this.escapeForDoubleQuotes(label)}", address: String(${varName}) });`);
|
|
723
|
+
lines.push("} else {");
|
|
724
|
+
lines.push(` console.log("[binary-instrument] unresolved ${this.escapeForDoubleQuotes(label)}");`);
|
|
725
|
+
lines.push("}");
|
|
726
|
+
lines.push("");
|
|
727
|
+
}
|
|
728
|
+
lines.push("console.log(\"[binary-instrument] hooks=\" + JSON.stringify(installedHooks));");
|
|
729
|
+
return lines.join("\n");
|
|
730
|
+
}
|
|
731
|
+
generateInterceptorScript(targetFuncs) {
|
|
732
|
+
return this.generateFridaHookScript(targetFuncs, {
|
|
733
|
+
includeArgs: true,
|
|
734
|
+
includeRetAddr: false
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
toDescriptor(symbol) {
|
|
738
|
+
if (typeof symbol === "string") return { name: symbol };
|
|
739
|
+
return symbol;
|
|
740
|
+
}
|
|
741
|
+
escapeForDoubleQuotes(value) {
|
|
742
|
+
return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
//#endregion
|
|
746
|
+
//#region src/modules/binary-instrument/UnidbgRunner.ts
|
|
747
|
+
const UNIDBG_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
748
|
+
var UnidbgRunner = class {
|
|
749
|
+
sessions = /* @__PURE__ */ new Map();
|
|
750
|
+
close() {
|
|
751
|
+
for (const session of this.sessions.values()) if (session.childProcess) try {
|
|
752
|
+
process.kill(session.childProcess.pid, "SIGTERM");
|
|
753
|
+
} catch {}
|
|
754
|
+
this.sessions.clear();
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Launch a .so library in the Unidbg emulator via JVM subprocess.
|
|
758
|
+
* Returns a sessionId for subsequent call/trace operations.
|
|
759
|
+
*/
|
|
760
|
+
async launch(soPath, arch = "arm", jarPath) {
|
|
761
|
+
const resolvedJar = jarPath ?? process.env["UNIDBG_JAR"];
|
|
762
|
+
if (!resolvedJar) throw new Error("UNIDBG_JAR is not configured. Set the UNIDBG_JAR env var or pass jarPath.");
|
|
763
|
+
try {
|
|
764
|
+
await access(resolvedJar);
|
|
765
|
+
} catch {
|
|
766
|
+
throw new Error(`Unidbg JAR not found: ${resolvedJar}`);
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
await access(soPath);
|
|
770
|
+
} catch {
|
|
771
|
+
throw new Error(`Shared library not found: ${soPath}`);
|
|
772
|
+
}
|
|
773
|
+
const sessionId = randomUUID();
|
|
774
|
+
const command = this.getJavaCommand();
|
|
775
|
+
const args = [
|
|
776
|
+
"-jar",
|
|
777
|
+
resolvedJar,
|
|
778
|
+
"--so",
|
|
779
|
+
soPath,
|
|
780
|
+
"--arch",
|
|
781
|
+
arch,
|
|
782
|
+
"--server"
|
|
783
|
+
];
|
|
784
|
+
try {
|
|
785
|
+
const result = await this.execFileUtf8(command, args, UNIDBG_TIMEOUT_MS);
|
|
786
|
+
const sessionInfo = this.parseLaunchOutput(result.stdout, sessionId);
|
|
787
|
+
const session = {
|
|
788
|
+
id: sessionInfo.id,
|
|
789
|
+
soPath,
|
|
790
|
+
arch,
|
|
791
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
792
|
+
childProcess: sessionInfo.pid ? { pid: sessionInfo.pid } : void 0
|
|
793
|
+
};
|
|
794
|
+
this.sessions.set(sessionId, session);
|
|
795
|
+
return {
|
|
796
|
+
sessionId,
|
|
797
|
+
soPath,
|
|
798
|
+
arch
|
|
799
|
+
};
|
|
800
|
+
} catch (error) {
|
|
801
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
802
|
+
logger.warn("[binary-instrument] Unidbg launch failed, registering stub session", {
|
|
803
|
+
soPath,
|
|
804
|
+
message
|
|
805
|
+
});
|
|
806
|
+
const session = {
|
|
807
|
+
id: sessionId,
|
|
808
|
+
soPath,
|
|
809
|
+
arch,
|
|
810
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
811
|
+
};
|
|
812
|
+
this.sessions.set(sessionId, session);
|
|
813
|
+
return {
|
|
814
|
+
sessionId,
|
|
815
|
+
soPath,
|
|
816
|
+
arch
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async callFunction(sessionId, functionName, args = {}) {
|
|
821
|
+
if (!this.sessions.get(sessionId)) throw new Error(`No unidbg session found for ${sessionId}`);
|
|
822
|
+
const jarPath = process.env["UNIDBG_JAR"];
|
|
823
|
+
if (!jarPath) return {
|
|
824
|
+
sessionId,
|
|
825
|
+
functionName,
|
|
826
|
+
args,
|
|
827
|
+
returnValue: "0x0",
|
|
828
|
+
stdout: "",
|
|
829
|
+
stderr: "",
|
|
830
|
+
trace: ["mock-unidbg-unavailable"],
|
|
831
|
+
_note: "Unidbg emulation requires UNIDBG_JAR to be configured"
|
|
832
|
+
};
|
|
833
|
+
const command = this.getJavaCommand();
|
|
834
|
+
const callArgs = [
|
|
835
|
+
"-jar",
|
|
836
|
+
jarPath,
|
|
837
|
+
"--session",
|
|
838
|
+
sessionId,
|
|
839
|
+
"--call",
|
|
840
|
+
functionName,
|
|
841
|
+
"--args",
|
|
842
|
+
JSON.stringify(args)
|
|
843
|
+
];
|
|
844
|
+
try {
|
|
845
|
+
const result = await this.execFileUtf8(command, callArgs, UNIDBG_TIMEOUT_MS);
|
|
846
|
+
return {
|
|
847
|
+
sessionId,
|
|
848
|
+
functionName,
|
|
849
|
+
args,
|
|
850
|
+
returnValue: this.extractReturnValue(result.stdout),
|
|
851
|
+
stdout: result.stdout.trim(),
|
|
852
|
+
stderr: result.stderr.trim(),
|
|
853
|
+
trace: []
|
|
854
|
+
};
|
|
855
|
+
} catch (error) {
|
|
856
|
+
return {
|
|
857
|
+
sessionId,
|
|
858
|
+
functionName,
|
|
859
|
+
args,
|
|
860
|
+
returnValue: "0x0",
|
|
861
|
+
stdout: "",
|
|
862
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
863
|
+
trace: ["error"]
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
async trace(sessionId) {
|
|
868
|
+
if (!this.sessions.get(sessionId)) throw new Error(`No unidbg session found for ${sessionId}`);
|
|
869
|
+
const jarPath = process.env["UNIDBG_JAR"];
|
|
870
|
+
if (!jarPath) return {
|
|
871
|
+
sessionId,
|
|
872
|
+
trace: ["mock-unidbg-unavailable"],
|
|
873
|
+
_note: "Unidbg tracing requires UNIDBG_JAR to be configured"
|
|
874
|
+
};
|
|
875
|
+
const command = this.getJavaCommand();
|
|
876
|
+
const traceArgs = [
|
|
877
|
+
"-jar",
|
|
878
|
+
jarPath,
|
|
879
|
+
"--session",
|
|
880
|
+
sessionId,
|
|
881
|
+
"--trace"
|
|
882
|
+
];
|
|
883
|
+
try {
|
|
884
|
+
const result = await this.execFileUtf8(command, traceArgs, UNIDBG_TIMEOUT_MS);
|
|
885
|
+
return {
|
|
886
|
+
sessionId,
|
|
887
|
+
trace: this.parseTraceOutput(result.stdout),
|
|
888
|
+
instructionCount: this.countInstructions(result.stdout)
|
|
889
|
+
};
|
|
890
|
+
} catch (error) {
|
|
891
|
+
return {
|
|
892
|
+
sessionId,
|
|
893
|
+
trace: ["error"],
|
|
894
|
+
error: error instanceof Error ? error.message : String(error)
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Get info about an active Unidbg session.
|
|
900
|
+
*/
|
|
901
|
+
getSessionInfo(sessionId) {
|
|
902
|
+
return this.sessions.get(sessionId);
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* List all active Unidbg sessions.
|
|
906
|
+
*/
|
|
907
|
+
listSessions() {
|
|
908
|
+
return Array.from(this.sessions.values()).map((s) => ({
|
|
909
|
+
id: s.id,
|
|
910
|
+
soPath: s.soPath,
|
|
911
|
+
arch: s.arch,
|
|
912
|
+
startedAt: s.startedAt
|
|
913
|
+
}));
|
|
914
|
+
}
|
|
915
|
+
getJavaCommand() {
|
|
916
|
+
return process.env["JAVA_HOME"] ? `${process.env["JAVA_HOME"]}/bin/java` : "java";
|
|
917
|
+
}
|
|
918
|
+
parseLaunchOutput(stdout, fallbackId) {
|
|
919
|
+
const lines = stdout.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
920
|
+
for (const line of lines.toReversed()) try {
|
|
921
|
+
const parsed = JSON.parse(line);
|
|
922
|
+
if (typeof parsed["id"] === "string") return {
|
|
923
|
+
id: parsed["id"],
|
|
924
|
+
pid: typeof parsed["pid"] === "number" ? parsed["pid"] : null
|
|
925
|
+
};
|
|
926
|
+
} catch {}
|
|
927
|
+
return {
|
|
928
|
+
id: fallbackId,
|
|
929
|
+
pid: null
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
extractReturnValue(stdout) {
|
|
933
|
+
const match = /return[=:\s]+(0x[0-9a-fA-F]+|-?\d+)/.exec(stdout);
|
|
934
|
+
if (match?.[1]) return match[1];
|
|
935
|
+
return "0x0";
|
|
936
|
+
}
|
|
937
|
+
parseTraceOutput(stdout) {
|
|
938
|
+
return stdout.split(/\r?\n/).filter((line) => line.trim().length > 0 && !line.startsWith("{")).slice(0, 1e4);
|
|
939
|
+
}
|
|
940
|
+
countInstructions(stdout) {
|
|
941
|
+
return stdout.split(/\r?\n/).filter((line) => line.trim().length > 0 && !line.startsWith("{") && /\b(ldr|str|mov|bl|b|add|sub)\b/i.test(line)).length;
|
|
942
|
+
}
|
|
943
|
+
async execFileUtf8(file, args, timeoutMs) {
|
|
944
|
+
return new Promise((resolve, reject) => {
|
|
945
|
+
execFile(file, args, {
|
|
946
|
+
timeout: timeoutMs,
|
|
947
|
+
windowsHide: true,
|
|
948
|
+
maxBuffer: UNIDBG_MAX_BUFFER_BYTES,
|
|
949
|
+
encoding: "utf8"
|
|
950
|
+
}, (error, stdout, stderr) => {
|
|
951
|
+
if (error) {
|
|
952
|
+
reject(error);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
resolve({
|
|
956
|
+
stdout: typeof stdout === "string" ? stdout : "",
|
|
957
|
+
stderr: typeof stderr === "string" ? stderr : "",
|
|
958
|
+
exitCode: 0
|
|
959
|
+
});
|
|
960
|
+
});
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
//#endregion
|
|
965
|
+
//#region src/modules/binary-instrument/index.ts
|
|
966
|
+
var binary_instrument_exports = /* @__PURE__ */ __exportAll({
|
|
967
|
+
GhidraAnalyzer: () => GhidraAnalyzer,
|
|
968
|
+
HookGenerator: () => HookGenerator
|
|
969
|
+
});
|
|
970
|
+
//#endregion
|
|
971
|
+
export { invokePlugin as a, FridaSession as c, getAvailablePlugins as i, UnidbgRunner as n, HookCodeGenerator as o, HookGenerator as r, GhidraAnalyzer as s, binary_instrument_exports as t };
|