cipher-security 5.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/bin/cipher.js +465 -0
- package/lib/api/billing.js +321 -0
- package/lib/api/compliance.js +693 -0
- package/lib/api/controls.js +1401 -0
- package/lib/api/index.js +49 -0
- package/lib/api/marketplace.js +467 -0
- package/lib/api/openai-proxy.js +383 -0
- package/lib/api/server.js +685 -0
- package/lib/autonomous/feedback-loop.js +554 -0
- package/lib/autonomous/framework.js +512 -0
- package/lib/autonomous/index.js +97 -0
- package/lib/autonomous/leaderboard.js +594 -0
- package/lib/autonomous/modes/architect.js +412 -0
- package/lib/autonomous/modes/blue.js +386 -0
- package/lib/autonomous/modes/incident.js +684 -0
- package/lib/autonomous/modes/privacy.js +369 -0
- package/lib/autonomous/modes/purple.js +294 -0
- package/lib/autonomous/modes/recon.js +250 -0
- package/lib/autonomous/parallel.js +587 -0
- package/lib/autonomous/researcher.js +583 -0
- package/lib/autonomous/runner.js +955 -0
- package/lib/autonomous/scheduler.js +615 -0
- package/lib/autonomous/task-parser.js +127 -0
- package/lib/autonomous/validators/forensic.js +266 -0
- package/lib/autonomous/validators/osint.js +216 -0
- package/lib/autonomous/validators/privacy.js +296 -0
- package/lib/autonomous/validators/purple.js +298 -0
- package/lib/autonomous/validators/sigma.js +248 -0
- package/lib/autonomous/validators/threat-model.js +363 -0
- package/lib/benchmark/agent.js +119 -0
- package/lib/benchmark/baselines.js +43 -0
- package/lib/benchmark/builder.js +143 -0
- package/lib/benchmark/config.js +35 -0
- package/lib/benchmark/coordinator.js +91 -0
- package/lib/benchmark/index.js +20 -0
- package/lib/benchmark/llm.js +58 -0
- package/lib/benchmark/models.js +137 -0
- package/lib/benchmark/reporter.js +103 -0
- package/lib/benchmark/runner.js +103 -0
- package/lib/benchmark/sandbox.js +96 -0
- package/lib/benchmark/scorer.js +32 -0
- package/lib/benchmark/solver.js +166 -0
- package/lib/benchmark/tools.js +62 -0
- package/lib/bot/bot.js +130 -0
- package/lib/commands.js +99 -0
- package/lib/complexity.js +377 -0
- package/lib/config.js +213 -0
- package/lib/gateway/client.js +309 -0
- package/lib/gateway/commands.js +830 -0
- package/lib/gateway/config-validate.js +109 -0
- package/lib/gateway/gateway.js +367 -0
- package/lib/gateway/index.js +62 -0
- package/lib/gateway/mode.js +309 -0
- package/lib/gateway/plugins.js +222 -0
- package/lib/gateway/prompt.js +214 -0
- package/lib/mcp/server.js +262 -0
- package/lib/memory/compressor.js +425 -0
- package/lib/memory/engine.js +763 -0
- package/lib/memory/evolution.js +668 -0
- package/lib/memory/index.js +58 -0
- package/lib/memory/orchestrator.js +506 -0
- package/lib/memory/retriever.js +515 -0
- package/lib/memory/synthesizer.js +333 -0
- package/lib/pipeline/async-scanner.js +510 -0
- package/lib/pipeline/binary-analysis.js +1043 -0
- package/lib/pipeline/dom-xss-scanner.js +435 -0
- package/lib/pipeline/github-actions.js +792 -0
- package/lib/pipeline/index.js +124 -0
- package/lib/pipeline/osint.js +498 -0
- package/lib/pipeline/sarif.js +373 -0
- package/lib/pipeline/scanner.js +880 -0
- package/lib/pipeline/template-manager.js +525 -0
- package/lib/pipeline/xss-scanner.js +353 -0
- package/lib/setup-wizard.js +229 -0
- package/package.json +30 -0
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
// CIPHER is a trademark of defconxt.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pure-Node.js ELF binary analysis — no native dependencies.
|
|
7
|
+
*
|
|
8
|
+
* Provides:
|
|
9
|
+
* - ELF header parsing (arch, entry point, type, endianness)
|
|
10
|
+
* - Section enumeration (names, sizes, permissions)
|
|
11
|
+
* - Symbol table extraction (imports/exports)
|
|
12
|
+
* - Security feature detection (NX, PIE, RELRO, stack canaries, RPATH)
|
|
13
|
+
* - Format string vulnerability pattern detection in source code
|
|
14
|
+
* - ROP gadget scanning via objdump subprocess
|
|
15
|
+
*
|
|
16
|
+
* Ported from pipeline/binary_analysis.py (799 LOC Python).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
20
|
+
import { join, extname } from 'node:path';
|
|
21
|
+
import { execFileSync } from 'node:child_process';
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// ELF constants
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
const ELF_MAGIC = Buffer.from([0x7f, 0x45, 0x4c, 0x46]); // \x7fELF
|
|
28
|
+
|
|
29
|
+
// e_ident offsets
|
|
30
|
+
const EI_CLASS = 4;
|
|
31
|
+
const EI_DATA = 5;
|
|
32
|
+
|
|
33
|
+
// ELF classes
|
|
34
|
+
const ELFCLASS32 = 1;
|
|
35
|
+
const ELFCLASS64 = 2;
|
|
36
|
+
const CLASS_NAMES = Object.freeze({ [ELFCLASS32]: 'ELF32', [ELFCLASS64]: 'ELF64' });
|
|
37
|
+
|
|
38
|
+
// Data encoding
|
|
39
|
+
const ELFDATA2LSB = 1;
|
|
40
|
+
const ELFDATA2MSB = 2;
|
|
41
|
+
const ENDIAN_NAMES = Object.freeze({
|
|
42
|
+
[ELFDATA2LSB]: 'little-endian',
|
|
43
|
+
[ELFDATA2MSB]: 'big-endian',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Object file types
|
|
47
|
+
const TYPE_NAMES = Object.freeze({
|
|
48
|
+
0: 'NONE', 1: 'REL', 2: 'EXEC', 3: 'DYN', 4: 'CORE',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Machine types
|
|
52
|
+
const MACHINE_NAMES = Object.freeze({
|
|
53
|
+
0: 'NONE', 3: 'x86', 8: 'MIPS', 20: 'PowerPC', 40: 'ARM',
|
|
54
|
+
43: 'SPARC v9', 62: 'x86-64', 183: 'AArch64', 243: 'RISC-V',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Section types
|
|
58
|
+
const SHT_SYMTAB = 2;
|
|
59
|
+
const SHT_STRTAB = 3;
|
|
60
|
+
const SHT_DYNSYM = 11;
|
|
61
|
+
|
|
62
|
+
// Program header types
|
|
63
|
+
const PT_DYNAMIC = 2;
|
|
64
|
+
const PT_INTERP = 3;
|
|
65
|
+
const PT_GNU_RELRO = 0x6474e552;
|
|
66
|
+
const PT_GNU_STACK = 0x6474e551;
|
|
67
|
+
|
|
68
|
+
// Dynamic tags
|
|
69
|
+
const DT_NEEDED = 1;
|
|
70
|
+
const DT_RPATH = 15;
|
|
71
|
+
const DT_RUNPATH = 29;
|
|
72
|
+
const DT_FLAGS = 30;
|
|
73
|
+
const DT_FLAGS_1 = 0x6ffffffb;
|
|
74
|
+
const DF_BIND_NOW = 0x8;
|
|
75
|
+
const DF_1_NOW = 0x1;
|
|
76
|
+
const DF_1_PIE = 0x08000000;
|
|
77
|
+
|
|
78
|
+
// Section header flags
|
|
79
|
+
const SHF_WRITE = 0x1;
|
|
80
|
+
const SHF_ALLOC = 0x2;
|
|
81
|
+
const SHF_EXECINSTR = 0x4;
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Buffer read helpers — abstract endianness
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read unsigned 16-bit integer.
|
|
89
|
+
* @param {Buffer} buf
|
|
90
|
+
* @param {number} off byte offset
|
|
91
|
+
* @param {boolean} le true = little-endian
|
|
92
|
+
* @returns {number}
|
|
93
|
+
*/
|
|
94
|
+
function readU16(buf, off, le) {
|
|
95
|
+
return le ? buf.readUInt16LE(off) : buf.readUInt16BE(off);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Read unsigned 32-bit integer.
|
|
100
|
+
* @param {Buffer} buf
|
|
101
|
+
* @param {number} off byte offset
|
|
102
|
+
* @param {boolean} le true = little-endian
|
|
103
|
+
* @returns {number}
|
|
104
|
+
*/
|
|
105
|
+
function readU32(buf, off, le) {
|
|
106
|
+
return le ? buf.readUInt32LE(off) : buf.readUInt32BE(off);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Read unsigned 64-bit integer as Number (safe for file offsets < 2^53).
|
|
111
|
+
* @param {Buffer} buf
|
|
112
|
+
* @param {number} off byte offset
|
|
113
|
+
* @param {boolean} le true = little-endian
|
|
114
|
+
* @returns {number}
|
|
115
|
+
*/
|
|
116
|
+
function readU64(buf, off, le) {
|
|
117
|
+
const big = le ? buf.readBigUInt64LE(off) : buf.readBigUInt64BE(off);
|
|
118
|
+
return Number(big);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Read signed 32-bit integer.
|
|
123
|
+
* @param {Buffer} buf
|
|
124
|
+
* @param {number} off byte offset
|
|
125
|
+
* @param {boolean} le true = little-endian
|
|
126
|
+
* @returns {number}
|
|
127
|
+
*/
|
|
128
|
+
function readI32(buf, off, le) {
|
|
129
|
+
return le ? buf.readInt32LE(off) : buf.readInt32BE(off);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Read signed 64-bit integer as Number.
|
|
134
|
+
* @param {Buffer} buf
|
|
135
|
+
* @param {number} off byte offset
|
|
136
|
+
* @param {boolean} le true = little-endian
|
|
137
|
+
* @returns {number}
|
|
138
|
+
*/
|
|
139
|
+
function readI64(buf, off, le) {
|
|
140
|
+
const big = le ? buf.readBigInt64LE(off) : buf.readBigInt64BE(off);
|
|
141
|
+
return Number(big);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Read null-terminated ASCII string from buffer.
|
|
146
|
+
* @param {Buffer} data
|
|
147
|
+
* @param {number} offset
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
function readStr(data, offset) {
|
|
151
|
+
const end = data.indexOf(0, offset);
|
|
152
|
+
if (end < 0) return '';
|
|
153
|
+
return data.subarray(offset, end).toString('ascii');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Data classes
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
class ELFSection {
|
|
161
|
+
/**
|
|
162
|
+
* @param {object} opts
|
|
163
|
+
* @param {string} opts.name
|
|
164
|
+
* @param {number} opts.typeId
|
|
165
|
+
* @param {number} opts.flags
|
|
166
|
+
* @param {number} opts.addr
|
|
167
|
+
* @param {number} opts.offset
|
|
168
|
+
* @param {number} opts.size
|
|
169
|
+
* @param {number} [opts.link=0]
|
|
170
|
+
* @param {number} [opts.info=0]
|
|
171
|
+
* @param {number} [opts.entsize=0]
|
|
172
|
+
*/
|
|
173
|
+
constructor(opts) {
|
|
174
|
+
this.name = opts.name;
|
|
175
|
+
this.typeId = opts.typeId;
|
|
176
|
+
this.flags = opts.flags;
|
|
177
|
+
this.addr = opts.addr;
|
|
178
|
+
this.offset = opts.offset;
|
|
179
|
+
this.size = opts.size;
|
|
180
|
+
this.link = opts.link ?? 0;
|
|
181
|
+
this.info = opts.info ?? 0;
|
|
182
|
+
this.entsize = opts.entsize ?? 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
get isExecutable() {
|
|
186
|
+
return Boolean(this.flags & SHF_EXECINSTR);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
get isWritable() {
|
|
190
|
+
return Boolean(this.flags & SHF_WRITE);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
get permissions() {
|
|
194
|
+
const r = this.flags & SHF_ALLOC ? 'R' : '-';
|
|
195
|
+
const w = this.flags & SHF_WRITE ? 'W' : '-';
|
|
196
|
+
const x = this.flags & SHF_EXECINSTR ? 'X' : '-';
|
|
197
|
+
return `${r}${w}${x}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
toDict() {
|
|
201
|
+
return {
|
|
202
|
+
name: this.name,
|
|
203
|
+
size: this.size,
|
|
204
|
+
permissions: this.permissions,
|
|
205
|
+
address: '0x' + this.addr.toString(16),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
class ELFSymbol {
|
|
211
|
+
/**
|
|
212
|
+
* @param {object} opts
|
|
213
|
+
* @param {string} opts.name
|
|
214
|
+
* @param {number} opts.value
|
|
215
|
+
* @param {number} opts.size
|
|
216
|
+
* @param {string} opts.bind LOCAL | GLOBAL | WEAK
|
|
217
|
+
* @param {string} opts.type FUNC | OBJECT | NOTYPE
|
|
218
|
+
* @param {number} opts.sectionIndex
|
|
219
|
+
*/
|
|
220
|
+
constructor(opts) {
|
|
221
|
+
this.name = opts.name;
|
|
222
|
+
this.value = opts.value;
|
|
223
|
+
this.size = opts.size;
|
|
224
|
+
this.bind = opts.bind;
|
|
225
|
+
this.type = opts.type;
|
|
226
|
+
this.sectionIndex = opts.sectionIndex;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
toDict() {
|
|
230
|
+
return {
|
|
231
|
+
name: this.name,
|
|
232
|
+
value: '0x' + this.value.toString(16),
|
|
233
|
+
bind: this.bind,
|
|
234
|
+
type: this.type,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
class SecurityFeatures {
|
|
240
|
+
/**
|
|
241
|
+
* @param {object} [opts]
|
|
242
|
+
*/
|
|
243
|
+
constructor(opts = {}) {
|
|
244
|
+
this.nx = opts.nx ?? false;
|
|
245
|
+
this.pie = opts.pie ?? false;
|
|
246
|
+
this.relro = opts.relro ?? 'none';
|
|
247
|
+
this.canary = opts.canary ?? false;
|
|
248
|
+
this.fortify = opts.fortify ?? false;
|
|
249
|
+
this.rpath = opts.rpath ?? false;
|
|
250
|
+
this.runpath = opts.runpath ?? false;
|
|
251
|
+
this.stripped = opts.stripped ?? false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Hardening score 0-100. */
|
|
255
|
+
get score() {
|
|
256
|
+
let s = 0;
|
|
257
|
+
if (this.nx) s += 20;
|
|
258
|
+
if (this.pie) s += 20;
|
|
259
|
+
if (this.relro === 'full') s += 20;
|
|
260
|
+
else if (this.relro === 'partial') s += 10;
|
|
261
|
+
if (this.canary) s += 20;
|
|
262
|
+
if (this.fortify) s += 10;
|
|
263
|
+
if (!this.rpath) s += 10;
|
|
264
|
+
return Math.min(s, 100);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
toDict() {
|
|
268
|
+
return {
|
|
269
|
+
NX: this.nx,
|
|
270
|
+
PIE: this.pie,
|
|
271
|
+
RELRO: this.relro,
|
|
272
|
+
Canary: this.canary,
|
|
273
|
+
Fortify: this.fortify,
|
|
274
|
+
RPATH: this.rpath,
|
|
275
|
+
Stripped: this.stripped,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
class ELFAnalysis {
|
|
281
|
+
/**
|
|
282
|
+
* @param {object} opts
|
|
283
|
+
* @param {string} opts.filename
|
|
284
|
+
*/
|
|
285
|
+
constructor(opts) {
|
|
286
|
+
this.filename = opts.filename;
|
|
287
|
+
this.elfClass = opts.elfClass ?? '';
|
|
288
|
+
this.endianness = opts.endianness ?? '';
|
|
289
|
+
this.elfType = opts.elfType ?? '';
|
|
290
|
+
this.machine = opts.machine ?? '';
|
|
291
|
+
this.entryPoint = opts.entryPoint ?? 0;
|
|
292
|
+
this.sections = opts.sections ?? [];
|
|
293
|
+
this.symbols = opts.symbols ?? [];
|
|
294
|
+
this.imports = opts.imports ?? [];
|
|
295
|
+
this.libraries = opts.libraries ?? [];
|
|
296
|
+
this.security = opts.security ?? new SecurityFeatures();
|
|
297
|
+
this.suspicious = opts.suspicious ?? [];
|
|
298
|
+
this.errors = opts.errors ?? [];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
toDict() {
|
|
302
|
+
return {
|
|
303
|
+
filename: this.filename,
|
|
304
|
+
class: this.elfClass,
|
|
305
|
+
endianness: this.endianness,
|
|
306
|
+
type: this.elfType,
|
|
307
|
+
machine: this.machine,
|
|
308
|
+
entry_point: '0x' + this.entryPoint.toString(16),
|
|
309
|
+
sections: this.sections.length,
|
|
310
|
+
symbols: this.symbols.length,
|
|
311
|
+
imports: this.imports.slice(0, 50),
|
|
312
|
+
libraries: this.libraries,
|
|
313
|
+
security: this.security.toDict(),
|
|
314
|
+
hardening_score: this.security.score,
|
|
315
|
+
suspicious: this.suspicious,
|
|
316
|
+
errors: this.errors,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// ELF Parser
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
class ELFParser {
|
|
326
|
+
/**
|
|
327
|
+
* Parse an ELF binary and return full analysis.
|
|
328
|
+
* @param {string} filepath
|
|
329
|
+
* @returns {ELFAnalysis}
|
|
330
|
+
*/
|
|
331
|
+
parse(filepath) {
|
|
332
|
+
const result = new ELFAnalysis({ filename: filepath });
|
|
333
|
+
|
|
334
|
+
let data;
|
|
335
|
+
try {
|
|
336
|
+
data = readFileSync(filepath);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
result.errors.push(`Cannot read file: ${err.message}`);
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (data.length < 64) {
|
|
343
|
+
result.errors.push('File too small to be an ELF binary');
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!data.subarray(0, 4).equals(ELF_MAGIC)) {
|
|
348
|
+
result.errors.push('Not an ELF file (bad magic bytes)');
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Check for UPX packing in first 4KB
|
|
353
|
+
if (data.subarray(0, 4096).includes(Buffer.from('UPX!'))) {
|
|
354
|
+
result.suspicious.push('UPX-packed binary');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const eiClass = data[EI_CLASS];
|
|
358
|
+
const eiData = data[EI_DATA];
|
|
359
|
+
result.elfClass = CLASS_NAMES[eiClass] ?? `unknown(${eiClass})`;
|
|
360
|
+
result.endianness = ENDIAN_NAMES[eiData] ?? `unknown(${eiData})`;
|
|
361
|
+
|
|
362
|
+
const is64 = eiClass === ELFCLASS64;
|
|
363
|
+
const le = eiData === ELFDATA2LSB;
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
this._parseHeader(data, result, is64, le);
|
|
367
|
+
this._parseSections(data, result, is64, le);
|
|
368
|
+
this._parseProgramHeaders(data, result, is64, le);
|
|
369
|
+
this._checkSecurity(result, data);
|
|
370
|
+
this._detectSuspicious(result, data);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
result.errors.push(`Parse error: ${err.message}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return result;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Parse ELF header fields starting at offset 16.
|
|
380
|
+
*
|
|
381
|
+
* ELF32 header (from offset 16):
|
|
382
|
+
* offset 16: e_type (uint16)
|
|
383
|
+
* offset 18: e_machine (uint16)
|
|
384
|
+
* offset 20: e_version (uint32)
|
|
385
|
+
* offset 24: e_entry (uint32) — entry point
|
|
386
|
+
*
|
|
387
|
+
* ELF64 header (from offset 16):
|
|
388
|
+
* offset 16: e_type (uint16)
|
|
389
|
+
* offset 18: e_machine (uint16)
|
|
390
|
+
* offset 20: e_version (uint32)
|
|
391
|
+
* offset 24: e_entry (uint64) — entry point
|
|
392
|
+
*/
|
|
393
|
+
_parseHeader(data, result, is64, le) {
|
|
394
|
+
const eType = readU16(data, 16, le);
|
|
395
|
+
const eMachine = readU16(data, 18, le);
|
|
396
|
+
// e_version at 20 (uint32) — skipped
|
|
397
|
+
const eEntry = is64 ? readU64(data, 24, le) : readU32(data, 24, le);
|
|
398
|
+
|
|
399
|
+
result.elfType = TYPE_NAMES[eType] ?? `unknown(${eType})`;
|
|
400
|
+
result.machine = MACHINE_NAMES[eMachine] ?? `unknown(${eMachine})`;
|
|
401
|
+
result.entryPoint = eEntry;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Parse section headers.
|
|
406
|
+
*
|
|
407
|
+
* ELF64: e_shoff at offset 40 (uint64), e_shentsize at 58 (uint16),
|
|
408
|
+
* e_shnum at 60 (uint16), e_shstrndx at 62 (uint16).
|
|
409
|
+
* ELF32: e_shoff at offset 32 (uint32), e_shentsize at 46 (uint16),
|
|
410
|
+
* e_shnum at 48 (uint16), e_shstrndx at 50 (uint16).
|
|
411
|
+
*/
|
|
412
|
+
_parseSections(data, result, is64, le) {
|
|
413
|
+
let eShoff, eShentsize, eShnum, eShstrndx;
|
|
414
|
+
if (is64) {
|
|
415
|
+
eShoff = readU64(data, 40, le);
|
|
416
|
+
eShentsize = readU16(data, 58, le);
|
|
417
|
+
eShnum = readU16(data, 60, le);
|
|
418
|
+
eShstrndx = readU16(data, 62, le);
|
|
419
|
+
} else {
|
|
420
|
+
eShoff = readU32(data, 32, le);
|
|
421
|
+
eShentsize = readU16(data, 46, le);
|
|
422
|
+
eShnum = readU16(data, 48, le);
|
|
423
|
+
eShstrndx = readU16(data, 50, le);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (eShoff === 0 || eShnum === 0) return;
|
|
427
|
+
|
|
428
|
+
// Read section header string table
|
|
429
|
+
const shstrtabOff = eShoff + eShstrndx * eShentsize;
|
|
430
|
+
let strtabOffset, strtabSize;
|
|
431
|
+
if (is64) {
|
|
432
|
+
// sh_offset at +24 (uint64), sh_size at +32 (uint64) in 64-bit section header
|
|
433
|
+
strtabOffset = readU64(data, shstrtabOff + 24, le);
|
|
434
|
+
strtabSize = readU64(data, shstrtabOff + 32, le);
|
|
435
|
+
} else {
|
|
436
|
+
// sh_offset at +16 (uint32), sh_size at +20 (uint32) in 32-bit section header
|
|
437
|
+
strtabOffset = readU32(data, shstrtabOff + 16, le);
|
|
438
|
+
strtabSize = readU32(data, shstrtabOff + 20, le);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const strtab = data.subarray(strtabOffset, strtabOffset + strtabSize);
|
|
442
|
+
|
|
443
|
+
for (let i = 0; i < eShnum; i++) {
|
|
444
|
+
const off = eShoff + i * eShentsize;
|
|
445
|
+
let sec;
|
|
446
|
+
if (is64) {
|
|
447
|
+
/**
|
|
448
|
+
* ELF64 section header (64 bytes):
|
|
449
|
+
* +0 sh_name (uint32)
|
|
450
|
+
* +4 sh_type (uint32)
|
|
451
|
+
* +8 sh_flags (uint64)
|
|
452
|
+
* +16 sh_addr (uint64)
|
|
453
|
+
* +24 sh_offset (uint64)
|
|
454
|
+
* +32 sh_size (uint64)
|
|
455
|
+
* +40 sh_link (uint32)
|
|
456
|
+
* +44 sh_info (uint32)
|
|
457
|
+
* +48 sh_addralign (uint64)
|
|
458
|
+
* +56 sh_entsize (uint64)
|
|
459
|
+
*/
|
|
460
|
+
sec = new ELFSection({
|
|
461
|
+
name: readStr(strtab, readU32(data, off, le)),
|
|
462
|
+
typeId: readU32(data, off + 4, le),
|
|
463
|
+
flags: readU64(data, off + 8, le),
|
|
464
|
+
addr: readU64(data, off + 16, le),
|
|
465
|
+
offset: readU64(data, off + 24, le),
|
|
466
|
+
size: readU64(data, off + 32, le),
|
|
467
|
+
link: readU32(data, off + 40, le),
|
|
468
|
+
info: readU32(data, off + 44, le),
|
|
469
|
+
entsize: readU64(data, off + 56, le),
|
|
470
|
+
});
|
|
471
|
+
} else {
|
|
472
|
+
/**
|
|
473
|
+
* ELF32 section header (40 bytes):
|
|
474
|
+
* +0 sh_name (uint32)
|
|
475
|
+
* +4 sh_type (uint32)
|
|
476
|
+
* +8 sh_flags (uint32)
|
|
477
|
+
* +12 sh_addr (uint32)
|
|
478
|
+
* +16 sh_offset (uint32)
|
|
479
|
+
* +20 sh_size (uint32)
|
|
480
|
+
* +24 sh_link (uint32)
|
|
481
|
+
* +28 sh_info (uint32)
|
|
482
|
+
* +32 sh_addralign (uint32)
|
|
483
|
+
* +36 sh_entsize (uint32)
|
|
484
|
+
*/
|
|
485
|
+
sec = new ELFSection({
|
|
486
|
+
name: readStr(strtab, readU32(data, off, le)),
|
|
487
|
+
typeId: readU32(data, off + 4, le),
|
|
488
|
+
flags: readU32(data, off + 8, le),
|
|
489
|
+
addr: readU32(data, off + 12, le),
|
|
490
|
+
offset: readU32(data, off + 16, le),
|
|
491
|
+
size: readU32(data, off + 20, le),
|
|
492
|
+
link: readU32(data, off + 24, le),
|
|
493
|
+
info: readU32(data, off + 28, le),
|
|
494
|
+
entsize: readU32(data, off + 36, le),
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
result.sections.push(sec);
|
|
498
|
+
|
|
499
|
+
// Extract symbols from .dynsym
|
|
500
|
+
if (sec.typeId === SHT_DYNSYM && sec.entsize > 0) {
|
|
501
|
+
this._parseSymbols(data, sec, result, is64, le);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Parse symbols from a .dynsym section.
|
|
508
|
+
*/
|
|
509
|
+
_parseSymbols(data, sec, result, is64, le) {
|
|
510
|
+
// Find .dynstr section
|
|
511
|
+
let dynstrSec = null;
|
|
512
|
+
for (const s of result.sections) {
|
|
513
|
+
if (s.typeId === SHT_STRTAB && s.name === '.dynstr') {
|
|
514
|
+
dynstrSec = s;
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (!dynstrSec) return;
|
|
519
|
+
|
|
520
|
+
const symStrtab = data.subarray(dynstrSec.offset, dynstrSec.offset + dynstrSec.size);
|
|
521
|
+
const nSyms = sec.entsize ? Math.floor(sec.size / sec.entsize) : 0;
|
|
522
|
+
const bindNames = { 0: 'LOCAL', 1: 'GLOBAL', 2: 'WEAK' };
|
|
523
|
+
const typeNames = { 0: 'NOTYPE', 1: 'OBJECT', 2: 'FUNC', 10: 'IFUNC' };
|
|
524
|
+
|
|
525
|
+
for (let i = 0; i < nSyms; i++) {
|
|
526
|
+
const off = sec.offset + i * sec.entsize;
|
|
527
|
+
let nameIdx, info, shndx, value, size;
|
|
528
|
+
|
|
529
|
+
if (is64) {
|
|
530
|
+
/**
|
|
531
|
+
* ELF64 symbol (24 bytes):
|
|
532
|
+
* +0 st_name (uint32)
|
|
533
|
+
* +4 st_info (uint8)
|
|
534
|
+
* +5 st_other (uint8)
|
|
535
|
+
* +6 st_shndx (uint16)
|
|
536
|
+
* +8 st_value (uint64)
|
|
537
|
+
* +16 st_size (uint64)
|
|
538
|
+
*/
|
|
539
|
+
nameIdx = readU32(data, off, le);
|
|
540
|
+
info = data[off + 4];
|
|
541
|
+
shndx = readU16(data, off + 6, le);
|
|
542
|
+
value = readU64(data, off + 8, le);
|
|
543
|
+
size = readU64(data, off + 16, le);
|
|
544
|
+
} else {
|
|
545
|
+
/**
|
|
546
|
+
* ELF32 symbol (16 bytes):
|
|
547
|
+
* +0 st_name (uint32)
|
|
548
|
+
* +4 st_value (uint32)
|
|
549
|
+
* +8 st_size (uint32)
|
|
550
|
+
* +12 st_info (uint8)
|
|
551
|
+
* +13 st_other (uint8)
|
|
552
|
+
* +14 st_shndx (uint16)
|
|
553
|
+
*/
|
|
554
|
+
nameIdx = readU32(data, off, le);
|
|
555
|
+
value = readU32(data, off + 4, le);
|
|
556
|
+
size = readU32(data, off + 8, le);
|
|
557
|
+
info = data[off + 12];
|
|
558
|
+
shndx = readU16(data, off + 14, le);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const name = readStr(symStrtab, nameIdx);
|
|
562
|
+
if (!name) continue;
|
|
563
|
+
|
|
564
|
+
const bind = bindNames[info >> 4] ?? 'OTHER';
|
|
565
|
+
const stype = typeNames[info & 0xf] ?? 'OTHER';
|
|
566
|
+
|
|
567
|
+
const sym = new ELFSymbol({ name, value, size, bind, type: stype, sectionIndex: shndx });
|
|
568
|
+
result.symbols.push(sym);
|
|
569
|
+
|
|
570
|
+
// Import = FUNC with section index 0 (UND)
|
|
571
|
+
if (shndx === 0 && stype === 'FUNC') {
|
|
572
|
+
result.imports.push(name);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Parse program headers.
|
|
579
|
+
*
|
|
580
|
+
* ELF64: e_phoff at 32 (uint64), e_phentsize at 54 (uint16), e_phnum at 56 (uint16).
|
|
581
|
+
* ELF32: e_phoff at 28 (uint32), e_phentsize at 42 (uint16), e_phnum at 44 (uint16).
|
|
582
|
+
*/
|
|
583
|
+
_parseProgramHeaders(data, result, is64, le) {
|
|
584
|
+
let ePhoff, ePhentsize, ePhnum;
|
|
585
|
+
if (is64) {
|
|
586
|
+
ePhoff = readU64(data, 32, le);
|
|
587
|
+
ePhentsize = readU16(data, 54, le);
|
|
588
|
+
ePhnum = readU16(data, 56, le);
|
|
589
|
+
} else {
|
|
590
|
+
ePhoff = readU32(data, 28, le);
|
|
591
|
+
ePhentsize = readU16(data, 42, le);
|
|
592
|
+
ePhnum = readU16(data, 44, le);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (ePhoff === 0 || ePhnum === 0) return;
|
|
596
|
+
|
|
597
|
+
for (let i = 0; i < ePhnum; i++) {
|
|
598
|
+
const off = ePhoff + i * ePhentsize;
|
|
599
|
+
let pType, pFlags, pOffset, pFilesz;
|
|
600
|
+
|
|
601
|
+
if (is64) {
|
|
602
|
+
/**
|
|
603
|
+
* ELF64 program header (56 bytes):
|
|
604
|
+
* +0 p_type (uint32)
|
|
605
|
+
* +4 p_flags (uint32)
|
|
606
|
+
* +8 p_offset (uint64)
|
|
607
|
+
* +32 p_filesz (uint64)
|
|
608
|
+
*/
|
|
609
|
+
pType = readU32(data, off, le);
|
|
610
|
+
pFlags = readU32(data, off + 4, le);
|
|
611
|
+
pOffset = readU64(data, off + 8, le);
|
|
612
|
+
pFilesz = readU64(data, off + 32, le);
|
|
613
|
+
} else {
|
|
614
|
+
/**
|
|
615
|
+
* ELF32 program header (32 bytes):
|
|
616
|
+
* +0 p_type (uint32)
|
|
617
|
+
* +4 p_offset (uint32)
|
|
618
|
+
* +16 p_filesz (uint32)
|
|
619
|
+
* +24 p_flags (uint32)
|
|
620
|
+
*/
|
|
621
|
+
pType = readU32(data, off, le);
|
|
622
|
+
pOffset = readU32(data, off + 4, le);
|
|
623
|
+
pFilesz = readU32(data, off + 16, le);
|
|
624
|
+
pFlags = readU32(data, off + 24, le);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (pType === PT_GNU_STACK) {
|
|
628
|
+
// PF_X = 0x1 — if executable bit set, NX is NOT enabled
|
|
629
|
+
result.security.nx = !(pFlags & 0x1);
|
|
630
|
+
} else if (pType === PT_GNU_RELRO) {
|
|
631
|
+
result.security.relro = 'partial';
|
|
632
|
+
} else if (pType === PT_INTERP) {
|
|
633
|
+
const interp = data.subarray(pOffset, pOffset + pFilesz).toString('ascii').replace(/\0+$/, '');
|
|
634
|
+
result.suspicious.push(`interpreter: ${interp}`);
|
|
635
|
+
} else if (pType === PT_DYNAMIC) {
|
|
636
|
+
this._parseDynamic(data, pOffset, pFilesz, result, is64, le);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Parse dynamic section entries for library dependencies and security flags.
|
|
643
|
+
*/
|
|
644
|
+
_parseDynamic(data, offset, size, result, is64, le) {
|
|
645
|
+
// Find .dynstr for library name resolution
|
|
646
|
+
let dynstr = Buffer.alloc(0);
|
|
647
|
+
for (const sec of result.sections) {
|
|
648
|
+
if (sec.name === '.dynstr') {
|
|
649
|
+
dynstr = data.subarray(sec.offset, sec.offset + sec.size);
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const entrySize = is64 ? 16 : 8;
|
|
655
|
+
const nEntries = Math.floor(size / entrySize);
|
|
656
|
+
let bindNow = false;
|
|
657
|
+
|
|
658
|
+
for (let i = 0; i < nEntries; i++) {
|
|
659
|
+
const entOff = offset + i * entrySize;
|
|
660
|
+
let dTag, dVal;
|
|
661
|
+
|
|
662
|
+
if (is64) {
|
|
663
|
+
dTag = readI64(data, entOff, le);
|
|
664
|
+
dVal = readU64(data, entOff + 8, le);
|
|
665
|
+
} else {
|
|
666
|
+
dTag = readI32(data, entOff, le);
|
|
667
|
+
dVal = readU32(data, entOff + 4, le);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (dTag === 0) break; // DT_NULL
|
|
671
|
+
if (dTag === DT_NEEDED && dynstr.length > 0) {
|
|
672
|
+
result.libraries.push(readStr(dynstr, dVal));
|
|
673
|
+
} else if (dTag === DT_RPATH) {
|
|
674
|
+
result.security.rpath = true;
|
|
675
|
+
} else if (dTag === DT_RUNPATH) {
|
|
676
|
+
result.security.runpath = true;
|
|
677
|
+
} else if (dTag === DT_FLAGS && (dVal & DF_BIND_NOW)) {
|
|
678
|
+
bindNow = true;
|
|
679
|
+
} else if (dTag === DT_FLAGS_1) {
|
|
680
|
+
if (dVal & DF_1_NOW) bindNow = true;
|
|
681
|
+
if (dVal & DF_1_PIE) result.security.pie = true;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (bindNow && result.security.relro === 'partial') {
|
|
686
|
+
result.security.relro = 'full';
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Additional security feature detection.
|
|
692
|
+
*/
|
|
693
|
+
_checkSecurity(result, data) {
|
|
694
|
+
// PIE heuristic: DYN type + entry != 0
|
|
695
|
+
if (result.elfType === 'DYN' && result.entryPoint !== 0) {
|
|
696
|
+
result.security.pie = true;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Stack canary: check for __stack_chk_fail import
|
|
700
|
+
for (const sym of result.symbols) {
|
|
701
|
+
if (sym.name.includes('__stack_chk_fail')) {
|
|
702
|
+
result.security.canary = true;
|
|
703
|
+
}
|
|
704
|
+
if (sym.name.startsWith('__') && sym.name.includes('_chk')) {
|
|
705
|
+
result.security.fortify = true;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Stripped: no .symtab section
|
|
710
|
+
const hasSymtab = result.sections.some((s) => s.name === '.symtab');
|
|
711
|
+
result.security.stripped = !hasSymtab;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Detect suspicious binary indicators.
|
|
716
|
+
*/
|
|
717
|
+
_detectSuspicious(result, data) {
|
|
718
|
+
const sectionNames = new Set(result.sections.map((s) => s.name));
|
|
719
|
+
|
|
720
|
+
if (sectionNames.has('.upx0') || sectionNames.has('.upx1')) {
|
|
721
|
+
result.suspicious.push('UPX-packed (section names)');
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// All sections stripped — only empty name and .shstrtab remain
|
|
725
|
+
const meaningful = new Set(sectionNames);
|
|
726
|
+
meaningful.delete('');
|
|
727
|
+
meaningful.delete('.shstrtab');
|
|
728
|
+
if (meaningful.size === 0 && result.sections.length > 0) {
|
|
729
|
+
result.suspicious.push('All sections stripped — possibly packed');
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// W+X sections
|
|
733
|
+
for (const sec of result.sections) {
|
|
734
|
+
if (sec.isWritable && sec.isExecutable && sec.name) {
|
|
735
|
+
result.suspicious.push(`W+X section: ${sec.name} (${sec.size} bytes)`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Anti-debug imports
|
|
740
|
+
const antiDebugFuncs = new Set(['ptrace', 'prctl', 'getppid', 'isDebuggerPresent']);
|
|
741
|
+
for (const imp of result.imports) {
|
|
742
|
+
if (antiDebugFuncs.has(imp)) {
|
|
743
|
+
result.suspicious.push(`Anti-debug function: ${imp}`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// ---------------------------------------------------------------------------
|
|
750
|
+
// Format String Scanner
|
|
751
|
+
// ---------------------------------------------------------------------------
|
|
752
|
+
|
|
753
|
+
const PRINTF_FAMILY = /\b(s?n?printf|fprintf|vprintf|vsprintf|vsnprintf|vfprintf|syslog|err|errx|warn|warnx)\s*\(/i;
|
|
754
|
+
|
|
755
|
+
const C_EXTENSIONS = new Set(['.c', '.h', '.cpp', '.cc', '.cxx', '.hpp']);
|
|
756
|
+
|
|
757
|
+
class FormatStringFinding {
|
|
758
|
+
/**
|
|
759
|
+
* @param {object} opts
|
|
760
|
+
* @param {string} opts.file
|
|
761
|
+
* @param {number} opts.lineNumber
|
|
762
|
+
* @param {string} opts.lineContent
|
|
763
|
+
* @param {string} opts.function
|
|
764
|
+
* @param {string} opts.severity
|
|
765
|
+
* @param {string} opts.reason
|
|
766
|
+
*/
|
|
767
|
+
constructor(opts) {
|
|
768
|
+
this.file = opts.file;
|
|
769
|
+
this.lineNumber = opts.lineNumber;
|
|
770
|
+
this.lineContent = opts.lineContent;
|
|
771
|
+
this.function = opts.function;
|
|
772
|
+
this.severity = opts.severity;
|
|
773
|
+
this.reason = opts.reason;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
toDict() {
|
|
777
|
+
return {
|
|
778
|
+
file: this.file,
|
|
779
|
+
line: this.lineNumber,
|
|
780
|
+
function: this.function,
|
|
781
|
+
severity: this.severity,
|
|
782
|
+
reason: this.reason,
|
|
783
|
+
content: this.lineContent.trim().slice(0, 200),
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
class FormatStringScanner {
|
|
789
|
+
/**
|
|
790
|
+
* Scan a source file for format string vulnerabilities.
|
|
791
|
+
* @param {string} filepath
|
|
792
|
+
* @returns {FormatStringFinding[]}
|
|
793
|
+
*/
|
|
794
|
+
scanFile(filepath) {
|
|
795
|
+
const ext = extname(filepath).toLowerCase();
|
|
796
|
+
if (!C_EXTENSIONS.has(ext)) return [];
|
|
797
|
+
|
|
798
|
+
let content;
|
|
799
|
+
try {
|
|
800
|
+
content = readFileSync(filepath, 'utf-8');
|
|
801
|
+
} catch {
|
|
802
|
+
return [];
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const lines = content.split('\n');
|
|
806
|
+
const findings = [];
|
|
807
|
+
|
|
808
|
+
for (let i = 0; i < lines.length; i++) {
|
|
809
|
+
const line = lines[i];
|
|
810
|
+
const stripped = line.trim();
|
|
811
|
+
if (stripped.startsWith('//') || stripped.startsWith('/*')) continue;
|
|
812
|
+
|
|
813
|
+
const m = PRINTF_FAMILY.exec(line);
|
|
814
|
+
if (!m) continue;
|
|
815
|
+
|
|
816
|
+
const func = m[1];
|
|
817
|
+
const after = line.slice(m.index + m[0].length).trimStart();
|
|
818
|
+
|
|
819
|
+
// String literal format = safe
|
|
820
|
+
if (after.startsWith('"') || after.startsWith("'")) continue;
|
|
821
|
+
|
|
822
|
+
// Variable as format string — dangerous
|
|
823
|
+
const severity = ['printf', 'sprintf', 'fprintf'].includes(func) ? 'critical' : 'high';
|
|
824
|
+
|
|
825
|
+
findings.push(
|
|
826
|
+
new FormatStringFinding({
|
|
827
|
+
file: filepath,
|
|
828
|
+
lineNumber: i + 1,
|
|
829
|
+
lineContent: line,
|
|
830
|
+
function: func,
|
|
831
|
+
severity,
|
|
832
|
+
reason: `Variable passed as format string to ${func}()`,
|
|
833
|
+
}),
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return findings;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Scan a directory recursively for format string vulnerabilities.
|
|
842
|
+
* @param {string} directory
|
|
843
|
+
* @returns {FormatStringFinding[]}
|
|
844
|
+
*/
|
|
845
|
+
scanDirectory(directory) {
|
|
846
|
+
const findings = [];
|
|
847
|
+
const walk = (dir) => {
|
|
848
|
+
let entries;
|
|
849
|
+
try {
|
|
850
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
851
|
+
} catch {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
for (const ent of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
855
|
+
const full = join(dir, ent.name);
|
|
856
|
+
if (ent.isDirectory()) {
|
|
857
|
+
walk(full);
|
|
858
|
+
} else if (ent.isFile() && C_EXTENSIONS.has(extname(ent.name).toLowerCase())) {
|
|
859
|
+
findings.push(...this.scanFile(full));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
walk(directory);
|
|
864
|
+
return findings;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// ---------------------------------------------------------------------------
|
|
869
|
+
// ROP Gadget Scanner — uses objdump subprocess per D050
|
|
870
|
+
// ---------------------------------------------------------------------------
|
|
871
|
+
|
|
872
|
+
class ROPGadget {
|
|
873
|
+
/**
|
|
874
|
+
* @param {object} opts
|
|
875
|
+
* @param {number} opts.address
|
|
876
|
+
* @param {string} opts.instructions
|
|
877
|
+
* @param {string} opts.bytesHex
|
|
878
|
+
* @param {number} opts.length number of instructions
|
|
879
|
+
*/
|
|
880
|
+
constructor(opts) {
|
|
881
|
+
this.address = opts.address;
|
|
882
|
+
this.instructions = opts.instructions;
|
|
883
|
+
this.bytesHex = opts.bytesHex;
|
|
884
|
+
this.length = opts.length;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
toDict() {
|
|
888
|
+
return {
|
|
889
|
+
address: '0x' + this.address.toString(16),
|
|
890
|
+
instructions: this.instructions,
|
|
891
|
+
bytes: this.bytesHex,
|
|
892
|
+
length: this.length,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
class ROPScanner {
|
|
898
|
+
/** Max instructions per gadget. */
|
|
899
|
+
static MAX_GADGET_INSNS = 6;
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Check if objdump is available.
|
|
903
|
+
* @returns {boolean}
|
|
904
|
+
*/
|
|
905
|
+
static available() {
|
|
906
|
+
try {
|
|
907
|
+
execFileSync('objdump', ['--version'], { stdio: 'pipe', timeout: 5000 });
|
|
908
|
+
return true;
|
|
909
|
+
} catch {
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Scan an ELF binary for ROP gadgets using objdump disassembly.
|
|
916
|
+
* @param {string} filepath
|
|
917
|
+
* @param {object} [opts]
|
|
918
|
+
* @param {number} [opts.maxGadgets=200]
|
|
919
|
+
* @param {number} [opts.maxDepth=5] max instructions before gadget-ending instruction
|
|
920
|
+
* @returns {ROPGadget[]}
|
|
921
|
+
*/
|
|
922
|
+
scan(filepath, opts = {}) {
|
|
923
|
+
const maxGadgets = opts.maxGadgets ?? 200;
|
|
924
|
+
const maxDepth = opts.maxDepth ?? 5;
|
|
925
|
+
|
|
926
|
+
let output;
|
|
927
|
+
try {
|
|
928
|
+
output = execFileSync('objdump', ['-d', '-j', '.text', filepath], {
|
|
929
|
+
encoding: 'utf-8',
|
|
930
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
931
|
+
timeout: 30000,
|
|
932
|
+
maxBuffer: 50 * 1024 * 1024, // 50MB
|
|
933
|
+
});
|
|
934
|
+
} catch {
|
|
935
|
+
return [];
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Parse objdump output lines
|
|
939
|
+
// Format: " 4004c3: c3 ret"
|
|
940
|
+
// " addr: hex_bytes mnemonic operands"
|
|
941
|
+
const lines = output.split('\n');
|
|
942
|
+
const parsed = []; // { addr, bytes, mnemonic, operands }
|
|
943
|
+
|
|
944
|
+
for (const line of lines) {
|
|
945
|
+
const m = line.match(/^\s*([0-9a-f]+):\s+((?:[0-9a-f]{2}\s?)+)\s+(\S+)(?:\s+(.*))?$/);
|
|
946
|
+
if (!m) continue;
|
|
947
|
+
const addr = parseInt(m[1], 16);
|
|
948
|
+
const bytes = m[2].trim();
|
|
949
|
+
const mnemonic = m[3];
|
|
950
|
+
const operands = (m[4] ?? '').trim();
|
|
951
|
+
parsed.push({ addr, bytes, mnemonic, operands });
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Find gadget-ending instructions
|
|
955
|
+
const gadgetEnds = new Set(['ret', 'retq']);
|
|
956
|
+
const syscallEnds = new Set(['syscall']);
|
|
957
|
+
// int 0x80 represented as "int" with operand "$0x80"
|
|
958
|
+
|
|
959
|
+
const gadgets = [];
|
|
960
|
+
const seen = new Set();
|
|
961
|
+
|
|
962
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
963
|
+
const ins = parsed[i];
|
|
964
|
+
const isEnd =
|
|
965
|
+
gadgetEnds.has(ins.mnemonic) ||
|
|
966
|
+
syscallEnds.has(ins.mnemonic) ||
|
|
967
|
+
(ins.mnemonic === 'int' && ins.operands === '$0x80');
|
|
968
|
+
|
|
969
|
+
if (!isEnd) continue;
|
|
970
|
+
|
|
971
|
+
// Collect preceding instruction sequences of varying depths
|
|
972
|
+
for (let depth = 1; depth <= maxDepth && depth <= i; depth++) {
|
|
973
|
+
const startIdx = i - depth;
|
|
974
|
+
const sequence = parsed.slice(startIdx, i + 1);
|
|
975
|
+
|
|
976
|
+
const insnStr = sequence
|
|
977
|
+
.map((s) => (s.operands ? `${s.mnemonic} ${s.operands}` : s.mnemonic))
|
|
978
|
+
.join(' ; ');
|
|
979
|
+
|
|
980
|
+
if (seen.has(insnStr)) continue;
|
|
981
|
+
seen.add(insnStr);
|
|
982
|
+
|
|
983
|
+
const bytesHex = sequence.map((s) => s.bytes.replace(/\s/g, '')).join('');
|
|
984
|
+
|
|
985
|
+
gadgets.push(
|
|
986
|
+
new ROPGadget({
|
|
987
|
+
address: sequence[0].addr,
|
|
988
|
+
instructions: insnStr,
|
|
989
|
+
bytesHex,
|
|
990
|
+
length: sequence.length,
|
|
991
|
+
}),
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
if (gadgets.length >= maxGadgets) {
|
|
995
|
+
return gadgets.sort((a, b) => a.address - b.address);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return gadgets.sort((a, b) => a.address - b.address);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Find gadgets matching an instruction pattern (regex).
|
|
1005
|
+
* @param {ROPGadget[]} gadgets
|
|
1006
|
+
* @param {string|RegExp} pattern
|
|
1007
|
+
* @returns {ROPGadget[]}
|
|
1008
|
+
*/
|
|
1009
|
+
static findGadgetsByPattern(gadgets, pattern) {
|
|
1010
|
+
const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'i');
|
|
1011
|
+
return gadgets.filter((g) => re.test(g.instructions));
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// ---------------------------------------------------------------------------
|
|
1016
|
+
// Exports
|
|
1017
|
+
// ---------------------------------------------------------------------------
|
|
1018
|
+
|
|
1019
|
+
export {
|
|
1020
|
+
// Constants
|
|
1021
|
+
ELF_MAGIC,
|
|
1022
|
+
SHF_WRITE,
|
|
1023
|
+
SHF_ALLOC,
|
|
1024
|
+
SHF_EXECINSTR,
|
|
1025
|
+
// Helpers
|
|
1026
|
+
readU16,
|
|
1027
|
+
readU32,
|
|
1028
|
+
readU64,
|
|
1029
|
+
readStr,
|
|
1030
|
+
// Data classes
|
|
1031
|
+
ELFSection,
|
|
1032
|
+
ELFSymbol,
|
|
1033
|
+
SecurityFeatures,
|
|
1034
|
+
ELFAnalysis,
|
|
1035
|
+
// Parser
|
|
1036
|
+
ELFParser,
|
|
1037
|
+
// Format string scanner
|
|
1038
|
+
FormatStringFinding,
|
|
1039
|
+
FormatStringScanner,
|
|
1040
|
+
// ROP
|
|
1041
|
+
ROPGadget,
|
|
1042
|
+
ROPScanner,
|
|
1043
|
+
};
|