summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/__init__.py0
-rw-r--r--modules/entropy.py56
-rw-r--r--modules/grep_count.py234
-rw-r--r--modules/levenshtein.py73
-rw-r--r--modules/libfuzzy.py98
-rw-r--r--modules/scanmodule.py56
-rw-r--r--modules/whitelist.py46
7 files changed, 563 insertions, 0 deletions
diff --git a/modules/__init__.py b/modules/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/modules/__init__.py
diff --git a/modules/entropy.py b/modules/entropy.py
new file mode 100644
index 0000000..48b2924
--- /dev/null
+++ b/modules/entropy.py
@@ -0,0 +1,56 @@
1''' This module uses shannon's Entropy to detect packed malwares
2'''
3import os
4import math
5import logging
6logging.basicConfig(level=logging.DEBUG)
7
8import scanmodule
9
10def main():
11 return Entropy()
12
13class Entropy(scanmodule.ScanModule):
14 name = 'entropy'
15 def populate(self, path):
16 pass
17 def load(self, path):
18 pass
19 def save(self, path):
20 pass
21
22 def __compute_score(self, path):
23 return (self.__entropy(path) - 5) * 100
24
25 def is_malware(self, path):
26 score = self.__compute_score(path)
27 logging.info('Entropy score for ' + path + ' : ' + str(score))
28 return score > 75
29
30 def evaluate(self, path):
31 ''' Computes an arbitraty score for the given path
32 @ret A sorted list of the form [name, match_in_percent_superior_to_zero]
33 '''
34 score = self.__compute_score(path)
35 if score > 0:
36 return [['MALWARE', score],]
37 return None
38
39 def __entropy(self, path):
40 ''' Computes shannon's entropy for the given file
41 @param path Path to the file
42 '''
43 # Computes the frequency of each byte in the file
44 fsize = max(float(os.path.getsize(path)), 1.0)
45
46 freq = [0] * 256
47 with open(path, 'rb') as f:
48 for c in f.read():
49 freq[ord(c)] += 1
50
51 entropy = 0.0
52 for f in freq:
53 if f:
54 f /= fsize
55 entropy += f * math.log(f, 2)
56 return -entropy
diff --git a/modules/grep_count.py b/modules/grep_count.py
new file mode 100644
index 0000000..2431960
--- /dev/null
+++ b/modules/grep_count.py
@@ -0,0 +1,234 @@
1''' This module count the occurences of dodgy terms present in a file
2'''
3import os
4import logging
5logging.basicConfig(level=logging.DEBUG)
6
7import scanmodule
8
9def main():
10 return GrepCount()
11
12class GrepCount(scanmodule.ScanModule):
13 name = 'grep count'
14
15 # ranked from 1 to 10, 10 being EVIL
16 # Also, 100 is awarded to MEGA-DUH-OBVIOUS things.
17 dodgy_terms = {
18 '$GLOBALS': 6,
19 'WWW-Authenticate': 7,
20 'ZipArchive': 6,
21 'apache_get_modules': 8,
22 'assert': 5,
23 'base64_decode': 7,
24 'bzdecompress': 7,
25 'chmod': 8,
26 'curl_init("file://': 100, # safe mode bypass exploit
27 #'dl': 10,
28 #'exec': 10,
29 'eval(': 10,
30 'eval(base64_decode': 100,
31 'eval($_GET': 100,
32 'eval($_POST': 100,
33 'eval($_REQUEST': 100,
34 'eval(base64_decode': 100,
35 'eval(gzinflate': 100,
36 'file_get_contents': 6,
37 'fpassthru': 100,
38 'fsockopen': 7,
39 'ftp_connect': 7,
40 'ftp_exec': 7,
41 'ftp_login': 7,
42 'function_exists': 4,
43 'get_current_user': 10,
44 'getcwd': 8,
45 'getenv': 9,
46 'getmxrr': 5,
47 'getmygid': 10,
48 'getmygid': 10,
49 'getmyinode': 10,
50 'getmypid': 10,
51 'getmyuid': 10,
52 'gzinflate': 7,
53 'gzinflate(base64_decode(': 100,
54 'gzuncompress': 7,
55 'ini_get': 6,
56 'ini_set': 6,
57 'is_readable': 10,
58 'mysql_get_client_info': 7,
59 'open_basedir': 9,
60 'passthru': 10,
61 'passthru($_GET': 100,
62 'passthru($_POST': 100,
63 'passthru($_REQUEST': 100,
64 'pclose': 9,
65 'pcntl_fork': 10,
66 'php_logo_guid': 10,
67 'php_uname': 8,
68 'phpcredits': 10,
69 'phpinfo': 10,
70 'phpversion': 5,
71 'pnctl_exec': 10,
72 'pnctl_fork': 10,
73 'popen': 10,
74 'posix_getegid': 10,
75 'posix_geteuid': 10,
76 'posix_getgetgruid': 10,
77 'posix_getpwuid': 10,
78 'posix_kill': 10,
79 'posix_mkfifo': 10,
80 'posix_setgid': 10,
81 'posix_setpgid': 10,
82 'posix_setsid': 10,
83 'posix_setuid': 10,
84 'posix_uname': 10,
85 'php://input': 7,
86 'proc_close': 10,
87 'proc_get_status': 10,
88 'proc_nice': 10,
89 'proc_open': 10,
90 'proc_terminate': 10,
91 'putenv': 10,
92 'putenv("PHP': 100, # Shellshock exploit
93 'putenv(\'PHP': 100, # Shellshock exploit
94 'safe_mode': 10,
95 'shell_exec': 10,
96 'show_source': 10,
97 'socket_create(AF_INET, SOCK_STREAM, SOL_TCP)': 10, # Used for SYN flood
98 'symlink': 8,
99 'system(': 9,
100 'system($_GET': 100,
101 'system($_POST': 100,
102 'system($_REQUEST': 100,
103 'win_shell_execute': 10,
104 'win_create_service': 100,
105 'wscript': 8,
106 'zend_logo_guid': 10,
107 'zend_thread_id': 9,
108 'zend_version': 9,
109 }
110
111 dodgy_terms.update({
112 '/bin/bash ': 100,
113 '/bin/sh ': 100,
114 '/etc/hosts': 100,
115 '/etc/passwd': 100,
116 '/etc/resolv.conf ': 100,
117 '/etc/shadow': 100,
118 '/etc/syslog.conf': 100,
119 '/proc/cpuinfo': 10,
120 '/tmp': 8,
121 '/var/cpanel/accounting.log': 100,
122 'IRC server': 100,
123 'LD_PRELOAD': 100,
124 'PRIVMSG': 100,
125 'Safe Mod Bypass': 100,
126 'Shell ': 9,
127 '\\x': 3, # Shellcodes
128 '\x00/../': 100, # safe mode bypass
129 'backdoor': 10,
130 'bypass': 8,
131 'chkrootkit': 100,
132 'chmod 777': 7,
133 'cmd.exe': 100,
134 'dir /OG /X': 100,
135 'find . -type f': 100,
136 'gcc ': 8,
137 'id_rsa': 100,
138 'ipconfig /all': 100,
139 'jschl_vc': 100, # Cloudflare bypass
140 'jschl_answer': 100, # Cloudflare bypass
141 'kernel32.dll': 100,
142 'ls -la': 100,
143 'milw0rm': 100,
144 'my.cnf': 100,
145 'my.conf': 100,
146 'nc -l': 100,
147 'netstat ': 100,
148 'file:file://': 100, # basedir bypass
149 'portsentry': 100,
150 'proftpd.conf': 100,
151 'ps -aux': 100,
152 'rkhunter': 100,
153 'shellcode': 100,
154 'slowloris': 100,
155 'snort': 100,
156 'system32': 9,
157 'tripwire': 100,
158 'uname -a': 100,
159 'wget': 8,
160 'WinExec': 10,
161 })
162
163 dodgy_terms.update({
164 '/cdn-cgi/l/chk_jschl': 100, # Cloudflare bypass for DDoS'ing
165 'Antichat Shell': 100,
166 'Cr@zy_King': 100,
167 'KAdot@ngs.ru': 100,
168 'Kacak': 100,
169 'KingDefacer': 100,
170 'SimAttacker': 100,
171 'SoldiersOfAllah': 100,
172 'ak74-team.net': 100,
173 'alturks.com': 100,
174 'egy_spider' : 100,
175 'egyspider.eu' : 100,
176 'exploit-db.com': 100,
177 'forever5pi': 100,
178 'grayhatz.org': 100,
179 'kacaq.blogspot.com': 100,
180 'locus7s.com': 100,
181 'michaeldaw.org': 100,
182 'milw0rm.com': 100,
183 'pentestmonkey': 100,
184 'r57.biz': 100,
185 'r57shell.net': 100,
186 'rootshell-team.info': 100,
187 'simorgh': 100,
188 'thecrowsrew.org': 100,
189 'vnhacker.org': 100,
190 'xdevil.org': 100,
191 'zehirhacker': 100,
192 '~z0mbie': 100,
193 })
194
195 def populate(self, path):
196 ''' Does nothing :<
197 '''
198 pass
199
200 def evaluate(self, path):
201 ''' Check the given file against a list of know dodgy strings.
202 The calculation formulae is empirical.
203 @ret A sorted list of the form [name, match_in_percent_superior_to_zero]
204 '''
205 fsize = os.path.getsize(path)
206 if not fsize:
207 return None
208
209 content = ''
210 with open(path, 'r') as f:
211 content = f.read()
212
213 score = 0
214 for key,data in self.dodgy_terms.iteritems():
215 nb = content.find(key) * data
216 if nb > 0:
217 score += nb
218 score /= fsize
219
220 logging.info('Grep score for ' + path + ' : ' + str(score))
221
222 if score > 75:
223 return [['MALWARE', min(score, 100)],]
224 return None
225
226 def load(self, path):
227 pass
228
229 def save(self, path):
230 pass
231
232 def is_malware(self, path):
233 return self.evaluate(path) is not None
234
diff --git a/modules/levenshtein.py b/modules/levenshtein.py
new file mode 100644
index 0000000..2e854e2
--- /dev/null
+++ b/modules/levenshtein.py
@@ -0,0 +1,73 @@
1'''
2This modules has a super-awful complexity (something along n^4),
3so I'm quite sure that you don't want to run it by default ;)
4
5Anyway, this modules computes the Levenshtein distance between samples of malwares
6and files to check, to find similarities.
7'''
8import os
9
10import scanmodule
11
12def main():
13 return Levenshtein()
14
15class Levenshtein(scanmodule.ScanModule):
16 name = 'levenshtein'
17 def populate(self, path):
18 ''' We can't really populate the database with Levenshtein scores,
19 but we can speedup the calculation by storing files lenghts
20 '''
21 for root, _, filenames in os.walk(path):
22 for filename in filenames:
23 full_path = os.path.join(root, filename)
24 with open(full_path, 'r') as f:
25 self.samples[full_path] = [os.path.getsize(full_path), f.read().lower()]
26
27 def evaluate(self, path):
28 ''' Compare the hash of the given path to every samples one.
29 @ret A sorted list of the form [name, match_in_percent_superior_to_zero]
30 '''
31 file_to_test = path
32 file_size = os.path.getsize(file_to_test)
33
34 lst = list()
35 for sample_name, sample_intel in self.samples.iteritems():
36 if sample_name != file_to_test:
37 score = self.__levenshtein(file_to_test, sample_intel[1])
38 score = score / ((file_size + sample_intel[0]) / 2.0) # mean value
39 if score > 25: # if the match is under 10%, we don't care
40 lst.append([sample_name, score * 10])
41 return sorted(lst, key=lambda lst: lst[1], reverse=True)
42
43 def __levenshtein_file(self, f, b):
44 ''' Computes the Levenshtein's distance between a file and a buffer
45 @param f1 File
46 @param fs2 Buffer
47 @return The levenshtein distance
48 '''
49 with open(f, 'r') as of:
50 return self.__levenshtein(of.read().lower(), b)
51
52 def __levenshtein(self, s1, s2):
53 ''' Computes the Levenshtein's distance between two strings
54 @param s1 First string
55 @param s2 Second string
56 @return The levenshtein distance
57 '''
58
59 if len(s1) < len(s2): # Minimize computation
60 s1, s2 = s2, s1
61
62 previous_row = range(len(s2) + 1)
63 for i, c1 in enumerate(s1):
64 current_row = [i + 1]
65 for j, c2 in enumerate(s2):
66 insertions = previous_row[j + 1] + 1
67 deletions = current_row[j] + 1
68 substitutions = previous_row[j] + (c1 != c2)
69 current_row.append(min(insertions, deletions, substitutions))
70 previous_row = current_row
71
72 return previous_row[-1]
73
diff --git a/modules/libfuzzy.py b/modules/libfuzzy.py
new file mode 100644
index 0000000..a0d3f15
--- /dev/null
+++ b/modules/libfuzzy.py
@@ -0,0 +1,98 @@
1''' Ugly-pseudo-bindings to libfuzzy (used by ssdeep) to check
2if a file is similar to a given list of samples
3'''
4
5import os
6import ctypes
7import pickle
8import sys
9import logging
10logging.basicConfig(level=logging.DEBUG)
11
12import scanmodule
13
14
15SPAMSUM_LENGTH = 64
16FUZZY_MAX_RESULT = SPAMSUM_LENGTH + SPAMSUM_LENGTH // 2 + 20
17
18def main():
19 return FuzzyMatcher()
20
21class FuzzyMatcher(scanmodule.ScanModule):
22 name = 'libfuzzy'
23 def __init__(self, samples_path=None, persistence_path=None):
24 self.__initialize_libfuzzy()
25 super(FuzzyMatcher, self).__init__()
26
27 def __initialize_libfuzzy(self):
28 ''' Bind to libfuzzy thanks to ctypes.
29 This will create the "fuzzy_hash_buf" and
30 the "fuzzy_compare" methods
31 '''
32 try:
33 fuzzy = ctypes.CDLL('libfuzzy.so')
34 except OSError:
35 print('[-] Please check that you installed libfuzzy')
36 sys.exit(1)
37
38 self.__fuzzy_hash_buf = fuzzy.fuzzy_hash_buf
39 self.__fuzzy_hash_buf.restype = ctypes.c_int
40 self.__fuzzy_hash_buf.argtypes = [
41 ctypes.c_char_p, #buf
42 ctypes.c_uint32, #buf_len
43 ctypes.c_char_p, #result
44 ]
45 self.__fuzzy_compare = fuzzy.fuzzy_compare
46 self.__fuzzy_compare.restype = ctypes.c_int
47 self.__fuzzy_compare.argtypes = [
48 ctypes.c_char_p, #sig1
49 ctypes.c_char_p, #sig2
50 ]
51
52 def populate(self, path):
53 ''' Computes fuzzy hashes of files under the given path,
54 and store them in the dict self.samples with the form dict {name: fuzzy_hash}
55 @param path Path containing the samples
56 '''
57 for root, _, filenames in os.walk(path):
58 for filename in filenames:
59 full_path = os.path.join(root, filename)
60 self.samples[full_path] = self.__hash_from_file(full_path)
61
62 def __hash_from_file(self, path):
63 ''' Return the hash of the given file
64 @param path Path to the file to hash
65 @ret Fuzzy hash of the given file
66 '''
67 with open(path, 'r') as f:
68 out = ctypes.create_string_buffer('\x00' * FUZZY_MAX_RESULT)
69 content = f.read()
70 self.__fuzzy_hash_buf(content, len(content), out)
71 return out.value
72
73 def evaluate(self, path):
74 ''' Compare the hash of the given path to every samples one.
75 @ret A sorted list of the form [name, match_in_percent_superior_to_zero]
76 '''
77 fuzzy_hash = self.__hash_from_file(path)
78
79 lst = list()
80 for f in self.samples:
81 score = self.__fuzzy_compare(fuzzy_hash, self.samples[f])
82 if score:
83 lst.append([f, score])
84 return sorted(lst, key=lambda lst: lst[1], reverse=True)
85
86 def is_malware(self, path):
87 max_score = 0
88 fuzzy_hash = self.__hash_from_file(path)
89
90 for f in self.samples:
91 score = self.__fuzzy_compare(fuzzy_hash, self.samples[f])
92 if score > max_score:
93 score = max_score
94 logging.info('fuzzy score for ' + path + ' matches ' + f + ' at ' + str(score) + '%%')
95
96 return max_score > 90
97
98
diff --git a/modules/scanmodule.py b/modules/scanmodule.py
new file mode 100644
index 0000000..6ace387
--- /dev/null
+++ b/modules/scanmodule.py
@@ -0,0 +1,56 @@
1import ConfigParser
2import pickle
3
4
5class ScanModule(object):
6 def __init__(self):
7 self.config = ConfigParser.ConfigParser()
8 self.config.read('modules.conf')
9
10 self.samples = dict()
11
12 try:
13 self.populate(self.config.get(self.name, 'samples'))
14 except ConfigParser.NoOptionError:
15 pass
16
17 try:
18 self.load(self.config.get(self.name, 'persistence'))
19 except ConfigParser.NoOptionError:
20 pass
21
22 def is_disable(self):
23 try:
24 return self.config.getboolean(self.name, 'disable')
25 except ConfigParser.NoOptionError:
26 return False
27
28 def evaluate(self, path):
29 ''' Return in percent, the probability that
30 the file is a malware
31 @param path File to evaluate
32 '''
33 raise NotImplemented
34
35 def populate(self, path):
36 ''' Populate the module's internal database
37 with data from the given path
38 @param path Path to the data
39 '''
40 raise NotImplemented
41
42
43 def load(self, path):
44 ''' Unpickle the given path, and updates the samples dict with it.
45 @param path Path to the dict to unpickle
46 '''
47 with open(path, 'r') as f:
48 self.samples.update(pickle.load(f))
49
50 def save(self, path):
51 ''' Save the database to the given file
52 @param path Path where to save the database
53 '''
54 with open(path, 'w') as f:
55 pickle.dump(self.samples, f)
56
diff --git a/modules/whitelist.py b/modules/whitelist.py
new file mode 100644
index 0000000..587b392
--- /dev/null
+++ b/modules/whitelist.py
@@ -0,0 +1,46 @@
1import os
2import hashlib
3import scanmodule
4
5def main():
6 return HashWhitelist()
7
8class HashWhitelist(scanmodule.ScanModule):
9 name = 'hashwhitelist'
10 def evaluate(self, path):
11 ''' Return in percent, the probability that
12 the file is a malware
13 @param path File to evaluate
14 '''
15 sha1 = ''
16 with open(path, 'r') as f:
17 sha1 = hashlib.sha1(f.read()).hexdigest()
18
19 lst = list()
20 for f in self.samples:
21 if sha1 == self.samples[f]:
22 lst.append([f, 100])
23 return sorted(lst, key=lambda lst: lst[1], reverse=True)
24
25 def is_malware(self, path):
26 ''' Return False if the file is whitelisted
27 '''
28 sha1 = ''
29 with open(path, 'r') as f:
30 sha1 = hashlib.sha1(f.read()).hexdigest()
31
32 for f in self.samples:
33 if sha1 == self.samples[f]:
34 return False
35 return True
36
37 def populate(self, path):
38 ''' Populate the module's internal database
39 with data from the given path
40 @param path Path to the data
41 '''
42 for root, _, filenames in os.walk(path):
43 for filename in filenames:
44 full_path = os.path.join(root, filename)
45 with open(full_path, 'r') as f:
46 self.samples[full_path] = hashlib.sha1(f.read()).hexdigest()