This repository has been archived by the owner on Aug 5, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathheyserial.py
675 lines (580 loc) · 23.9 KB
/
heyserial.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
# Copyright (C) 2021 Alyssa Rahman, Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
HEYSERIAL = """\n __ __ _____ _ ____
/ / / /__ __ __ / ___/___ _____(_)___ _/ / /
/ /_/ / _ \/ / / / \__ \/ _ \/ ___/ / __ `/ / /
/ __ / __/ /_/ / ___/ / __/ / / / /_/ / /_/
/_/ /_/\___/\__, ( ) /____/\___/_/ /_/\__,_/_(_)
/____/|/ \n"""
AUTHOR = "Alyssa Rahman @ramen0x3f"
CREATED = "2021-10-27"
LASTUPDATED = "2021-12-02"
DESCRIPTION = """Programmatically create detections for deserialization exploitation with multiple
- keywords (e.g. cmd.exe)
- gadget chains (e.g. CommonsCollection)
- object types (e.g. ViewState, Java, Python Pickle, PHP)
- encodings (e.g. Base64, raw)
- formats (e.g. Snort, Yara)"""
DISCLAIMER="""--------------\n| DISCLAIMER |\n--------------
Rules generated by this tool are intended for hunting/research purposes and are not designed for high fidelity/blocking purposes.
Please test thoroughly before deploying to any production systems."""
HELP = "python3 heyserial.py -h"
EXAMPLES = """ python3 heyserial.py -k cmd.exe powershell -c 'ExampleChain::mscorlib+ActivitySurrogateSelector' -o snort -e base64 raw
python3 heyserial.py -c 'ExampleChain::mscorlib+ActivitySurrogateSelector' -t NETViewState"""
from argparse import ArgumentParser,ArgumentTypeError,RawTextHelpFormatter
from base64 import b64encode
from chardet import detect
from datetime import datetime
from hashlib import md5
from pathlib import Path
from re import match,sub
from sys import stderr
# Global Lists defined at beginning of MAIN FUNCTIONS section
def enc_base64(term, isprefix=False):
"""Encode provided term as Base64 with various offsets
Parameters
----------
term : byte string
keyword or object prefix to be encoded
isprefix : bool, optional
specify if it's a keyword or object prefix, by default False
Output
------
base64 encoded term values : list of ASCII strings
if isprefix==False
encoded prefix value : ASCII string
if isprefix==True
"""
# Calc off0 B64 and remove any variable characters
term = term if not isinstance(term, str) else term.encode('utf-8')
off0 = b64encode(term).decode('ascii')
off0 = off0[0:off0.find('=')-1] if '=' in off0 else off0
# Calc off1 B64 and remove any variable characters
temp = b'\x00' + term
off1 = b64encode(temp).decode('ascii')[2:]
off1 = off1[0:off1.find('=')-1] if '=' in off1 else off1
# Calc off2 B64 and remove any variable characters
temp = b'\x00' + temp
off2 = b64encode(temp).decode('ascii')[3:]
off2 = off2[0:off2.find('=')-1] if '=' in off2 else off2
# The most minimal of safety checking
if isprefix:
return off0
elif len(off0) < 4 or len(off1) < 4 or len(off2) < 4:
print("[*] WARNING - 1 or more terms is shorter than 4 characters. This may result in a resource intensive rule.", file=stderr)
return [off0.encode('utf-8'), off1.encode('utf-8'), off2.encode('utf-8')]
def enc_raw(term, isprefix=False):
"""Encode provided term as raw hex
Parameters
----------
term : byte string
keyword or object prefix to be encoded
isprefix : bool, optional
specify if it's a keyword or object prefix, by default False
Output
------
encoded term value: list of ASCII string with \\x hex bytes
if isprefix==False
encoded prefix value : ASCII string with |<hex bytes>|
if isprefix==True
"""
term = term.encode("utf-8") if isinstance(term, str) else term
delim = " " if isprefix else "\\x"
pre = "|" if isprefix else "\\x"
post = "|" if isprefix else ""
return "{}{}{}".format(pre, delim.join('%02x' % i for i in term), post)
def enc_utf8(term, isprefix=False):
"""Encode provided term as UTF8
Parameters
----------
term : byte string
keyword or object prefix to be encoded
isprefix : bool, optional
specify if it's a keyword or object prefix, by default False
Output
------
encoded term value: list of byte strings
if isprefix==False
encoded prefix value : byte string
if isprefix==True
"""
decoded_term = term.decode(detect(term)['encoding']) if not isinstance(term, str) else term
return decoded_term.encode('utf-8') if isprefix else [decoded_term.encode('utf-8')]
def enc_utf16le(term, isprefix=False):
"""Encode provided term as UTF16LE
Parameters
----------
term : byte string
keyword or object prefix to be encoded
isprefix : bool, optional
specify if it's a keyword or object prefix, by default False
Output
------
encoded term value: list of byte strings
if isprefix==False
encoded prefix value : byte string
if isprefix==True
"""
if isprefix and term == b'\xff\xfe': #This is for YSoSerialNET prefixes which are already UTF16
return term
decoded_term = term.decode(detect(term)['encoding']) if not isinstance(term, str) else term
return decoded_term.encode('utf-16le') if isprefix else [decoded_term.encode('utf-16le')]
def enc_combo(term, encchain, isprefix=False):
"""Encode provided term with a list of encoders like it's the wild frickin west
WARNING: RECURSION AHEAD
Parameters
----------
term : byte string
keyword or object prefix to be encoded
encchain : list of functions
encoding functions to use (in order)
isprefix : bool, optional
specify if it's a keyword or object prefix, by default False
Output
------
base64 encoded term values : list of ASCII strings
if isprefix==False
encoded prefix value : ASCII string
if isprefix==True
"""
new_encoded = []
# Encode the thing
if isinstance(term, (str, bytes)):
new_encoded = encchain[0](term, isprefix)
# Or Encode the things
else:
for t in term:
new_encoded.extend(encchain[0](t, isprefix))
# Uncomment below for debugging
#print("Recursion {}:\n\tTerm:{}\n\tEncoded: {}".format(len(encchain),term,new_encoded))
# Recurse recursively
if len(encchain) == 1:
return new_encoded
else:
return enc_combo(new_encoded, encchain[1:], isprefix)
def encode_all(keywords, encoders, ischain=False, isprefix=False):
"""Encode all provided keywords/chains with provided encoders
Parameters
----------
keywords : list of strings
either list of keywords or + delimited chains
encoders : dictionary of enc_type:enc_func
original encoders dictionary filtered to those selected by the user
ischain : bool, optional
specify if it's a + delimited chain or not, by default False
isprefix : bool, optional
specify if it's a keyword or object prefix, by default False
Output
------
Success : dictionary of enc_type:sub-dict where sub-dict is keyword:list_of_encoded_strings
Error : None
"""
# Create discrete keyword/search term list
results = {}
if ischain:
all_keywords = "".join(keywords.split("::")[1:])
all_keywords = all_keywords.split("+")
elif not isprefix:
all_keywords = [keywords] if isinstance(keywords, (str,bytes)) else keywords
# Encode all keywords
try:
for enc_type,enc_func in encoders.items():
#Won't be a list for prefixes so keeping it separate
if isprefix:
if "+" in enc_type: #Chained encoders
results[enc_type] = enc_raw(enc_combo(keywords, enc_func, isprefix), isprefix)
elif enc_type != "base64" and enc_type != "raw": #Make sure everything is escaped and ASCII-fied
results[enc_type] = enc_raw(enc_func(keywords, isprefix), isprefix)
else: #Base64 is already ASCII
x = enc_func(keywords, isprefix)
results[enc_type] = x if isinstance(x, str) else str(x.decode('ascii'))
else:
results[enc_type] = {}
#This was too gross to be a dict comprehension
for key in all_keywords:
rawkey = key.encode('utf-8')
if "+" in enc_type: #Chained encoders
enc_key = [enc_raw(x, isprefix) for x in enc_combo(rawkey, enc_func, isprefix)]
elif enc_type != "base64" and enc_type != "raw": #Make sure everything is escaped and ASCII-fied
enc_key = [enc_raw(x, isprefix) for x in enc_func(rawkey, isprefix)]
elif enc_type == "base64": #Base64 is already list
enc_key = []
for x in enc_func(rawkey, isprefix):
enc_key.append(x if isinstance(x,str) else str(x.decode('ascii')))
elif enc_type == "raw": #raw is already ASCII
enc_key = [enc_func(rawkey, isprefix)]
# Add to results
if enc_key is not None:
results[enc_type][key] = enc_key
return results
except Exception as e:
print("[!] ERROR - Skipping due to encoding error: {}".format(keywords), file=stderr)
print(e)
return None
"""
OUTPUT FUNCTIONS
"""
def build_regex(searchterm, keywords, objtype, objheader, ischain):
"""Build a regex string based on encoded keywords and object type
Parameters
----------
searchterm : ASCII string
original user-provided input
keywords : dictionary <keyword>:list_of_encoded_versions
dictionary of all keywords and a list of their encoded versions
some methods (e.g. Base64 offsets) may have multiple values
if ischain==True, this list will include all individual keywords from the chain.
objtype : ASCII string
must be one of supported types
objheader : ASCII string
prefix. may be encoded. some prefixes/encodings require special modifications.
ischain : bool, optional
specify if it's a + delimited chain, by default False
Output
------
Success : list of strings
arr[0] == encoded prefix and rest are keywords
"""
#Escaping
edgecase = objheader
#Handle terms (prefixes) that had to be left as byte strings vs ASCII
if edgecase[0] == "|" and edgecase[-1] == "|":
edgecase = "\\x{}".format(edgecase[1:-1].replace(" ", "\\x"))
# Ok this is cheating BUT I'm using a [0-9]+ regex with a Base64 version special case
if objtype == "PHPObj":
edgecase = edgecase+"[0-9]+:" if objheader != "Tz" else edgecase+"[x-z0-5]"
#Build it all
if ischain:
term_only = "".join(searchterm.split("::")[1:]) #Take out the chain name
combined_keywords = [edgecase] # This makes the regex more performant
for s in term_only.split("+"):
combined_keywords.append("(?:" + "|".join(keywords[s]) + ")" if len(keywords[s]) > 1 else keywords[s][0])
else:
combined_keywords = [edgecase, "(?:" + "|".join(keywords[searchterm]) + ")" if len(keywords[searchterm]) > 1 else keywords[searchterm][0]]
return combined_keywords
def prep_for_name(searchterm, extract_chain_name=False):
"""Return string that can be used for rule names. Also extracts <Name> from chains where format is <Name>::<Chain>
Parameters
----------
searchterm : string
original term/chain provided by user
extract_chain_name : bool, optional
Output
------
extract_chain_name==False : string, \W characters only
extract_chain_name==True : string, extracted <Name>
"""
if extract_chain_name:
return sub('\W+', '', searchterm.split("::")[0])
else:
return sub('\W+', '', searchterm.lower())
def gen_snort(searchterm, keywords, objtype, objheader, encoding, ischain=False):
"""Generate Snort rule for provided searchterm+object type+encoding
Parameters
----------
searchterm : ASCII string
original user-provided input
keywords : dictionary <keyword>:list_of_encoded_versions
dictionary of all keywords and a list of their encoded versions
some methods (e.g. Base64 offsets) may have multiple values
if ischain==True, this list will include all individual keywords from the chain.
objtype : ASCII string
must be one of supported types
objheader : ASCII string
prefix. may be encoded. some prefixes/encodings require special modifications.
encoding : ASCII string
name of encoding method
ischain : bool, optional
specify if it's a + delimited chain, by default False
Output
------
Success : (string, string)
name and rule
Error : None
"""
try:
# Metadata
name = "M.Methodology.HTTP.SerializedObject.{}.{}.[{}]".format(objtype, prep_for_name(searchterm, ischain), prep_for_name(encoding))
reg_default = build_regex(searchterm, keywords, objtype, objheader, ischain)
reg_snort = ""
# Do some Snort specifics mods to the regex
for r in reg_default[1:]:
if "|" not in r:
if "\\x" in r:
reg_snort = "{} content:\"|{}|\"; distance:0;".format(reg_snort, r.replace("\\x", " ")[1:])
else:
reg_snort = "{} content:\"{}\"; distance:0;".format(reg_snort, r)
else:
reg_snort = "{} pcre:\"/{}/Rs\";".format(reg_snort, r.replace("/", "\/"))
# Glue it together
rule = "alert tcp any any -> any any (msg:\"{}\"; content:\"T \"; offset:2; depth:3; content:\"{}\";{} threshold:type limit, track by_src, count 1, seconds 1800; sid:<REPLACE_SID>; rev:1;)"\
.format(name, objheader, reg_snort)
return name, rule
except Exception as e:
print("[!] ERROR - Something went wrong trying to create a Snort rule for {}\n{}".format(searchterm, e), file=stderr)
return None
def gen_yara(searchterm, keywords, objtype, objheader, encoding, ischain=False):
"""Generate Yara rule for provided searchterm+object type+encoding
Parameters
----------
searchterm : ASCII string
original user-provided input
keywords : dictionary <keyword>:list_of_encoded_versions
dictionary of all keywords and a list of their encoded versions
some methods (e.g. Base64 offsets) may have multiple values
if ischain==True, this list will include all individual keywords from the chain.
objtype : ASCII string
must be one of supported types
objheader : ASCII string
prefix. may be encoded. some prefixes/encodings require special modifications.
encoding : ASCII string
name of encoding method
ischain : bool, optional
specify if it's a + delimited chain, by default False
Output
------
Success : (string, string)
name and rule
Error : None
"""
try:
name = "M_Methodology_HTTP_SerializedObject_{}_{}_{}".format(objtype, prep_for_name(searchterm,ischain), prep_for_name(encoding))
# Use {} if the objheader is raw bytes otherwise use ""
objheader = "{{{}}}".format(objheader[1:-1]) if objheader[0] == "|" and objheader[-1] == "|" else "\"{}\"".format(objheader)
# Get base regex pieces
condition = "$objheader"
counter = 0
reg_default = build_regex(searchterm, keywords, objtype, objheader, ischain)
reg_yara = ""
# Do some Yara specifics mods to the regex
for r in reg_default[1:]:
if "|" not in r: #Single keyword
if "\\x" in r: #Hex needs to be bytes {}
reg_yara = "{}\n\t\t$keyword{} = {{{}}}".format(reg_yara, counter, r.replace("\\x", " "))
else: #ASCII can be a string ""
reg_yara = "{}\n\t\t$keyword{} = \"{}\"".format(reg_yara, counter, r)
else: #Chain needs a regex //
reg_yara = "{}\n\t\t$keyword{} = /{}/".format(reg_yara, counter, r.replace("(?:", "(").replace("/", "\/")) #Yara doesn't like the ?: modifier
# Build the condition so they're sequenced
if counter == 0:
condition = "{} and (@keyword0[1] > @objheader[1])".format(condition)
else:
condition = "{} and (@keyword{}[1] > @keyword{}[1])".format(condition, counter, counter-1)
counter += 1
rule = "rule {} {{\n\tmeta:\n\t\tauthor=\"Auto-generated by heyserial.py - Alyssa Rahman (@ramen0x3f)\"\n\t\tdescription=\"Auto-generated rule for serialized objects with the keyword/chain: {}\"\n\tstrings:\n\t\t$objheader={}{}\n\tcondition:\n\t\t{}\n}}"\
.format(name, prep_for_name(searchterm), objheader, reg_yara, condition)
return name, rule
except Exception as e:
print("[!] ERROR - Something went wrong trying to create a Yara rule: {}\n{}".format(name,e), file=stderr)
return None
def write_rule(searchterm, ruletype, rules):
"""Output rules to a file in ./rules/
Parameters
----------
searchterm : ASCII string
original user-provided input
ruletype : ASCII string
name of rule type (e.g. snort or yara)
rules : list of ASCII strings
all rules generateed for that keyword/chain (incl. different objtypes and encoding methods)
Output
------
Success : True
<build_name()>.<ruletype>
Error : False
"""
# Create necessary directories
Path("rules").mkdir(exist_ok=True)
# Create a file with a name like mykeyword_snort.tsv or mykeyword_yara.yar
rulename = prep_for_name(searchterm, True)
filename = "./rules/{}.{}".format(rulename,ruletype)
try:
# Print everything
with open(filename, "w") as output:
print("[+] Saving rules to file: {}".format(filename), file=stderr)
for name,rule in rules:
print(rule, file=output)
return True
# Handle exceptions cleanly
except Exception as e:
print("[!] Could not write rule to file: {}\n{}".format(filename, e), file=stderr)
return False
def write_report(keys, chains, format, used):
"""Output report of generated rules to a file
Parameters
----------
keys : ASCII string
original user-provided input
chains : ASCII string
name of rule type (e.g. snort or yara)
format : ASCII string
supported format type (e.g. bar or tsv delimited)
used : list of ASCII strings
used encoders, object types, and rule types
Output
------
Success : True
heyserial_report_<datetime>.<format>
Error : False
"""
# Set up report template
avail = [*encoding_types] + [*object_types] + [*output_types]
headers = ["Keyword(s)", "Is Chain"] + avail
delim = "|" if format == "bar" else "\t"
suffix = delim.join(["Y" if a in used else "N" for a in avail])
# Build rows
rows = ["{}{}N{}{}".format(k,delim,delim,suffix) for k in keys]
rows = rows + ["{}{}Y{}{}".format(c,delim,delim,suffix) for c in chains]
# Write report
filename = "heyserial_report_{}.{}".format(datetime.now().strftime("%Y%m%d-%H%M%S"),format)
try:
with open(filename, "w") as output:
print("[+] Saving report to file: {}".format(filename), file=stderr)
if format == "bar":
print("||" + "||".join(headers) + "||", file=output)
print("|" + "|\n|".join(rows) + "|", file=output)
else:
print(delim.join(headers), file=output)
print("\n".join(rows), file=output)
return True
except Exception as e:
print("[!] Could not write report to file: {}\n{}".format(filename, e), file=stderr)
return False
"""
MAIN FUNCTIONS
"""
# Global Lists
encoding_types = {
"base64": enc_base64,
"raw": enc_raw,
"utf8": enc_utf8,
"utf16le": enc_utf16le
}
object_types = {
"JavaObj": {"raw": b'\xac\xed'},
"JNDIObj": {"raw": b'\x24\x7b\x6a\x6e\x64\x69\x3a'},
"PHPObj": {"raw": b'\x4f\x3a'},
"PythonPickle": {"raw": b'\x80\x04\x95'},
# Including YSoSerial.NET formatters, but only NETViewState/LosFormatter has been tested.
## YSoSerial.NET encodes in UTF16-LE by default. The following is the UTF8 version.
## PLEASE test before using in production environments.
"NETGeneric" : {"raw": b'\x74\x79\x70\x65'}, #type
"NETObject" : {"raw": b'\x00\x01\x00\x00\x00'}, #AAEAAAD
"NETJavaScript": {"raw": b'\x5f\x74\x79\x70\x65'},
"NETSharpBinary": {"raw": b'\x01\x06\x01'},
"NETSharpXML" : {"raw": b'\x79\x70\x65'}, #(T)ype
"NETSOAP": {"raw": b'\x3c\x53\x4f\x41\x50'}, #<SOAP
"NETViewState": {"raw":b'\xff\x01'}, #Also for LosFormatter
"YSoSerialNET": {"raw":b'\xff\xfe'} #YSoSerial.NET encodes in UTF16-LE by default which prefixes 0xFFF1
}
output_types = {
"snort": gen_snort,
"yara": gen_yara
}
report_types = ["bar","tsv"]
def CHAIN_ARG(chaininput):
if not match(".*::.*\+", chaininput):
raise ArgumentTypeError("Must specify a chain with the format <Name>::<Chain>. \n\tFor example: CommonsCollection1::firstterm+secondterm+thirdterm")
return chaininput
def ENCODER_ARG(encinput):
for e in encinput.split("+"):
if e not in encoding_types.keys():
raise ArgumentTypeError("Must choose encoder(s) from: {}".format(",".join(list(encoding_types))))
return encinput
def parse_arguments():
"""Parse arguments from user
Output
------
Success : ArgumentParser object
Error : Exit program
"""
# Setting up argparser
parser = ArgumentParser(description="{}By: {}\tLast Updated: {}\n\n{}".format(HEYSERIAL, AUTHOR, LASTUPDATED, DESCRIPTION),
formatter_class=RawTextHelpFormatter, epilog="examples: \n{}\n\n{}".format(EXAMPLES, DISCLAIMER))
parser.add_argument('-k', '--keyword', nargs="+", help='keyword(s) to generate a rule for', default=[])
parser.add_argument('-c', '--chain', nargs="+", help='chain(s) to generate a rule for. Chains are an ordered and "+" delimited list of keywords. Prepended with <Chain Name>::.',
type=CHAIN_ARG, default=[])
parser.add_argument('-e', '--encoding', nargs="+", required=False,
help='format/encoder(s) to use. default: all. options: {}'.format(", ".join(list(encoding_types))),
type=ENCODER_ARG, default=list(encoding_types))
parser.add_argument('-t', '--type', nargs="+", required=False,
help='object type(s) to use. default: all',
choices=list(object_types), default=list(object_types))
parser.add_argument('--noprefixencode', action='store_true',
help='switch to skip encoding for the object prefixes (e.g. /w)')
parser.add_argument('-o', '--output', nargs="+", required=False,
help='output type(s) to use. default: all',
choices=list(output_types), default=list(output_types))
parser.add_argument('-r', '--report', nargs="+", required=False,
help='report type(s) to use. default: bar delimited',
choices=report_types, default=[report_types[0]])
args = parser.parse_args()
# Data, data, data! I cannot make bricks without clay!
if len(args.keyword) + len(args.chain) == 0:
print("[!] ERROR - No chains or keywords provided. Run with -h to see full help.", file=stderr)
exit()
# PHPObj headers are annoying. Trust me I'm trying to save you from doing broad searchers for 'O:'
encodingwillbreakphpobj = any(item not in ['base64', 'raw'] for item in args.encoding)
if "PHPObj" in args.type and encodingwillbreakphpobj:
print("[!] ERROR: You can only use raw and/or base64 encoding with PHPObj (otherwise the regex will break).", file=stderr)
exit()
# Parse and print all choices
key = ", ".join(args.keyword) if args.keyword else "None provided."
chain = ", ".join(args.chain) if args.chain else "None provided."
print("[+] Generating rules for\n\tKeywords\t{}\n\tChains\t\t{}\n\tObjects\t\t{}\n\tEncodings\t{}\n\tOutputs\t\t{}\n\n{}"\
.format(key, chain, ", ".join(args.type), ", ".join(args.encoding), ", ".join(args.output), DISCLAIMER), file=stderr)
return args
if __name__ == "__main__":
"""Main function
Run with python3 heyserial.py -h for full details
"""
# Set up arguments and options
args = parse_arguments()
# Combine the lists, but track which ones are chains.
searchterms = dict({k:False for k in args.keyword}, **{c:True for c in args.chain})
# Get relevant encoder functions
selected_encoders = {}
for e in args.encoding:
if "+" in e:
selected_encoders[e] = [encoding_types[x] for x in e.split("+")]
else:
selected_encoders[e] = encoding_types[e]
# Encode all prefixes
for obj,enc in object_types.items():
try:
if obj in args.type:
if args.noprefixencode: #Skip encoding if told to
enc.update(dict.fromkeys(args.encoding,enc_raw(enc['raw'], True)))
else: #Encode like normal otherwise
enc.update(encode_all(enc['raw'], selected_encoders, False, True))
except:
continue
# Generate relevant rule types
for key,ischain in searchterms.items():
# Encode keyword(s) with all specified types
encoded = encode_all(key, selected_encoders, ischain)
rules = dict.fromkeys(list(args.output), [])
rulename = prep_for_name(key, True)
# Make separate files for each output type (e.g. Snort, Gene)
for ruletype,rulelist in rules.items():
gen_func = output_types[ruletype]
# Create separate rules for each encoding type
for enctype,encvals in encoded.items():
# Only add successful ones
for obj in args.type:
obj_pre = object_types[obj][enctype]
x = gen_func(key, encvals, obj, obj_pre, enctype, ischain)
if x is not None:
rules[ruletype] = rules[ruletype] + [x]
# Output rules to file
write_rule(rulename, ruletype, rules[ruletype])
# Output report
used = args.encoding + args.type + args.output
for x in args.report:
write_report(args.keyword, args.chain, x, used)
print("[+] All done!", file=stderr)