@ebowwa/mcp-nm 1.1.0 → 2.0.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 +861 -2
- package/package.json +14 -4
- package/src/index.ts +1078 -4
package/src/index.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* @ebowwa/nm
|
|
3
|
+
* @ebowwa/mcp-nm - Comprehensive binary analysis MCP server
|
|
4
4
|
*
|
|
5
|
-
* Provides tools for
|
|
5
|
+
* Provides tools for:
|
|
6
|
+
* - Symbol analysis (nm): list, external, undefined, defined, dynamic, search
|
|
7
|
+
* - Hex operations (xxd): hexdump, plain, C include, binary, extract, find pattern
|
|
8
|
+
* - Extended analysis: strings, file info, section sizes, disassembly
|
|
9
|
+
* - Security audit: ASLR, PIE, RELRO, stack canary, NX bit
|
|
10
|
+
* - Entropy analysis: detect packed/encrypted sections
|
|
11
|
+
* - Import/export: dependency analysis
|
|
12
|
+
* - Binary diff: byte-level comparison
|
|
13
|
+
* - Archive extraction: extract from .a files
|
|
14
|
+
* - Binary patching: patch bytes, NOP sled, hex editor
|
|
6
15
|
*/
|
|
7
16
|
|
|
8
17
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -19,8 +28,8 @@ const execAsync = promisify(exec);
|
|
|
19
28
|
// Server instance
|
|
20
29
|
const server = new Server(
|
|
21
30
|
{
|
|
22
|
-
name: "@ebowwa/nm
|
|
23
|
-
version: "
|
|
31
|
+
name: "@ebowwa/mcp-nm",
|
|
32
|
+
version: "2.0.0",
|
|
24
33
|
},
|
|
25
34
|
{
|
|
26
35
|
capabilities: {
|
|
@@ -54,6 +63,78 @@ interface XxdResult {
|
|
|
54
63
|
startOffset: number;
|
|
55
64
|
}
|
|
56
65
|
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// File type detection
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
interface FileAnalysis {
|
|
71
|
+
hasDynamicSymbols: boolean;
|
|
72
|
+
fileType: "elf-shared" | "elf-executable" | "mach-o" | "static-library" | "object-file" | "unknown";
|
|
73
|
+
format: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function analyzeFile(filePath: string): Promise<FileAnalysis> {
|
|
77
|
+
try {
|
|
78
|
+
// Use file command to get basic type
|
|
79
|
+
const { stdout: fileInfo } = await execAsync(`file "${filePath}"`);
|
|
80
|
+
const info = fileInfo.toLowerCase();
|
|
81
|
+
|
|
82
|
+
// Determine file type
|
|
83
|
+
let fileType: FileAnalysis["fileType"] = "unknown";
|
|
84
|
+
let hasDynamicSymbols = false;
|
|
85
|
+
|
|
86
|
+
// ELF shared libraries and dynamically linked executables
|
|
87
|
+
if (info.includes("elf")) {
|
|
88
|
+
if (info.includes("shared object") || info.includes("dynamically linked")) {
|
|
89
|
+
fileType = info.includes("shared object") ? "elf-shared" : "elf-executable";
|
|
90
|
+
hasDynamicSymbols = true;
|
|
91
|
+
} else if (info.includes("statically linked")) {
|
|
92
|
+
fileType = "elf-executable";
|
|
93
|
+
hasDynamicSymbols = false;
|
|
94
|
+
} else if (info.includes("relocatable")) {
|
|
95
|
+
fileType = "object-file";
|
|
96
|
+
hasDynamicSymbols = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Mach-O (macOS)
|
|
100
|
+
else if (info.includes("mach-o")) {
|
|
101
|
+
if (info.includes("dynamically linked") || info.includes("shared library") || info.includes("dylib")) {
|
|
102
|
+
fileType = "mach-o";
|
|
103
|
+
hasDynamicSymbols = true;
|
|
104
|
+
} else if (info.includes("bundle") || info.includes("statically linked")) {
|
|
105
|
+
fileType = "mach-o";
|
|
106
|
+
hasDynamicSymbols = false;
|
|
107
|
+
} else {
|
|
108
|
+
// Most Mach-O executables on macOS are dynamically linked
|
|
109
|
+
fileType = "mach-o";
|
|
110
|
+
hasDynamicSymbols = info.includes("executable");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Static libraries (.a files)
|
|
114
|
+
else if (info.includes("ar archive") || info.includes("current ar archive")) {
|
|
115
|
+
fileType = "static-library";
|
|
116
|
+
hasDynamicSymbols = false;
|
|
117
|
+
}
|
|
118
|
+
// Object files
|
|
119
|
+
else if (info.includes("relocatable")) {
|
|
120
|
+
fileType = "object-file";
|
|
121
|
+
hasDynamicSymbols = false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
hasDynamicSymbols,
|
|
126
|
+
fileType,
|
|
127
|
+
format: fileInfo.trim(),
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return {
|
|
131
|
+
hasDynamicSymbols: false,
|
|
132
|
+
fileType: "unknown",
|
|
133
|
+
format: "Unable to determine file type",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
57
138
|
// ============================================================================
|
|
58
139
|
// nm execution
|
|
59
140
|
// ============================================================================
|
|
@@ -677,6 +758,46 @@ async function handleDefinedSymbols(args: { filePath: string; demangle?: boolean
|
|
|
677
758
|
}
|
|
678
759
|
|
|
679
760
|
async function handleDynamicSymbols(args: { filePath: string; demangle?: boolean; definedOnly?: boolean }) {
|
|
761
|
+
// Check if file supports dynamic symbols before running nm -D
|
|
762
|
+
const analysis = await analyzeFile(args.filePath);
|
|
763
|
+
|
|
764
|
+
if (!analysis.hasDynamicSymbols) {
|
|
765
|
+
const suggestions: string[] = [];
|
|
766
|
+
|
|
767
|
+
switch (analysis.fileType) {
|
|
768
|
+
case "static-library":
|
|
769
|
+
suggestions.push("Static libraries (.a) contain object files without dynamic symbols.");
|
|
770
|
+
suggestions.push("Use nm_list_symbols or nm_defined_symbols to see available symbols.");
|
|
771
|
+
break;
|
|
772
|
+
case "object-file":
|
|
773
|
+
suggestions.push("Object files (.o) are not linked and have no dynamic symbol table.");
|
|
774
|
+
suggestions.push("Use nm_list_symbols to see symbols in this object file.");
|
|
775
|
+
break;
|
|
776
|
+
case "elf-executable":
|
|
777
|
+
suggestions.push("This appears to be a statically linked executable.");
|
|
778
|
+
suggestions.push("Use nm_list_symbols or nm_defined_symbols instead.");
|
|
779
|
+
break;
|
|
780
|
+
default:
|
|
781
|
+
suggestions.push("This file type does not have a dynamic symbol table.");
|
|
782
|
+
suggestions.push("Try nm_list_symbols or nm_defined_symbols instead.");
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return {
|
|
786
|
+
content: [{
|
|
787
|
+
type: "text",
|
|
788
|
+
text: [
|
|
789
|
+
`Error: File has no dynamic symbol table`,
|
|
790
|
+
"",
|
|
791
|
+
`File: ${args.filePath}`,
|
|
792
|
+
`Type: ${analysis.format}`,
|
|
793
|
+
"",
|
|
794
|
+
...suggestions,
|
|
795
|
+
].join("\n"),
|
|
796
|
+
}],
|
|
797
|
+
isError: true,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
680
801
|
const result = await runNm(args.filePath, {
|
|
681
802
|
dynamicSymbols: true,
|
|
682
803
|
demangle: args.demangle ?? true,
|
|
@@ -685,6 +806,7 @@ async function handleDynamicSymbols(args: { filePath: string; demangle?: boolean
|
|
|
685
806
|
|
|
686
807
|
const summary = [
|
|
687
808
|
`File: ${result.filePath}`,
|
|
809
|
+
`File type: ${analysis.format}`,
|
|
688
810
|
`Dynamic symbols: ${result.totalCount}`,
|
|
689
811
|
"",
|
|
690
812
|
"Symbols:",
|
|
@@ -852,6 +974,705 @@ async function handleSummary(args: { filePath: string }) {
|
|
|
852
974
|
return { content: [{ type: "text", text: summary }] };
|
|
853
975
|
}
|
|
854
976
|
|
|
977
|
+
// ============================================================================
|
|
978
|
+
// Extended Binary Analysis Tools
|
|
979
|
+
// ============================================================================
|
|
980
|
+
|
|
981
|
+
// --- strings extraction ---
|
|
982
|
+
async function handleStrings(args: {
|
|
983
|
+
filePath: string;
|
|
984
|
+
minLength?: number;
|
|
985
|
+
encoding?: "ascii" | "unicode" | "all";
|
|
986
|
+
}) {
|
|
987
|
+
const minLen = args.minLength ?? 4;
|
|
988
|
+
const encFlags = {
|
|
989
|
+
ascii: "-a",
|
|
990
|
+
unicode: "-e l",
|
|
991
|
+
all: "-a -e l",
|
|
992
|
+
};
|
|
993
|
+
const encFlag = encFlags[args.encoding ?? "all"];
|
|
994
|
+
|
|
995
|
+
try {
|
|
996
|
+
const { stdout } = await execAsync(
|
|
997
|
+
`strings ${encFlag} -n ${minLen} "${args.filePath}"`,
|
|
998
|
+
{ maxBuffer: 50 * 1024 * 1024 }
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
const strings = stdout.trim().split("\n").filter(Boolean);
|
|
1002
|
+
const summary = [
|
|
1003
|
+
`Strings in: ${args.filePath}`,
|
|
1004
|
+
`Encoding: ${args.encoding ?? "all"}`,
|
|
1005
|
+
`Minimum length: ${minLen}`,
|
|
1006
|
+
`Total strings: ${strings.length}`,
|
|
1007
|
+
"",
|
|
1008
|
+
"Strings:",
|
|
1009
|
+
...strings.slice(0, 200).map((s) => ` ${s}`),
|
|
1010
|
+
strings.length > 200 ? ` ... and ${strings.length - 200} more` : "",
|
|
1011
|
+
].join("\n");
|
|
1012
|
+
|
|
1013
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
throw new Error(`strings command failed: ${error instanceof Error ? error.message : error}`);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// --- file type with MIME ---
|
|
1020
|
+
async function handleFileInfo(args: { filePath: string }) {
|
|
1021
|
+
try {
|
|
1022
|
+
const { stdout: mimeInfo } = await execAsync(
|
|
1023
|
+
`file --mime --brief "${args.filePath}"`
|
|
1024
|
+
);
|
|
1025
|
+
const { stdout: detailedInfo } = await execAsync(
|
|
1026
|
+
`file "${args.filePath}"`
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
const summary = [
|
|
1030
|
+
`File: ${args.filePath}`,
|
|
1031
|
+
"",
|
|
1032
|
+
"MIME Type:",
|
|
1033
|
+
` ${mimeInfo.trim()}`,
|
|
1034
|
+
"",
|
|
1035
|
+
"Detailed Type:",
|
|
1036
|
+
` ${detailedInfo.trim()}`,
|
|
1037
|
+
].join("\n");
|
|
1038
|
+
|
|
1039
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
throw new Error(`file command failed: ${error instanceof Error ? error.message : error}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// --- section sizes (size command) ---
|
|
1046
|
+
async function handleSectionSizes(args: { filePath: string }) {
|
|
1047
|
+
try {
|
|
1048
|
+
const { stdout } = await execAsync(`size -A -x "${args.filePath}"`);
|
|
1049
|
+
const lines = stdout.trim().split("\n");
|
|
1050
|
+
|
|
1051
|
+
const summary = [
|
|
1052
|
+
`Section Sizes: ${args.filePath}`,
|
|
1053
|
+
"",
|
|
1054
|
+
...lines.map((l) => ` ${l}`),
|
|
1055
|
+
].join("\n");
|
|
1056
|
+
|
|
1057
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
// Try alternative format
|
|
1060
|
+
try {
|
|
1061
|
+
const { stdout } = await execAsync(`size -m "${args.filePath}"`);
|
|
1062
|
+
return { content: [{ type: "text", text: `Section Sizes:\n${stdout}` }] };
|
|
1063
|
+
} catch {
|
|
1064
|
+
throw new Error(`size command failed: ${error instanceof Error ? error.message : error}`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// --- objdump section headers ---
|
|
1070
|
+
async function handleObjdumpSections(args: { filePath: string }) {
|
|
1071
|
+
try {
|
|
1072
|
+
const { stdout } = await execAsync(`objdump -h "${args.filePath}"`, {
|
|
1073
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
const summary = [
|
|
1077
|
+
`Section Headers: ${args.filePath}`,
|
|
1078
|
+
"",
|
|
1079
|
+
stdout,
|
|
1080
|
+
].join("\n");
|
|
1081
|
+
|
|
1082
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
throw new Error(`objdump -h failed: ${error instanceof Error ? error.message : error}`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// --- objdump program headers / DLL imports ---
|
|
1089
|
+
async function handleObjdumpProgramHeaders(args: { filePath: string }) {
|
|
1090
|
+
try {
|
|
1091
|
+
const { stdout } = await execAsync(`objdump -p "${args.filePath}"`, {
|
|
1092
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
const summary = [
|
|
1096
|
+
`Program Headers / Imports: ${args.filePath}`,
|
|
1097
|
+
"",
|
|
1098
|
+
stdout,
|
|
1099
|
+
].join("\n");
|
|
1100
|
+
|
|
1101
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
throw new Error(`objdump -p failed: ${error instanceof Error ? error.message : error}`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// --- Mach-O dynamic libraries (otool -L) ---
|
|
1108
|
+
async function handleOtoolLibs(args: { filePath: string }) {
|
|
1109
|
+
try {
|
|
1110
|
+
const { stdout } = await execAsync(`otool -L "${args.filePath}"`);
|
|
1111
|
+
const lines = stdout.trim().split("\n");
|
|
1112
|
+
|
|
1113
|
+
const summary = [
|
|
1114
|
+
`Dynamic Library Dependencies: ${args.filePath}`,
|
|
1115
|
+
"",
|
|
1116
|
+
...lines.map((l) => ` ${l}`),
|
|
1117
|
+
].join("\n");
|
|
1118
|
+
|
|
1119
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
throw new Error(`otool -L failed (macOS only): ${error instanceof Error ? error.message : error}`);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// --- readelf full analysis ---
|
|
1126
|
+
async function handleReadelf(args: {
|
|
1127
|
+
filePath: string;
|
|
1128
|
+
sections?: ("headers" | "sections" | "segments" | "symbols" | "dynamic" | "all")[];
|
|
1129
|
+
}) {
|
|
1130
|
+
const sections = args.sections ?? ["all"];
|
|
1131
|
+
const flags: string[] = [];
|
|
1132
|
+
|
|
1133
|
+
if (sections.includes("all")) {
|
|
1134
|
+
flags.push("-a");
|
|
1135
|
+
} else {
|
|
1136
|
+
if (sections.includes("headers")) flags.push("-h");
|
|
1137
|
+
if (sections.includes("sections")) flags.push("-S");
|
|
1138
|
+
if (sections.includes("segments")) flags.push("-l");
|
|
1139
|
+
if (sections.includes("symbols")) flags.push("-s");
|
|
1140
|
+
if (sections.includes("dynamic")) flags.push("-d");
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
try {
|
|
1144
|
+
const { stdout } = await execAsync(`readelf ${flags.join(" ")} "${args.filePath}"`, {
|
|
1145
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
const summary = [
|
|
1149
|
+
`ELF Analysis: ${args.filePath}`,
|
|
1150
|
+
`Sections: ${sections.join(", ")}`,
|
|
1151
|
+
"",
|
|
1152
|
+
stdout.slice(0, 50000),
|
|
1153
|
+
stdout.length > 50000 ? `\n... truncated (${stdout.length - 50000} more chars)` : "",
|
|
1154
|
+
].join("\n");
|
|
1155
|
+
|
|
1156
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
throw new Error(`readelf failed (Linux ELF only): ${error instanceof Error ? error.message : error}`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// --- ldd shared library dependencies ---
|
|
1163
|
+
async function handleLdd(args: { filePath: string }) {
|
|
1164
|
+
try {
|
|
1165
|
+
const { stdout } = await execAsync(`ldd "${args.filePath}"`);
|
|
1166
|
+
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
1167
|
+
|
|
1168
|
+
const summary = [
|
|
1169
|
+
`Shared Library Dependencies: ${args.filePath}`,
|
|
1170
|
+
"",
|
|
1171
|
+
...lines.map((l) => ` ${l}`),
|
|
1172
|
+
].join("\n");
|
|
1173
|
+
|
|
1174
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
throw new Error(`ldd failed (Linux only): ${error instanceof Error ? error.message : error}`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// --- Disassembly (objdump -d) ---
|
|
1181
|
+
async function handleDisassembly(args: {
|
|
1182
|
+
filePath: string;
|
|
1183
|
+
symbol?: string;
|
|
1184
|
+
startOffset?: number;
|
|
1185
|
+
length?: number;
|
|
1186
|
+
}) {
|
|
1187
|
+
let cmd = "objdump -d";
|
|
1188
|
+
|
|
1189
|
+
if (args.symbol) {
|
|
1190
|
+
cmd += ` --disassemble="${args.symbol}"`;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
cmd += ` "${args.filePath}"`;
|
|
1194
|
+
|
|
1195
|
+
try {
|
|
1196
|
+
const { stdout } = await execAsync(cmd, {
|
|
1197
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
let output = stdout;
|
|
1201
|
+
if (args.startOffset !== undefined || args.length !== undefined) {
|
|
1202
|
+
const lines = stdout.split("\n");
|
|
1203
|
+
const start = args.startOffset ?? 0;
|
|
1204
|
+
const len = args.length ?? 500;
|
|
1205
|
+
output = lines.slice(start, start + len).join("\n");
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
const summary = [
|
|
1209
|
+
`Disassembly: ${args.filePath}`,
|
|
1210
|
+
args.symbol ? `Symbol: ${args.symbol}` : "",
|
|
1211
|
+
"",
|
|
1212
|
+
output.slice(0, 100000),
|
|
1213
|
+
output.length > 100000 ? `\n... truncated (${output.length - 100000} more chars)` : "",
|
|
1214
|
+
].filter(Boolean).join("\n");
|
|
1215
|
+
|
|
1216
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
throw new Error(`objdump -d failed: ${error instanceof Error ? error.message : error}`);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// --- Security audit ---
|
|
1223
|
+
async function handleSecurityAudit(args: { filePath: string }) {
|
|
1224
|
+
const results: { check: string; status: string; details: string }[] = [];
|
|
1225
|
+
|
|
1226
|
+
try {
|
|
1227
|
+
const { stdout: fileInfo } = await execAsync(`file "${args.filePath}"`);
|
|
1228
|
+
const info = fileInfo.toLowerCase();
|
|
1229
|
+
|
|
1230
|
+
// PIE (Position Independent Executable)
|
|
1231
|
+
if (info.includes("pie") || info.includes("position independent")) {
|
|
1232
|
+
results.push({ check: "PIE", status: "ENABLED", details: "Position Independent Executable" });
|
|
1233
|
+
} else if (info.includes("executable")) {
|
|
1234
|
+
results.push({ check: "PIE", status: "DISABLED", details: "Not a PIE binary" });
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Check with readelf for RELRO (Linux)
|
|
1238
|
+
try {
|
|
1239
|
+
const { stdout: relro } = await execAsync(`readelf -l "${args.filePath}" 2>/dev/null | grep -i gnu_relro`);
|
|
1240
|
+
if (relro.trim()) {
|
|
1241
|
+
results.push({ check: "RELRO", status: "ENABLED", details: "Read-Only relocations" });
|
|
1242
|
+
}
|
|
1243
|
+
} catch {
|
|
1244
|
+
// Not Linux or no RELRO
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Check for stack canary
|
|
1248
|
+
try {
|
|
1249
|
+
const { stdout: canary } = await execAsync(
|
|
1250
|
+
`nm "${args.filePath}" 2>/dev/null | grep -i "__stack_chk_fail"`
|
|
1251
|
+
);
|
|
1252
|
+
if (canary.trim()) {
|
|
1253
|
+
results.push({ check: "Stack Canary", status: "ENABLED", details: "Stack smashing detected symbol found" });
|
|
1254
|
+
} else {
|
|
1255
|
+
results.push({ check: "Stack Canary", status: "UNKNOWN", details: "No stack canary symbol found" });
|
|
1256
|
+
}
|
|
1257
|
+
} catch {
|
|
1258
|
+
results.push({ check: "Stack Canary", status: "UNKNOWN", details: "Could not check" });
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// NX bit (non-executable stack)
|
|
1262
|
+
try {
|
|
1263
|
+
const { stdout: nx } = await execAsync(
|
|
1264
|
+
`readelf -l "${args.filePath}" 2>/dev/null | grep -i "gnu_stack"`
|
|
1265
|
+
);
|
|
1266
|
+
if (nx.toLowerCase().includes("rwe")) {
|
|
1267
|
+
results.push({ check: "NX", status: "DISABLED", details: "Stack is executable (RWE)" });
|
|
1268
|
+
} else if (nx.trim()) {
|
|
1269
|
+
results.push({ check: "NX", status: "ENABLED", details: "Non-executable stack" });
|
|
1270
|
+
}
|
|
1271
|
+
} catch {
|
|
1272
|
+
// Not Linux
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// FORTIFY_SOURCE
|
|
1276
|
+
try {
|
|
1277
|
+
const { stdout: fortify } = await execAsync(
|
|
1278
|
+
`nm "${args.filePath}" 2>/dev/null | grep -i "_chk@"`
|
|
1279
|
+
);
|
|
1280
|
+
if (fortify.trim()) {
|
|
1281
|
+
results.push({ check: "FORTIFY", status: "ENABLED", details: "Fortified functions detected" });
|
|
1282
|
+
}
|
|
1283
|
+
} catch {
|
|
1284
|
+
// Not applicable
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// macOS specific: check with otool
|
|
1288
|
+
if (info.includes("mach-o")) {
|
|
1289
|
+
try {
|
|
1290
|
+
const { stdout: loadCmds } = await execAsync(`otool -l "${args.filePath}"`);
|
|
1291
|
+
if (loadCmds.toLowerCase().includes("lc_main")) {
|
|
1292
|
+
results.push({ check: "PIE", status: "ENABLED", details: "Mach-O with LC_MAIN (likely PIE)" });
|
|
1293
|
+
}
|
|
1294
|
+
} catch {
|
|
1295
|
+
// Ignore
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
results.push({ check: "Error", status: "FAILED", details: error instanceof Error ? error.message : String(error) });
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const summary = [
|
|
1304
|
+
`Security Audit: ${args.filePath}`,
|
|
1305
|
+
"",
|
|
1306
|
+
"Security Features:",
|
|
1307
|
+
...results.map((r) => ` ${r.check}: ${r.status} - ${r.details}`),
|
|
1308
|
+
"",
|
|
1309
|
+
"Recommendations:",
|
|
1310
|
+
...results
|
|
1311
|
+
.filter((r) => r.status === "DISABLED" || r.status === "UNKNOWN")
|
|
1312
|
+
.map((r) => ` - Consider enabling ${r.check}`),
|
|
1313
|
+
].join("\n");
|
|
1314
|
+
|
|
1315
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// --- Section entropy (detect packed/encrypted) ---
|
|
1319
|
+
async function handleEntropyAnalysis(args: { filePath: string; blockSize?: number }) {
|
|
1320
|
+
const blockSize = args.blockSize ?? 1024;
|
|
1321
|
+
|
|
1322
|
+
try {
|
|
1323
|
+
// Read file and calculate entropy
|
|
1324
|
+
const { stdout: hexData } = await execAsync(`xxd -p "${args.filePath}" | tr -d '\\n'`);
|
|
1325
|
+
const bytes = hexData.match(/.{2}/g)?.map((h) => parseInt(h, 16)) ?? [];
|
|
1326
|
+
|
|
1327
|
+
const results: { section: string; offset: number; entropy: number; status: string }[] = [];
|
|
1328
|
+
|
|
1329
|
+
for (let i = 0; i < bytes.length; i += blockSize) {
|
|
1330
|
+
const block = bytes.slice(i, i + blockSize);
|
|
1331
|
+
if (block.length === 0) continue;
|
|
1332
|
+
|
|
1333
|
+
// Calculate Shannon entropy
|
|
1334
|
+
const freq: Record<number, number> = {};
|
|
1335
|
+
for (const byte of block) {
|
|
1336
|
+
freq[byte] = (freq[byte] ?? 0) + 1;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
let entropy = 0;
|
|
1340
|
+
for (const count of Object.values(freq)) {
|
|
1341
|
+
const p = count / block.length;
|
|
1342
|
+
entropy -= p * Math.log2(p);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// Max entropy for byte is 8
|
|
1346
|
+
const normalizedEntropy = entropy / 8;
|
|
1347
|
+
let status = "normal";
|
|
1348
|
+
if (normalizedEntropy > 0.95) status = "high (possibly encrypted/compressed)";
|
|
1349
|
+
else if (normalizedEntropy < 0.3) status = "low (possibly padding/zeros)";
|
|
1350
|
+
|
|
1351
|
+
results.push({
|
|
1352
|
+
section: `Block ${Math.floor(i / blockSize)}`,
|
|
1353
|
+
offset: i,
|
|
1354
|
+
entropy: Math.round(entropy * 100) / 100,
|
|
1355
|
+
status,
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const avgEntropy = results.reduce((a, b) => a + b.entropy, 0) / results.length;
|
|
1360
|
+
|
|
1361
|
+
const summary = [
|
|
1362
|
+
`Entropy Analysis: ${args.filePath}`,
|
|
1363
|
+
`Block size: ${blockSize} bytes`,
|
|
1364
|
+
`Total blocks: ${results.length}`,
|
|
1365
|
+
`Average entropy: ${(avgEntropy / 8 * 100).toFixed(1)}% of max`,
|
|
1366
|
+
"",
|
|
1367
|
+
"Blocks with unusual entropy:",
|
|
1368
|
+
...results
|
|
1369
|
+
.filter((r) => r.status !== "normal")
|
|
1370
|
+
.slice(0, 50)
|
|
1371
|
+
.map((r) => ` ${r.section} (offset ${r.offset}): ${r.entropy}/8 bits - ${r.status}`),
|
|
1372
|
+
results.filter((r) => r.status !== "normal").length > 50
|
|
1373
|
+
? ` ... and ${results.filter((r) => r.status !== "normal").length - 50} more`
|
|
1374
|
+
: "",
|
|
1375
|
+
].join("\n");
|
|
1376
|
+
|
|
1377
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1378
|
+
} catch (error) {
|
|
1379
|
+
throw new Error(`Entropy analysis failed: ${error instanceof Error ? error.message : error}`);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// --- Import/Export tables ---
|
|
1384
|
+
async function handleImportExport(args: { filePath: string }) {
|
|
1385
|
+
const analysis = await analyzeFile(args.filePath);
|
|
1386
|
+
const imports: string[] = [];
|
|
1387
|
+
const exports: string[] = [];
|
|
1388
|
+
|
|
1389
|
+
try {
|
|
1390
|
+
if (analysis.format.toLowerCase().includes("mach-o")) {
|
|
1391
|
+
// macOS: use nm and otool
|
|
1392
|
+
const { stdout: importSyms } = await execAsync(
|
|
1393
|
+
`nm -u "${args.filePath}" 2>/dev/null | awk '{print $2}'`
|
|
1394
|
+
);
|
|
1395
|
+
imports.push(...importSyms.trim().split("\n").filter(Boolean));
|
|
1396
|
+
|
|
1397
|
+
const { stdout: exportSyms } = await execAsync(
|
|
1398
|
+
`nm -g "${args.filePath}" 2>/dev/null | grep -v "U " | awk '{print $3}'`
|
|
1399
|
+
);
|
|
1400
|
+
exports.push(...exportSyms.trim().split("\n").filter(Boolean));
|
|
1401
|
+
} else {
|
|
1402
|
+
// Linux ELF: use nm and objdump
|
|
1403
|
+
const { stdout: importSyms } = await execAsync(
|
|
1404
|
+
`nm -D -u "${args.filePath}" 2>/dev/null | awk '{print $2}'`
|
|
1405
|
+
);
|
|
1406
|
+
imports.push(...importSyms.trim().split("\n").filter(Boolean));
|
|
1407
|
+
|
|
1408
|
+
const { stdout: exportSyms } = await execAsync(
|
|
1409
|
+
`nm -D --defined-only "${args.filePath}" 2>/dev/null | awk '{print $3}'`
|
|
1410
|
+
);
|
|
1411
|
+
exports.push(...exportSyms.trim().split("\n").filter(Boolean));
|
|
1412
|
+
}
|
|
1413
|
+
} catch {
|
|
1414
|
+
// Fallback to regular nm
|
|
1415
|
+
try {
|
|
1416
|
+
const { stdout: allSyms } = await execAsync(`nm "${args.filePath}" 2>/dev/null`);
|
|
1417
|
+
for (const line of allSyms.split("\n")) {
|
|
1418
|
+
const match = line.match(/^\s*([0-9a-fA-F]+)?\s+([UTDDBRC])\s+(.+)/);
|
|
1419
|
+
if (match) {
|
|
1420
|
+
if (match[2] === "U") imports.push(match[3]);
|
|
1421
|
+
else if (match[1] && match[2] === match[2].toUpperCase()) exports.push(match[3]);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
} catch {
|
|
1425
|
+
// Ignore
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
const summary = [
|
|
1430
|
+
`Import/Export Analysis: ${args.filePath}`,
|
|
1431
|
+
`File type: ${analysis.format}`,
|
|
1432
|
+
"",
|
|
1433
|
+
`Imports (${imports.length}):`,
|
|
1434
|
+
...imports.slice(0, 100).map((s) => ` - ${s}`),
|
|
1435
|
+
imports.length > 100 ? ` ... and ${imports.length - 100} more` : "",
|
|
1436
|
+
"",
|
|
1437
|
+
`Exports (${exports.length}):`,
|
|
1438
|
+
...exports.slice(0, 100).map((s) => ` + ${s}`),
|
|
1439
|
+
exports.length > 100 ? ` ... and ${exports.length - 100} more` : "",
|
|
1440
|
+
].join("\n");
|
|
1441
|
+
|
|
1442
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// --- Binary diff ---
|
|
1446
|
+
async function handleBinaryDiff(args: {
|
|
1447
|
+
file1: string;
|
|
1448
|
+
file2: string;
|
|
1449
|
+
contextBytes?: number;
|
|
1450
|
+
}) {
|
|
1451
|
+
const contextBytes = args.contextBytes ?? 32;
|
|
1452
|
+
|
|
1453
|
+
try {
|
|
1454
|
+
const { stdout: hex1 } = await execAsync(`xxd -p "${args.file1}" | tr -d '\\n'`);
|
|
1455
|
+
const { stdout: hex2 } = await execAsync(`xxd -p "${args.file2}" | tr -d '\\n'`);
|
|
1456
|
+
|
|
1457
|
+
const bytes1 = hex1.match(/.{2}/g) ?? [];
|
|
1458
|
+
const bytes2 = hex2.match(/.{2}/g) ?? [];
|
|
1459
|
+
|
|
1460
|
+
const maxLen = Math.max(bytes1.length, bytes2.length);
|
|
1461
|
+
const diffs: { offset: number; file1: string; file2: string }[] = [];
|
|
1462
|
+
|
|
1463
|
+
for (let i = 0; i < maxLen; i++) {
|
|
1464
|
+
const b1 = bytes1[i] ?? "??";
|
|
1465
|
+
const b2 = bytes2[i] ?? "??";
|
|
1466
|
+
if (b1 !== b2) {
|
|
1467
|
+
diffs.push({ offset: i, file1: b1, file2: b2 });
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
const diffPercent = ((diffs.length / maxLen) * 100).toFixed(2);
|
|
1472
|
+
|
|
1473
|
+
const summary = [
|
|
1474
|
+
`Binary Diff: ${args.file1} vs ${args.file2}`,
|
|
1475
|
+
"",
|
|
1476
|
+
`File 1 size: ${bytes1.length} bytes`,
|
|
1477
|
+
`File 2 size: ${bytes2.length} bytes`,
|
|
1478
|
+
`Differences: ${diffs.length} bytes (${diffPercent}%)`,
|
|
1479
|
+
"",
|
|
1480
|
+
"Differences (first 200):",
|
|
1481
|
+
...diffs.slice(0, 200).map((d) => {
|
|
1482
|
+
const ctx1 = bytes1.slice(Math.max(0, d.offset - 4), d.offset + 5).join(" ");
|
|
1483
|
+
const ctx2 = bytes2.slice(Math.max(0, d.offset - 4), d.offset + 5).join(" ");
|
|
1484
|
+
return ` Offset 0x${d.offset.toString(16).padStart(8, "0")}: ${d.file1} -> ${d.file2}\n Context: [${ctx1}] -> [${ctx2}]`;
|
|
1485
|
+
}),
|
|
1486
|
+
diffs.length > 200 ? ` ... and ${diffs.length - 200} more differences` : "",
|
|
1487
|
+
].join("\n");
|
|
1488
|
+
|
|
1489
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
throw new Error(`Binary diff failed: ${error instanceof Error ? error.message : error}`);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// --- Archive extraction ---
|
|
1496
|
+
async function handleArchiveExtract(args: {
|
|
1497
|
+
filePath: string;
|
|
1498
|
+
outputDir?: string;
|
|
1499
|
+
listOnly?: boolean;
|
|
1500
|
+
}) {
|
|
1501
|
+
const outputDir = args.outputDir ?? `/tmp/archive_${Date.now()}`;
|
|
1502
|
+
|
|
1503
|
+
try {
|
|
1504
|
+
// Check if it's an archive
|
|
1505
|
+
const { stdout: fileInfo } = await execAsync(`file "${args.filePath}"`);
|
|
1506
|
+
if (!fileInfo.toLowerCase().includes("ar archive") && !fileInfo.toLowerCase().includes("archive")) {
|
|
1507
|
+
return {
|
|
1508
|
+
content: [{ type: "text", text: `Error: ${args.filePath} is not an archive file.\n${fileInfo}` }],
|
|
1509
|
+
isError: true,
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// List contents
|
|
1514
|
+
const { stdout: contents } = await execAsync(`ar -t "${args.filePath}"`);
|
|
1515
|
+
const files = contents.trim().split("\n").filter(Boolean);
|
|
1516
|
+
|
|
1517
|
+
if (args.listOnly) {
|
|
1518
|
+
return {
|
|
1519
|
+
content: [{
|
|
1520
|
+
type: "text",
|
|
1521
|
+
text: [
|
|
1522
|
+
`Archive Contents: ${args.filePath}`,
|
|
1523
|
+
`Total files: ${files.length}`,
|
|
1524
|
+
"",
|
|
1525
|
+
"Files:",
|
|
1526
|
+
...files.map((f) => ` - ${f}`),
|
|
1527
|
+
].join("\n"),
|
|
1528
|
+
}],
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Extract
|
|
1533
|
+
await execAsync(`mkdir -p "${outputDir}"`);
|
|
1534
|
+
await execAsync(`cd "${outputDir}" && ar -x "${args.filePath}"`);
|
|
1535
|
+
|
|
1536
|
+
const summary = [
|
|
1537
|
+
`Archive Extracted: ${args.filePath}`,
|
|
1538
|
+
`Output directory: ${outputDir}`,
|
|
1539
|
+
`Files extracted: ${files.length}`,
|
|
1540
|
+
"",
|
|
1541
|
+
"Extracted files:",
|
|
1542
|
+
...files.map((f) => ` - ${f}`),
|
|
1543
|
+
].join("\n");
|
|
1544
|
+
|
|
1545
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1546
|
+
} catch (error) {
|
|
1547
|
+
throw new Error(`Archive extraction failed: ${error instanceof Error ? error.message : error}`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// ============================================================================
|
|
1552
|
+
// Binary Patching Tools (DESTRUCTIVE - use with caution)
|
|
1553
|
+
// ============================================================================
|
|
1554
|
+
|
|
1555
|
+
// --- Patch bytes ---
|
|
1556
|
+
async function handlePatchBytes(args: {
|
|
1557
|
+
filePath: string;
|
|
1558
|
+
offset: number;
|
|
1559
|
+
hexData: string;
|
|
1560
|
+
createBackup?: boolean;
|
|
1561
|
+
}) {
|
|
1562
|
+
const backup = args.createBackup ?? true;
|
|
1563
|
+
|
|
1564
|
+
try {
|
|
1565
|
+
// Validate hex data
|
|
1566
|
+
const hex = args.hexData.replace(/\s/g, "");
|
|
1567
|
+
if (!/^[0-9a-fA-F]*$/.test(hex) || hex.length % 2 !== 0) {
|
|
1568
|
+
throw new Error("Invalid hex data. Must be even number of hex characters.");
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// Create backup if requested
|
|
1572
|
+
if (backup) {
|
|
1573
|
+
await execAsync(`cp "${args.filePath}" "${args.filePath}.bak"`);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// Convert hex to bytes and patch
|
|
1577
|
+
const bytes = hex.match(/.{2}/g)?.map((h) => parseInt(h, 16)) ?? [];
|
|
1578
|
+
|
|
1579
|
+
// Use printf and dd to patch
|
|
1580
|
+
const hexBytes = bytes.map((b) => `\\x${b.toString(16).padStart(2, "0")}`).join("");
|
|
1581
|
+
await execAsync(
|
|
1582
|
+
`printf '${hexBytes}' | dd of="${args.filePath}" bs=1 seek=${args.offset} conv=notrunc 2>/dev/null`
|
|
1583
|
+
);
|
|
1584
|
+
|
|
1585
|
+
const summary = [
|
|
1586
|
+
`Bytes Patched: ${args.filePath}`,
|
|
1587
|
+
`Offset: 0x${args.offset.toString(16)} (${args.offset})`,
|
|
1588
|
+
`Bytes written: ${bytes.length}`,
|
|
1589
|
+
`Hex data: ${hex}`,
|
|
1590
|
+
backup ? `Backup created: ${args.filePath}.bak` : "WARNING: No backup created",
|
|
1591
|
+
].join("\n");
|
|
1592
|
+
|
|
1593
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1594
|
+
} catch (error) {
|
|
1595
|
+
throw new Error(`Patch failed: ${error instanceof Error ? error.message : error}`);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// --- NOP sled ---
|
|
1600
|
+
async function handleNopSled(args: {
|
|
1601
|
+
filePath: string;
|
|
1602
|
+
offset: number;
|
|
1603
|
+
count: number;
|
|
1604
|
+
createBackup?: boolean;
|
|
1605
|
+
}) {
|
|
1606
|
+
const backup = args.createBackup ?? true;
|
|
1607
|
+
const nopHex = "90".repeat(args.count); // x86 NOP
|
|
1608
|
+
|
|
1609
|
+
return handlePatchBytes({
|
|
1610
|
+
filePath: args.filePath,
|
|
1611
|
+
offset: args.offset,
|
|
1612
|
+
hexData: nopHex,
|
|
1613
|
+
createBackup: backup,
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// --- Hex editor mode (read/modify at offset) ---
|
|
1618
|
+
async function handleHexEditor(args: {
|
|
1619
|
+
filePath: string;
|
|
1620
|
+
offset: number;
|
|
1621
|
+
length: number;
|
|
1622
|
+
newHex?: string;
|
|
1623
|
+
}) {
|
|
1624
|
+
try {
|
|
1625
|
+
// Read current bytes
|
|
1626
|
+
const { stdout: currentHex } = await execAsync(
|
|
1627
|
+
`xxd -s ${args.offset} -l ${args.length} -p "${args.filePath}" | tr -d '\\n'`
|
|
1628
|
+
);
|
|
1629
|
+
|
|
1630
|
+
if (args.newHex) {
|
|
1631
|
+
// Modify
|
|
1632
|
+
const hex = args.newHex.replace(/\s/g, "");
|
|
1633
|
+
if (!/^[0-9a-fA-F]*$/.test(hex)) {
|
|
1634
|
+
throw new Error("Invalid hex data.");
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
await execAsync(`cp "${args.filePath}" "${args.filePath}.bak"`);
|
|
1638
|
+
const bytes = hex.match(/.{2}/g)?.map((h) => parseInt(h, 16)) ?? [];
|
|
1639
|
+
const hexBytes = bytes.map((b) => `\\x${b.toString(16).padStart(2, "0")}`).join("");
|
|
1640
|
+
await execAsync(
|
|
1641
|
+
`printf '${hexBytes}' | dd of="${args.filePath}" bs=1 seek=${args.offset} conv=notrunc 2>/dev/null`
|
|
1642
|
+
);
|
|
1643
|
+
|
|
1644
|
+
return {
|
|
1645
|
+
content: [{
|
|
1646
|
+
type: "text",
|
|
1647
|
+
text: [
|
|
1648
|
+
`Hex Editor: ${args.filePath}`,
|
|
1649
|
+
`Offset: 0x${args.offset.toString(16)}`,
|
|
1650
|
+
"",
|
|
1651
|
+
`Previous: ${currentHex}`,
|
|
1652
|
+
`New: ${hex}`,
|
|
1653
|
+
`Backup: ${args.filePath}.bak`,
|
|
1654
|
+
].join("\n"),
|
|
1655
|
+
}],
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// Read only
|
|
1660
|
+
const summary = [
|
|
1661
|
+
`Hex Editor (read mode): ${args.filePath}`,
|
|
1662
|
+
`Offset: 0x${args.offset.toString(16)} (${args.offset})`,
|
|
1663
|
+
`Length: ${args.length} bytes`,
|
|
1664
|
+
"",
|
|
1665
|
+
`Hex: ${currentHex}`,
|
|
1666
|
+
"",
|
|
1667
|
+
"To modify, provide newHex parameter",
|
|
1668
|
+
].join("\n");
|
|
1669
|
+
|
|
1670
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
throw new Error(`Hex editor failed: ${error instanceof Error ? error.message : error}`);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
855
1676
|
// ============================================================================
|
|
856
1677
|
// Tool definitions
|
|
857
1678
|
// ============================================================================
|
|
@@ -1078,6 +1899,223 @@ const TOOLS = [
|
|
|
1078
1899
|
required: ["filePath", "pattern"],
|
|
1079
1900
|
},
|
|
1080
1901
|
},
|
|
1902
|
+
// Extended Binary Analysis Tools
|
|
1903
|
+
{
|
|
1904
|
+
name: "bin_strings",
|
|
1905
|
+
description: "Extract readable strings (ASCII, Unicode) from a binary file",
|
|
1906
|
+
inputSchema: {
|
|
1907
|
+
type: "object" as const,
|
|
1908
|
+
properties: {
|
|
1909
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
1910
|
+
minLength: { type: "number", description: "Minimum string length (default: 4)" },
|
|
1911
|
+
encoding: {
|
|
1912
|
+
type: "string",
|
|
1913
|
+
enum: ["ascii", "unicode", "all"],
|
|
1914
|
+
description: "String encoding to search (default: all)",
|
|
1915
|
+
},
|
|
1916
|
+
},
|
|
1917
|
+
required: ["filePath"],
|
|
1918
|
+
},
|
|
1919
|
+
},
|
|
1920
|
+
{
|
|
1921
|
+
name: "bin_file_info",
|
|
1922
|
+
description: "Get detailed file type information with MIME type",
|
|
1923
|
+
inputSchema: {
|
|
1924
|
+
type: "object" as const,
|
|
1925
|
+
properties: {
|
|
1926
|
+
filePath: { type: "string", description: "Path to the file" },
|
|
1927
|
+
},
|
|
1928
|
+
required: ["filePath"],
|
|
1929
|
+
},
|
|
1930
|
+
},
|
|
1931
|
+
{
|
|
1932
|
+
name: "bin_section_sizes",
|
|
1933
|
+
description: "Get section sizes (text, data, bss) of a binary",
|
|
1934
|
+
inputSchema: {
|
|
1935
|
+
type: "object" as const,
|
|
1936
|
+
properties: {
|
|
1937
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
1938
|
+
},
|
|
1939
|
+
required: ["filePath"],
|
|
1940
|
+
},
|
|
1941
|
+
},
|
|
1942
|
+
{
|
|
1943
|
+
name: "bin_objdump_sections",
|
|
1944
|
+
description: "Get section headers with flags using objdump -h",
|
|
1945
|
+
inputSchema: {
|
|
1946
|
+
type: "object" as const,
|
|
1947
|
+
properties: {
|
|
1948
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
1949
|
+
},
|
|
1950
|
+
required: ["filePath"],
|
|
1951
|
+
},
|
|
1952
|
+
},
|
|
1953
|
+
{
|
|
1954
|
+
name: "bin_objdump_program_headers",
|
|
1955
|
+
description: "Get program headers and DLL imports using objdump -p",
|
|
1956
|
+
inputSchema: {
|
|
1957
|
+
type: "object" as const,
|
|
1958
|
+
properties: {
|
|
1959
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
1960
|
+
},
|
|
1961
|
+
required: ["filePath"],
|
|
1962
|
+
},
|
|
1963
|
+
},
|
|
1964
|
+
{
|
|
1965
|
+
name: "bin_otool_libs",
|
|
1966
|
+
description: "Get dynamic library dependencies for Mach-O files (macOS only)",
|
|
1967
|
+
inputSchema: {
|
|
1968
|
+
type: "object" as const,
|
|
1969
|
+
properties: {
|
|
1970
|
+
filePath: { type: "string", description: "Path to the Mach-O file" },
|
|
1971
|
+
},
|
|
1972
|
+
required: ["filePath"],
|
|
1973
|
+
},
|
|
1974
|
+
},
|
|
1975
|
+
{
|
|
1976
|
+
name: "bin_readelf",
|
|
1977
|
+
description: "Comprehensive ELF structure analysis using readelf",
|
|
1978
|
+
inputSchema: {
|
|
1979
|
+
type: "object" as const,
|
|
1980
|
+
properties: {
|
|
1981
|
+
filePath: { type: "string", description: "Path to the ELF file" },
|
|
1982
|
+
sections: {
|
|
1983
|
+
type: "array",
|
|
1984
|
+
items: { type: "string", enum: ["headers", "sections", "segments", "symbols", "dynamic", "all"] },
|
|
1985
|
+
description: "Sections to analyze (default: all)",
|
|
1986
|
+
},
|
|
1987
|
+
},
|
|
1988
|
+
required: ["filePath"],
|
|
1989
|
+
},
|
|
1990
|
+
},
|
|
1991
|
+
{
|
|
1992
|
+
name: "bin_ldd",
|
|
1993
|
+
description: "Get shared library dependencies using ldd (Linux only)",
|
|
1994
|
+
inputSchema: {
|
|
1995
|
+
type: "object" as const,
|
|
1996
|
+
properties: {
|
|
1997
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
1998
|
+
},
|
|
1999
|
+
required: ["filePath"],
|
|
2000
|
+
},
|
|
2001
|
+
},
|
|
2002
|
+
{
|
|
2003
|
+
name: "bin_disassembly",
|
|
2004
|
+
description: "Disassemble binary code using objdump -d",
|
|
2005
|
+
inputSchema: {
|
|
2006
|
+
type: "object" as const,
|
|
2007
|
+
properties: {
|
|
2008
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
2009
|
+
symbol: { type: "string", description: "Specific symbol to disassemble (optional)" },
|
|
2010
|
+
startOffset: { type: "number", description: "Line offset for output pagination" },
|
|
2011
|
+
length: { type: "number", description: "Number of lines to output" },
|
|
2012
|
+
},
|
|
2013
|
+
required: ["filePath"],
|
|
2014
|
+
},
|
|
2015
|
+
},
|
|
2016
|
+
{
|
|
2017
|
+
name: "bin_security_audit",
|
|
2018
|
+
description: "Check binary security features (ASLR, PIE, RELRO, stack canary, NX bit)",
|
|
2019
|
+
inputSchema: {
|
|
2020
|
+
type: "object" as const,
|
|
2021
|
+
properties: {
|
|
2022
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
2023
|
+
},
|
|
2024
|
+
required: ["filePath"],
|
|
2025
|
+
},
|
|
2026
|
+
},
|
|
2027
|
+
{
|
|
2028
|
+
name: "bin_entropy",
|
|
2029
|
+
description: "Analyze section entropy to detect packed/encrypted sections",
|
|
2030
|
+
inputSchema: {
|
|
2031
|
+
type: "object" as const,
|
|
2032
|
+
properties: {
|
|
2033
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
2034
|
+
blockSize: { type: "number", description: "Block size for entropy calculation (default: 1024)" },
|
|
2035
|
+
},
|
|
2036
|
+
required: ["filePath"],
|
|
2037
|
+
},
|
|
2038
|
+
},
|
|
2039
|
+
{
|
|
2040
|
+
name: "bin_import_export",
|
|
2041
|
+
description: "Get detailed import/export table analysis",
|
|
2042
|
+
inputSchema: {
|
|
2043
|
+
type: "object" as const,
|
|
2044
|
+
properties: {
|
|
2045
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
2046
|
+
},
|
|
2047
|
+
required: ["filePath"],
|
|
2048
|
+
},
|
|
2049
|
+
},
|
|
2050
|
+
{
|
|
2051
|
+
name: "bin_diff",
|
|
2052
|
+
description: "Byte-level comparison between two binary files",
|
|
2053
|
+
inputSchema: {
|
|
2054
|
+
type: "object" as const,
|
|
2055
|
+
properties: {
|
|
2056
|
+
file1: { type: "string", description: "First binary file" },
|
|
2057
|
+
file2: { type: "string", description: "Second binary file" },
|
|
2058
|
+
contextBytes: { type: "number", description: "Bytes of context to show around differences (default: 32)" },
|
|
2059
|
+
},
|
|
2060
|
+
required: ["file1", "file2"],
|
|
2061
|
+
},
|
|
2062
|
+
},
|
|
2063
|
+
{
|
|
2064
|
+
name: "bin_archive_extract",
|
|
2065
|
+
description: "Extract or list contents of static library (.a) archives",
|
|
2066
|
+
inputSchema: {
|
|
2067
|
+
type: "object" as const,
|
|
2068
|
+
properties: {
|
|
2069
|
+
filePath: { type: "string", description: "Path to the archive file" },
|
|
2070
|
+
outputDir: { type: "string", description: "Output directory for extraction (optional)" },
|
|
2071
|
+
listOnly: { type: "boolean", description: "Only list contents without extracting (default: false)" },
|
|
2072
|
+
},
|
|
2073
|
+
required: ["filePath"],
|
|
2074
|
+
},
|
|
2075
|
+
},
|
|
2076
|
+
// Binary Patching Tools (DESTRUCTIVE)
|
|
2077
|
+
{
|
|
2078
|
+
name: "bin_patch_bytes",
|
|
2079
|
+
description: "Patch bytes at a specific offset in a binary file (DESTRUCTIVE)",
|
|
2080
|
+
inputSchema: {
|
|
2081
|
+
type: "object" as const,
|
|
2082
|
+
properties: {
|
|
2083
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
2084
|
+
offset: { type: "number", description: "Byte offset to patch" },
|
|
2085
|
+
hexData: { type: "string", description: "Hex bytes to write (e.g., '90 90 90')" },
|
|
2086
|
+
createBackup: { type: "boolean", description: "Create .bak backup before patching (default: true)" },
|
|
2087
|
+
},
|
|
2088
|
+
required: ["filePath", "offset", "hexData"],
|
|
2089
|
+
},
|
|
2090
|
+
},
|
|
2091
|
+
{
|
|
2092
|
+
name: "bin_nop_sled",
|
|
2093
|
+
description: "Insert NOP instructions at a specific offset (DESTRUCTIVE)",
|
|
2094
|
+
inputSchema: {
|
|
2095
|
+
type: "object" as const,
|
|
2096
|
+
properties: {
|
|
2097
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
2098
|
+
offset: { type: "number", description: "Byte offset for NOP sled" },
|
|
2099
|
+
count: { type: "number", description: "Number of NOP instructions to insert" },
|
|
2100
|
+
createBackup: { type: "boolean", description: "Create .bak backup (default: true)" },
|
|
2101
|
+
},
|
|
2102
|
+
required: ["filePath", "offset", "count"],
|
|
2103
|
+
},
|
|
2104
|
+
},
|
|
2105
|
+
{
|
|
2106
|
+
name: "bin_hex_editor",
|
|
2107
|
+
description: "Read or modify bytes at a specific offset (hex editor mode)",
|
|
2108
|
+
inputSchema: {
|
|
2109
|
+
type: "object" as const,
|
|
2110
|
+
properties: {
|
|
2111
|
+
filePath: { type: "string", description: "Path to the file" },
|
|
2112
|
+
offset: { type: "number", description: "Byte offset" },
|
|
2113
|
+
length: { type: "number", description: "Number of bytes to read/modify" },
|
|
2114
|
+
newHex: { type: "string", description: "New hex bytes to write (optional - read mode if omitted)" },
|
|
2115
|
+
},
|
|
2116
|
+
required: ["filePath", "offset", "length"],
|
|
2117
|
+
},
|
|
2118
|
+
},
|
|
1081
2119
|
];
|
|
1082
2120
|
|
|
1083
2121
|
// ============================================================================
|
|
@@ -1161,6 +2199,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1161
2199
|
patternFormat?: "hex" | "text";
|
|
1162
2200
|
maxLength?: number;
|
|
1163
2201
|
});
|
|
2202
|
+
// Extended Binary Analysis handlers
|
|
2203
|
+
case "bin_strings":
|
|
2204
|
+
return await handleStrings(args as { filePath: string; minLength?: number; encoding?: "ascii" | "unicode" | "all" });
|
|
2205
|
+
case "bin_file_info":
|
|
2206
|
+
return await handleFileInfo(args as { filePath: string });
|
|
2207
|
+
case "bin_section_sizes":
|
|
2208
|
+
return await handleSectionSizes(args as { filePath: string });
|
|
2209
|
+
case "bin_objdump_sections":
|
|
2210
|
+
return await handleObjdumpSections(args as { filePath: string });
|
|
2211
|
+
case "bin_objdump_program_headers":
|
|
2212
|
+
return await handleObjdumpProgramHeaders(args as { filePath: string });
|
|
2213
|
+
case "bin_otool_libs":
|
|
2214
|
+
return await handleOtoolLibs(args as { filePath: string });
|
|
2215
|
+
case "bin_readelf":
|
|
2216
|
+
return await handleReadelf(args as { filePath: string; sections?: ("headers" | "sections" | "segments" | "symbols" | "dynamic" | "all")[] });
|
|
2217
|
+
case "bin_ldd":
|
|
2218
|
+
return await handleLdd(args as { filePath: string });
|
|
2219
|
+
case "bin_disassembly":
|
|
2220
|
+
return await handleDisassembly(args as { filePath: string; symbol?: string; startOffset?: number; length?: number });
|
|
2221
|
+
case "bin_security_audit":
|
|
2222
|
+
return await handleSecurityAudit(args as { filePath: string });
|
|
2223
|
+
case "bin_entropy":
|
|
2224
|
+
return await handleEntropyAnalysis(args as { filePath: string; blockSize?: number });
|
|
2225
|
+
case "bin_import_export":
|
|
2226
|
+
return await handleImportExport(args as { filePath: string });
|
|
2227
|
+
case "bin_diff":
|
|
2228
|
+
return await handleBinaryDiff(args as { file1: string; file2: string; contextBytes?: number });
|
|
2229
|
+
case "bin_archive_extract":
|
|
2230
|
+
return await handleArchiveExtract(args as { filePath: string; outputDir?: string; listOnly?: boolean });
|
|
2231
|
+
// Binary Patching handlers
|
|
2232
|
+
case "bin_patch_bytes":
|
|
2233
|
+
return await handlePatchBytes(args as { filePath: string; offset: number; hexData: string; createBackup?: boolean });
|
|
2234
|
+
case "bin_nop_sled":
|
|
2235
|
+
return await handleNopSled(args as { filePath: string; offset: number; count: number; createBackup?: boolean });
|
|
2236
|
+
case "bin_hex_editor":
|
|
2237
|
+
return await handleHexEditor(args as { filePath: string; offset: number; length: number; newHex?: string });
|
|
1164
2238
|
default:
|
|
1165
2239
|
throw new Error(`Unknown tool: ${name}`);
|
|
1166
2240
|
}
|