-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathmlrd.py
More file actions
459 lines (392 loc) · 19.6 KB
/
mlrd.py
File metadata and controls
459 lines (392 loc) · 19.6 KB
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
'''
File name: mlrd.py
Author: Callum Lock
Date created: 31/03/2018
Date last modified: 31/03/2018
Python Version: 3.6
'''
import os
import sys
import argparse
import array
import math
import pickle
import pefile
import hashlib
import yara
import pandas as pd
import numpy as np
from sklearn.externals import joblib
import urllib
import urllib3
import json
import requests
from requests.auth import HTTPBasicAuth
from termcolor import colored, cprint
import colorama
import base64
import webbrowser
# Class to extract features from input file.
class ExtractFeatures():
# Defining init method taking parameter file.
def __init__(self, file):
self.file = file
# Method for extracting the MD5 hash of a file.
# It is not always possible to fit the entire file into memory so chunks of
# 4096 bytes are read and sequentially fed into the function.
def get_md5(self, file):
md5 = hashlib.md5()
with open(file, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
md5.update(chunk)
return md5.hexdigest()
# Method for compiling the yara rule for searching files for
# signs of bitcoin addresses.
def compile_bitcoin(self):
if not os.path.isdir("rules_compiled/Bitcoin"):
os.makedirs("rules_compiled/Bitcoin")
print("success")
for n in os.listdir("rules/Bitcoin"):
rule = yara.compile("rules/Bitcoin/" + n)
rule.save("rules_compiled/Bitcoin/" + n)
# Method for checking the input file for any signs of embedded bitcoin
# addresses. If the file does contain a bitcoin address a 1 is returned.
# Otherwise a 0 is returned.
def check_bitcoin(self, file):
for n in os.listdir("rules/Bitcoin"):
rule = yara.load("rules_compiled/Bitcoin/" + n)
m = rule.match(file)
if m:
return 1
else:
return 0
# Method for extracting all features from an input file.
def get_fileinfo(self, file):
# Creates a dictionary that will hold feature names as keys and
# their feature values as values.
features = {}
# Assigns pe to the input file. fast_load loads all directory
# information.
pe = pefile.PE(file, fast_load=True)
# CPU that the file is intended for.
features['Machine'] = pe.FILE_HEADER.Machine
# DebugSize is the size of the debug directory table. Clean files
# typically have a debug directory and thus, will have a non-zero
# values.
features['DebugSize'] = pe.OPTIONAL_HEADER.DATA_DIRECTORY[6].Size
# Debug Relative Virtual Address (RVA).
features['DebugRVA'] = pe.OPTIONAL_HEADER.DATA_DIRECTORY[6].\
VirtualAddress
# MajorImageVersion is the version of the file. This is user defined
# and for clean programs is often populated. Malware often has a
# value of 0 for this.
features['MajorImageVersion'] = pe.OPTIONAL_HEADER.MajorImageVersion
# MajorOSVersion is the major operating system required to run exe.
features['MajorOSVersion'] = pe.OPTIONAL_HEADER.\
MajorOperatingSystemVersion
# Export Relative Virtual Address (VRA).
features['ExportRVA'] = pe.OPTIONAL_HEADER.DATA_DIRECTORY[0].\
VirtualAddress
# ExportSize is the size of the export table. Usually non-zero for
# clean files.
features['ExportSize'] = pe.OPTIONAL_HEADER.DATA_DIRECTORY[0].Size
# IatRVA is the relative virtual address of import address
# table. Clean files typically have 4096 for this where as malware
# often has 0 or a very large number.
features['IatVRA'] = pe.OPTIONAL_HEADER.DATA_DIRECTORY[12].\
VirtualAddress
# ResourcesSize is the size of resources section of PE header.
# Malware sometimes has 0 resources.
features['MajorLinkerVersion'] = pe.OPTIONAL_HEADER.\
MajorLinkerVersion
# MinorLinkerVersion is the minor version linker that produced the
# file.
features['MinorLinkerVersion'] = pe.OPTIONAL_HEADER.MinorLinkerVersion
# NumberOfSections is the number of sections in file.
features['NumberOfSections'] = pe.FILE_HEADER.NumberOfSections
# SizeOfStackReserve denotes the amount of virtual memory to reserve
# for the initial thread's stack.
features['SizeOfStackReserve'] = pe.OPTIONAL_HEADER.SizeOfStackReserve
# DllCharacteristics is a set of flags indicating under which
# circumstances a DLL's initialization function will be called.
features['DllCharacteristics'] = pe.OPTIONAL_HEADER.DllCharacteristics
# ResourceSize denotes the size of the resources section.
# Malware may often have no resources but clean files will.
features['ResourceSize'] = pe.OPTIONAL_HEADER.DATA_DIRECTORY[2].Size
# Creates an object of Extract features and passes in the input
# file. The object get_bitcoin accesses the check_bitcoin method
# for which a 1 or 0 is returned and added as a value in the
# dictionary.
get_bitcoin = ExtractFeatures(file)
bitcoin_check = get_bitcoin.check_bitcoin(file)
features['BitcoinAddresses'] = bitcoin_check
# Returns features for the given input file.
return features
# Class to search third party reputation checkers and malware analysis
# websites to cross check if the tool is making correct decisions.
class RepChecker():
# Init method to initalise api keys and base urls.
def __init__(self):
# Virus Total api key
vtapi = base64.b64decode('M2FlNzgwMDU5MTE3ZThkYzdmNjA5YjVlOWU1Y2JmOTRkMGJkNTA3NTAyNzI3NWJiOTM3YTg0NGEwYTYzNDNlYQ==')
self.vtapi = vtapi.decode('utf-8')
# Virus Total base URL
self.vtbase = 'https://www.virustotal.com/vtapi/v2/file/report'
self.http = urllib3.PoolManager()
# Threat Crowd base URL.
self.tcbase = 'http://www.threatcrowd.org/searchApi/v2/file/report/?resource='
# Hybrid Analysis api key.
hapi = base64.b64decode('OGtzMDhrc3NrOGNja3Nnd3dnY2NnZzRzOG8wczA0Y2tzODA4c2NjYzAwZ2s0a2trZzRnc2s4Zzg0OGc4b2NvNA==')
self.hapi = hapi.decode('utf-8')
# Hybrid Analysis secret key.
hsecret = base64.b64decode('MTFhYjc1OTMxZGYzOWFjMmVjYmI3ZGNhNmI1MzYxMmE3YmU4ZjM3MTM5YTAwY2Nm')
self.hsecret = hsecret.decode('utf-8')
# Hybrid Analysis base URL.
self.hbase = 'https://www.hybrid-analysis.com/api/scan/'
# Method for authenticating to Virus Total API and file information
# in JSON.
def get_virus_total(self, md5):
params = {'apikey': self.vtapi, 'resource':md5}
data = urllib.parse.urlencode(params).encode("utf-8")
r = requests.get(self.vtbase, params=params)
return r.json()
# Method for returning file information in JSON from
# Threat Crowd.
def get_threatcrowd(self, md5):
r = requests.get(self.tcbase)
return r.json()
# Method for authenticating to Hybrid Analysis API and
# returning file information in JSON.
def get_hybrid(self, md5):
headers = {'User-Agent': 'Falcon'}
query = self.hbase + md5
r = requests.get(query, headers=headers, auth=HTTPBasicAuth(self.hapi, self.hsecret))
return r.json()
# Open up survey to evaluate program.
def survey_mail():
print('\n[*] Opening up survey in browser.\n')
webbrowser.open('https://www.surveymonkey.de/r/N289B82', new=2)
# Function to parse user input. Takes in input file, extracted features,
# and parsed options.
def parse(file, features, display, virustotal, threatcrowd, hybridanalysis):
# Creates an object of RepChecker to return third party about
# input file.
get_data = RepChecker()
# Creates an object of ExtractFeatures to return information about the
# input file.
md5 = ExtractFeatures(file)
md5_hash = md5.get_md5(file)
# If display option is selected, the extracted features are printed
# to the screen.
if display:
print("[*] Printing extracted file features...")
print("\n\tMD5: ", md5_hash)
print("\tDebug Size: ", features[0])
print("\tDebug RVA: ", features[1])
print("\tMajor Image Version:", features[2])
print("\tMajor OS Version:", features[3])
print("\tExport RVA:", features[4])
print("\tExport Size:", features[5])
print("\tIat RVA: ", features[6])
print("\tMajor Linker Version: ", features[7])
print("\tMinor Linker Version", features[8])
print("\tNumber Of Sections: ", features[9])
print("\tSize Of Stack Reserve: ", features[10])
print("\tDll Characteristics: ", features[11])
if features[12] == 1:
print("\tBitcoin Addresses: Yes\n")
else:
print("\tBitcoin Addresses: No\n")
# If Virus Total option is selected, file information from Virus
# total is returned.
if virustotal:
print("[+] Running Virus Total reputation check...\n")
# Retrieves data from virus total. Searches by passing in
# md5 hash of input file.
data = get_data.get_virus_total(md5_hash)
# If the response code is 0, error message is returned indicating
# that the md5 hash is not in virus total. Otherwise, the number
# of AV companies that detected the file as malicious is returned
# If 0, output is in green.
# Between 0 and 25, output is yellow.
# Over 25, output is red.
if data['response_code'] == 0:
print("[-] The file %s with MD5 hash %s was not found in Virus Total" % (os.path.basename(file), md5_hash))
else:
print("\tResults for file %s with MD5 %s:" % (os.path.basename(file), md5_hash))
if data['positives'] == 0:
print("\n\tDetected by: ", colored(str(data['positives']), 'green'), '/', data['total'], '\n')
elif data['positives'] > 0 and data['positives'] <= 25:
print("\n\tDetected by: ", colored(str(data['positives']), 'yellow'), '/', data['total'], '\n')
else:
print("\n\tDetected by: ", colored(str(data['positives']), 'red'), '/', data['total'], '\n')
# Creates two lists to store the AV companies who detected the file
# as malicious and to store corresponding malware names.
av_firms = []
malware_names = []
fmt = '%-4s%-23s%s'
# If any AV company indicated that the file is malicious, it is
# printed to the screen.
if data['positives'] > 0:
for scan in data['scans']:
if data['scans'][scan]['detected'] == True:
av_firms.append(scan)
malware_names.append(data['scans'][scan]['result'])
print('\t', fmt % ('', 'AV Firm', 'Malware Name'))
for i, (l1, l2) in enumerate(zip(av_firms, malware_names)):
print('\t', fmt % (i, l1, l2))
if data['permalink']:
print("\n\tVirus Total Report: ", data['permalink'], '\n')
# Prints if Virus Total has found the file to be malicious.
if data['positives'] == 0:
print(colored('[*] ', 'green') + "Virus Total has found the file %s " % os.path.basename(file) + colored("not malicious.", 'green'))
if data['permalink']:
print("\n\tVirus Total Report: ", data['permalink'], '\n')
elif data['positives'] > 0 and data['positives'] <= 25:
print(colored('[*] ', 'red') + "Virus Total has found the file %s " % os.path.basename(file) + colored("has malicious properties.\n", 'yellow'))
else:
print(colored('[*] ', 'red') + "Virus Total has found the file %s " % os.path.basename(file) + colored("is malicious.\n", 'red'))
# If threat crowd option is selected, file information is returned.
if threatcrowd:
fmt = '%-4s%-23s'
print("[+] Retrieving information from Threat Crowd...\n")
data = get_data.get_threatcrowd(md5_hash)
# If response code is 0, an error message is thrown to indicate
# the file is not in Threat Crowd. Otherwise, the SHA1 Hash,
# domain names, and malware names given by AV companies for
# the file is printed to the screen.
if data['response_code'] == "0":
print("[-] The file %s with MD5 hash %s was not found in Threat Crowd.\n" % (os.path.basename(file), md5_hash))
else:
print("\n\tSHA1: ", data['sha1'])
if data['ips']:
print('\n\t', fmt % ('', 'IPs'))
for i, ip in enumerate((data['ips'])):
print('\t', fmt % (i+1, ip))
if data['domains']:
print('\n\t', fmt % ('', 'Domains'))
for i, domain in enumerate((data['domains'])):
print('\t', fmt % (i+1, domain))
if data['scans']:
if data['scans'][1:]:
print('\n\t', fmt % ('', 'Antivirus'))
for i, scan in enumerate(data['scans'][1:]):
print('\t', fmt % (i+1, scan))
print('\n\tThreat Crowd Report: ', data['permalink'], '\n')
# If hybrid analysis option is selected, file information is returned.
if hybridanalysis:
# Searches hybrid analysis with md5 hash of file and attempts
# to return its information in JSON format.
data = get_data.get_hybrid(md5_hash)
fmt = '%-4s%-23s'
print("[+] Retrieving information from Hybrid Analysis...\n")
# If no response, error message is thrown to indicate that the file
# is not in Hybrid Analysis. Otherwise, SHA256, SHA1, Threat Level,
# Threat Score, Verdict (malicious / not malicious), malware family,
# and network information is returned
if not data['response']:
print("[-] The file %s with MD5 hash %s was not found in Hybrid Analysis." % (os.path.basename(file), md5_hash), '\n')
else:
try:
print('\t', data['response'][0]['submitname'])
except:
pass
print('\tSHA256:', data['response'][0]['sha256'])
print('\tSHA1: ', data['response'][0]['sha1'])
print('\tThreat Level: ', data['response'][0]['threatlevel'])
print('\tThreat Score: ', data['response'][0]['threatscore'])
print('\tVerdict: ', data['response'][0]['verdict'])
try:
print('\tFamily: ', data['response'][0]['vxfamily'])
except:
pass
try:
if data['response'][0]['classification_tags']:
print('\n\t', fmt % ('', 'Class Tags'))
for i, tag in enumerate(data['response'][0]['classification_tags']):
print('\t', fmt % (i+1, tag))
else:
print("\tClass Tags: No Classification Tags.")
except:
pass
try:
if data['response'][0]['compromised_hosts']:
print('\n\t', fmt % ('', 'Compromised Hosts'))
for i, host in enumerate(data['response'][0]['compromised_hosts']):
print('\t', fmt % (i+1, host))
else:
print('\t\nCompromised Hosts: No Compromised Hosts.')
except:
pass
try:
if data['response'][0]['domains']:
print('\n\t', fmt % ('', 'Domains'))
for i, domain in enumerate(data['response'][0]['domains']):
print('\t', fmt % (i+1, domain))
else:
print('\tDomains: No Domains.')
except:
pass
try:
if data['response'][0]['total_network_connections']:
print('\tNetwork Connections: ', data['response'][0]['total_network_connections'])
else:
print('\n\tNetwork Connections: No Network Connections')
except:
pass
try:
if data['response'][0]['families']:
print('\tFamilies: ', data['response'][0]['families'])
except:
pass
# Verdict is printed to screen.
# Malicious = red.
# Benign = green.
if data['response'][0]['verdict'] == "malicious":
print(colored('\n[*] ', 'red') + "Hybrid Analysis has found that the file %s " % os.path.basename(file) + colored("is malicious.\n", 'red'))
else:
print(colored('\n[*] ', 'green') + "Hybrid Analysis has found that the file %s " % os.path.basename(file) + colored("is not malicious.\n", 'green'))
def main():
parser = argparse.ArgumentParser(epilog="MLRD uses machine learning to detect ransomware\n\
. Supply a file to determine whether or not it is ransomware. Virus Total\
, Threat Crowd and Hybrid Analysis can be queried for verification.",
description="Machine Learning Ransowmare Detector (MLRD)")
parser.add_argument('file', nargs='?', help="File To Parse", )
parser.add_argument('-d', '--displayfeatures', action='store_true', dest='display', help='Display extracted file features.')
parser.add_argument('-v', "--virustotal", action='store_true', dest='virustotal', help="Run with Virus Total check.")
parser.add_argument('-t', '--threatcrowd', action='store_true', dest='threatcrowd', help="Run with Threat Crowd check.")
parser.add_argument('-z', '--hybridanalysis', action='store_true', dest='hybridanalysis', help="Run Hybrid Analysis check.")
parser.add_argument('-s', '--survey', nargs='*', help='Evaluate Program using Survey.')
args = parser.parse_args()
colorama.init()
if args.survey is not None:
survey_mail()
sys.exit(0)
# Loads classifier
clf = joblib.load(os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'classifier/classifier.pkl'))
# Loads saved features
features = pickle.loads(open(os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'classifier/features.pkl'),
'rb').read())
# Creates an object of ExtractFeatures and passes in input file.
get_features = ExtractFeatures(args.file)
# Assigns data to extracted features
data = get_features.get_fileinfo(args.file)
feature_list = list(map(lambda x:data[x], features))
print("\n[+] Running analyzer...\n")
# Asssings result as the prediction of the input file based on its given features.
result = clf.predict([feature_list])[0]
# If result is 1, the file is benign.
# Otherwise, the file is malicious.
if result == 1:
print(colored('[*] ', 'green') + "The file %s has been identified as " % os.path.basename(sys.argv[1]) + colored('benign.\n', 'green'))
else:
print(colored('[*] ', 'red') + "The file %s has been identified as " % os.path.basename(sys.argv[1]) + colored('malicious.\n', 'red'))
# Passes command line arguments to parse function for parsing.
if args.display or args.virustotal or args.threatcrowd or args.hybridanalysis:
parse(args.file, feature_list, args.display, args.virustotal, args.threatcrowd, args.hybridanalysis)
if __name__ == '__main__':
main()