jame 1.0.dev0__py3-none-any.whl
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.
- jame/__init__.py +109 -0
- jame/__main__.py +6 -0
- jame/cli/__init__.py +1 -0
- jame/cli/jame_cli.py +151 -0
- jame/core/__init__.py +5 -0
- jame/core/filetype.py +69 -0
- jame/core/jame.py +372 -0
- jame/core/patterns.py +72 -0
- jame/core/utils.py +74 -0
- jame/modules/__init__.py +53 -0
- jame/modules/archive.py +63 -0
- jame/modules/binary.py +60 -0
- jame/modules/crypto.py +169 -0
- jame/modules/forensics.py +69 -0
- jame/modules/stego.py +65 -0
- jame/results/__init__.py +18 -0
- jame/results/binary_result.py +51 -0
- jame/results/crypto_result.py +26 -0
- jame/results/forensics_result.py +26 -0
- jame/results/scan_result.py +26 -0
- jame/results/solve_result.py +26 -0
- jame/results/stego_result.py +26 -0
- jame-1.0.dev0.dist-info/METADATA +169 -0
- jame-1.0.dev0.dist-info/RECORD +28 -0
- jame-1.0.dev0.dist-info/WHEEL +5 -0
- jame-1.0.dev0.dist-info/entry_points.txt +2 -0
- jame-1.0.dev0.dist-info/licenses/LICENSE +21 -0
- jame-1.0.dev0.dist-info/top_level.txt +1 -0
jame/core/jame.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
'''Public API for the JAMe library.'''
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
from jame.core.filetype import get_file_type
|
|
7
|
+
from jame.core.patterns import find_pattern_tokens, match_pattern
|
|
8
|
+
from jame.core.utils import read_file_bytes, read_file_text, safe_run_command
|
|
9
|
+
from jame.modules.archive import extract_archive
|
|
10
|
+
from jame.modules.binary import (
|
|
11
|
+
detect_branches,
|
|
12
|
+
extract_symbols,
|
|
13
|
+
run_objdump,
|
|
14
|
+
run_readelf,
|
|
15
|
+
run_strings,
|
|
16
|
+
)
|
|
17
|
+
from jame.modules.crypto import (
|
|
18
|
+
caesar_detect,
|
|
19
|
+
detect_encoding,
|
|
20
|
+
entropy_score_for_text,
|
|
21
|
+
identify_hash,
|
|
22
|
+
xor_bruteforce,
|
|
23
|
+
)
|
|
24
|
+
from jame.modules.forensics import (
|
|
25
|
+
analyze_pdf,
|
|
26
|
+
detect_carved_files,
|
|
27
|
+
extract_metadata,
|
|
28
|
+
triage_pcap,
|
|
29
|
+
)
|
|
30
|
+
from jame.modules.stego import (
|
|
31
|
+
detect_appended_zip,
|
|
32
|
+
detect_exif_anomalies,
|
|
33
|
+
lsb_check,
|
|
34
|
+
zsteg_scan,
|
|
35
|
+
)
|
|
36
|
+
from jame.modules.stego import extract_steghide as _detect_steghide
|
|
37
|
+
from jame.results.binary_result import BinaryResult, BinwalkResult
|
|
38
|
+
from jame.results.crypto_result import CryptoResult
|
|
39
|
+
from jame.results.forensics_result import ForensicsResult
|
|
40
|
+
from jame.results.scan_result import ScanResult
|
|
41
|
+
from jame.results.solve_result import SolveResult
|
|
42
|
+
from jame.results.stego_result import StegoResult
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Jame:
|
|
46
|
+
'''Main public API for JAMe: Just Another Method of Exploitation.'''
|
|
47
|
+
|
|
48
|
+
def __init__(self, flag_patterns: list[str] | None = None, max_depth: int = 3, verbose: bool = False):
|
|
49
|
+
'''Initialize JAMe with optional flag patterns and archive settings.'''
|
|
50
|
+
self.flag_patterns = flag_patterns or ['flag{*}', 'hcr{*}', 'CTFx{*}']
|
|
51
|
+
self.max_depth = max_depth
|
|
52
|
+
self.verbose = verbose
|
|
53
|
+
|
|
54
|
+
def setFlagPatterns(self, patterns: list[str]) -> None:
|
|
55
|
+
'''Set the flag patterns used by automatic flag discovery.'''
|
|
56
|
+
self.flag_patterns = patterns
|
|
57
|
+
|
|
58
|
+
def setMaxDepth(self, depth: int) -> None:
|
|
59
|
+
'''Set the maximum recursive archive extraction depth.'''
|
|
60
|
+
self.max_depth = depth if depth > 0 else 1
|
|
61
|
+
|
|
62
|
+
def setVerbose(self, enabled: bool) -> None:
|
|
63
|
+
'''Enable or disable verbose-mode configuration.'''
|
|
64
|
+
self.verbose = enabled
|
|
65
|
+
|
|
66
|
+
def getFileType(self, path: str) -> str:
|
|
67
|
+
'''Return a best-effort file type label for a path.'''
|
|
68
|
+
return get_file_type(path)
|
|
69
|
+
|
|
70
|
+
def runStrings(self, path: str) -> list[str]:
|
|
71
|
+
'''Extract printable strings from a file.'''
|
|
72
|
+
return run_strings(path)
|
|
73
|
+
|
|
74
|
+
def runBinwalk(self, path: str) -> BinwalkResult:
|
|
75
|
+
'''Run binwalk when installed and return its structured result.'''
|
|
76
|
+
if shutil.which('binwalk') is None:
|
|
77
|
+
return BinwalkResult(
|
|
78
|
+
file_path=path,
|
|
79
|
+
available=False,
|
|
80
|
+
summary='binwalk is not installed',
|
|
81
|
+
raw_output={'stderr': 'binwalk not found'},
|
|
82
|
+
confidence=0.0,
|
|
83
|
+
)
|
|
84
|
+
code, stdout, stderr = safe_run_command(['binwalk', '-e', path], timeout=20)
|
|
85
|
+
extracted: list[str] = []
|
|
86
|
+
if stdout:
|
|
87
|
+
for line in stdout.splitlines():
|
|
88
|
+
if 'extracting' in line.lower() or 'created' in line.lower():
|
|
89
|
+
extracted.append(line.strip())
|
|
90
|
+
if code == 0:
|
|
91
|
+
message = 'binwalk completed successfully'
|
|
92
|
+
confidence = 0.9
|
|
93
|
+
else:
|
|
94
|
+
message = 'binwalk returned an error'
|
|
95
|
+
confidence = 0.2
|
|
96
|
+
return BinwalkResult(
|
|
97
|
+
file_path=path,
|
|
98
|
+
available=True,
|
|
99
|
+
summary=message,
|
|
100
|
+
extracted=extracted,
|
|
101
|
+
matches=[line.strip() for line in stdout.splitlines() if line.strip()],
|
|
102
|
+
raw_output={'stdout': stdout, 'stderr': stderr, 'returncode': code},
|
|
103
|
+
confidence=confidence,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def scanFile(self, path: str) -> ScanResult:
|
|
107
|
+
'''Run the available triage analyzers and summarize their findings.'''
|
|
108
|
+
if not os.path.exists(path):
|
|
109
|
+
return ScanResult(
|
|
110
|
+
file_path=path,
|
|
111
|
+
file_type='missing',
|
|
112
|
+
summary='Target file does not exist.',
|
|
113
|
+
details={'exists': False},
|
|
114
|
+
confidence=0.0,
|
|
115
|
+
raw_output={'error': 'missing file'},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
file_type = self.getFileType(path)
|
|
119
|
+
crypto_result = self.cryptoAnalyze(path)
|
|
120
|
+
binary_result = self.binaryAnalyze(path)
|
|
121
|
+
stego_result = self.stegoAnalyze(path)
|
|
122
|
+
forensics_result = self.forensicsAnalyze(path)
|
|
123
|
+
|
|
124
|
+
summary = (
|
|
125
|
+
f'{crypto_result.summary} | {binary_result.summary} | '
|
|
126
|
+
f'{stego_result.summary} | {forensics_result.summary}'
|
|
127
|
+
)
|
|
128
|
+
confidence = round(
|
|
129
|
+
(crypto_result.confidence + binary_result.confidence + stego_result.confidence + forensics_result.confidence) / 4,
|
|
130
|
+
3,
|
|
131
|
+
)
|
|
132
|
+
return ScanResult(
|
|
133
|
+
file_path=path,
|
|
134
|
+
file_type=file_type,
|
|
135
|
+
summary=summary,
|
|
136
|
+
details={
|
|
137
|
+
'crypto': crypto_result.to_dict(),
|
|
138
|
+
'binary': binary_result.to_dict(),
|
|
139
|
+
'stego': stego_result.to_dict(),
|
|
140
|
+
'forensics': forensics_result.to_dict(),
|
|
141
|
+
},
|
|
142
|
+
confidence=confidence,
|
|
143
|
+
raw_output={
|
|
144
|
+
'crypto': crypto_result.raw_output,
|
|
145
|
+
'binary': binary_result.raw_output,
|
|
146
|
+
'stego': stego_result.raw_output,
|
|
147
|
+
'forensics': forensics_result.raw_output,
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def findFlag(self, path: str, format: str) -> str | None:
|
|
152
|
+
'''Find the first brace-wrapped candidate matching a wildcard format.'''
|
|
153
|
+
text = read_file_text(path, errors='replace')
|
|
154
|
+
candidates = find_pattern_tokens(text)
|
|
155
|
+
if not candidates:
|
|
156
|
+
return None
|
|
157
|
+
for candidate in candidates:
|
|
158
|
+
if match_pattern(candidate, format):
|
|
159
|
+
return candidate
|
|
160
|
+
for pattern in self.flag_patterns:
|
|
161
|
+
for candidate in candidates:
|
|
162
|
+
if match_pattern(candidate, pattern):
|
|
163
|
+
return candidate
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
def autoSolve(self, path: str, **ctx) -> SolveResult:
|
|
167
|
+
'''Search a file for a flag and extract supported archives.'''
|
|
168
|
+
format_value = str(ctx.get('format', self.flag_patterns[0]))
|
|
169
|
+
max_depth = int(ctx.get('maxDepth', self.max_depth))
|
|
170
|
+
verbose = bool(ctx.get('verbose', self.verbose))
|
|
171
|
+
self.setVerbose(verbose)
|
|
172
|
+
self.setMaxDepth(max_depth)
|
|
173
|
+
|
|
174
|
+
flag = self.findFlag(path, format_value)
|
|
175
|
+
archive_files: list[str] = []
|
|
176
|
+
if os.path.exists(path):
|
|
177
|
+
archive_files = self.archiveExtract(path)
|
|
178
|
+
|
|
179
|
+
summary = 'No flag extracted.' if flag is None else f'Flag candidate found: {flag}'
|
|
180
|
+
if archive_files:
|
|
181
|
+
summary += f' | extracted {len(archive_files)} archive entries'
|
|
182
|
+
return SolveResult(
|
|
183
|
+
file_path=path,
|
|
184
|
+
summary=summary,
|
|
185
|
+
flag=flag,
|
|
186
|
+
confidence=0.8 if flag else 0.3,
|
|
187
|
+
details={
|
|
188
|
+
'format': format_value,
|
|
189
|
+
'max_depth': max_depth,
|
|
190
|
+
'verbose': verbose,
|
|
191
|
+
'archives': archive_files,
|
|
192
|
+
},
|
|
193
|
+
raw_output={'flag': flag, 'archives': archive_files},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def cryptoAnalyze(self, path: str) -> CryptoResult:
|
|
197
|
+
'''Analyze a file for common encodings, simple ciphers, and hashes.'''
|
|
198
|
+
data = read_file_bytes(path)
|
|
199
|
+
text = data.decode('utf-8', errors='replace')
|
|
200
|
+
encoding = detect_encoding(text)
|
|
201
|
+
xor = xor_bruteforce(data)
|
|
202
|
+
caesar = caesar_detect(text)
|
|
203
|
+
hash_result = identify_hash(text)
|
|
204
|
+
entropy = entropy_score_for_text(data)
|
|
205
|
+
|
|
206
|
+
summary = 'No obvious crypto patterns detected.'
|
|
207
|
+
confidence = 0.1
|
|
208
|
+
algorithm = 'unknown'
|
|
209
|
+
if encoding['encoding']:
|
|
210
|
+
summary = 'Likely encoded or obfuscated data detected.'
|
|
211
|
+
confidence = max(confidence, 0.8)
|
|
212
|
+
algorithm = 'encoded'
|
|
213
|
+
if xor:
|
|
214
|
+
summary = 'XOR-bruteforce candidates present.'
|
|
215
|
+
confidence = max(confidence, 0.75)
|
|
216
|
+
algorithm = 'xor'
|
|
217
|
+
if caesar['best'] and caesar['best']['score'] > 0:
|
|
218
|
+
summary = 'Caesar-style substitution detected.'
|
|
219
|
+
confidence = max(confidence, 0.7)
|
|
220
|
+
algorithm = 'caesar'
|
|
221
|
+
if hash_result['hash_type'] != 'unknown':
|
|
222
|
+
summary = 'Hash-like value detected.'
|
|
223
|
+
confidence = max(confidence, 0.9)
|
|
224
|
+
algorithm = 'hash'
|
|
225
|
+
|
|
226
|
+
return CryptoResult(
|
|
227
|
+
file_path=path,
|
|
228
|
+
summary=summary,
|
|
229
|
+
algorithm=algorithm,
|
|
230
|
+
confidence=round(confidence, 3),
|
|
231
|
+
findings={
|
|
232
|
+
'encoding': encoding,
|
|
233
|
+
'xor': xor[:10],
|
|
234
|
+
'caesar': caesar,
|
|
235
|
+
'hash': hash_result,
|
|
236
|
+
'entropy': round(entropy, 3),
|
|
237
|
+
},
|
|
238
|
+
raw_output={
|
|
239
|
+
'encoding': encoding,
|
|
240
|
+
'xor': xor,
|
|
241
|
+
'caesar': caesar,
|
|
242
|
+
'hash': hash_result,
|
|
243
|
+
'entropy': entropy,
|
|
244
|
+
},
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def binaryAnalyze(self, path: str) -> BinaryResult:
|
|
248
|
+
'''Collect printable strings, symbols, and executable indicators.'''
|
|
249
|
+
strings = run_strings(path)
|
|
250
|
+
objdump = run_objdump(path)
|
|
251
|
+
readelf = run_readelf(path)
|
|
252
|
+
symbols = extract_symbols(path)
|
|
253
|
+
branch_hits = detect_branches(path)
|
|
254
|
+
summary = 'Binary-like file with no obvious anomalies.'
|
|
255
|
+
confidence = 0.3
|
|
256
|
+
architecture = 'unknown'
|
|
257
|
+
if strings:
|
|
258
|
+
summary = 'Printable strings suggest a binary or packed payload.'
|
|
259
|
+
confidence = 0.7
|
|
260
|
+
if readelf.get('available'):
|
|
261
|
+
architecture = 'ELF-like'
|
|
262
|
+
if branch_hits:
|
|
263
|
+
summary = 'Control-flow instructions suggest executable logic.'
|
|
264
|
+
confidence = 0.8
|
|
265
|
+
return BinaryResult(
|
|
266
|
+
file_path=path,
|
|
267
|
+
summary=summary,
|
|
268
|
+
architecture=architecture,
|
|
269
|
+
confidence=round(confidence, 3),
|
|
270
|
+
findings={
|
|
271
|
+
'strings': strings[:50],
|
|
272
|
+
'symbols': symbols[:50],
|
|
273
|
+
'branches': branch_hits,
|
|
274
|
+
'objdump': objdump,
|
|
275
|
+
'readelf': readelf,
|
|
276
|
+
},
|
|
277
|
+
raw_output={
|
|
278
|
+
'strings': strings,
|
|
279
|
+
'objdump': objdump,
|
|
280
|
+
'readelf': readelf,
|
|
281
|
+
'symbols': symbols,
|
|
282
|
+
'branches': branch_hits,
|
|
283
|
+
},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def stegoAnalyze(self, path: str) -> StegoResult:
|
|
287
|
+
'''Check a file for common steganography indicators.'''
|
|
288
|
+
zsteg = zsteg_scan(path)
|
|
289
|
+
lsb = lsb_check(path)
|
|
290
|
+
appended_zip = detect_appended_zip(path)
|
|
291
|
+
exif = detect_exif_anomalies(path)
|
|
292
|
+
steghide = _detect_steghide(path)
|
|
293
|
+
techniques: list[str] = []
|
|
294
|
+
if zsteg['findings']:
|
|
295
|
+
techniques.append('zsteg')
|
|
296
|
+
if lsb['likely_lsb']:
|
|
297
|
+
techniques.append('lsb')
|
|
298
|
+
if appended_zip['found']:
|
|
299
|
+
techniques.append('appended zip')
|
|
300
|
+
if exif['anomalies']:
|
|
301
|
+
techniques.append('exif')
|
|
302
|
+
summary = 'No steganography signals detected.' if not techniques else 'Steganography indicators detected.'
|
|
303
|
+
confidence = 0.8 if techniques else 0.1
|
|
304
|
+
return StegoResult(
|
|
305
|
+
file_path=path,
|
|
306
|
+
summary=summary,
|
|
307
|
+
confidence=round(confidence, 3),
|
|
308
|
+
techniques=techniques,
|
|
309
|
+
findings={
|
|
310
|
+
'zsteg': zsteg,
|
|
311
|
+
'lsb': lsb,
|
|
312
|
+
'appended_zip': appended_zip,
|
|
313
|
+
'exif': exif,
|
|
314
|
+
'steghide': steghide,
|
|
315
|
+
},
|
|
316
|
+
raw_output={
|
|
317
|
+
'zsteg': zsteg,
|
|
318
|
+
'lsb': lsb,
|
|
319
|
+
'appended_zip': appended_zip,
|
|
320
|
+
'exif': exif,
|
|
321
|
+
'steghide': steghide,
|
|
322
|
+
},
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def forensicsAnalyze(self, path: str) -> ForensicsResult:
|
|
326
|
+
'''Collect file metadata and run PDF, PCAP, and carving checks.'''
|
|
327
|
+
metadata = extract_metadata(path)
|
|
328
|
+
pdf = analyze_pdf(path)
|
|
329
|
+
pcap = triage_pcap(path)
|
|
330
|
+
carved = detect_carved_files(path)
|
|
331
|
+
findings = {'pdf': pdf, 'pcap': pcap, 'carved': carved}
|
|
332
|
+
summary = 'No major forensic anomalies detected.'
|
|
333
|
+
confidence = 0.2
|
|
334
|
+
if carved:
|
|
335
|
+
summary = 'Carved file signatures detected.'
|
|
336
|
+
confidence = 0.8
|
|
337
|
+
if pdf.get('valid'):
|
|
338
|
+
summary = 'PDF structure looks valid.'
|
|
339
|
+
confidence = max(confidence, 0.7)
|
|
340
|
+
return ForensicsResult(
|
|
341
|
+
file_path=path,
|
|
342
|
+
summary=summary,
|
|
343
|
+
confidence=round(confidence, 3),
|
|
344
|
+
metadata=metadata,
|
|
345
|
+
findings=findings,
|
|
346
|
+
raw_output={'metadata': metadata, 'pdf': pdf, 'pcap': pcap, 'carved': carved},
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def archiveExtract(self, path: str) -> list[str]:
|
|
350
|
+
'''Extract supported archive contents using the configured depth limit.'''
|
|
351
|
+
return extract_archive(path, max_depth=self.max_depth)
|
|
352
|
+
|
|
353
|
+
def decodeCaesar(self, text: str, shift: int | str = 'brute') -> dict:
|
|
354
|
+
'''Decode one Caesar shift or return ranked candidates for all shifts.'''
|
|
355
|
+
if shift == 'brute':
|
|
356
|
+
return caesar_detect(text)
|
|
357
|
+
if not isinstance(shift, int):
|
|
358
|
+
return {'error': "shift must be an integer or 'brute'"}
|
|
359
|
+
decoded = []
|
|
360
|
+
for char in text:
|
|
361
|
+
if 'a' <= char <= 'z':
|
|
362
|
+
decoded.append(chr((ord(char) - ord('a') - shift) % 26 + ord('a')))
|
|
363
|
+
elif 'A' <= char <= 'Z':
|
|
364
|
+
decoded.append(chr((ord(char) - ord('A') - shift) % 26 + ord('A')))
|
|
365
|
+
else:
|
|
366
|
+
decoded.append(char)
|
|
367
|
+
return {'shift': shift, 'decoded': ''.join(decoded)}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def create_jame() -> Jame:
|
|
371
|
+
'''Create a JAMe instance with its default configuration.'''
|
|
372
|
+
return Jame()
|
jame/core/patterns.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'''Pattern matching helpers for JAMe.'''
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _parse_pattern(pattern: str) -> list[str | tuple[str, int]]:
|
|
7
|
+
'''Convert a wildcard pattern into a simple token stream.'''
|
|
8
|
+
tokens: list[str | tuple[str, int]] = []
|
|
9
|
+
index = 0
|
|
10
|
+
while index < len(pattern):
|
|
11
|
+
char = pattern[index]
|
|
12
|
+
if char == '*':
|
|
13
|
+
end = index + 1
|
|
14
|
+
while end < len(pattern) and pattern[end].isdigit():
|
|
15
|
+
end += 1
|
|
16
|
+
if end > index + 1:
|
|
17
|
+
amount = int(pattern[index + 1:end])
|
|
18
|
+
else:
|
|
19
|
+
amount = 0
|
|
20
|
+
tokens.append(('*', amount))
|
|
21
|
+
index = end
|
|
22
|
+
continue
|
|
23
|
+
if char == '\\' and index + 1 < len(pattern):
|
|
24
|
+
tokens.append(pattern[index + 1])
|
|
25
|
+
index += 2
|
|
26
|
+
continue
|
|
27
|
+
tokens.append(char)
|
|
28
|
+
index += 1
|
|
29
|
+
return tokens
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def match_pattern(text: str, pattern: str) -> bool:
|
|
33
|
+
'''Match text against a wildcard pattern using a custom matcher.
|
|
34
|
+
|
|
35
|
+
Supported patterns are literal text with '*' placeholders such as:
|
|
36
|
+
- hcr{*4}
|
|
37
|
+
- flag{*2-******\2}
|
|
38
|
+
- flag{*}
|
|
39
|
+
'''
|
|
40
|
+
if pattern == '':
|
|
41
|
+
return text == ''
|
|
42
|
+
|
|
43
|
+
tokens = _parse_pattern(pattern)
|
|
44
|
+
|
|
45
|
+
def _match(text_index: int, token_index: int) -> bool:
|
|
46
|
+
if token_index == len(tokens):
|
|
47
|
+
return text_index == len(text)
|
|
48
|
+
|
|
49
|
+
token = tokens[token_index]
|
|
50
|
+
if isinstance(token, tuple):
|
|
51
|
+
_, amount = token
|
|
52
|
+
if amount == 0:
|
|
53
|
+
for span in range(len(text) - text_index + 1):
|
|
54
|
+
if _match(text_index + span, token_index + 1):
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
57
|
+
if text_index + amount > len(text):
|
|
58
|
+
return False
|
|
59
|
+
return _match(text_index + amount, token_index + 1)
|
|
60
|
+
|
|
61
|
+
literal = str(token)
|
|
62
|
+
if text.startswith(literal, text_index):
|
|
63
|
+
return _match(text_index + len(literal), token_index + 1)
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
return _match(0, 0)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def find_pattern_tokens(text: str) -> list[str]:
|
|
70
|
+
'''Extract braces-wrapped strings likely to contain flag-like values.'''
|
|
71
|
+
matches = re.findall(r'[A-Za-z0-9_]{2,}\{[^\n\r}]{1,120}\}', text)
|
|
72
|
+
return matches
|
jame/core/utils.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'''Miscellaneous helpers used by JAMe.'''
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import math
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def read_file_bytes(path: str) -> bytes:
|
|
12
|
+
'''Read bytes from a file.'''
|
|
13
|
+
with open(path, 'rb') as handle:
|
|
14
|
+
return handle.read()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def read_file_text(path: str, errors: str = 'replace') -> str:
|
|
18
|
+
'''Read a file as text.'''
|
|
19
|
+
with open(path, 'r', encoding='utf-8', errors=errors) as handle:
|
|
20
|
+
return handle.read()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def safe_run_command(command: list[str], timeout: int = 15) -> tuple[int, str, str]:
|
|
24
|
+
'''Run an external command safely and capture output.'''
|
|
25
|
+
try:
|
|
26
|
+
completed = subprocess.run(
|
|
27
|
+
command,
|
|
28
|
+
capture_output=True,
|
|
29
|
+
text=True,
|
|
30
|
+
timeout=timeout,
|
|
31
|
+
check=False,
|
|
32
|
+
)
|
|
33
|
+
return completed.returncode, completed.stdout, completed.stderr
|
|
34
|
+
except FileNotFoundError:
|
|
35
|
+
return 127, '', 'command not found'
|
|
36
|
+
except subprocess.TimeoutExpired:
|
|
37
|
+
return 124, '', 'command timed out'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def entropy_score(data: bytes) -> float:
|
|
41
|
+
'''Compute Shannon entropy for a byte string.'''
|
|
42
|
+
if not data:
|
|
43
|
+
return 0.0
|
|
44
|
+
counts = {}
|
|
45
|
+
for byte in data:
|
|
46
|
+
counts[byte] = counts.get(byte, 0) + 1
|
|
47
|
+
total = len(data)
|
|
48
|
+
entropy = 0.0
|
|
49
|
+
for count in counts.values():
|
|
50
|
+
probability = count / total
|
|
51
|
+
entropy -= probability * math.log2(probability)
|
|
52
|
+
return entropy
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def sha256_hex(data: bytes) -> str:
|
|
56
|
+
'''Return the SHA256 hex digest of the supplied data.'''
|
|
57
|
+
return hashlib.sha256(data).hexdigest()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def strip_ansi(value: str) -> str:
|
|
61
|
+
'''Remove ANSI escape sequences from output.'''
|
|
62
|
+
return re.sub(r'\x1b\[[0-9;]*[A-Za-z]', '', value)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def file_exists(path: str) -> bool:
|
|
66
|
+
'''Return whether a path exists and is a file.'''
|
|
67
|
+
return os.path.isfile(path)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def ensure_directory(path: str) -> str:
|
|
71
|
+
'''Create a directory if needed and return the path.'''
|
|
72
|
+
directory = Path(path)
|
|
73
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
return str(directory)
|
jame/modules/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'''Module analyzers for JAMe.'''
|
|
2
|
+
|
|
3
|
+
from jame.modules.archive import extract_archive
|
|
4
|
+
from jame.modules.binary import (
|
|
5
|
+
detect_branches,
|
|
6
|
+
extract_symbols,
|
|
7
|
+
run_objdump,
|
|
8
|
+
run_readelf,
|
|
9
|
+
run_strings,
|
|
10
|
+
)
|
|
11
|
+
from jame.modules.crypto import (
|
|
12
|
+
caesar_detect,
|
|
13
|
+
detect_encoding,
|
|
14
|
+
entropy_score,
|
|
15
|
+
identify_hash,
|
|
16
|
+
xor_bruteforce,
|
|
17
|
+
)
|
|
18
|
+
from jame.modules.forensics import (
|
|
19
|
+
analyze_pdf,
|
|
20
|
+
detect_carved_files,
|
|
21
|
+
extract_metadata,
|
|
22
|
+
triage_pcap,
|
|
23
|
+
)
|
|
24
|
+
from jame.modules.stego import (
|
|
25
|
+
detect_appended_zip,
|
|
26
|
+
detect_exif_anomalies,
|
|
27
|
+
extract_steghide,
|
|
28
|
+
lsb_check,
|
|
29
|
+
zsteg_scan,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
'analyze_pdf',
|
|
34
|
+
'caesar_detect',
|
|
35
|
+
'detect_appended_zip',
|
|
36
|
+
'detect_branches',
|
|
37
|
+
'detect_carved_files',
|
|
38
|
+
'detect_encoding',
|
|
39
|
+
'detect_exif_anomalies',
|
|
40
|
+
'entropy_score',
|
|
41
|
+
'extract_archive',
|
|
42
|
+
'extract_metadata',
|
|
43
|
+
'extract_steghide',
|
|
44
|
+
'extract_symbols',
|
|
45
|
+
'identify_hash',
|
|
46
|
+
'lsb_check',
|
|
47
|
+
'run_objdump',
|
|
48
|
+
'run_readelf',
|
|
49
|
+
'run_strings',
|
|
50
|
+
'triage_pcap',
|
|
51
|
+
'xor_bruteforce',
|
|
52
|
+
'zsteg_scan',
|
|
53
|
+
]
|
jame/modules/archive.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'''Archive extraction helpers for JAMe.'''
|
|
2
|
+
|
|
3
|
+
import gzip
|
|
4
|
+
import os
|
|
5
|
+
import tarfile
|
|
6
|
+
import zipfile
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _extract_zip(path: str, output_dir: str) -> list[str]:
|
|
10
|
+
extracted: list[str] = []
|
|
11
|
+
with zipfile.ZipFile(path, 'r') as archive:
|
|
12
|
+
archive.extractall(output_dir)
|
|
13
|
+
extracted.extend([os.path.join(output_dir, item) for item in archive.namelist()])
|
|
14
|
+
return extracted
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _extract_tar(path: str, output_dir: str) -> list[str]:
|
|
18
|
+
extracted: list[str] = []
|
|
19
|
+
with tarfile.open(path, 'r:*') as archive:
|
|
20
|
+
archive.extractall(output_dir)
|
|
21
|
+
for member in archive.getmembers():
|
|
22
|
+
if member.isfile():
|
|
23
|
+
extracted.append(os.path.join(output_dir, member.name))
|
|
24
|
+
return extracted
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _extract_gzip(path: str, output_dir: str) -> list[str]:
|
|
28
|
+
target = os.path.join(output_dir, os.path.basename(path).replace('.gz', ''))
|
|
29
|
+
with gzip.open(path, 'rb') as source, open(target, 'wb') as destination:
|
|
30
|
+
destination.write(source.read())
|
|
31
|
+
return [target]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def extract_archive(path: str, max_depth: int = 3) -> list[str]:
|
|
35
|
+
'''Extract a zip/tar/gzip archive, recursively until max_depth is reached.'''
|
|
36
|
+
if not os.path.isfile(path):
|
|
37
|
+
return []
|
|
38
|
+
|
|
39
|
+
output_dir = os.path.join(os.path.dirname(path), '.jame_extract')
|
|
40
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
41
|
+
extracted: list[str] = []
|
|
42
|
+
stack = [(path, 0)]
|
|
43
|
+
seen: set[str] = set()
|
|
44
|
+
|
|
45
|
+
while stack:
|
|
46
|
+
current_path, depth = stack.pop()
|
|
47
|
+
if current_path in seen or depth > max_depth:
|
|
48
|
+
continue
|
|
49
|
+
seen.add(current_path)
|
|
50
|
+
lower = current_path.lower()
|
|
51
|
+
if lower.endswith('.zip'):
|
|
52
|
+
files = _extract_zip(current_path, output_dir)
|
|
53
|
+
extracted.extend(files)
|
|
54
|
+
elif lower.endswith(('.tar', '.tar.gz', '.tgz', '.tar.bz2', '.tbz2', '.tar.xz')):
|
|
55
|
+
files = _extract_tar(current_path, output_dir)
|
|
56
|
+
extracted.extend(files)
|
|
57
|
+
elif lower.endswith('.gz') and not lower.endswith('.tar.gz'):
|
|
58
|
+
files = _extract_gzip(current_path, output_dir)
|
|
59
|
+
extracted.extend(files)
|
|
60
|
+
for item in extracted:
|
|
61
|
+
if os.path.isfile(item) and item.lower().endswith(('.zip', '.tar', '.gz')):
|
|
62
|
+
stack.append((item, depth + 1))
|
|
63
|
+
return extracted
|
jame/modules/binary.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'''Binary analysis helpers for JAMe.'''
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from jame.core.utils import read_file_bytes, safe_run_command
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_strings(path: str) -> list[str]:
|
|
9
|
+
'''Return printable strings from a binary-like file.'''
|
|
10
|
+
data = read_file_bytes(path)
|
|
11
|
+
matches = re.findall(rb'[ -~]{4,}', data)
|
|
12
|
+
return [item.decode('utf-8', errors='ignore') for item in matches]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run_objdump(path: str) -> dict:
|
|
16
|
+
'''Run objdump if present and return structured output.'''
|
|
17
|
+
code, stdout, stderr = safe_run_command(['objdump', '-d', path], timeout=10)
|
|
18
|
+
return {
|
|
19
|
+
'available': code == 0,
|
|
20
|
+
'output': stdout or stderr,
|
|
21
|
+
'returncode': code,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_readelf(path: str) -> dict:
|
|
26
|
+
'''Run readelf if present and return structured output.'''
|
|
27
|
+
code, stdout, stderr = safe_run_command(['readelf', '-h', path], timeout=10)
|
|
28
|
+
return {
|
|
29
|
+
'available': code == 0,
|
|
30
|
+
'output': stdout or stderr,
|
|
31
|
+
'returncode': code,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def extract_symbols(path: str) -> list[str]:
|
|
36
|
+
'''Extract symbol-like names from an ELF binary using readelf if available.'''
|
|
37
|
+
result = run_readelf(path)
|
|
38
|
+
output = result.get('output', '')
|
|
39
|
+
matches = re.findall(r'\b[A-Za-z_][A-Za-z0-9_]+\b', output)
|
|
40
|
+
return matches[:200]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def detect_branches(path: str) -> list[str]:
|
|
44
|
+
'''Identify common branch patterns in disassembly output.'''
|
|
45
|
+
result = run_objdump(path)
|
|
46
|
+
output = result.get('output', '')
|
|
47
|
+
patterns = [
|
|
48
|
+
'call',
|
|
49
|
+
'jmp',
|
|
50
|
+
'jne',
|
|
51
|
+
'je',
|
|
52
|
+
'ret',
|
|
53
|
+
'test',
|
|
54
|
+
'cmp',
|
|
55
|
+
]
|
|
56
|
+
hits: list[str] = []
|
|
57
|
+
for pattern in patterns:
|
|
58
|
+
if pattern in output:
|
|
59
|
+
hits.append(pattern)
|
|
60
|
+
return hits
|