@ebowwa/mcp-nm 2.0.2 → 2.1.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/dist/index.js +1139 -1
- package/package.json +8 -3
- package/src/handlers/bin.ts +558 -0
- package/src/handlers/nm.ts +368 -0
- package/src/handlers/xxd.ts +304 -0
- package/src/index.ts +1452 -1
- package/src/types/index.ts +109 -0
- package/src/utils/convert.ts +166 -0
- package/src/utils/exec.ts +69 -0
- package/src/utils/file.ts +108 -0
- package/src/utils/index.ts +11 -0
- package/src/utils/nm.ts +152 -0
- package/src/utils/xxd.ts +173 -0
package/src/index.ts
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* - Binary diff: byte-level comparison
|
|
13
13
|
* - Archive extraction: extract from .a files
|
|
14
14
|
* - Binary patching: patch bytes, NOP sled, hex editor
|
|
15
|
+
* - Number conversion: hex, decimal, binary, octal, ASCII
|
|
16
|
+
* - macOS binary patching: code signing, quarantine, safe patch workflow
|
|
17
|
+
* - Patch management: persistent patch registry, auto-apply on binary updates
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -22,6 +25,9 @@ import {
|
|
|
22
25
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
23
26
|
import { exec } from "child_process";
|
|
24
27
|
import { promisify } from "util";
|
|
28
|
+
import * as fs from "fs";
|
|
29
|
+
import * as path from "path";
|
|
30
|
+
import * as os from "os";
|
|
25
31
|
|
|
26
32
|
const execAsync = promisify(exec);
|
|
27
33
|
|
|
@@ -29,7 +35,7 @@ const execAsync = promisify(exec);
|
|
|
29
35
|
const server = new Server(
|
|
30
36
|
{
|
|
31
37
|
name: "@ebowwa/mcp-nm",
|
|
32
|
-
version: "2.
|
|
38
|
+
version: "2.1.0",
|
|
33
39
|
},
|
|
34
40
|
{
|
|
35
41
|
capabilities: {
|
|
@@ -1901,6 +1907,1229 @@ async function handleHexEditor(args: {
|
|
|
1901
1907
|
}
|
|
1902
1908
|
}
|
|
1903
1909
|
|
|
1910
|
+
// ============================================================================
|
|
1911
|
+
// macOS Binary Patching Tools
|
|
1912
|
+
// ============================================================================
|
|
1913
|
+
|
|
1914
|
+
// Check if running on macOS
|
|
1915
|
+
function isMacOS(): boolean {
|
|
1916
|
+
return process.platform === "darwin";
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// --- Code signature info ---
|
|
1920
|
+
async function handleCodesignInfo(args: { filePath: string }) {
|
|
1921
|
+
if (!isMacOS()) {
|
|
1922
|
+
return {
|
|
1923
|
+
content: [{
|
|
1924
|
+
type: "text",
|
|
1925
|
+
text: "Error: Code signing tools are only available on macOS. This tool requires the 'codesign' utility."
|
|
1926
|
+
}],
|
|
1927
|
+
isError: true
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
try {
|
|
1932
|
+
const results: string[] = [];
|
|
1933
|
+
results.push(`Code Signature Info: ${args.filePath}`);
|
|
1934
|
+
results.push("");
|
|
1935
|
+
|
|
1936
|
+
// Check if file exists and is a Mach-O
|
|
1937
|
+
const { stdout: fileInfo } = await execAsync(`file "${args.filePath}"`);
|
|
1938
|
+
if (!fileInfo.toLowerCase().includes("mach-o")) {
|
|
1939
|
+
results.push("Warning: File does not appear to be a Mach-O binary");
|
|
1940
|
+
results.push(`File type: ${fileInfo.trim()}`);
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// Get code signature information
|
|
1944
|
+
try {
|
|
1945
|
+
const { stdout: signInfo } = await execAsync(`codesign -dv "${args.filePath}" 2>&1`);
|
|
1946
|
+
results.push("Signature Details:");
|
|
1947
|
+
results.push(signInfo.split("\n").map(line => ` ${line}`).join("\n"));
|
|
1948
|
+
} catch (e) {
|
|
1949
|
+
const output = e instanceof Error ? e.message : String(e);
|
|
1950
|
+
if (output.includes("no signature") || output.includes("code object is not signed")) {
|
|
1951
|
+
results.push("Status: NOT SIGNED");
|
|
1952
|
+
results.push(" The binary has no code signature");
|
|
1953
|
+
} else {
|
|
1954
|
+
results.push(`Signature check: ${output}`);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// Check signature validity
|
|
1959
|
+
try {
|
|
1960
|
+
await execAsync(`codesign -v "${args.filePath}" 2>&1`);
|
|
1961
|
+
results.push("");
|
|
1962
|
+
results.push("Validity: VALID - Signature is intact");
|
|
1963
|
+
} catch (e) {
|
|
1964
|
+
const output = e instanceof Error ? e.message : String(e);
|
|
1965
|
+
if (output.includes("not signed")) {
|
|
1966
|
+
results.push("");
|
|
1967
|
+
results.push("Validity: NOT SIGNED");
|
|
1968
|
+
} else {
|
|
1969
|
+
results.push("");
|
|
1970
|
+
results.push(`Validity: INVALID - ${output}`);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// Get signature requirements
|
|
1975
|
+
try {
|
|
1976
|
+
const { stdout: reqs } = await execAsync(`codesign -dr - "${args.filePath}" 2>&1`);
|
|
1977
|
+
if (reqs.trim() && !reqs.includes("no signature")) {
|
|
1978
|
+
results.push("");
|
|
1979
|
+
results.push("Designated Requirements:");
|
|
1980
|
+
results.push(reqs.split("\n").map(line => ` ${line}`).join("\n"));
|
|
1981
|
+
}
|
|
1982
|
+
} catch {
|
|
1983
|
+
// No requirements or not signed
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
1987
|
+
} catch (error) {
|
|
1988
|
+
return {
|
|
1989
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
1990
|
+
isError: true
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
// --- Remove code signature ---
|
|
1996
|
+
async function handleCodesignRemove(args: { filePath: string; createBackup?: boolean }) {
|
|
1997
|
+
if (!isMacOS()) {
|
|
1998
|
+
return {
|
|
1999
|
+
content: [{
|
|
2000
|
+
type: "text",
|
|
2001
|
+
text: "Error: Code signing tools are only available on macOS."
|
|
2002
|
+
}],
|
|
2003
|
+
isError: true
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
const createBackup = args.createBackup !== false;
|
|
2008
|
+
|
|
2009
|
+
try {
|
|
2010
|
+
const results: string[] = [];
|
|
2011
|
+
results.push(`Remove Code Signature: ${args.filePath}`);
|
|
2012
|
+
results.push("");
|
|
2013
|
+
|
|
2014
|
+
// Create backup if requested
|
|
2015
|
+
if (createBackup) {
|
|
2016
|
+
const backupPath = `${args.filePath}.bak`;
|
|
2017
|
+
await execAsync(`cp "${args.filePath}" "${backupPath}"`);
|
|
2018
|
+
results.push(`Backup created: ${backupPath}`);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// Check current signature status
|
|
2022
|
+
try {
|
|
2023
|
+
const { stdout: signInfo } = await execAsync(`codesign -dv "${args.filePath}" 2>&1`);
|
|
2024
|
+
results.push(`Previous signature: ${signInfo.split("\n")[0] || "signed"}`);
|
|
2025
|
+
} catch {
|
|
2026
|
+
results.push("Previous signature: not signed or invalid");
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// Remove the signature
|
|
2030
|
+
try {
|
|
2031
|
+
await execAsync(`codesign --remove-signature "${args.filePath}"`);
|
|
2032
|
+
results.push("Status: SUCCESS - Code signature removed");
|
|
2033
|
+
} catch (e) {
|
|
2034
|
+
const output = e instanceof Error ? e.message : String(e);
|
|
2035
|
+
if (output.includes("no signature")) {
|
|
2036
|
+
results.push("Status: NO ACTION - Binary was not signed");
|
|
2037
|
+
} else {
|
|
2038
|
+
throw new Error(`Failed to remove signature: ${output}`);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// Verify removal
|
|
2043
|
+
try {
|
|
2044
|
+
await execAsync(`codesign -v "${args.filePath}" 2>&1`);
|
|
2045
|
+
results.push("Warning: Signature still present after removal");
|
|
2046
|
+
} catch (e) {
|
|
2047
|
+
if (e instanceof Error && e.message.includes("not signed")) {
|
|
2048
|
+
results.push("Verification: Confirmed - Binary is now unsigned");
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
2053
|
+
} catch (error) {
|
|
2054
|
+
return {
|
|
2055
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2056
|
+
isError: true
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// --- Sign binary (ad-hoc or with certificate) ---
|
|
2062
|
+
async function handleCodesignSign(args: {
|
|
2063
|
+
filePath: string;
|
|
2064
|
+
certificate?: string;
|
|
2065
|
+
force?: boolean;
|
|
2066
|
+
createBackup?: boolean;
|
|
2067
|
+
deep?: boolean;
|
|
2068
|
+
options?: string;
|
|
2069
|
+
}) {
|
|
2070
|
+
if (!isMacOS()) {
|
|
2071
|
+
return {
|
|
2072
|
+
content: [{
|
|
2073
|
+
type: "text",
|
|
2074
|
+
text: "Error: Code signing tools are only available on macOS."
|
|
2075
|
+
}],
|
|
2076
|
+
isError: true
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
const force = args.force !== false;
|
|
2081
|
+
const createBackup = args.createBackup === true;
|
|
2082
|
+
const deep = args.deep === true;
|
|
2083
|
+
const certificate = args.certificate || "-"; // "-" means ad-hoc signing
|
|
2084
|
+
|
|
2085
|
+
try {
|
|
2086
|
+
const results: string[] = [];
|
|
2087
|
+
results.push(`Sign Binary: ${args.filePath}`);
|
|
2088
|
+
results.push("");
|
|
2089
|
+
|
|
2090
|
+
// Create backup if requested
|
|
2091
|
+
if (createBackup) {
|
|
2092
|
+
const backupPath = `${args.filePath}.bak`;
|
|
2093
|
+
await execAsync(`cp "${args.filePath}" "${backupPath}"`);
|
|
2094
|
+
results.push(`Backup created: ${backupPath}`);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// Build codesign command
|
|
2098
|
+
const forceFlag = force ? "--force" : "";
|
|
2099
|
+
const deepFlag = deep ? "--deep" : "";
|
|
2100
|
+
const optionsFlag = args.options ? `--options ${args.options}` : "";
|
|
2101
|
+
|
|
2102
|
+
const cmd = `codesign -s ${certificate} ${forceFlag} ${deepFlag} ${optionsFlag} "${args.filePath}"`.replace(/\s+/g, " ").trim();
|
|
2103
|
+
|
|
2104
|
+
results.push(`Command: ${cmd}`);
|
|
2105
|
+
results.push(`Signing with: ${certificate === "-" ? "ad-hoc signature" : certificate}`);
|
|
2106
|
+
|
|
2107
|
+
// Execute signing
|
|
2108
|
+
await execAsync(cmd);
|
|
2109
|
+
results.push("Status: SUCCESS - Binary signed");
|
|
2110
|
+
|
|
2111
|
+
// Verify the signature
|
|
2112
|
+
try {
|
|
2113
|
+
await execAsync(`codesign -v "${args.filePath}" 2>&1`);
|
|
2114
|
+
results.push("Verification: VALID - Signature is intact");
|
|
2115
|
+
} catch (e) {
|
|
2116
|
+
results.push(`Verification: WARNING - ${e instanceof Error ? e.message : String(e)}`);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// Show new signature info
|
|
2120
|
+
try {
|
|
2121
|
+
const { stdout: signInfo } = await execAsync(`codesign -dv "${args.filePath}" 2>&1`);
|
|
2122
|
+
const infoLine = signInfo.split("\n").find(l => l.includes("Identifier") || l.includes("Authority"));
|
|
2123
|
+
if (infoLine) {
|
|
2124
|
+
results.push(`New signature: ${infoLine.trim()}`);
|
|
2125
|
+
}
|
|
2126
|
+
} catch {
|
|
2127
|
+
// Ignore
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
2131
|
+
} catch (error) {
|
|
2132
|
+
return {
|
|
2133
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2134
|
+
isError: true
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
// --- Check quarantine status ---
|
|
2140
|
+
async function handleQuarantineCheck(args: { filePath: string }) {
|
|
2141
|
+
if (!isMacOS()) {
|
|
2142
|
+
return {
|
|
2143
|
+
content: [{
|
|
2144
|
+
type: "text",
|
|
2145
|
+
text: "Error: Quarantine tools are only available on macOS."
|
|
2146
|
+
}],
|
|
2147
|
+
isError: true
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
try {
|
|
2152
|
+
const results: string[] = [];
|
|
2153
|
+
results.push(`Quarantine Check: ${args.filePath}`);
|
|
2154
|
+
results.push("");
|
|
2155
|
+
|
|
2156
|
+
// Check for com.apple.quarantine attribute
|
|
2157
|
+
try {
|
|
2158
|
+
const { stdout: quarantine } = await execAsync(`xattr -p com.apple.quarantine "${args.filePath}" 2>&1`);
|
|
2159
|
+
if (quarantine.trim()) {
|
|
2160
|
+
results.push("Status: QUARANTINED");
|
|
2161
|
+
results.push("");
|
|
2162
|
+
results.push("Quarantine data:");
|
|
2163
|
+
results.push(` ${quarantine.trim()}`);
|
|
2164
|
+
|
|
2165
|
+
// Try to decode quarantine info
|
|
2166
|
+
try {
|
|
2167
|
+
const { stdout: details } = await execAsync(`xattr -l "${args.filePath}" 2>&1`);
|
|
2168
|
+
const quarantineSection = details.split("\n").filter(l =>
|
|
2169
|
+
l.includes("quarantine") || l.includes("provenance")
|
|
2170
|
+
);
|
|
2171
|
+
if (quarantineSection.length > 0) {
|
|
2172
|
+
results.push("");
|
|
2173
|
+
results.push("Extended attributes:");
|
|
2174
|
+
quarantineSection.forEach(l => results.push(` ${l}`));
|
|
2175
|
+
}
|
|
2176
|
+
} catch {
|
|
2177
|
+
// Ignore
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
results.push("");
|
|
2181
|
+
results.push("Note: Quarantine prevents execution of downloaded files.");
|
|
2182
|
+
results.push("Use bin_quarantine_remove to allow execution.");
|
|
2183
|
+
} else {
|
|
2184
|
+
results.push("Status: NOT QUARANTINED");
|
|
2185
|
+
results.push("The file has no quarantine attribute.");
|
|
2186
|
+
}
|
|
2187
|
+
} catch (e) {
|
|
2188
|
+
const output = e instanceof Error ? e.message : String(e);
|
|
2189
|
+
if (output.includes("No such xattr") || output.includes("not found")) {
|
|
2190
|
+
results.push("Status: NOT QUARANTINED");
|
|
2191
|
+
results.push("The file has no quarantine attribute.");
|
|
2192
|
+
} else {
|
|
2193
|
+
throw new Error(`Failed to check quarantine: ${output}`);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
// Also check for com.apple.provenance
|
|
2198
|
+
try {
|
|
2199
|
+
const { stdout: provenance } = await execAsync(`xattr -p com.apple.provenance "${args.filePath}" 2>&1`);
|
|
2200
|
+
if (provenance.trim()) {
|
|
2201
|
+
results.push("");
|
|
2202
|
+
results.push("Provenance: PRESENT");
|
|
2203
|
+
results.push(" File has provenance metadata (tracks download source)");
|
|
2204
|
+
}
|
|
2205
|
+
} catch {
|
|
2206
|
+
// No provenance attribute
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
2210
|
+
} catch (error) {
|
|
2211
|
+
return {
|
|
2212
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2213
|
+
isError: true
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
// --- Remove quarantine attributes ---
|
|
2219
|
+
async function handleQuarantineRemove(args: { filePath: string }) {
|
|
2220
|
+
if (!isMacOS()) {
|
|
2221
|
+
return {
|
|
2222
|
+
content: [{
|
|
2223
|
+
type: "text",
|
|
2224
|
+
text: "Error: Quarantine tools are only available on macOS."
|
|
2225
|
+
}],
|
|
2226
|
+
isError: true
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
try {
|
|
2231
|
+
const results: string[] = [];
|
|
2232
|
+
results.push(`Remove Quarantine: ${args.filePath}`);
|
|
2233
|
+
results.push("");
|
|
2234
|
+
|
|
2235
|
+
let removedAny = false;
|
|
2236
|
+
|
|
2237
|
+
// Remove com.apple.quarantine
|
|
2238
|
+
try {
|
|
2239
|
+
await execAsync(`xattr -d com.apple.quarantine "${args.filePath}" 2>&1`);
|
|
2240
|
+
results.push("Removed: com.apple.quarantine");
|
|
2241
|
+
removedAny = true;
|
|
2242
|
+
} catch (e) {
|
|
2243
|
+
const output = e instanceof Error ? e.message : String(e);
|
|
2244
|
+
if (output.includes("No such xattr") || output.includes("not found")) {
|
|
2245
|
+
results.push("Skipped: com.apple.quarantine (not present)");
|
|
2246
|
+
} else {
|
|
2247
|
+
results.push(`Warning: Could not remove quarantine - ${output}`);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
// Remove com.apple.provenance
|
|
2252
|
+
try {
|
|
2253
|
+
await execAsync(`xattr -d com.apple.provenance "${args.filePath}" 2>&1`);
|
|
2254
|
+
results.push("Removed: com.apple.provenance");
|
|
2255
|
+
removedAny = true;
|
|
2256
|
+
} catch {
|
|
2257
|
+
// Not present, ignore
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
// Remove com.apple.metadata:kMDItemWhereFroms
|
|
2261
|
+
try {
|
|
2262
|
+
await execAsync(`xattr -d com.apple.metadata:kMDItemWhereFroms "${args.filePath}" 2>&1`);
|
|
2263
|
+
results.push("Removed: com.apple.metadata:kMDItemWhereFroms");
|
|
2264
|
+
removedAny = true;
|
|
2265
|
+
} catch {
|
|
2266
|
+
// Not present, ignore
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
if (removedAny) {
|
|
2270
|
+
results.push("");
|
|
2271
|
+
results.push("Status: SUCCESS - Quarantine attributes removed");
|
|
2272
|
+
results.push("The file should now be able to execute without quarantine warnings.");
|
|
2273
|
+
} else {
|
|
2274
|
+
results.push("");
|
|
2275
|
+
results.push("Status: NO ACTION - No quarantine attributes found");
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
2279
|
+
} catch (error) {
|
|
2280
|
+
return {
|
|
2281
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2282
|
+
isError: true
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// --- Safe patch workflow (patch + resign + remove quarantine) ---
|
|
2288
|
+
async function handleSafePatch(args: {
|
|
2289
|
+
filePath: string;
|
|
2290
|
+
offset: number;
|
|
2291
|
+
hexData: string;
|
|
2292
|
+
createBackup?: boolean;
|
|
2293
|
+
}) {
|
|
2294
|
+
if (!isMacOS()) {
|
|
2295
|
+
return {
|
|
2296
|
+
content: [{
|
|
2297
|
+
type: "text",
|
|
2298
|
+
text: "Error: Safe patching tools are only available on macOS."
|
|
2299
|
+
}],
|
|
2300
|
+
isError: true
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
const createBackup = args.createBackup !== false;
|
|
2305
|
+
|
|
2306
|
+
try {
|
|
2307
|
+
const results: string[] = [];
|
|
2308
|
+
results.push(`Safe Patch Workflow: ${args.filePath}`);
|
|
2309
|
+
results.push(` Offset: 0x${args.offset.toString(16)} (${args.offset})`);
|
|
2310
|
+
results.push(` Data: ${args.hexData}`);
|
|
2311
|
+
results.push("");
|
|
2312
|
+
|
|
2313
|
+
// Step 1: Create backup
|
|
2314
|
+
if (createBackup) {
|
|
2315
|
+
const backupPath = `${args.filePath}.bak`;
|
|
2316
|
+
await execAsync(`cp "${args.filePath}" "${backupPath}"`);
|
|
2317
|
+
results.push(`[1/4] Backup created: ${backupPath}`);
|
|
2318
|
+
} else {
|
|
2319
|
+
results.push("[1/4] Backup: SKIPPED (createBackup=false)");
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
// Step 2: Apply the patch
|
|
2323
|
+
results.push("[2/4] Applying binary patch...");
|
|
2324
|
+
const hexBytes = args.hexData.replace(/\s+/g, "");
|
|
2325
|
+
const byteCount = hexBytes.length / 2;
|
|
2326
|
+
const patchCmd = `printf '${hexBytes}' | dd of="${args.filePath}" bs=1 seek=${args.offset} count=${byteCount} conv=notrunc 2>/dev/null`;
|
|
2327
|
+
await execAsync(patchCmd);
|
|
2328
|
+
results.push(` Patched ${byteCount} bytes at offset 0x${args.offset.toString(16)}`);
|
|
2329
|
+
|
|
2330
|
+
// Step 3: Remove old signature (invalidates the signature)
|
|
2331
|
+
results.push("[3/4] Removing old code signature...");
|
|
2332
|
+
try {
|
|
2333
|
+
await execAsync(`codesign --remove-signature "${args.filePath}" 2>&1`);
|
|
2334
|
+
results.push(" Old signature removed");
|
|
2335
|
+
} catch (e) {
|
|
2336
|
+
if (e instanceof Error && e.message.includes("no signature")) {
|
|
2337
|
+
results.push(" No previous signature to remove");
|
|
2338
|
+
} else {
|
|
2339
|
+
results.push(` Warning: ${e instanceof Error ? e.message : String(e)}`);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// Step 4: Re-sign with ad-hoc signature
|
|
2344
|
+
results.push("[4/4] Applying ad-hoc signature...");
|
|
2345
|
+
await execAsync(`codesign --force --sign - "${args.filePath}"`);
|
|
2346
|
+
results.push(" Ad-hoc signature applied");
|
|
2347
|
+
|
|
2348
|
+
// Step 5: Remove quarantine attributes
|
|
2349
|
+
results.push("[5/5] Removing quarantine attributes...");
|
|
2350
|
+
try {
|
|
2351
|
+
await execAsync(`xattr -d com.apple.quarantine "${args.filePath}" 2>&1`);
|
|
2352
|
+
results.push(" Quarantine attribute removed");
|
|
2353
|
+
} catch {
|
|
2354
|
+
results.push(" No quarantine attribute present");
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
try {
|
|
2358
|
+
await execAsync(`xattr -d com.apple.provenance "${args.filePath}" 2>&1`);
|
|
2359
|
+
results.push(" Provenance attribute removed");
|
|
2360
|
+
} catch {
|
|
2361
|
+
// Ignore
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
// Final verification
|
|
2365
|
+
results.push("");
|
|
2366
|
+
results.push("Verification:");
|
|
2367
|
+
try {
|
|
2368
|
+
await execAsync(`codesign -v "${args.filePath}" 2>&1`);
|
|
2369
|
+
results.push(" Code signature: VALID");
|
|
2370
|
+
} catch (e) {
|
|
2371
|
+
results.push(` Code signature: WARNING - ${e instanceof Error ? e.message : String(e)}`);
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
// Check if executable
|
|
2375
|
+
try {
|
|
2376
|
+
const { stdout: fileInfo } = await execAsync(`file "${args.filePath}"`);
|
|
2377
|
+
if (fileInfo.includes("executable")) {
|
|
2378
|
+
results.push(" File type: Executable");
|
|
2379
|
+
}
|
|
2380
|
+
} catch {
|
|
2381
|
+
// Ignore
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
results.push("");
|
|
2385
|
+
results.push("Status: SUCCESS - Binary patched and ready to run");
|
|
2386
|
+
results.push("Note: The binary is now signed with an ad-hoc signature.");
|
|
2387
|
+
results.push(" Some apps may require proper code signing for full functionality.");
|
|
2388
|
+
|
|
2389
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
2390
|
+
} catch (error) {
|
|
2391
|
+
return {
|
|
2392
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2393
|
+
isError: true
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
// --- Verify binary will run ---
|
|
2399
|
+
async function handleBinVerify(args: { filePath: string }) {
|
|
2400
|
+
if (!isMacOS()) {
|
|
2401
|
+
return {
|
|
2402
|
+
content: [{
|
|
2403
|
+
type: "text",
|
|
2404
|
+
text: "Error: Binary verification tools are only available on macOS."
|
|
2405
|
+
}],
|
|
2406
|
+
isError: true
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
try {
|
|
2411
|
+
const results: string[] = [];
|
|
2412
|
+
const issues: string[] = [];
|
|
2413
|
+
|
|
2414
|
+
results.push(`Binary Verification: ${args.filePath}`);
|
|
2415
|
+
results.push("");
|
|
2416
|
+
|
|
2417
|
+
// Check file exists and type
|
|
2418
|
+
let isMachO = false;
|
|
2419
|
+
try {
|
|
2420
|
+
const { stdout: fileInfo } = await execAsync(`file "${args.filePath}"`);
|
|
2421
|
+
results.push(`File type: ${fileInfo.trim()}`);
|
|
2422
|
+
isMachO = fileInfo.toLowerCase().includes("mach-o");
|
|
2423
|
+
} catch (e) {
|
|
2424
|
+
return {
|
|
2425
|
+
content: [{ type: "text", text: `Error: Cannot read file - ${e instanceof Error ? e.message : String(e)}` }],
|
|
2426
|
+
isError: true
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
// Check permissions
|
|
2431
|
+
try {
|
|
2432
|
+
const { stdout: perms } = await execAsync(`ls -la "${args.filePath}"`);
|
|
2433
|
+
const permLine = perms.split("\n").find(l => l.includes(args.filePath.split("/").pop() || ""));
|
|
2434
|
+
if (permLine) {
|
|
2435
|
+
results.push(`Permissions: ${permLine.split(/\s+/)[0]}`);
|
|
2436
|
+
if (!permLine.includes("x")) {
|
|
2437
|
+
issues.push("File is not executable (no execute permission)");
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
} catch {
|
|
2441
|
+
results.push("Permissions: Unable to check");
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
results.push("");
|
|
2445
|
+
|
|
2446
|
+
// Check code signature (macOS specific)
|
|
2447
|
+
if (isMachO) {
|
|
2448
|
+
results.push("Code Signature:");
|
|
2449
|
+
try {
|
|
2450
|
+
const { stdout: signInfo } = await execAsync(`codesign -dv "${args.filePath}" 2>&1`);
|
|
2451
|
+
const authority = signInfo.split("\n").find(l => l.includes("Authority"));
|
|
2452
|
+
if (authority) {
|
|
2453
|
+
results.push(` Status: SIGNED (${authority.split(":")[1]?.trim() || "unknown authority"})`);
|
|
2454
|
+
} else {
|
|
2455
|
+
results.push(" Status: SIGNED (ad-hoc or unknown)");
|
|
2456
|
+
}
|
|
2457
|
+
} catch (e) {
|
|
2458
|
+
const output = e instanceof Error ? e.message : String(e);
|
|
2459
|
+
if (output.includes("not signed")) {
|
|
2460
|
+
results.push(" Status: UNSIGNED");
|
|
2461
|
+
issues.push("Binary is not code signed");
|
|
2462
|
+
} else {
|
|
2463
|
+
results.push(` Status: INVALID - ${output}`);
|
|
2464
|
+
issues.push("Code signature is invalid");
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// Verify signature
|
|
2469
|
+
try {
|
|
2470
|
+
await execAsync(`codesign -v "${args.filePath}" 2>&1`);
|
|
2471
|
+
results.push(" Validity: VALID");
|
|
2472
|
+
} catch (e) {
|
|
2473
|
+
const output = e instanceof Error ? e.message : String(e);
|
|
2474
|
+
if (!output.includes("not signed")) {
|
|
2475
|
+
results.push(` Validity: INVALID - ${output}`);
|
|
2476
|
+
issues.push("Code signature verification failed");
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
// Check quarantine
|
|
2482
|
+
results.push("");
|
|
2483
|
+
results.push("Quarantine:");
|
|
2484
|
+
try {
|
|
2485
|
+
const { stdout: quar } = await execAsync(`xattr -p com.apple.quarantine "${args.filePath}" 2>&1`);
|
|
2486
|
+
if (quar.trim()) {
|
|
2487
|
+
results.push(" Status: QUARANTINED");
|
|
2488
|
+
issues.push("File is quarantined and may be blocked from running");
|
|
2489
|
+
} else {
|
|
2490
|
+
results.push(" Status: NOT QUARANTINED");
|
|
2491
|
+
}
|
|
2492
|
+
} catch {
|
|
2493
|
+
results.push(" Status: NOT QUARANTINED");
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// Check architecture compatibility
|
|
2497
|
+
if (isMachO) {
|
|
2498
|
+
results.push("");
|
|
2499
|
+
results.push("Architecture:");
|
|
2500
|
+
try {
|
|
2501
|
+
const { stdout: arch } = await execAsync(`lipo -info "${args.filePath}" 2>&1`);
|
|
2502
|
+
results.push(` ${arch.trim()}`);
|
|
2503
|
+
|
|
2504
|
+
// Check if compatible with current system
|
|
2505
|
+
try {
|
|
2506
|
+
const { stdout: currentArch } = await execAsync(`uname -m`);
|
|
2507
|
+
if (!arch.includes(currentArch.trim()) && !arch.includes("universal")) {
|
|
2508
|
+
issues.push(`Binary may not be compatible with current architecture (${currentArch.trim()})`);
|
|
2509
|
+
}
|
|
2510
|
+
} catch {
|
|
2511
|
+
// Ignore
|
|
2512
|
+
}
|
|
2513
|
+
} catch (e) {
|
|
2514
|
+
results.push(` Unable to determine: ${e instanceof Error ? e.message : String(e)}`);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// Summary
|
|
2519
|
+
results.push("");
|
|
2520
|
+
results.push("=== SUMMARY ===");
|
|
2521
|
+
if (issues.length === 0) {
|
|
2522
|
+
results.push("Status: READY TO RUN");
|
|
2523
|
+
results.push("The binary should execute without issues.");
|
|
2524
|
+
} else {
|
|
2525
|
+
results.push("Status: ISSUES DETECTED");
|
|
2526
|
+
results.push("");
|
|
2527
|
+
results.push("Issues:");
|
|
2528
|
+
issues.forEach((issue, i) => results.push(` ${i + 1}. ${issue}`));
|
|
2529
|
+
results.push("");
|
|
2530
|
+
results.push("Suggested fixes:");
|
|
2531
|
+
if (issues.some(i => i.includes("quarantined"))) {
|
|
2532
|
+
results.push(" - Run: xattr -d com.apple.quarantine <file>");
|
|
2533
|
+
}
|
|
2534
|
+
if (issues.some(i => i.includes("not code signed") || i.includes("signature"))) {
|
|
2535
|
+
results.push(" - Run: codesign --force --sign - <file>");
|
|
2536
|
+
}
|
|
2537
|
+
if (issues.some(i => i.includes("not executable"))) {
|
|
2538
|
+
results.push(" - Run: chmod +x <file>");
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
2543
|
+
} catch (error) {
|
|
2544
|
+
return {
|
|
2545
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2546
|
+
isError: true
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
// ============================================================================
|
|
2552
|
+
// Patch Management System
|
|
2553
|
+
// ============================================================================
|
|
2554
|
+
|
|
2555
|
+
interface PatchEntry {
|
|
2556
|
+
id: string;
|
|
2557
|
+
offset: number;
|
|
2558
|
+
originalBytes: string;
|
|
2559
|
+
patchedBytes: string;
|
|
2560
|
+
description: string;
|
|
2561
|
+
createdAt: string;
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
interface BinaryPatchRecord {
|
|
2565
|
+
binaryPath: string;
|
|
2566
|
+
binaryName: string;
|
|
2567
|
+
lastPatchedChecksum: string;
|
|
2568
|
+
lastAppliedAt: string;
|
|
2569
|
+
patches: PatchEntry[];
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
interface PatchManifest {
|
|
2573
|
+
version: string;
|
|
2574
|
+
createdAt: string;
|
|
2575
|
+
updatedAt: string;
|
|
2576
|
+
binaries: Record<string, BinaryPatchRecord>;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
const PATCH_DIR = path.join(os.homedir(), ".claude", "patches");
|
|
2580
|
+
const MANIFEST_PATH = path.join(PATCH_DIR, "manifest.json");
|
|
2581
|
+
const BACKUPS_DIR = path.join(PATCH_DIR, "backups");
|
|
2582
|
+
const MANIFEST_VERSION = "1.0.0";
|
|
2583
|
+
|
|
2584
|
+
// Ensure patch directory exists
|
|
2585
|
+
function ensurePatchDir(): void {
|
|
2586
|
+
if (!fs.existsSync(PATCH_DIR)) {
|
|
2587
|
+
fs.mkdirSync(PATCH_DIR, { recursive: true });
|
|
2588
|
+
}
|
|
2589
|
+
if (!fs.existsSync(BACKUPS_DIR)) {
|
|
2590
|
+
fs.mkdirSync(BACKUPS_DIR, { recursive: true });
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// Load manifest
|
|
2595
|
+
function loadManifest(): PatchManifest {
|
|
2596
|
+
ensurePatchDir();
|
|
2597
|
+
if (fs.existsSync(MANIFEST_PATH)) {
|
|
2598
|
+
try {
|
|
2599
|
+
const data = fs.readFileSync(MANIFEST_PATH, "utf-8");
|
|
2600
|
+
return JSON.parse(data);
|
|
2601
|
+
} catch {
|
|
2602
|
+
// Return new manifest if corrupted
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
return {
|
|
2606
|
+
version: MANIFEST_VERSION,
|
|
2607
|
+
createdAt: new Date().toISOString(),
|
|
2608
|
+
updatedAt: new Date().toISOString(),
|
|
2609
|
+
binaries: {}
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// Save manifest
|
|
2614
|
+
function saveManifest(manifest: PatchManifest): void {
|
|
2615
|
+
ensurePatchDir();
|
|
2616
|
+
manifest.updatedAt = new Date().toISOString();
|
|
2617
|
+
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
// Get file checksum
|
|
2621
|
+
async function getFileChecksum(filePath: string): Promise<string> {
|
|
2622
|
+
try {
|
|
2623
|
+
const { stdout } = await execAsync(`shasum -a 256 "${filePath}" | cut -d' ' -f1`);
|
|
2624
|
+
return stdout.trim();
|
|
2625
|
+
} catch {
|
|
2626
|
+
return "unknown";
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
// Read bytes at offset
|
|
2631
|
+
function readBytesAtOffset(filePath: string, offset: number, length: number): string {
|
|
2632
|
+
const fd = fs.openSync(filePath, "r");
|
|
2633
|
+
const buffer = Buffer.alloc(length);
|
|
2634
|
+
fs.readSync(fd, buffer, 0, length, offset);
|
|
2635
|
+
fs.closeSync(fd);
|
|
2636
|
+
return buffer.toString("hex").match(/.{2}/g)?.join(" ").toUpperCase() || "";
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
// Write bytes at offset
|
|
2640
|
+
function writeBytesAtOffset(filePath: string, offset: number, hexData: string): void {
|
|
2641
|
+
const bytes = Buffer.from(hexData.replace(/\s+/g, ""), "hex");
|
|
2642
|
+
const fd = fs.openSync(filePath, "r+");
|
|
2643
|
+
fs.writeSync(fd, bytes, 0, bytes.length, offset);
|
|
2644
|
+
fs.closeSync(fd);
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// --- Register a patch ---
|
|
2648
|
+
async function handlePatchRegister(args: {
|
|
2649
|
+
filePath: string;
|
|
2650
|
+
patchId: string;
|
|
2651
|
+
offset: number;
|
|
2652
|
+
patchedBytes: string;
|
|
2653
|
+
description: string;
|
|
2654
|
+
captureOriginal?: boolean;
|
|
2655
|
+
}) {
|
|
2656
|
+
try {
|
|
2657
|
+
const results: string[] = [];
|
|
2658
|
+
results.push(`Register Patch: ${args.patchId}`);
|
|
2659
|
+
results.push(`Binary: ${args.filePath}`);
|
|
2660
|
+
results.push(`Offset: 0x${args.offset.toString(16)} (${args.offset})`);
|
|
2661
|
+
results.push("");
|
|
2662
|
+
|
|
2663
|
+
// Validate file exists
|
|
2664
|
+
if (!fs.existsSync(args.filePath)) {
|
|
2665
|
+
return {
|
|
2666
|
+
content: [{ type: "text", text: `Error: File not found: ${args.filePath}` }],
|
|
2667
|
+
isError: true
|
|
2668
|
+
};
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
// Normalize hex data
|
|
2672
|
+
const patchedBytes = args.patchedBytes.replace(/\s+/g, "").toUpperCase();
|
|
2673
|
+
const byteLength = patchedBytes.length / 2;
|
|
2674
|
+
|
|
2675
|
+
// Capture original bytes if requested
|
|
2676
|
+
let originalBytes = "";
|
|
2677
|
+
if (args.captureOriginal !== false) {
|
|
2678
|
+
try {
|
|
2679
|
+
originalBytes = readBytesAtOffset(args.filePath, args.offset, byteLength);
|
|
2680
|
+
results.push(`Original bytes: ${originalBytes}`);
|
|
2681
|
+
} catch (e) {
|
|
2682
|
+
return {
|
|
2683
|
+
content: [{ type: "text", text: `Error: Could not read original bytes - ${e instanceof Error ? e.message : String(e)}` }],
|
|
2684
|
+
isError: true
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// Save backup of original bytes
|
|
2690
|
+
const backupFileName = `${path.basename(args.filePath)}.offset_${args.offset}.bin`;
|
|
2691
|
+
const backupPath = path.join(BACKUPS_DIR, backupFileName);
|
|
2692
|
+
if (originalBytes) {
|
|
2693
|
+
fs.writeFileSync(backupPath, Buffer.from(originalBytes.replace(/\s+/g, ""), "hex"));
|
|
2694
|
+
results.push(`Backup saved: ${backupPath}`);
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
// Load and update manifest
|
|
2698
|
+
const manifest = loadManifest();
|
|
2699
|
+
const checksum = await getFileChecksum(args.filePath);
|
|
2700
|
+
|
|
2701
|
+
if (!manifest.binaries[args.filePath]) {
|
|
2702
|
+
manifest.binaries[args.filePath] = {
|
|
2703
|
+
binaryPath: args.filePath,
|
|
2704
|
+
binaryName: path.basename(args.filePath),
|
|
2705
|
+
lastPatchedChecksum: checksum,
|
|
2706
|
+
lastAppliedAt: new Date().toISOString(),
|
|
2707
|
+
patches: []
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
// Check if patch ID already exists
|
|
2712
|
+
const existingIndex = manifest.binaries[args.filePath].patches.findIndex(p => p.id === args.patchId);
|
|
2713
|
+
const newPatch: PatchEntry = {
|
|
2714
|
+
id: args.patchId,
|
|
2715
|
+
offset: args.offset,
|
|
2716
|
+
originalBytes: originalBytes || "UNKNOWN",
|
|
2717
|
+
patchedBytes: patchedBytes.match(/.{2}/g)?.join(" ") || patchedBytes,
|
|
2718
|
+
description: args.description,
|
|
2719
|
+
createdAt: new Date().toISOString()
|
|
2720
|
+
};
|
|
2721
|
+
|
|
2722
|
+
if (existingIndex >= 0) {
|
|
2723
|
+
manifest.binaries[args.filePath].patches[existingIndex] = newPatch;
|
|
2724
|
+
results.push(`Updated existing patch: ${args.patchId}`);
|
|
2725
|
+
} else {
|
|
2726
|
+
manifest.binaries[args.filePath].patches.push(newPatch);
|
|
2727
|
+
results.push(`Registered new patch: ${args.patchId}`);
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
saveManifest(manifest);
|
|
2731
|
+
|
|
2732
|
+
results.push("");
|
|
2733
|
+
results.push("Patch registered successfully!");
|
|
2734
|
+
results.push(`Manifest: ${MANIFEST_PATH}`);
|
|
2735
|
+
results.push("");
|
|
2736
|
+
results.push("To apply this patch, use: bin_patch_apply");
|
|
2737
|
+
|
|
2738
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
2739
|
+
} catch (error) {
|
|
2740
|
+
return {
|
|
2741
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2742
|
+
isError: true
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
// --- Apply all registered patches ---
|
|
2748
|
+
async function handlePatchApply(args: {
|
|
2749
|
+
filePath: string;
|
|
2750
|
+
resign?: boolean;
|
|
2751
|
+
removeQuarantine?: boolean;
|
|
2752
|
+
}) {
|
|
2753
|
+
const isMac = isMacOS();
|
|
2754
|
+
const resign = args.resign !== false && isMac;
|
|
2755
|
+
const removeQuarantine = args.removeQuarantine !== false && isMac;
|
|
2756
|
+
|
|
2757
|
+
try {
|
|
2758
|
+
const results: string[] = [];
|
|
2759
|
+
results.push(`Apply Patches: ${args.filePath}`);
|
|
2760
|
+
results.push("");
|
|
2761
|
+
|
|
2762
|
+
// Load manifest
|
|
2763
|
+
const manifest = loadManifest();
|
|
2764
|
+
const record = manifest.binaries[args.filePath];
|
|
2765
|
+
|
|
2766
|
+
if (!record || record.patches.length === 0) {
|
|
2767
|
+
results.push("No registered patches found for this binary.");
|
|
2768
|
+
results.push("");
|
|
2769
|
+
results.push("To register a patch, use: bin_patch_register");
|
|
2770
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
// Check current checksum
|
|
2774
|
+
const currentChecksum = await getFileChecksum(args.filePath);
|
|
2775
|
+
results.push(`Current checksum: ${currentChecksum.substring(0, 16)}...`);
|
|
2776
|
+
results.push(`Last patched checksum: ${record.lastPatchedChecksum.substring(0, 16)}...`);
|
|
2777
|
+
|
|
2778
|
+
if (currentChecksum === record.lastPatchedChecksum) {
|
|
2779
|
+
results.push("Status: Binary appears to already have patches applied.");
|
|
2780
|
+
results.push("Use force=true to re-apply anyway.");
|
|
2781
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
results.push("Binary has been updated - applying patches...");
|
|
2785
|
+
results.push("");
|
|
2786
|
+
|
|
2787
|
+
// Create backup of entire binary
|
|
2788
|
+
const backupPath = `${args.filePath}.prepatch`;
|
|
2789
|
+
fs.copyFileSync(args.filePath, backupPath);
|
|
2790
|
+
results.push(`Backup: ${backupPath}`);
|
|
2791
|
+
|
|
2792
|
+
// Apply each patch
|
|
2793
|
+
let appliedCount = 0;
|
|
2794
|
+
for (const patch of record.patches) {
|
|
2795
|
+
try {
|
|
2796
|
+
results.push(`Applying: ${patch.id}`);
|
|
2797
|
+
results.push(` Offset: 0x${patch.offset.toString(16)}`);
|
|
2798
|
+
results.push(` Bytes: ${patch.patchedBytes}`);
|
|
2799
|
+
|
|
2800
|
+
writeBytesAtOffset(args.filePath, patch.offset, patch.patchedBytes);
|
|
2801
|
+
appliedCount++;
|
|
2802
|
+
results.push(" Status: Applied");
|
|
2803
|
+
} catch (e) {
|
|
2804
|
+
results.push(` Status: FAILED - ${e instanceof Error ? e.message : String(e)}`);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
// macOS: resign and remove quarantine
|
|
2809
|
+
if (isMac && appliedCount > 0) {
|
|
2810
|
+
results.push("");
|
|
2811
|
+
|
|
2812
|
+
if (resign) {
|
|
2813
|
+
results.push("Re-signing binary...");
|
|
2814
|
+
try {
|
|
2815
|
+
await execAsync(`codesign --remove-signature "${args.filePath}" 2>&1`);
|
|
2816
|
+
await execAsync(`codesign --force --sign - "${args.filePath}"`);
|
|
2817
|
+
results.push(" Status: Re-signed with ad-hoc signature");
|
|
2818
|
+
} catch (e) {
|
|
2819
|
+
results.push(` Warning: ${e instanceof Error ? e.message : String(e)}`);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
if (removeQuarantine) {
|
|
2824
|
+
results.push("Removing quarantine...");
|
|
2825
|
+
try {
|
|
2826
|
+
await execAsync(`xattr -d com.apple.quarantine "${args.filePath}" 2>&1`);
|
|
2827
|
+
await execAsync(`xattr -d com.apple.provenance "${args.filePath}" 2>&1`);
|
|
2828
|
+
results.push(" Status: Quarantine attributes removed");
|
|
2829
|
+
} catch {
|
|
2830
|
+
results.push(" Status: No quarantine to remove");
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// Update manifest with new checksum
|
|
2836
|
+
const newChecksum = await getFileChecksum(args.filePath);
|
|
2837
|
+
record.lastPatchedChecksum = newChecksum;
|
|
2838
|
+
record.lastAppliedAt = new Date().toISOString();
|
|
2839
|
+
saveManifest(manifest);
|
|
2840
|
+
|
|
2841
|
+
results.push("");
|
|
2842
|
+
results.push(`=== SUMMARY ===`);
|
|
2843
|
+
results.push(`Patches applied: ${appliedCount}/${record.patches.length}`);
|
|
2844
|
+
results.push(`New checksum: ${newChecksum.substring(0, 16)}...`);
|
|
2845
|
+
results.push("Binary is ready to use.");
|
|
2846
|
+
|
|
2847
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
2848
|
+
} catch (error) {
|
|
2849
|
+
return {
|
|
2850
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2851
|
+
isError: true
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
// --- Verify patches are applied ---
|
|
2857
|
+
async function handlePatchVerify(args: { filePath: string }) {
|
|
2858
|
+
try {
|
|
2859
|
+
const results: string[] = [];
|
|
2860
|
+
results.push(`Verify Patches: ${args.filePath}`);
|
|
2861
|
+
results.push("");
|
|
2862
|
+
|
|
2863
|
+
// Load manifest
|
|
2864
|
+
const manifest = loadManifest();
|
|
2865
|
+
const record = manifest.binaries[args.filePath];
|
|
2866
|
+
|
|
2867
|
+
if (!record || record.patches.length === 0) {
|
|
2868
|
+
results.push("No registered patches found for this binary.");
|
|
2869
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
// Get current checksum
|
|
2873
|
+
const currentChecksum = await getFileChecksum(args.filePath);
|
|
2874
|
+
results.push(`Current checksum: ${currentChecksum.substring(0, 16)}...`);
|
|
2875
|
+
results.push(`Expected checksum: ${record.lastPatchedChecksum.substring(0, 16)}...`);
|
|
2876
|
+
results.push("");
|
|
2877
|
+
|
|
2878
|
+
// Check if binary was updated
|
|
2879
|
+
if (currentChecksum !== record.lastPatchedChecksum) {
|
|
2880
|
+
results.push("⚠️ Binary has been UPDATED since last patch.");
|
|
2881
|
+
results.push(" Run bin_patch_apply to re-apply patches.");
|
|
2882
|
+
results.push("");
|
|
2883
|
+
} else {
|
|
2884
|
+
results.push("✓ Binary checksum matches patched version.");
|
|
2885
|
+
results.push("");
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// Verify each patch by reading current bytes
|
|
2889
|
+
results.push("Patch Status:");
|
|
2890
|
+
let allApplied = true;
|
|
2891
|
+
for (const patch of record.patches) {
|
|
2892
|
+
try {
|
|
2893
|
+
const byteLength = patch.patchedBytes.replace(/\s+/g, "").length / 2;
|
|
2894
|
+
const currentBytes = readBytesAtOffset(args.filePath, patch.offset, byteLength);
|
|
2895
|
+
const normalizedCurrent = currentBytes.replace(/\s+/g, "").toUpperCase();
|
|
2896
|
+
const normalizedPatched = patch.patchedBytes.replace(/\s+/g, "").toUpperCase();
|
|
2897
|
+
|
|
2898
|
+
if (normalizedCurrent === normalizedPatched) {
|
|
2899
|
+
results.push(` ✓ ${patch.id}: APPLIED`);
|
|
2900
|
+
} else if (normalizedCurrent === patch.originalBytes.replace(/\s+/g, "").toUpperCase()) {
|
|
2901
|
+
results.push(` ✗ ${patch.id}: NOT APPLIED (original bytes present)`);
|
|
2902
|
+
allApplied = false;
|
|
2903
|
+
} else {
|
|
2904
|
+
results.push(` ? ${patch.id}: UNKNOWN (bytes don't match expected)`);
|
|
2905
|
+
results.push(` Current: ${currentBytes}`);
|
|
2906
|
+
results.push(` Expected: ${patch.patchedBytes}`);
|
|
2907
|
+
allApplied = false;
|
|
2908
|
+
}
|
|
2909
|
+
} catch (e) {
|
|
2910
|
+
results.push(` ✗ ${patch.id}: ERROR - ${e instanceof Error ? e.message : String(e)}`);
|
|
2911
|
+
allApplied = false;
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
results.push("");
|
|
2916
|
+
if (allApplied && currentChecksum === record.lastPatchedChecksum) {
|
|
2917
|
+
results.push("Status: All patches verified and applied.");
|
|
2918
|
+
} else if (!allApplied) {
|
|
2919
|
+
results.push("Status: Some patches need to be applied.");
|
|
2920
|
+
results.push("Action: Run bin_patch_apply");
|
|
2921
|
+
} else {
|
|
2922
|
+
results.push("Status: Binary may have been updated. Re-apply patches.");
|
|
2923
|
+
results.push("Action: Run bin_patch_apply");
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
2927
|
+
} catch (error) {
|
|
2928
|
+
return {
|
|
2929
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
2930
|
+
isError: true
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
// --- Restore original bytes ---
|
|
2936
|
+
async function handlePatchRestore(args: {
|
|
2937
|
+
filePath: string;
|
|
2938
|
+
patchId?: string;
|
|
2939
|
+
resign?: boolean;
|
|
2940
|
+
}) {
|
|
2941
|
+
const isMac = isMacOS();
|
|
2942
|
+
const resign = args.resign !== false && isMac;
|
|
2943
|
+
|
|
2944
|
+
try {
|
|
2945
|
+
const results: string[] = [];
|
|
2946
|
+
results.push(`Restore Patches: ${args.filePath}`);
|
|
2947
|
+
results.push("");
|
|
2948
|
+
|
|
2949
|
+
// Load manifest
|
|
2950
|
+
const manifest = loadManifest();
|
|
2951
|
+
const record = manifest.binaries[args.filePath];
|
|
2952
|
+
|
|
2953
|
+
if (!record || record.patches.length === 0) {
|
|
2954
|
+
results.push("No registered patches found for this binary.");
|
|
2955
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
// Filter patches if specific ID requested
|
|
2959
|
+
const patchesToRestore = args.patchId
|
|
2960
|
+
? record.patches.filter(p => p.id === args.patchId)
|
|
2961
|
+
: record.patches;
|
|
2962
|
+
|
|
2963
|
+
if (patchesToRestore.length === 0) {
|
|
2964
|
+
results.push(`No patch found with ID: ${args.patchId}`);
|
|
2965
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
// Create backup
|
|
2969
|
+
const backupPath = `${args.filePath}.prerestore`;
|
|
2970
|
+
fs.copyFileSync(args.filePath, backupPath);
|
|
2971
|
+
results.push(`Backup: ${backupPath}`);
|
|
2972
|
+
results.push("");
|
|
2973
|
+
|
|
2974
|
+
// Restore each patch
|
|
2975
|
+
let restoredCount = 0;
|
|
2976
|
+
for (const patch of patchesToRestore) {
|
|
2977
|
+
if (patch.originalBytes === "UNKNOWN") {
|
|
2978
|
+
results.push(`${patch.id}: SKIPPED (no original bytes captured)`);
|
|
2979
|
+
continue;
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
try {
|
|
2983
|
+
results.push(`Restoring: ${patch.id}`);
|
|
2984
|
+
writeBytesAtOffset(args.filePath, patch.offset, patch.originalBytes);
|
|
2985
|
+
results.push(` Offset: 0x${patch.offset.toString(16)}`);
|
|
2986
|
+
results.push(` Bytes: ${patch.originalBytes}`);
|
|
2987
|
+
results.push(" Status: Restored");
|
|
2988
|
+
restoredCount++;
|
|
2989
|
+
} catch (e) {
|
|
2990
|
+
results.push(` Status: FAILED - ${e instanceof Error ? e.message : String(e)}`);
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
// macOS: resign
|
|
2995
|
+
if (isMac && restoredCount > 0 && resign) {
|
|
2996
|
+
results.push("");
|
|
2997
|
+
results.push("Re-signing binary...");
|
|
2998
|
+
try {
|
|
2999
|
+
await execAsync(`codesign --remove-signature "${args.filePath}" 2>&1`);
|
|
3000
|
+
await execAsync(`codesign --force --sign - "${args.filePath}"`);
|
|
3001
|
+
results.push(" Status: Re-signed with ad-hoc signature");
|
|
3002
|
+
} catch (e) {
|
|
3003
|
+
results.push(` Warning: ${e instanceof Error ? e.message : String(e)}`);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
// Update manifest
|
|
3008
|
+
const newChecksum = await getFileChecksum(args.filePath);
|
|
3009
|
+
record.lastPatchedChecksum = newChecksum;
|
|
3010
|
+
saveManifest(manifest);
|
|
3011
|
+
|
|
3012
|
+
results.push("");
|
|
3013
|
+
results.push(`Restored: ${restoredCount}/${patchesToRestore.length} patches`);
|
|
3014
|
+
|
|
3015
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
3016
|
+
} catch (error) {
|
|
3017
|
+
return {
|
|
3018
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
3019
|
+
isError: true
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
// --- List registered patches ---
|
|
3025
|
+
async function handlePatchList(args: { filePath?: string }) {
|
|
3026
|
+
try {
|
|
3027
|
+
const results: string[] = [];
|
|
3028
|
+
results.push("Patch Registry");
|
|
3029
|
+
results.push(`Manifest: ${MANIFEST_PATH}`);
|
|
3030
|
+
results.push("");
|
|
3031
|
+
|
|
3032
|
+
const manifest = loadManifest();
|
|
3033
|
+
|
|
3034
|
+
if (Object.keys(manifest.binaries).length === 0) {
|
|
3035
|
+
results.push("No patches registered.");
|
|
3036
|
+
results.push("");
|
|
3037
|
+
results.push("To register a patch, use: bin_patch_register");
|
|
3038
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
// Filter by file if specified
|
|
3042
|
+
const binariesToShow = args.filePath
|
|
3043
|
+
? { [args.filePath]: manifest.binaries[args.filePath] }
|
|
3044
|
+
: manifest.binaries;
|
|
3045
|
+
|
|
3046
|
+
for (const [binaryPath, record] of Object.entries(binariesToShow)) {
|
|
3047
|
+
if (!record) continue;
|
|
3048
|
+
|
|
3049
|
+
results.push(`═══════════════════════════════════════════════════════════`);
|
|
3050
|
+
results.push(`Binary: ${binaryPath}`);
|
|
3051
|
+
results.push(`Name: ${record.binaryName}`);
|
|
3052
|
+
results.push(`Patches: ${record.patches.length}`);
|
|
3053
|
+
results.push(`Last Applied: ${record.lastAppliedAt}`);
|
|
3054
|
+
results.push(`Checksum: ${record.lastPatchedChecksum.substring(0, 16)}...`);
|
|
3055
|
+
results.push("");
|
|
3056
|
+
|
|
3057
|
+
if (record.patches.length > 0) {
|
|
3058
|
+
results.push("Registered Patches:");
|
|
3059
|
+
for (const patch of record.patches) {
|
|
3060
|
+
results.push(` ┌─ ${patch.id} ─────────────────────────────────`);
|
|
3061
|
+
results.push(` │ Offset: 0x${patch.offset.toString(16)} (${patch.offset})`);
|
|
3062
|
+
results.push(` │ Original: ${patch.originalBytes}`);
|
|
3063
|
+
results.push(` │ Patched: ${patch.patchedBytes}`);
|
|
3064
|
+
results.push(` │ Description: ${patch.description}`);
|
|
3065
|
+
results.push(` │ Created: ${patch.createdAt}`);
|
|
3066
|
+
results.push(` └────────────────────────────────────────────`);
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
results.push("");
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
3073
|
+
} catch (error) {
|
|
3074
|
+
return {
|
|
3075
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
3076
|
+
isError: true
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
// --- Remove a patch from registry ---
|
|
3082
|
+
async function handlePatchRemove(args: {
|
|
3083
|
+
filePath: string;
|
|
3084
|
+
patchId: string;
|
|
3085
|
+
}) {
|
|
3086
|
+
try {
|
|
3087
|
+
const results: string[] = [];
|
|
3088
|
+
results.push(`Remove Patch: ${args.patchId}`);
|
|
3089
|
+
results.push("");
|
|
3090
|
+
|
|
3091
|
+
const manifest = loadManifest();
|
|
3092
|
+
const record = manifest.binaries[args.filePath];
|
|
3093
|
+
|
|
3094
|
+
if (!record) {
|
|
3095
|
+
results.push("No patches registered for this binary.");
|
|
3096
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
const index = record.patches.findIndex(p => p.id === args.patchId);
|
|
3100
|
+
if (index < 0) {
|
|
3101
|
+
results.push(`Patch not found: ${args.patchId}`);
|
|
3102
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
const removed = record.patches.splice(index, 1)[0];
|
|
3106
|
+
results.push("Removed patch:");
|
|
3107
|
+
results.push(` ID: ${removed.id}`);
|
|
3108
|
+
results.push(` Offset: 0x${removed.offset.toString(16)}`);
|
|
3109
|
+
results.push(` Description: ${removed.description}`);
|
|
3110
|
+
|
|
3111
|
+
// Remove binary entry if no patches left
|
|
3112
|
+
if (record.patches.length === 0) {
|
|
3113
|
+
delete manifest.binaries[args.filePath];
|
|
3114
|
+
results.push("");
|
|
3115
|
+
results.push("No more patches for this binary - removed from registry.");
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
saveManifest(manifest);
|
|
3119
|
+
results.push("");
|
|
3120
|
+
results.push("Patch removed from registry.");
|
|
3121
|
+
results.push("Note: This does NOT undo the patch in the binary.");
|
|
3122
|
+
results.push("Use bin_patch_restore to restore original bytes.");
|
|
3123
|
+
|
|
3124
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
3125
|
+
} catch (error) {
|
|
3126
|
+
return {
|
|
3127
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
3128
|
+
isError: true
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
|
|
1904
3133
|
// ============================================================================
|
|
1905
3134
|
// Tool definitions
|
|
1906
3135
|
// ============================================================================
|
|
@@ -2372,6 +3601,170 @@ const TOOLS = [
|
|
|
2372
3601
|
required: ["filePath", "offset", "length"],
|
|
2373
3602
|
},
|
|
2374
3603
|
},
|
|
3604
|
+
// macOS Binary Patching Tools
|
|
3605
|
+
{
|
|
3606
|
+
name: "bin_codesign_info",
|
|
3607
|
+
description: "Show code signature details for a macOS binary (macOS only)",
|
|
3608
|
+
inputSchema: {
|
|
3609
|
+
type: "object" as const,
|
|
3610
|
+
properties: {
|
|
3611
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
3612
|
+
},
|
|
3613
|
+
required: ["filePath"],
|
|
3614
|
+
},
|
|
3615
|
+
},
|
|
3616
|
+
{
|
|
3617
|
+
name: "bin_codesign_remove",
|
|
3618
|
+
description: "Remove code signature from a macOS binary (DESTRUCTIVE - macOS only)",
|
|
3619
|
+
inputSchema: {
|
|
3620
|
+
type: "object" as const,
|
|
3621
|
+
properties: {
|
|
3622
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
3623
|
+
createBackup: { type: "boolean", description: "Create .bak backup (default: true)" },
|
|
3624
|
+
},
|
|
3625
|
+
required: ["filePath"],
|
|
3626
|
+
},
|
|
3627
|
+
},
|
|
3628
|
+
{
|
|
3629
|
+
name: "bin_codesign_sign",
|
|
3630
|
+
description: "Apply ad-hoc or certificate signature to a macOS binary (macOS only)",
|
|
3631
|
+
inputSchema: {
|
|
3632
|
+
type: "object" as const,
|
|
3633
|
+
properties: {
|
|
3634
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
3635
|
+
certificate: { type: "string", description: "Certificate identity (default: '-' for ad-hoc signature)" },
|
|
3636
|
+
force: { type: "boolean", description: "Force replace existing signature (default: true)" },
|
|
3637
|
+
createBackup: { type: "boolean", description: "Create .bak backup before signing" },
|
|
3638
|
+
deep: { type: "boolean", description: "Sign all nested code" },
|
|
3639
|
+
options: { type: "string", description: "Additional signing options (e.g., 'runtime' for hardened runtime)" },
|
|
3640
|
+
},
|
|
3641
|
+
required: ["filePath"],
|
|
3642
|
+
},
|
|
3643
|
+
},
|
|
3644
|
+
{
|
|
3645
|
+
name: "bin_quarantine_check",
|
|
3646
|
+
description: "Check quarantine status of a file on macOS (macOS only)",
|
|
3647
|
+
inputSchema: {
|
|
3648
|
+
type: "object" as const,
|
|
3649
|
+
properties: {
|
|
3650
|
+
filePath: { type: "string", description: "Path to the file" },
|
|
3651
|
+
},
|
|
3652
|
+
required: ["filePath"],
|
|
3653
|
+
},
|
|
3654
|
+
},
|
|
3655
|
+
{
|
|
3656
|
+
name: "bin_quarantine_remove",
|
|
3657
|
+
description: "Remove quarantine attributes from a file on macOS (DESTRUCTIVE - macOS only)",
|
|
3658
|
+
inputSchema: {
|
|
3659
|
+
type: "object" as const,
|
|
3660
|
+
properties: {
|
|
3661
|
+
filePath: { type: "string", description: "Path to the file" },
|
|
3662
|
+
},
|
|
3663
|
+
required: ["filePath"],
|
|
3664
|
+
},
|
|
3665
|
+
},
|
|
3666
|
+
{
|
|
3667
|
+
name: "bin_safe_patch",
|
|
3668
|
+
description: "Safe binary patching workflow for macOS: patch, remove signature, re-sign, remove quarantine (DESTRUCTIVE - macOS only)",
|
|
3669
|
+
inputSchema: {
|
|
3670
|
+
type: "object" as const,
|
|
3671
|
+
properties: {
|
|
3672
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
3673
|
+
offset: { type: "number", description: "Byte offset to patch" },
|
|
3674
|
+
hexData: { type: "string", description: "Hex bytes to write (e.g., '90 90 90')" },
|
|
3675
|
+
createBackup: { type: "boolean", description: "Create .bak backup (default: true)" },
|
|
3676
|
+
},
|
|
3677
|
+
required: ["filePath", "offset", "hexData"],
|
|
3678
|
+
},
|
|
3679
|
+
},
|
|
3680
|
+
{
|
|
3681
|
+
name: "bin_verify",
|
|
3682
|
+
description: "Verify if a macOS binary will run: check signature, quarantine, architecture (macOS only)",
|
|
3683
|
+
inputSchema: {
|
|
3684
|
+
type: "object" as const,
|
|
3685
|
+
properties: {
|
|
3686
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
3687
|
+
},
|
|
3688
|
+
required: ["filePath"],
|
|
3689
|
+
},
|
|
3690
|
+
},
|
|
3691
|
+
// Patch Management Tools
|
|
3692
|
+
{
|
|
3693
|
+
name: "bin_patch_register",
|
|
3694
|
+
description: "Register a binary patch for persistence across updates. Captures original bytes and stores in manifest.",
|
|
3695
|
+
inputSchema: {
|
|
3696
|
+
type: "object" as const,
|
|
3697
|
+
properties: {
|
|
3698
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
3699
|
+
patchId: { type: "string", description: "Unique identifier for this patch (e.g., 'skip-permissions')" },
|
|
3700
|
+
offset: { type: "number", description: "Byte offset to patch" },
|
|
3701
|
+
patchedBytes: { type: "string", description: "Hex bytes to write (e.g., '90 90 90')" },
|
|
3702
|
+
description: { type: "string", description: "Description of what this patch does" },
|
|
3703
|
+
captureOriginal: { type: "boolean", description: "Capture original bytes (default: true)" },
|
|
3704
|
+
},
|
|
3705
|
+
required: ["filePath", "patchId", "offset", "patchedBytes", "description"],
|
|
3706
|
+
},
|
|
3707
|
+
},
|
|
3708
|
+
{
|
|
3709
|
+
name: "bin_patch_apply",
|
|
3710
|
+
description: "Apply all registered patches to a binary. Handles macOS re-signing and quarantine. Detects binary updates.",
|
|
3711
|
+
inputSchema: {
|
|
3712
|
+
type: "object" as const,
|
|
3713
|
+
properties: {
|
|
3714
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
3715
|
+
resign: { type: "boolean", description: "Re-sign binary after patching (macOS, default: true)" },
|
|
3716
|
+
removeQuarantine: { type: "boolean", description: "Remove quarantine after patching (macOS, default: true)" },
|
|
3717
|
+
},
|
|
3718
|
+
required: ["filePath"],
|
|
3719
|
+
},
|
|
3720
|
+
},
|
|
3721
|
+
{
|
|
3722
|
+
name: "bin_patch_verify",
|
|
3723
|
+
description: "Verify if registered patches are currently applied to a binary. Detects if binary was updated.",
|
|
3724
|
+
inputSchema: {
|
|
3725
|
+
type: "object" as const,
|
|
3726
|
+
properties: {
|
|
3727
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
3728
|
+
},
|
|
3729
|
+
required: ["filePath"],
|
|
3730
|
+
},
|
|
3731
|
+
},
|
|
3732
|
+
{
|
|
3733
|
+
name: "bin_patch_restore",
|
|
3734
|
+
description: "Restore original bytes for registered patches. Undoes patches in the binary.",
|
|
3735
|
+
inputSchema: {
|
|
3736
|
+
type: "object" as const,
|
|
3737
|
+
properties: {
|
|
3738
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
3739
|
+
patchId: { type: "string", description: "Specific patch to restore (optional - restores all if omitted)" },
|
|
3740
|
+
resign: { type: "boolean", description: "Re-sign binary after restoring (macOS, default: true)" },
|
|
3741
|
+
},
|
|
3742
|
+
required: ["filePath"],
|
|
3743
|
+
},
|
|
3744
|
+
},
|
|
3745
|
+
{
|
|
3746
|
+
name: "bin_patch_list",
|
|
3747
|
+
description: "List all registered patches in the manifest, optionally filtered by binary.",
|
|
3748
|
+
inputSchema: {
|
|
3749
|
+
type: "object" as const,
|
|
3750
|
+
properties: {
|
|
3751
|
+
filePath: { type: "string", description: "Filter by binary path (optional)" },
|
|
3752
|
+
},
|
|
3753
|
+
required: [],
|
|
3754
|
+
},
|
|
3755
|
+
},
|
|
3756
|
+
{
|
|
3757
|
+
name: "bin_patch_remove",
|
|
3758
|
+
description: "Remove a patch from the registry. Does NOT undo the patch in the binary.",
|
|
3759
|
+
inputSchema: {
|
|
3760
|
+
type: "object" as const,
|
|
3761
|
+
properties: {
|
|
3762
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
3763
|
+
patchId: { type: "string", description: "ID of the patch to remove from registry" },
|
|
3764
|
+
},
|
|
3765
|
+
required: ["filePath", "patchId"],
|
|
3766
|
+
},
|
|
3767
|
+
},
|
|
2375
3768
|
];
|
|
2376
3769
|
|
|
2377
3770
|
// ============================================================================
|
|
@@ -2500,6 +3893,64 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2500
3893
|
byteSize?: 8 | 16 | 32 | 64;
|
|
2501
3894
|
signed?: boolean;
|
|
2502
3895
|
});
|
|
3896
|
+
// macOS Binary Patching handlers
|
|
3897
|
+
case "bin_codesign_info":
|
|
3898
|
+
return await handleCodesignInfo(args as { filePath: string });
|
|
3899
|
+
case "bin_codesign_remove":
|
|
3900
|
+
return await handleCodesignRemove(args as { filePath: string; createBackup?: boolean });
|
|
3901
|
+
case "bin_codesign_sign":
|
|
3902
|
+
return await handleCodesignSign(args as {
|
|
3903
|
+
filePath: string;
|
|
3904
|
+
certificate?: string;
|
|
3905
|
+
force?: boolean;
|
|
3906
|
+
createBackup?: boolean;
|
|
3907
|
+
deep?: boolean;
|
|
3908
|
+
options?: string;
|
|
3909
|
+
});
|
|
3910
|
+
case "bin_quarantine_check":
|
|
3911
|
+
return await handleQuarantineCheck(args as { filePath: string });
|
|
3912
|
+
case "bin_quarantine_remove":
|
|
3913
|
+
return await handleQuarantineRemove(args as { filePath: string });
|
|
3914
|
+
case "bin_safe_patch":
|
|
3915
|
+
return await handleSafePatch(args as {
|
|
3916
|
+
filePath: string;
|
|
3917
|
+
offset: number;
|
|
3918
|
+
hexData: string;
|
|
3919
|
+
createBackup?: boolean;
|
|
3920
|
+
});
|
|
3921
|
+
case "bin_verify":
|
|
3922
|
+
return await handleBinVerify(args as { filePath: string });
|
|
3923
|
+
// Patch Management handlers
|
|
3924
|
+
case "bin_patch_register":
|
|
3925
|
+
return await handlePatchRegister(args as {
|
|
3926
|
+
filePath: string;
|
|
3927
|
+
patchId: string;
|
|
3928
|
+
offset: number;
|
|
3929
|
+
patchedBytes: string;
|
|
3930
|
+
description: string;
|
|
3931
|
+
captureOriginal?: boolean;
|
|
3932
|
+
});
|
|
3933
|
+
case "bin_patch_apply":
|
|
3934
|
+
return await handlePatchApply(args as {
|
|
3935
|
+
filePath: string;
|
|
3936
|
+
resign?: boolean;
|
|
3937
|
+
removeQuarantine?: boolean;
|
|
3938
|
+
});
|
|
3939
|
+
case "bin_patch_verify":
|
|
3940
|
+
return await handlePatchVerify(args as { filePath: string });
|
|
3941
|
+
case "bin_patch_restore":
|
|
3942
|
+
return await handlePatchRestore(args as {
|
|
3943
|
+
filePath: string;
|
|
3944
|
+
patchId?: string;
|
|
3945
|
+
resign?: boolean;
|
|
3946
|
+
});
|
|
3947
|
+
case "bin_patch_list":
|
|
3948
|
+
return await handlePatchList(args as { filePath?: string });
|
|
3949
|
+
case "bin_patch_remove":
|
|
3950
|
+
return await handlePatchRemove(args as {
|
|
3951
|
+
filePath: string;
|
|
3952
|
+
patchId: string;
|
|
3953
|
+
});
|
|
2503
3954
|
default:
|
|
2504
3955
|
throw new Error(`Unknown tool: ${name}`);
|
|
2505
3956
|
}
|