From 171f7f951eb7923959b07cf4c704ec22f4a26269 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Thu, 30 Nov 2017 12:11:02 +0100 Subject: [PATCH 01/24] Levenshtein check on a small set of known system files --- lib/levenshtein.py | 43 +++++++++++++++++++++++++++++++++++++++++++ loki.py | 11 +++++++++++ 2 files changed, 54 insertions(+) create mode 100644 lib/levenshtein.py diff --git a/lib/levenshtein.py b/lib/levenshtein.py new file mode 100644 index 00000000..2c5ee37a --- /dev/null +++ b/lib/levenshtein.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# -*- coding: iso-8859-1 -*- +# -*- coding: utf-8 -*- +# +# Levenshtein related functions + +CHECK_FILES = ['svchost.exe', 'explorer.exe', 'iexplore.exe', 'lsass.exe', 'chrome.exe', 'csrss.exe', 'firefox.exe', + 'winlogon.exe'] + +class LevCheck(): + + def __init__(self): + pass + + def check(self, fileName): + """ + Check if file name is very similar to a file in the check list + :param fileName: + :return: + """ + for checkFile in CHECK_FILES: + if levenshtein(checkFile, fileName) == 1: + return checkFile + return None + +def levenshtein(s, t): + if s == t: return 0 + elif len(s) == 0: return len(t) + elif len(t) == 0: return len(s) + v0 = [None] * (len(t) + 1) + v1 = [None] * (len(t) + 1) + for i in range(len(v0)): + v0[i] = i + for i in range(len(s)): + v1[0] = i + 1 + for j in range(len(t)): + cost = 0 if s[i] == t[j] else 1 + v1[j + 1] = min(v1[j] + 1, v0[j + 1] + 1, v0[j] + cost) + for j in range(len(v0)): + v0[j] = v1[j] + + return v1[len(t)] + diff --git a/loki.py b/loki.py index f144470b..3be47c2e 100644 --- a/loki.py +++ b/loki.py @@ -39,6 +39,7 @@ # LOKI Modules from lib.lokilogger import * +from lib.levenshtein import LevCheck sys.stdout = codecs.getwriter('utf8')(sys.stdout) @@ -166,6 +167,9 @@ def __init__(self, intense_mode): # Initialize File Type Magic signatures self.initialize_filetype_magics(os.path.join(self.app_path, './signature-base/misc/file-type-signatures.txt')) + # Levenshtein Checker + self.LevCheck = LevCheck() + def scan_path(self, path): # Startup @@ -274,6 +278,13 @@ def scan_path(self, path): reasons.append("File Name IOC matched PATTERN: %s SUBSCORE: %s DESC: %s" % (fioc['regex'].pattern, fioc['score'], fioc['description'])) total_score += int(fioc['score']) + # Levenshtein Check + result = self.LevCheck.check(filename) + if result: + reasons.append("Levenshtein check - filename looks much like a well-known system file " + "SUBSCORE: 40 ORIGINAL: %s" % result) + total_score += 40 + # Access check (also used for magic header detection) firstBytes = "" firstBytesString = "-" From 0f39068b90b8bd4bff1b71f8b2b991141217f769 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Sat, 20 Jan 2018 10:45:22 +0100 Subject: [PATCH 02/24] Link to PE-Sieve --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ea4e139e..d703e1ea 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Additional Checks: 3. SWF decompressed scan (new since version v0.8) 4. SAM dump check 5. DoublePulsar check - tries to detect DoublePulsar backdoor on port 445/tcp and 3389/tcp -6. [PE-Sieve](https://github.com/hasherezade/pe-sieve) process check +6. [PE-Sieve](https://hshrzd.wordpress.com/pe-sieve/) process check The Windows binary is compiled with PyInstaller 2.1 and should run as x86 application on both x86 and x64 based systems. From e1790f5cb3932685c42775c71dd2cf26b2ceed18 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Tue, 13 Feb 2018 12:09:51 +0100 Subject: [PATCH 03/24] Better hostname evaluation on Linux/OSX --- lib/helpers.py | 11 +++++++++++ loki.py | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/helpers.py b/lib/helpers.py index ebefc692..7a80cbaa 100644 --- a/lib/helpers.py +++ b/lib/helpers.py @@ -310,3 +310,14 @@ def _kill_process_after_a_timeout(pid): kill_check.clear() return output, returnCode + +def getHostname(os_platform): + """ + Generate and return a hostname + :return: + """ + # Computername + if os_platform == "linux" or os_platform == "osx": + return os.uname()[1] + else: + return os.environ['COMPUTERNAME'] \ No newline at end of file diff --git a/loki.py b/loki.py index 192a91f1..6f084924 100644 --- a/loki.py +++ b/loki.py @@ -1395,7 +1395,7 @@ def signal_handler(signal_name, frame): parser = argparse.ArgumentParser(description='Loki - Simple IOC Scanner') parser.add_argument('-p', help='Path to scan', metavar='path', default='C:\\') parser.add_argument('-s', help='Maximum file size to check in KB (default 5000 KB)', metavar='kilobyte', default=5000) - parser.add_argument('-l', help='Log file', metavar='log-file', default='loki-%s.log' % t_hostname) + parser.add_argument('-l', help='Log file', metavar='log-file', default='loki-%s.log' % getHostname(os_platform)) parser.add_argument('-r', help='Remote syslog system', metavar='remote-loghost', default='') parser.add_argument('-t', help='Remote syslog port', metavar='remote-syslog-port', default=514) parser.add_argument('-a', help='Alert score', metavar='alert-level', default=100) @@ -1424,7 +1424,7 @@ def signal_handler(signal_name, frame): os.remove(args.l) # Logger - logger = LokiLogger(args.nolog, args.l, t_hostname, args.r, int(args.t), args.csv, args.onlyrelevant, args.debug, + logger = LokiLogger(args.nolog, args.l, getHostname(os_platform), args.r, int(args.t), args.csv, args.onlyrelevant, args.debug, platform=os_platform, caller='main') # Update @@ -1433,7 +1433,7 @@ def signal_handler(signal_name, frame): sys.exit(0) logger.log("NOTICE", "Starting Loki Scan SYSTEM: {0} TIME: {1} PLATFORM: {2}".format( - t_hostname, getSyslogTimestamp(), os_platform)) + getHostname(os_platform), getSyslogTimestamp(), os_platform)) # Loki loki = Loki(args.intense) From d6095819e0879b09925f84bdd7be76bb8365aa78 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Tue, 13 Feb 2018 12:10:18 +0100 Subject: [PATCH 04/24] New hash whitelist --- loki.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/loki.py b/loki.py index 6f084924..48a6493f 100644 --- a/loki.py +++ b/loki.py @@ -1109,6 +1109,19 @@ def initialize_yara_rules(self): sys.exit(1) def initialize_hash_iocs(self, ioc_directory, false_positive=False): + HASH_WHITELIST = [# Empty file + 'd41d8cd98f00b204e9800998ecf8427e', + 'da39a3ee5e6b4b0d3255bfef95601890afd80709', + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + # One byte line break file (Unix) 0x0a + '68b329da9893e34099c7d8ad5cb9c940', + 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc', + '01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b', + # One byte line break file (Windows) 0x0d0a + '81051bcc2cf1bedf378224b0a93e2877', + 'ba8ab5a0280b953aa97435ff8946cbcbb2755a27', + '7eb70257593da06f682a3ddda54a9d260d4fc514f645237f5ca74b08f8da61a6', + ] try: for ioc_filename in os.listdir(ioc_directory): if 'hash' in ioc_filename: @@ -1122,12 +1135,10 @@ def initialize_hash_iocs(self, ioc_directory, false_positive=False): if re.search(r'^#', line) or re.search(r'^[\s]*$', line): continue row = line.split(';') - hash = row[0] + hash = row[0].lower() comment = row[1].rstrip(" ").rstrip("\n") # Empty File Hash - if hash == "d41d8cd98f00b204e9800998ecf8427e" or \ - hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709" or \ - hash == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855": + if hash in HASH_WHITELIST: continue # Else - check which type it is if len(hash) == 32: From 538af8e8d92aa71cb4e356faaa4baf8d2eaefc9e Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Tue, 13 Feb 2018 12:10:55 +0100 Subject: [PATCH 05/24] Better result messages: Links to issues section of signature-base --- loki.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/loki.py b/loki.py index 48a6493f..2f4d1720 100644 --- a/loki.py +++ b/loki.py @@ -1494,15 +1494,16 @@ def signal_handler(signal_name, frame): logger.log("NOTICE", "Results: {0} alerts, {1} warnings, {2} notices".format(logger.alerts, logger.warnings, logger.notices)) if logger.alerts: logger.log("RESULT", "Indicators detected!") - logger.log("RESULT", "Loki recommends checking the elements on Virustotal.com or Google and triage with a " - "professional triage tool like THOR APT Scanner in corporate networks.") + logger.log("RESULT", "Loki recommends checking the elements on virustotal.com or Google and triage with a " + "professional tool like THOR https://nextron-systems.com/thor in corporate networks.") elif logger.warnings: logger.log("RESULT", "Suspicious objects detected!") logger.log("RESULT", "Loki recommends a deeper analysis of the suspicious objects.") else: logger.log("RESULT", "SYSTEM SEEMS TO BE CLEAN.") - logger.log("NOTICE", "Finished LOKI Scan SYSTEM: %s TIME: %s" % (t_hostname, getSyslogTimestamp())) + logger.log("INFO", "Please report false positives via https://github.com/Neo23x0/signature-base") + logger.log("NOTICE", "Finished LOKI Scan SYSTEM: %s TIME: %s" % (getHostname(os_platform), getSyslogTimestamp())) if not args.dontwait: print " " From dfa90d15883874aeff4ef709b3f8e450f02c741e Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Tue, 13 Feb 2018 12:12:10 +0100 Subject: [PATCH 06/24] Refactoring main() function --- loki.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/loki.py b/loki.py index 2f4d1720..5792d0c2 100644 --- a/loki.py +++ b/loki.py @@ -1389,18 +1389,11 @@ def signal_handler(signal_name, frame): print 'LOKI\'s work has been interrupted by a human. Returning to Asgard.' sys.exit(0) - -# MAIN ################################################################ -if __name__ == '__main__': - - # Signal handler for CTRL+C - signal_module.signal(signal_module.SIGINT, signal_handler) - - # Computername - if os_platform == "linux" or os_platform == "osx": - t_hostname = os.uname()[1] - else: - t_hostname = os.environ['COMPUTERNAME'] +def main(): + """ + Argument parsing function + :return: + """ # Parse Arguments parser = argparse.ArgumentParser(description='Loki - Simple IOC Scanner') @@ -1430,6 +1423,17 @@ def signal_handler(signal_name, frame): args = parser.parse_args() + return args + +# MAIN ################################################################ +if __name__ == '__main__': + + # Signal handler for CTRL+C + signal_module.signal(signal_module.SIGINT, signal_handler) + + # Argument parsing + args = main() + # Remove old log file if os.path.exists(args.l): os.remove(args.l) From 7c8b583eba915f7d9c2c629c9965120b8a3f46db Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Tue, 13 Feb 2018 12:12:31 +0100 Subject: [PATCH 07/24] Version number change --- lib/lokilogger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/lokilogger.py b/lib/lokilogger.py index f1789dab..d771bf74 100644 --- a/lib/lokilogger.py +++ b/lib/lokilogger.py @@ -14,7 +14,7 @@ import socket from helpers import removeNonAsciiDrop -__version__ = '0.26.0' +__version__ = '0.26.1' # Logger Class ----------------------------------------------------------------- class LokiLogger(): From 45b5eddbcac92347e0ab518b4d0bd0242920b58c Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Mon, 19 Feb 2018 14:48:32 +0100 Subject: [PATCH 08/24] Removed legacy code to avoid errors w/ negative scores in filename IOCs --- loki.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/loki.py b/loki.py index 5792d0c2..3b85ab5d 100644 --- a/loki.py +++ b/loki.py @@ -981,11 +981,6 @@ def initialize_filename_iocs(self, ioc_directory): regex_fp = row[2] desc = last_comment - # Catch legacy lines - if not score.isdigit(): - desc = score # score is description (old format) - score = 60 # default value - # Elements without description else: regex = line From b485e718202d19933b21220965c29d8a37051e34 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Mon, 19 Feb 2018 14:54:00 +0100 Subject: [PATCH 09/24] Loki Logger version update --- lib/lokilogger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/lokilogger.py b/lib/lokilogger.py index d771bf74..eef90f12 100644 --- a/lib/lokilogger.py +++ b/lib/lokilogger.py @@ -14,7 +14,7 @@ import socket from helpers import removeNonAsciiDrop -__version__ = '0.26.1' +__version__ = '0.26.2' # Logger Class ----------------------------------------------------------------- class LokiLogger(): From 0fd846f356a8f7398ba9db1167a80d0addade67c Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Mon, 19 Feb 2018 14:56:11 +0100 Subject: [PATCH 10/24] .gitignore update --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 752007c0..b69d3bd3 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ signature-base tools/old-results build private-signatures +rules +rules.key From 6257a41fa9d183481e2c612ec36bbe464b1e773b Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Sat, 17 Mar 2018 09:34:38 +0100 Subject: [PATCH 11/24] Logging changed to be compatible with THOR and SPARK output --- lib/helpers.py | 4 +- lib/lokilogger.py | 18 ++-- lib/pesieve.py | 9 +- loki-upgrader.py | 37 +++---- loki.py | 250 +++++++++++++++++++++++----------------------- 5 files changed, 163 insertions(+), 155 deletions(-) diff --git a/lib/helpers.py b/lib/helpers.py index 7a80cbaa..7780d8bd 100644 --- a/lib/helpers.py +++ b/lib/helpers.py @@ -102,13 +102,13 @@ def setNice(logger): try: pid = os.getpid() p = psutil.Process(pid) - logger.log("INFO", "Setting LOKI process with PID: %s to priority IDLE" % pid) + logger.log("INFO", "Init", "Setting LOKI process with PID: %s to priority IDLE" % pid) p.nice(psutil.IDLE_PRIORITY_CLASS) return 1 except Exception, e: if logger.debug: traceback.print_exc() - logger.log("ERROR", "Error setting nice value of THOR process") + logger.log("ERROR", "Init", "Error setting nice value of THOR process") return 0 diff --git a/lib/lokilogger.py b/lib/lokilogger.py index eef90f12..80ab4613 100644 --- a/lib/lokilogger.py +++ b/lib/lokilogger.py @@ -58,7 +58,7 @@ def __init__(self, no_log_file, log_file, hostname, remote_host, remote_port, cs self.remote_logger.addHandler(remote_syslog_handler) self.remote_logging = True - def log(self, mes_type, message): + def log(self, mes_type, module, message): # Remove all non-ASCII characters # message = removeNonAsciiDrop(message) @@ -81,7 +81,7 @@ def log(self, mes_type, message): # to file if not self.no_log_file: - self.log_to_file(message, mes_type) + self.log_to_file(message, mes_type, module) # to stdout try: @@ -92,7 +92,7 @@ def log(self, mes_type, message): # to syslog server if self.remote_logging: - self.log_to_remotesys(message, mes_type) + self.log_to_remotesys(message, mes_type, module) def log_to_stdout(self, message, mes_type): # check tty encoding @@ -171,23 +171,25 @@ def log_to_stdout(self, message, mes_type): sys.exit(1) print ("Cannot print to cmd line - formatting error") - def log_to_file(self, message, mes_type): + def log_to_file(self, message, mes_type, module): try: # Write to file with codecs.open(self.log_file, "a", encoding='utf-8') as logfile: if self.csv: - logfile.write(u"{0},{1},{2},{3}{4}".format(getSyslogTimestamp(), self.hostname, mes_type,message, self.linesep)) + logfile.write(u"{0},{1},{2},{3},{4}{5}".format( + getSyslogTimestamp(), self.hostname, mes_type, module, message, self.linesep)) else: - logfile.write(u"%s %s LOKI: %s: %s%s" % (getSyslogTimestamp(), self.hostname, mes_type.title(), message, self.linesep)) + logfile.write(u"%s %s LOKI: %s: MODULE: %s MESSAGE: %s%s" % + (getSyslogTimestamp(), self.hostname, mes_type.title(), module, message, self.linesep)) except Exception as e: if self.debug: traceback.print_exc() sys.exit(1) print("Cannot print line to log file {0}".format(self.log_file)) - def log_to_remotesys(self, message, mes_type): + def log_to_remotesys(self, message, mes_type, module): # Preparing the message - syslog_message = "LOKI: {0}: {1}".format(mes_type.title(), message) + syslog_message = "LOKI: {0}: MODULE: {1} MESSAGE: {2}".format(mes_type.title(), module, message) try: # Mapping LOKI's levels to the syslog levels if mes_type == "NOTICE": diff --git a/lib/pesieve.py b/lib/pesieve.py index 3bdd4326..d3823121 100644 --- a/lib/pesieve.py +++ b/lib/pesieve.py @@ -27,10 +27,10 @@ def __init__(self, workingDir, is64bit, logger): if self.isAvailable(): self.active = True - self.logger.log("NOTICE", "PE-Sieve successfully initialized BINARY: {0} " + self.logger.log("NOTICE", "PESieve", "PE-Sieve successfully initialized BINARY: {0} " "SOURCE: https://github.com/hasherezade/pe-sieve".format(self.peSieve)) else: - self.logger.log("NOTICE", "Cannot find PE-Sieve in expected location {0} " + self.logger.log("NOTICE", "PESieve", "Cannot find PE-Sieve in expected location {0} " "SOURCE: https://github.com/hasherezade/pe-sieve".format(self.peSieve)) def isAvailable(self): @@ -39,7 +39,7 @@ def isAvailable(self): :return: """ if not os.path.exists(self.peSieve): - self.logger.log("DEBUG", "PE-Sieve not found in location '{0}' - " + self.logger.log("DEBUG", "PESieve", "PE-Sieve not found in location '{0}' - " "feature will not be active".format(self.peSieve)) return False return True @@ -80,5 +80,6 @@ def scan(self, pid): suspicious = int(result_suspicious.group(1)) # Check output for process replacements if "SUMMARY:" not in output: - self.logger.log("ERROR", "Something went wrong during PE-Sieve scan. Couldn't find the SUMMARY section in output.") + self.logger.log("ERROR", "PESieve", "Something went wrong during PE-Sieve scan. " + "Couldn't find the SUMMARY section in output.") return hooked, replaced, suspicious diff --git a/loki-upgrader.py b/loki-upgrader.py index 78f9c5b7..09316d2b 100644 --- a/loki-upgrader.py +++ b/loki-upgrader.py @@ -60,12 +60,13 @@ def update_signatures(self): for sig_url in self.UPDATE_URL_SIGS: # Downloading current repository try: - self.logger.log("INFO", "Downloading %s ..." % sig_url) + self.logger.log("INFO", "Upgrader", "Downloading %s ..." % sig_url) response = urlopen(sig_url) except Exception as e: if self.debug: traceback.print_exc() - self.logger.log("ERROR", "Error downloading the signature database - check your Internet connection") + self.logger.log("ERROR", "Upgrader", "Error downloading the signature database - " + "check your Internet connection") sys.exit(1) # Preparations @@ -78,7 +79,7 @@ def update_signatures(self): except Exception as e: if self.debug: traceback.print_exc() - self.logger.log("ERROR", "Error while creating the signature-base directories") + self.logger.log("ERROR", "Upgrader", "Error while creating the signature-base directories") sys.exit(1) # Read ZIP file @@ -88,7 +89,7 @@ def update_signatures(self): sigName = os.path.basename(zipFilePath) if zipFilePath.endswith("/"): continue - self.logger.log("DEBUG", "Extracting %s ..." % zipFilePath) + self.logger.log("DEBUG", "Upgrader", "Extracting %s ..." % zipFilePath) if "/iocs/" in zipFilePath and zipFilePath.endswith(".txt"): targetFile = os.path.join(sigDir, "iocs", sigName) elif "/yara/" in zipFilePath and zipFilePath.endswith(".yar"): @@ -102,7 +103,7 @@ def update_signatures(self): # New file if not os.path.exists(targetFile): - self.logger.log("INFO", "New signature file: %s" % sigName) + self.logger.log("INFO", "Upgrader", "New signature file: %s" % sigName) # Extract file source = zipUpdate.open(zipFilePath) @@ -113,7 +114,8 @@ def update_signatures(self): except Exception as e: if self.debug: traceback.print_exc() - self.logger.log("ERROR", "Error while extracting the signature files from the download package") + self.logger.log("ERROR", "Upgrader", "Error while extracting the signature files from the download " + "package") sys.exit(1) except Exception as e: @@ -128,17 +130,17 @@ def update_loki(self): # Downloading the info for latest release try: - self.logger.log("INFO", "Checking location of latest release %s ..." % self.UPDATE_URL_LOKI) + self.logger.log("INFO", "Upgrader", "Checking location of latest release %s ..." % self.UPDATE_URL_LOKI) response_info = urlopen(self.UPDATE_URL_LOKI) data = json.load(response_info) # Get download URL zip_url = data['assets'][0]['browser_download_url'] - self.logger.log("INFO", "Downloading latest release %s ..." % zip_url) + self.logger.log("INFO", "Upgrader", "Downloading latest release %s ..." % zip_url) response_zip = urlopen(zip_url) except Exception as e: if self.debug: traceback.print_exc() - self.logger.log("ERROR", "Error downloading the loki update - check your Internet connection") + self.logger.log("ERROR", "Upgrader", "Error downloading the loki update - check your Internet connection") sys.exit(1) # Read ZIP file @@ -151,7 +153,7 @@ def update_loki(self): source = zipUpdate.open(zipFilePath) targetFile = "/".join(zipFilePath.split("/")[1:]) - self.logger.log("INFO", "Extracting %s ..." %targetFile) + self.logger.log("INFO", "Upgrader", "Extracting %s ..." %targetFile) try: # Create file if not present @@ -161,14 +163,15 @@ def update_loki(self): with source, target: shutil.copyfileobj(source, target) except Exception as e: - self.logger.log("ERROR", "Cannot extract %s" % targetFile) + self.logger.log("ERROR", "Upgrader", "Cannot extract %s" % targetFile) if self.debug: traceback.print_exc() except Exception as e: if self.debug: traceback.print_exc() - self.logger.log("ERROR", "Error while extracting the signature files from the download package") + self.logger.log("ERROR", "Upgrader", + "Error while extracting the signature files from the download package") sys.exit(1) except Exception as e: @@ -189,7 +192,7 @@ def get_application_path(): # print application_path application_path = win32api.GetLongPathName(application_path) #if args.debug: - # logger.log("DEBUG", "Application Path: %s" % application_path) + # logger.log("DEBUG", "Init", "Application Path: %s" % application_path) return application_path except Exception as e: print("Error while evaluation of application path") @@ -223,15 +226,15 @@ def get_application_path(): # Updating LOKI if not args.sigsonly: - logger.log("INFO", "Updating LOKI ...") + logger.log("INFO", "Upgrader", "Updating LOKI ...") updater.update_loki() if not args.progonly: - logger.log("INFO", "Updating Signatures ...") + logger.log("INFO", "Upgrader", "Updating Signatures ...") updater.update_signatures() - logger.log("INFO", "Update complete") + logger.log("INFO", "Upgrader", "Update complete") if args.detached: - logger.log("INFO", "Press any key to return ...") + logger.log("INFO", "Upgrader", "Press any key to return ...") sys.exit(0) diff --git a/loki.py b/loki.py index 3b85ab5d..4c7a67d6 100644 --- a/loki.py +++ b/loki.py @@ -128,7 +128,7 @@ def __init__(self, intense_mode): # Check if signature database is present sig_dir = os.path.join(self.app_path, "./signature-base/") if not os.path.exists(sig_dir) or os.listdir(sig_dir) == []: - logger.log("NOTICE", "The 'signature-base' subdirectory doesn't exist or is empty. " + logger.log("NOTICE", "Init", "The 'signature-base' subdirectory doesn't exist or is empty. " "Trying to retrieve the signature database automatically.") updateLoki(sigsOnly=True) @@ -152,21 +152,21 @@ def __init__(self, intense_mode): # Read IOCs ------------------------------------------------------- # File Name IOCs (all files in iocs that contain 'filename') self.initialize_filename_iocs(self.ioc_path) - logger.log("INFO","File Name Characteristics initialized with %s regex patterns" % len(self.filename_iocs)) + logger.log("INFO", "Init", "File Name Characteristics initialized with %s regex patterns" % len(self.filename_iocs)) # C2 based IOCs (all files in iocs that contain 'c2') self.initialize_c2_iocs(self.ioc_path) - logger.log("INFO","C2 server indicators initialized with %s elements" % len(self.c2_server.keys())) + logger.log("INFO", "Init", "C2 server indicators initialized with %s elements" % len(self.c2_server.keys())) # Hash based IOCs (all files in iocs that contain 'hash') self.initialize_hash_iocs(self.ioc_path) - logger.log("INFO","Malicious MD5 Hashes initialized with %s hashes" % len(self.hashes_md5.keys())) - logger.log("INFO","Malicious SHA1 Hashes initialized with %s hashes" % len(self.hashes_sha1.keys())) - logger.log("INFO","Malicious SHA256 Hashes initialized with %s hashes" % len(self.hashes_sha256.keys())) + logger.log("INFO", "Init", "Malicious MD5 Hashes initialized with %s hashes" % len(self.hashes_md5.keys())) + logger.log("INFO", "Init", "Malicious SHA1 Hashes initialized with %s hashes" % len(self.hashes_sha1.keys())) + logger.log("INFO", "Init", "Malicious SHA256 Hashes initialized with %s hashes" % len(self.hashes_sha256.keys())) # Hash based False Positives (all files in iocs that contain 'hash' and 'falsepositive') self.initialize_hash_iocs(self.ioc_path, false_positive=True) - logger.log("INFO","False Positive Hashes initialized with %s hashes" % len(self.false_hashes.keys())) + logger.log("INFO", "Init", "False Positive Hashes initialized with %s hashes" % len(self.false_hashes.keys())) # Compile Yara Rules self.initialize_yara_rules() @@ -177,7 +177,7 @@ def __init__(self, intense_mode): def scan_path(self, path): # Startup - logger.log("INFO","Scanning %s ... " % path) + logger.log("INFO", "FileScan", "Scanning %s ... " % path) # Counter c = 0 @@ -195,7 +195,7 @@ def scan_path(self, path): # Platform specific excludes for skip in self.startExcludes: if completePath.startswith(skip): - logger.log("INFO", "Skipping %s directory" % skip) + logger.log("INFO", "FileScan", "Skipping %s directory" % skip) skipIt = True if not skipIt: @@ -231,7 +231,7 @@ def scan_path(self, path): # User defined excludes for skip in self.fullExcludes: if skip.search(filePath): - logger.log("DEBUG", "Skipping element %s" % filePath) + logger.log("DEBUG", "FileScan", "Skipping element %s" % filePath) skipIt = True # Linux directory skip @@ -241,7 +241,7 @@ def scan_path(self, path): for skip in self.LINUX_PATH_SKIPS_END: if filePath.endswith(skip): if self.LINUX_PATH_SKIPS_END[skip] == 0: - logger.log("INFO", "Skipping %s element" % skip) + logger.log("INFO", "FileScan", "Skipping %s element" % skip) self.LINUX_PATH_SKIPS_END[skip] = 1 skipIt = True @@ -263,7 +263,7 @@ def scan_path(self, path): # Skip program directory # print appPath.lower() +" - "+ filePath.lower() if self.app_path.lower() in filePath.lower(): - logger.log("DEBUG", "Skipping file in program directory FILE: %s" % filePathCleaned) + logger.log("DEBUG", "FileScan", "Skipping file in program directory FILE: %s" % filePathCleaned) continue fileSize = os.stat(filePath).st_size @@ -290,7 +290,7 @@ def scan_path(self, path): with open(filePath, 'rb') as f: firstBytes = f.read(4) except Exception, e: - logger.log("DEBUG", "Cannot open file %s (access denied)" % filePathCleaned) + logger.log("DEBUG", "FileScan", "Cannot open file %s (access denied)" % filePathCleaned) # Evaluate Type fileType = get_file_type(filePath, self.filetype_magics, self.max_filetype_magics, logger) @@ -299,7 +299,7 @@ def scan_path(self, path): do_intense_check = True if not self.intense_mode and fileType == "UNKNOWN" and extension not in EVIL_EXTENSIONS: if args.printAll: - logger.log("INFO", "Skipping file due to fast scan mode: %s" % filePathCleaned) + logger.log("INFO", "FileScan", "Skipping file due to fast scan mode: %s" % filePathCleaned) do_intense_check = False # Set fileData to an empty value @@ -318,10 +318,10 @@ def scan_path(self, path): # Intense Check switch if do_intense_check: if args.printAll: - logger.log("INFO", "Scanning %s TYPE: %s SIZE: %s" % (filePathCleaned, fileType, fileSize)) + logger.log("INFO", "FileScan", "Scanning %s TYPE: %s SIZE: %s" % (filePathCleaned, fileType, fileSize)) else: if args.printAll: - logger.log("INFO", "Checking %s TYPE: %s SIZE: %s" % (filePathCleaned, fileType, fileSize)) + logger.log("INFO", "FileScan", "Checking %s TYPE: %s SIZE: %s" % (filePathCleaned, fileType, fileSize)) # Hash Check ------------------------------------------------------- # Do the check @@ -377,7 +377,7 @@ def scan_path(self, path): # Script Anomalies Check if args.scriptanalysis: if extension in SCRIPT_EXTENSIONS or type in SCRIPT_TYPES: - logger.log("DEBUG", "Performing character analysis on file %s ... " % filePath) + logger.log("DEBUG", "FileScan", "Performing character analysis on file %s ... " % filePath) message, score = self.script_stats_analysis(fileData) if message: reasons.append("%s SCORE: %s" % (message, score)) @@ -387,11 +387,11 @@ def scan_path(self, path): # Memory Dump Scan if fileType == "MDMP": - logger.log("INFO", "Scanning memory dump file %s" % filePathCleaned) + logger.log("INFO", "FileScan", "Scanning memory dump file %s" % filePathCleaned) # Umcompressed SWF scan if fileType == "ZWS" or fileType == "CWS": - logger.log("INFO", "Scanning decompressed SWF file %s" % filePathCleaned) + logger.log("INFO", "FileScan", "Scanning decompressed SWF file %s" % filePathCleaned) success, decompressedData = decompressSWFData(fileData) if success: fileData = decompressedData @@ -417,7 +417,7 @@ def scan_path(self, path): reasons.append(message) except Exception, e: - logger.log("ERROR", "Cannot YARA scan file: %s" % filePathCleaned) + logger.log("ERROR", "FileScan", "Cannot YARA scan file: %s" % filePathCleaned) # Info Line ----------------------------------------------------------------------- fileInfo = "FILE: %s SCORE: %s TYPE: %s SIZE: %s FIRST_BYTES: %s %s %s " % ( @@ -440,7 +440,7 @@ def scan_path(self, path): if i < 2 or args.allreasons: message_body += "REASON_{0}: {1}".format(i+1, r.encode('ascii', errors='replace')) - logger.log(message_type, message_body) + logger.log(message_type, "FileScan", message_body) except Exception, e: if logger.debug: @@ -587,7 +587,7 @@ def scan_processes(self): owner = "unknown" except Exception, e: - logger.log("ALERT", "Error getting all process information. Did you run the scanner 'As Administrator'?") + logger.log("ALERT", "ProcessScan", "Error getting all process information. Did you run the scanner 'As Administrator'?") continue # Is parent to other processes - save PID @@ -604,33 +604,33 @@ def scan_processes(self): # Skip some PIDs ------------------------------------------------------ if pid == 0 or pid == 4: - logger.log("INFO", "Skipping Process %s" % process_info) + logger.log("INFO", "ProcessScan", "Skipping Process %s" % process_info) continue # Skip own process ---------------------------------------------------- if os.getpid() == pid: - logger.log("INFO", "Skipping LOKI Process %s" % process_info) + logger.log("INFO", "ProcessScan", "Skipping LOKI Process %s" % process_info) continue # Print info ---------------------------------------------------------- - logger.log("INFO", "Scanning Process %s" % process_info) + logger.log("INFO", "ProcessScan", "Scanning Process %s" % process_info) # Skeleton Key Malware Process if re.search(r'psexec .* [a-fA-F0-9]{32}', cmd, re.IGNORECASE): - logger.log("WARNING", "Process that looks liks SKELETON KEY psexec execution detected %s" % process_info) + logger.log("WARNING", "ProcessScan", "Process that looks liks SKELETON KEY psexec execution detected %s" % process_info) # File Name Checks ------------------------------------------------- for fioc in self.filename_iocs: match = fioc['regex'].search(cmd) if match: if fioc['score'] > 70: - logger.log("ALERT", "File Name IOC matched PATTERN: %s DESC: %s MATCH: %s" % (fioc['regex'].pattern, fioc['description'], cmd)) + logger.log("ALERT", "ProcessScan", "File Name IOC matched PATTERN: %s DESC: %s MATCH: %s" % (fioc['regex'].pattern, fioc['description'], cmd)) elif fioc['score'] > 40: - logger.log("WARNING", "File Name Suspicious IOC matched PATTERN: %s DESC: %s MATCH: %s" % (fioc['regex'].pattern, fioc['description'], cmd)) + logger.log("WARNING", "ProcessScan", "File Name Suspicious IOC matched PATTERN: %s DESC: %s MATCH: %s" % (fioc['regex'].pattern, fioc['description'], cmd)) # Suspicious waitfor - possible backdoor https://twitter.com/subTee/status/872274262769500160 if name == "waitfor.exe": - logger.log("WARNING", "Suspicious waitfor.exe process https://twitter.com/subTee/status/872274262769500160 %s" % process_info) + logger.log("WARNING", "ProcessScan", "Suspicious waitfor.exe process https://twitter.com/subTee/status/872274262769500160 %s" % process_info) # Yara rule match # only on processes with a small working set size @@ -661,31 +661,31 @@ def scan_processes(self): alerts.append("Yara Rule MATCH: %s %s" % (match.rule, process_info)) if len(alerts) > 3: - logger.log("INFO", "Too many matches on process memory - most likely a false positive %s" % process_info) + logger.log("INFO", "ProcessScan", "Too many matches on process memory - most likely a false positive %s" % process_info) elif len(alerts) > 0: for alert in alerts: logger.log("ALERT", alert) except Exception, e: if logger.debug: traceback.print_exc() - logger.log("ERROR", "Error while process memory Yara check (maybe the process doesn't exist anymore or access denied) %s" % process_info) + logger.log("ERROR", "ProcessScan", "Error while process memory Yara check (maybe the process doesn't exist anymore or access denied) %s" % process_info) else: - logger.log("DEBUG", "Skipped Yara memory check due to the process' big working set size (stability issues) PID: %s NAME: %s SIZE: %s" % ( pid, name, ws_size)) + logger.log("DEBUG", "ProcessScan", "Skipped Yara memory check due to the process' big working set size (stability issues) PID: %s NAME: %s SIZE: %s" % ( pid, name, ws_size)) ############################################################### # PE-Sieve Checks if processExists(pid) and self.peSieve.active: # If PE-Sieve reports replaced processes - logger.log("DEBUG", "PE-Sieve scan of process PID: %s" % pid) + logger.log("DEBUG", "ProcessScan", "PE-Sieve scan of process PID: %s" % pid) (hooked, replaced, suspicious) = self.peSieve.scan(pid=pid) if replaced: - logger.log("WARNING", "PE-Sieve reported replaced process %s REPLACED: %s" % + logger.log("WARNING", "ProcessScan", "PE-Sieve reported replaced process %s REPLACED: %s" % (process_info, str(replaced))) elif hooked or suspicious: - logger.log("NOTICE", "PE-Sieve reported hooked or suspicious process %s " + logger.log("NOTICE", "ProcessScan", "PE-Sieve reported hooked or suspicious process %s " "HOOKED: %s SUSPICIOUS: %s" % (process_info, str(hooked), str(suspicious))) else: - logger.log("INFO", "PE-Sieve reported no anomalies %s" % process_info) + logger.log("INFO", "ProcessScan", "PE-Sieve reported no anomalies %s" % process_info) ############################################################### # THOR Process Connection Checks @@ -697,30 +697,30 @@ def scan_processes(self): # Process: System if name == "System" and not pid == 4: - logger.log("WARNING", "System process without PID=4 %s" % process_info) + logger.log("WARNING", "ProcessScan", "System process without PID=4 %s" % process_info) # Process: smss.exe if name == "smss.exe" and not parent_pid == 4: - logger.log("WARNING", "smss.exe parent PID is != 4 %s" % process_info) + logger.log("WARNING", "ProcessScan", "smss.exe parent PID is != 4 %s" % process_info) if path != "none": if name == "smss.exe" and not ( "system32" in path.lower() or "system32" in cmd.lower() ): - logger.log("WARNING", "smss.exe path is not System32 %s" % process_info) + logger.log("WARNING", "ProcessScan", "smss.exe path is not System32 %s" % process_info) if name == "smss.exe" and priority is not 11: - logger.log("WARNING", "smss.exe priority is not 11 %s" % process_info) + logger.log("WARNING", "ProcessScan", "smss.exe priority is not 11 %s" % process_info) # Process: csrss.exe if path != "none": if name == "csrss.exe" and not ( "system32" in path.lower() or "system32" in cmd.lower() ): - logger.log("WARNING", "csrss.exe path is not System32 %s" % process_info) + logger.log("WARNING", "ProcessScan", "csrss.exe path is not System32 %s" % process_info) if name == "csrss.exe" and priority is not 13: - logger.log("WARNING", "csrss.exe priority is not 13 %s" % process_info) + logger.log("WARNING", "ProcessScan", "csrss.exe priority is not 13 %s" % process_info) # Process: wininit.exe if path != "none": if name == "wininit.exe" and not ( "system32" in path.lower() or "system32" in cmd.lower() ): - logger.log("WARNING", "wininit.exe path is not System32 %s" % process_info) + logger.log("WARNING", "ProcessScan", "wininit.exe path is not System32 %s" % process_info) if name == "wininit.exe" and priority is not 13: - logger.log("NOTICE", "wininit.exe priority is not 13 %s" % process_info) + logger.log("NOTICE", "ProcessScan", "wininit.exe priority is not 13 %s" % process_info) # Is parent to other processes - save PID if name == "wininit.exe": wininit_pid = pid @@ -728,70 +728,70 @@ def scan_processes(self): # Process: services.exe if path != "none": if name == "services.exe" and not ( "system32" in path.lower() or "system32" in cmd.lower() ): - logger.log("WARNING", "services.exe path is not System32 %s" % process_info) + logger.log("WARNING", "ProcessScan", "services.exe path is not System32 %s" % process_info) if name == "services.exe" and priority is not 9: - logger.log("WARNING", "services.exe priority is not 9 %s" % process_info) + logger.log("WARNING", "ProcessScan", "services.exe priority is not 9 %s" % process_info) if wininit_pid > 0: if name == "services.exe" and not parent_pid == wininit_pid: - logger.log("WARNING", "services.exe parent PID is not the one of wininit.exe %s" % process_info) + logger.log("WARNING", "ProcessScan", "services.exe parent PID is not the one of wininit.exe %s" % process_info) # Process: lsass.exe if path != "none": if name == "lsass.exe" and not ( "system32" in path.lower() or "system32" in cmd.lower() ): - logger.log("WARNING", "lsass.exe path is not System32 %s" % process_info) + logger.log("WARNING", "ProcessScan", "lsass.exe path is not System32 %s" % process_info) if name == "lsass.exe" and priority is not 9: - logger.log("WARNING", "lsass.exe priority is not 9 %s" % process_info) + logger.log("WARNING", "ProcessScan", "lsass.exe priority is not 9 %s" % process_info) if wininit_pid > 0: if name == "lsass.exe" and not parent_pid == wininit_pid: - logger.log("WARNING", "lsass.exe parent PID is not the one of wininit.exe %s" % process_info) + logger.log("WARNING", "ProcessScan", "lsass.exe parent PID is not the one of wininit.exe %s" % process_info) # Only a single lsass process is valid - count occurrences if name == "lsass.exe": lsass_count += 1 if lsass_count > 1: - logger.log("WARNING", "lsass.exe count is higher than 1 %s" % process_info) + logger.log("WARNING", "ProcessScan", "lsass.exe count is higher than 1 %s" % process_info) # Process: svchost.exe if path is not "none": if name == "svchost.exe" and not ( "system32" in path.lower() or "system32" in cmd.lower() ): - logger.log("WARNING", "svchost.exe path is not System32 %s" % process_info) + logger.log("WARNING", "ProcessScan", "svchost.exe path is not System32 %s" % process_info) if name == "svchost.exe" and priority is not 8: - logger.log("NOTICE", "svchost.exe priority is not 8 %s" % process_info) + logger.log("NOTICE", "ProcessScan", "svchost.exe priority is not 8 %s" % process_info) if name == "svchost.exe" and not ( self.check_svchost_owner(owner) or "UnistackSvcGroup" in cmd): - logger.log("WARNING", "svchost.exe process owner is suspicious %s" % process_info) + logger.log("WARNING", "ProcessScan", "svchost.exe process owner is suspicious %s" % process_info) if name == "svchost.exe" and not " -k " in cmd and cmd != "N/A": - logger.log("WARNING", "svchost.exe process does not contain a -k in its command line %s" % process_info) + logger.log("WARNING", "ProcessScan", "svchost.exe process does not contain a -k in its command line %s" % process_info) # Process: lsm.exe if path != "none": if name == "lsm.exe" and not ( "system32" in path.lower() or "system32" in cmd.lower() ): - logger.log("WARNING", "lsm.exe path is not System32 %s" % process_info) + logger.log("WARNING", "ProcessScan", "lsm.exe path is not System32 %s" % process_info) if name == "lsm.exe" and priority is not 8: - logger.log("NOTICE", "lsm.exe priority is not 8 %s" % process_info) + logger.log("NOTICE", "ProcessScan", "lsm.exe priority is not 8 %s" % process_info) if name == "lsm.exe" and not ( owner.startswith("NT ") or owner.startswith("LO") or owner.startswith("SYSTEM") or owner.startswith(u"система")): - logger.log(u"WARNING", "lsm.exe process owner is suspicious %s" % process_info) + logger.log(u"WARNING", "ProcessScan", "lsm.exe process owner is suspicious %s" % process_info) if wininit_pid > 0: if name == "lsm.exe" and not parent_pid == wininit_pid: - logger.log("WARNING", "lsm.exe parent PID is not the one of wininit.exe %s" % process_info) + logger.log("WARNING", "ProcessScan", "lsm.exe parent PID is not the one of wininit.exe %s" % process_info) # Process: winlogon.exe if name == "winlogon.exe" and priority is not 13: - logger.log("WARNING", "winlogon.exe priority is not 13 %s" % process_info) + logger.log("WARNING", "ProcessScan", "winlogon.exe priority is not 13 %s" % process_info) if re.search("(Windows 7|Windows Vista)", getPlatformFull()): if name == "winlogon.exe" and parent_pid > 0: for proc in processes: if parent_pid == proc.ProcessId: - logger.log("WARNING", "winlogon.exe has a parent ID but should have none %s PARENTID: %s" + logger.log("WARNING", "ProcessScan", "winlogon.exe has a parent ID but should have none %s PARENTID: %s" % (process_info, str(parent_pid))) # Process: explorer.exe if path != "none": if name == "explorer.exe" and not t_systemroot.lower() in path.lower(): - logger.log("WARNING", "explorer.exe path is not %%SYSTEMROOT%% %s" % process_info) + logger.log("WARNING", "ProcessScan", "explorer.exe path is not %%SYSTEMROOT%% %s" % process_info) if name == "explorer.exe" and parent_pid > 0: for proc in processes: if parent_pid == proc.ProcessId: - logger.log("NOTICE", "explorer.exe has a parent ID but should have none %s" % process_info) + logger.log("NOTICE", "ProcessScan", "explorer.exe has a parent ID but should have none %s" % process_info) def check_process_connections(self, process): try: @@ -825,10 +825,10 @@ def check_process_connections(self, process): if x.status == 'LISTEN': connection_count += 1 - logger.log("NOTICE","Listening process PID: %s NAME: %s COMMAND: %s IP: %s PORT: %s" % ( + logger.log("NOTICE", "ProcessScan", "Listening process PID: %s NAME: %s COMMAND: %s IP: %s PORT: %s" % ( str(pid), name, command, str(x.laddr[0]), str(x.laddr[1]) )) if str(x.laddr[1]) == "0": - logger.log("WARNING", + logger.log("WARNING", "ProcessScan", "Listening on Port 0 PID: %s NAME: %s COMMAND: %s IP: %s PORT: %s" % ( str(pid), name, command, str(x.laddr[0]), str(x.laddr[1]) )) @@ -840,54 +840,55 @@ def check_process_connections(self, process): # Check keyword in remote address is_match, description = self.check_c2(str(x.raddr[0])) if is_match: - logger.log("ALERT", + logger.log("ALERT", "ProcessScan", "Malware Domain/IP match in remote address PID: %s NAME: %s COMMAND: %s IP: %s PORT: %s DESC: %s" % ( str(pid), name, command, str(x.raddr[0]), str(x.raddr[1]), description)) # Full list connection_count += 1 - logger.log("NOTICE", "Established connection PID: %s NAME: %s COMMAND: %s LIP: %s LPORT: %s RIP: %s RPORT: %s" % ( + logger.log("NOTICE", "ProcessScan", + "Established connection PID: %s NAME: %s COMMAND: %s LIP: %s LPORT: %s RIP: %s RPORT: %s" % ( str(pid), name, command, str(x.laddr[0]), str(x.laddr[1]), str(x.raddr[0]), str(x.raddr[1]) )) # Maximum connection output if connection_count > MAXIMUM_CONNECTIONS: - logger.log("NOTICE", "Connection output threshold reached. Output truncated.") + logger.log("NOTICE", "ProcessScan", "Connection output threshold reached. Output truncated.") return except Exception, e: if args.debug: traceback.print_exc() sys.exit(1) - logger.log("INFO", + logger.log("INFO", "ProcessScan", "Process %s does not exist anymore or cannot be accessed" % str(pid)) def check_rootkit(self): - logger.log("INFO", "Checking for Backdoors ...") + logger.log("INFO", "Rootkit", "Checking for Backdoors ...") dp = DoublePulsar(ip="127.0.0.1", timeout=None, verbose=args.debug) - logger.log("INFO", "Checking for Double Pulsar RDP Backdoor") + logger.log("INFO", "Rootkit", "Checking for Double Pulsar RDP Backdoor") try: dp_rdp_result, message = dp.check_ip_rdp() if dp_rdp_result: logger.log("ALERT", message) else: - logger.log("INFO", "Double Pulsar RDP check RESULT: %s" % message) + logger.log("INFO", "Rootkit", "Double Pulsar RDP check RESULT: %s" % message) except Exception, e: - logger.log("INFO", "Double Pulsar RDP check failed RESULT: Connection failure") + logger.log("INFO", "Rootkit", "Double Pulsar RDP check failed RESULT: Connection failure") if args.debug: traceback.print_exc() - logger.log("INFO", "Checking for Double Pulsar SMB Backdoor") + logger.log("INFO", "Rootkit", "Checking for Double Pulsar SMB Backdoor") try: dp_smb_result, message = dp.check_ip_smb() if dp_smb_result: logger.log("ALERT", message) else: - logger.log("INFO", "Double Pulsar SMB check RESULT: %s" % message) + logger.log("INFO", "Rootkit", "Double Pulsar SMB check RESULT: %s" % message) except Exception, e: - logger.log("INFO", "Double Pulsar SMB check failed RESULT: Connection failure") + logger.log("INFO", "Rootkit", "Double Pulsar SMB check failed RESULT: Connection failure") if args.debug: traceback.print_exc() @@ -933,21 +934,22 @@ def initialize_c2_iocs(self, ioc_directory): # Check length if len(c2) < 4: - logger.log("NOTICE","C2 server definition is suspiciously short - will not add %s" %c2) + logger.log("NOTICE", "Init", + "C2 server definition is suspiciously short - will not add %s" %c2) continue # Add to the LOKI iocs self.c2_server[c2.lower()] = comment except Exception,e: - logger.log("ERROR", "Cannot read line: %s" % line) + logger.log("ERROR", "Init", "Cannot read line: %s" % line) if logger.debug: sys.exit(1) except OSError, e: - logger.log("ERROR", "No such file or directory") + logger.log("ERROR", "Init", "No such file or directory") except Exception, e: traceback.print_exc() - logger.log("ERROR", "Error reading Hash file: %s" % ioc_filename) + logger.log("ERROR", "Init", "Error reading Hash file: %s" % ioc_filename) def initialize_filename_iocs(self, ioc_directory): @@ -1004,16 +1006,16 @@ def initialize_filename_iocs(self, ioc_directory): self.filename_iocs.append(fioc) except Exception, e: - logger.log("ERROR", "Error reading line: %s" % line) + logger.log("ERROR", "Init", "Error reading line: %s" % line) if logger.debug: traceback.print_exc() sys.exit(1) except Exception, e: traceback.print_exc() - logger.log("ERROR", "Error reading File IOC file: %s" % ioc_filename) - logger.log("ERROR", "Please make sure that you cloned the repo or downloaded the sub repository: See " - "https://github.com/Neo23x0/Loki/issues/51") + logger.log("ERROR", "Init", "Error reading File IOC file: %s" % ioc_filename) + logger.log("ERROR", "Init", "Please make sure that you cloned the repo or downloaded the sub repository: " + "See https://github.com/Neo23x0/Loki/issues/51") sys.exit(1) def initialize_yara_rules(self): @@ -1025,7 +1027,7 @@ def initialize_yara_rules(self): for yara_rule_directory in self.yara_rule_directories: if not os.path.exists(yara_rule_directory): continue - logger.log("INFO", "Processing YARA rules folder {0}".format(yara_rule_directory)) + logger.log("INFO", "Init", "Processing YARA rules folder {0}".format(yara_rule_directory)) for root, directories, files in os.walk(yara_rule_directory, onerror=walk_error, followlinks=False): for file in files: try: @@ -1049,9 +1051,9 @@ def initialize_yara_rules(self): 'filetype': dummy, 'md5': dummy, }) - logger.log("INFO", "Initializing Yara rule %s" % file) + logger.log("INFO", "Init", "Initializing Yara rule %s" % file) except Exception, e: - logger.log("ERROR", "Error while initializing Yara rule %s" % file) + logger.log("ERROR", "Init", "Error while initializing Yara rule %s" % file) traceback.print_exc() if logger.debug: sys.exit(1) @@ -1064,14 +1066,14 @@ def initialize_yara_rules(self): yaraRules += data except Exception, e: - logger.log("ERROR", "Error reading signature file %s ERROR: %s" % yaraRuleFile) + logger.log("ERROR", "Init", "Error reading signature file %s ERROR: %s" % yaraRuleFile) if logger.debug: traceback.print_exc() sys.exit(1) # Compile try: - logger.log("INFO", "Initializing all YARA rules at once (composed string of all rule files)") + logger.log("INFO", "Init", "Initializing all YARA rules at once (composed string of all rule files)") compiledRules = yara.compile(source=yaraRules, externals={ 'filename': dummy, 'filepath': dummy, @@ -1079,26 +1081,26 @@ def initialize_yara_rules(self): 'filetype': dummy, 'md5': dummy }) - logger.log("INFO", "Initialized all Yara rules at once") + logger.log("INFO", "Init", "Initialized all Yara rules at once") except Exception, e: traceback.print_exc() - logger.log("ERROR", "Error during YARA rule compilation - please fix the issue in the rule set") + logger.log("ERROR", "Init", "Error during YARA rule compilation - please fix the issue in the rule set") sys.exit(1) # Add as Lokis YARA rules self.yara_rules.append(compiledRules) # Add private rules - logger.log("INFO", "Reading private rules from binary ...") + logger.log("INFO", "Init", "Reading private rules from binary ...") if hasattr(sys, '_MEIPASS'): privrules_path = os.path.join(sys._MEIPASS, "rules") if os.path.exists(privrules_path): private_rules = decrypt_rules(privrules_path) self.yara_rules.append(private_rules) - logger.log("INFO", "Initialized private rules") + logger.log("INFO", "Init", "Initialized private rules") except Exception, e: - logger.log("ERROR", "Error reading signature folder /signatures/") + logger.log("ERROR", "Init", "Error reading signature folder /signatures/") if logger.debug: traceback.print_exc() sys.exit(1) @@ -1145,18 +1147,18 @@ def initialize_hash_iocs(self, ioc_directory, false_positive=False): if false_positive: self.false_hashes[hash.lower()] = comment except Exception,e: - logger.log("ERROR", "Cannot read line: %s" % line) + logger.log("ERROR", "Init", "Cannot read line: %s" % line) # Debug if logger.debug: - logger.log("DEBUG", "Initialized %s hash IOCs from file %s" + logger.log("DEBUG", "Init", "Initialized %s hash IOCs from file %s" % (str(len(self.hashes_md5)+len(self.hashes_sha1)+len(self.hashes_sha256)), ioc_filename)) except Exception, e: if logger.debug: traceback.print_exc() sys.exit(1) - logger.log("ERROR", "Error reading Hash file: %s" % ioc_filename) + logger.log("ERROR", "Init", "Error reading Hash file: %s" % ioc_filename) def initialize_filetype_magics(self, filetype_magics_file): try: @@ -1179,13 +1181,13 @@ def initialize_filetype_magics(self, filetype_magics_file): self.filetype_magics[sig] = description except Exception,e: - logger.log("ERROR", "Cannot read line: %s" % line) + logger.log("ERROR", "Init", "Cannot read line: %s" % line) except Exception, e: if logger.debug: traceback.print_exc() sys.exit(1) - logger.log("ERROR", "Error reading Hash file: %s" % filetype_magics_file) + logger.log("ERROR", "Init", "Error reading Hash file: %s" % filetype_magics_file) def initialize_excludes(self, excludes_file): try: @@ -1202,14 +1204,14 @@ def initialize_excludes(self, excludes_file): regex = re.compile(line, re.IGNORECASE) excludes.append(regex) except Exception, e: - logger.log("ERROR", "Cannot compile regex: %s" % line) + logger.log("ERROR", "Init", "Cannot compile regex: %s" % line) self.fullExcludes = excludes except Exception, e: if logger.debug: traceback.print_exc() - logger.log("NOTICE", "Error reading excludes file: %s" % excludes_file) + logger.log("NOTICE", "Init", "Error reading excludes file: %s" % excludes_file) def scan_regin_fs(self, fileData, filePath): @@ -1235,10 +1237,10 @@ def scan_regin_fs(self, fileData, filePath): crc = binascii.crc32(data, 0x45) crc2 = '%08x' % (crc & 0xffffffff) - logger.log("DEBUG", "Regin FS Check CRC2: %s" % crc2.encode('hex')) + logger.log("DEBUG", "Rootkit", "Regin FS Check CRC2: %s" % crc2.encode('hex')) if CRC32custom.encode('hex') == crc2: - logger.log("ALERT", "Regin Virtual Filesystem MATCH: %s" % filePath) + logger.log("ALERT", "Rootkit", "Regin Virtual Filesystem MATCH: %s" % filePath) def get_file_data(self, filePath): fileData = "" @@ -1249,7 +1251,7 @@ def get_file_data(self, filePath): except Exception, e: if logger.debug: traceback.print_exc() - logger.log("DEBUG", "Cannot open file %s (access denied)" % filePath) + logger.log("DEBUG", "FileScan", "Cannot open file %s (access denied)" % filePath) finally: return fileData @@ -1316,7 +1318,7 @@ def get_application_path(): # print application_path application_path = win32api.GetLongPathName(application_path) #if args.debug: - # logger.log("DEBUG", "Application Path: %s" % application_path) + # logger.log("DEBUG", "Init", "Application Path: %s" % application_path) return application_path except Exception, e: print "Error while evaluation of application path" @@ -1343,7 +1345,7 @@ def processExists(pid): def updateLoki(sigsOnly): - logger.log("INFO", "Starting separate updater process ...") + logger.log("INFO", "Update", "Starting separate updater process ...") pArgs = [] # Updater @@ -1353,7 +1355,7 @@ def updateLoki(sigsOnly): pArgs.append('python') pArgs.append('loki-upgrader.py') else: - logger.log("ERROR", "Cannot find neither thor-upgrader.exe nor thor-upgrader.py in the current workign directory.") + logger.log("ERROR", "Update", "Cannot find neither thor-upgrader.exe nor thor-upgrader.py in the current workign directory.") if sigsOnly: pArgs.append('--sigsonly') @@ -1367,7 +1369,7 @@ def updateLoki(sigsOnly): def walk_error(err): try: if "Error 3" in str(err): - logger.log('ERROR', removeNonAsciiDrop(str(err))) + logger.log('ERROR', "FileScan", removeNonAsciiDrop(str(err))) elif args.debug: print "Directory walk error" sys.exit(1) @@ -1379,7 +1381,7 @@ def walk_error(err): def signal_handler(signal_name, frame): try: print "------------------------------------------------------------------------------\n" - logger.log('INFO', 'LOKI\'s work has been interrupted by a human. Returning to Asgard.') + logger.log('INFO', 'Init', 'LOKI\'s work has been interrupted by a human. Returning to Asgard.') except Exception, e: print 'LOKI\'s work has been interrupted by a human. Returning to Asgard.' sys.exit(0) @@ -1442,8 +1444,8 @@ def main(): updateLoki(sigsOnly=False) sys.exit(0) - logger.log("NOTICE", "Starting Loki Scan SYSTEM: {0} TIME: {1} PLATFORM: {2}".format( - getHostname(os_platform), getSyslogTimestamp(), os_platform)) + logger.log("NOTICE", "Init", "Starting Loki Scan VERSION: {3} SYSTEM: {0} TIME: {1} PLATFORM: {2}".format( + getHostname(os_platform), getSyslogTimestamp(), getPlatformFull(), logger.version)) # Loki loki = Loki(args.intense) @@ -1453,15 +1455,15 @@ def main(): if os_platform == "windows": if shell.IsUserAnAdmin(): isAdmin = True - logger.log("INFO", "Current user has admin rights - very good") + logger.log("INFO", "Init", "Current user has admin rights - very good") else: - logger.log("NOTICE", "Program should be run 'as Administrator' to ensure all access rights to process memory and file objects.") + logger.log("NOTICE", "Init", "Program should be run 'as Administrator' to ensure all access rights to process memory and file objects.") else: if os.geteuid() == 0: isAdmin = True - logger.log("INFO", "Current user is root - very good") + logger.log("INFO", "Init", "Current user is root - very good") else: - logger.log("NOTICE", "Program should be run as 'root' to ensure all access rights to process memory and file objects.") + logger.log("NOTICE", "Init", "Program should be run as 'root' to ensure all access rights to process memory and file objects.") # Set process to nice priority ------------------------------------ if os_platform == "windows": @@ -1473,7 +1475,7 @@ def main(): if isAdmin: loki.scan_processes() else: - logger.log("NOTICE", "Skipping process memory check. User has no admin rights.") + logger.log("NOTICE", "Init", "Skipping process memory check. User has no admin rights.") # Scan for Rootkits ----------------------------------------------- if args.rootkit and os_platform == "windows": @@ -1490,19 +1492,19 @@ def main(): loki.scan_path(defaultPath) # Result ---------------------------------------------------------- - logger.log("NOTICE", "Results: {0} alerts, {1} warnings, {2} notices".format(logger.alerts, logger.warnings, logger.notices)) + logger.log("NOTICE", "Results", "Results: {0} alerts, {1} warnings, {2} notices".format(logger.alerts, logger.warnings, logger.notices)) if logger.alerts: - logger.log("RESULT", "Indicators detected!") - logger.log("RESULT", "Loki recommends checking the elements on virustotal.com or Google and triage with a " + logger.log("RESULT", "Results", "Indicators detected!") + logger.log("RESULT", "Results", "Loki recommends checking the elements on virustotal.com or Google and triage with a " "professional tool like THOR https://nextron-systems.com/thor in corporate networks.") elif logger.warnings: - logger.log("RESULT", "Suspicious objects detected!") - logger.log("RESULT", "Loki recommends a deeper analysis of the suspicious objects.") + logger.log("RESULT", "Results", "Suspicious objects detected!") + logger.log("RESULT", "Results", "Loki recommends a deeper analysis of the suspicious objects.") else: - logger.log("RESULT", "SYSTEM SEEMS TO BE CLEAN.") + logger.log("RESULT", "Results", "SYSTEM SEEMS TO BE CLEAN.") - logger.log("INFO", "Please report false positives via https://github.com/Neo23x0/signature-base") - logger.log("NOTICE", "Finished LOKI Scan SYSTEM: %s TIME: %s" % (getHostname(os_platform), getSyslogTimestamp())) + logger.log("INFO", "Results", "Please report false positives via https://github.com/Neo23x0/signature-base") + logger.log("NOTICE", "Results", "Finished LOKI Scan SYSTEM: %s TIME: %s" % (getHostname(os_platform), getSyslogTimestamp())) if not args.dontwait: print " " From 58bef5c4f2f2c340f92b4beb6cf96fb89ba3a0b6 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Sat, 17 Mar 2018 09:35:20 +0100 Subject: [PATCH 12/24] Bugfix: Do not run PESieve on Windows XP systems --- loki.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/loki.py b/loki.py index 4c7a67d6..cfd9159f 100644 --- a/loki.py +++ b/loki.py @@ -123,7 +123,8 @@ def __init__(self, intense_mode): self.app_path = get_application_path() # PESieve - self.peSieve = PESieve(self.app_path, is64bit(), logger) + if "xp" not in getPlatformFull().lower(): + self.peSieve = PESieve(self.app_path, is64bit(), logger) # Check if signature database is present sig_dir = os.path.join(self.app_path, "./signature-base/") From da50876a5e082c600eec90b79b6a5b474aeb75e6 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Sat, 17 Mar 2018 09:36:08 +0100 Subject: [PATCH 13/24] Improved platform check --- lib/lokilogger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/lokilogger.py b/lib/lokilogger.py index 80ab4613..5345b4e4 100644 --- a/lib/lokilogger.py +++ b/lib/lokilogger.py @@ -39,7 +39,7 @@ def __init__(self, no_log_file, log_file, hostname, remote_host, remote_port, cs self.only_relevant = only_relevant self.debug = debug self.caller = caller - if platform == "windows": + if "windows" in platform.lower(): self.linesep = "\r\n" # Colorization ---------------------------------------------------- From a18c74a9eeebf98ec6c9ba6912aecb452399c2d1 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Sat, 17 Mar 2018 09:36:34 +0100 Subject: [PATCH 14/24] Make "version" available in the logger object --- lib/lokilogger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/lokilogger.py b/lib/lokilogger.py index 5345b4e4..0ac6abd8 100644 --- a/lib/lokilogger.py +++ b/lib/lokilogger.py @@ -14,7 +14,7 @@ import socket from helpers import removeNonAsciiDrop -__version__ = '0.26.2' +__version__ = '0.27.0' # Logger Class ----------------------------------------------------------------- class LokiLogger(): @@ -32,6 +32,7 @@ class LokiLogger(): linesep = "\n" def __init__(self, no_log_file, log_file, hostname, remote_host, remote_port, csv, only_relevant, debug, platform, caller): + self.version = __version__ self.no_log_file = no_log_file self.log_file = log_file self.hostname = hostname From 8123dce657ff696e847dba8981c0f3c2a33ecd05 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Tue, 10 Apr 2018 23:44:27 +0200 Subject: [PATCH 15/24] Bugfix in process scan --- lib/lokilogger.py | 2 +- loki.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/lokilogger.py b/lib/lokilogger.py index 0ac6abd8..446cb3fb 100644 --- a/lib/lokilogger.py +++ b/lib/lokilogger.py @@ -14,7 +14,7 @@ import socket from helpers import removeNonAsciiDrop -__version__ = '0.27.0' +__version__ = '0.27.1' # Logger Class ----------------------------------------------------------------- class LokiLogger(): diff --git a/loki.py b/loki.py index cfd9159f..33e469e5 100644 --- a/loki.py +++ b/loki.py @@ -665,7 +665,7 @@ def scan_processes(self): logger.log("INFO", "ProcessScan", "Too many matches on process memory - most likely a false positive %s" % process_info) elif len(alerts) > 0: for alert in alerts: - logger.log("ALERT", alert) + logger.log("ALERT", "ProcessScan", alert) except Exception, e: if logger.debug: traceback.print_exc() From f7790e94d213c3d97bae7e555905b54976cd1c8b Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Fri, 13 Apr 2018 00:36:22 +0200 Subject: [PATCH 16/24] Upgrade to PESieve v0.0.9.9.9 with XP support & changed cmdline options --- lib/lokilogger.py | 2 +- lib/pesieve.py | 2 +- loki.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/lokilogger.py b/lib/lokilogger.py index 446cb3fb..8330e2a1 100644 --- a/lib/lokilogger.py +++ b/lib/lokilogger.py @@ -14,7 +14,7 @@ import socket from helpers import removeNonAsciiDrop -__version__ = '0.27.1' +__version__ = '0.27.2' # Logger Class ----------------------------------------------------------------- class LokiLogger(): diff --git a/lib/pesieve.py b/lib/pesieve.py index d3823121..39bf9284 100644 --- a/lib/pesieve.py +++ b/lib/pesieve.py @@ -55,7 +55,7 @@ def scan(self, pid): replaced = 0 suspicious = 0 # Compose command - command = [self.peSieve, '/pid', str(pid), '/nodump', '/quiet'] + command = [self.peSieve, '/pid', str(pid), '/ofilter', '2', '/quiet'] # Run PE-Sieve on given process output, returnCode = runProcess(command) diff --git a/loki.py b/loki.py index 33e469e5..cb805f5f 100644 --- a/loki.py +++ b/loki.py @@ -123,8 +123,7 @@ def __init__(self, intense_mode): self.app_path = get_application_path() # PESieve - if "xp" not in getPlatformFull().lower(): - self.peSieve = PESieve(self.app_path, is64bit(), logger) + self.peSieve = PESieve(self.app_path, is64bit(), logger) # Check if signature database is present sig_dir = os.path.join(self.app_path, "./signature-base/") From 3dd430d76752ba547539d3522f38e50a3f8235e3 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Fri, 13 Apr 2018 00:50:01 +0200 Subject: [PATCH 17/24] Added detection for "implanted" processes --- lib/pesieve.py | 15 ++++++++------- loki.py | 14 +++++++++----- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/lib/pesieve.py b/lib/pesieve.py index 39bf9284..a09c0e40 100644 --- a/lib/pesieve.py +++ b/lib/pesieve.py @@ -51,9 +51,7 @@ def scan(self, pid): :return hooked, replaces, suspicious: number of findings per type """ # Presets - hooked = 0 - replaced = 0 - suspicious = 0 + results = {"hooked": 0, "replaced": 0, "suspicious": 0, "implanted": 0} # Compose command command = [self.peSieve, '/pid', str(pid), '/ofilter', '2', '/quiet'] # Run PE-Sieve on given process @@ -71,15 +69,18 @@ def scan(self, pid): # Extract the integer values result_hooked = re.search(r'Hooked:[\s\t]+([0-9]+)', line) if result_hooked: - hooked = int(result_hooked.group(1)) + results["hooked"] = int(result_hooked.group(1)) result_replaced = re.search(r'Replaced:[\s\t]+([0-9]+)', line) if result_replaced: - replaced = int(result_replaced.group(1)) + results["replaced"] = int(result_replaced.group(1)) result_suspicious = re.search(r'Other suspicious:[\s\t]+([0-9]+)', line) if result_suspicious: - suspicious = int(result_suspicious.group(1)) + results["suspicious"] = int(result_suspicious.group(1)) + result_implanted = re.search(r'Implanted:[\s\t]+([0-9]+)', line) + if result_implanted: + results["implanted"] = int(result_implanted.group(1)) # Check output for process replacements if "SUMMARY:" not in output: self.logger.log("ERROR", "PESieve", "Something went wrong during PE-Sieve scan. " "Couldn't find the SUMMARY section in output.") - return hooked, replaced, suspicious + return results diff --git a/loki.py b/loki.py index cb805f5f..85d92f6c 100644 --- a/loki.py +++ b/loki.py @@ -677,13 +677,17 @@ def scan_processes(self): if processExists(pid) and self.peSieve.active: # If PE-Sieve reports replaced processes logger.log("DEBUG", "ProcessScan", "PE-Sieve scan of process PID: %s" % pid) - (hooked, replaced, suspicious) = self.peSieve.scan(pid=pid) - if replaced: + results = self.peSieve.scan(pid=pid) + if results["replaced"]: logger.log("WARNING", "ProcessScan", "PE-Sieve reported replaced process %s REPLACED: %s" % - (process_info, str(replaced))) - elif hooked or suspicious: + (process_info, str(results["replaced"]))) + elif results["implanted"]: + logger.log("WARNING", "ProcessScan", "PE-Sieve reported implanted process %s IMPLANTED: %s" % + (process_info, str(results["implanted"]))) + elif results["hooked"] or results["suspicious"]: logger.log("NOTICE", "ProcessScan", "PE-Sieve reported hooked or suspicious process %s " - "HOOKED: %s SUSPICIOUS: %s" % (process_info, str(hooked), str(suspicious))) + "HOOKED: %s SUSPICIOUS: %s" % (process_info, str(results["hooked"]), + str(results["suspicious"]))) else: logger.log("INFO", "ProcessScan", "PE-Sieve reported no anomalies %s" % process_info) From 1c429c1bb82ac8fac09ab363de18de9963528655 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Sat, 14 Apr 2018 01:01:06 +0200 Subject: [PATCH 18/24] Using JSON output of PESieve --- lib/pesieve.py | 39 +++++++++++++-------------------------- loki.py | 6 +++--- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/lib/pesieve.py b/lib/pesieve.py index a09c0e40..8cbab3d6 100644 --- a/lib/pesieve.py +++ b/lib/pesieve.py @@ -4,6 +4,7 @@ import os import sys +import json import traceback from lib.lokilogger import * @@ -51,36 +52,22 @@ def scan(self, pid): :return hooked, replaces, suspicious: number of findings per type """ # Presets - results = {"hooked": 0, "replaced": 0, "suspicious": 0, "implanted": 0} + results = {"hooked": 0, "replaced": 0, "detached": 0, "implanted": 0} # Compose command - command = [self.peSieve, '/pid', str(pid), '/ofilter', '2', '/quiet'] + command = [self.peSieve, '/pid', str(pid), '/ofilter', '2', '/quiet', '/json'] # Run PE-Sieve on given process output, returnCode = runProcess(command) - # Process the output - lines = output.splitlines() - start_summary = False - for line in lines: + try: + # Debug output + results_raw = json.loads(output) + results = results_raw["scanned"]["modified"] + if pid == 360: + results["implanted"] = 1 if self.logger.debug: - if "SUMMARY:" in line: - start_summary = True - if start_summary: - print(line) - # Extract the integer values - result_hooked = re.search(r'Hooked:[\s\t]+([0-9]+)', line) - if result_hooked: - results["hooked"] = int(result_hooked.group(1)) - result_replaced = re.search(r'Replaced:[\s\t]+([0-9]+)', line) - if result_replaced: - results["replaced"] = int(result_replaced.group(1)) - result_suspicious = re.search(r'Other suspicious:[\s\t]+([0-9]+)', line) - if result_suspicious: - results["suspicious"] = int(result_suspicious.group(1)) - result_implanted = re.search(r'Implanted:[\s\t]+([0-9]+)', line) - if result_implanted: - results["implanted"] = int(result_implanted.group(1)) - # Check output for process replacements - if "SUMMARY:" not in output: + print results + except Exception as e: + traceback.print_exc() self.logger.log("ERROR", "PESieve", "Something went wrong during PE-Sieve scan. " - "Couldn't find the SUMMARY section in output.") + "Couldn't parse the JSON output.") return results diff --git a/loki.py b/loki.py index 85d92f6c..d0d67d24 100644 --- a/loki.py +++ b/loki.py @@ -684,10 +684,10 @@ def scan_processes(self): elif results["implanted"]: logger.log("WARNING", "ProcessScan", "PE-Sieve reported implanted process %s IMPLANTED: %s" % (process_info, str(results["implanted"]))) - elif results["hooked"] or results["suspicious"]: - logger.log("NOTICE", "ProcessScan", "PE-Sieve reported hooked or suspicious process %s " + elif results["hooked"] or results["detached"]: + logger.log("NOTICE", "ProcessScan", "PE-Sieve reported hooked or detached process %s " "HOOKED: %s SUSPICIOUS: %s" % (process_info, str(results["hooked"]), - str(results["suspicious"]))) + str(results["detached"]))) else: logger.log("INFO", "ProcessScan", "PE-Sieve reported no anomalies %s" % process_info) From 938053f95ebf2f7f7493a13c29827b8a532333b5 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Sat, 14 Apr 2018 09:09:59 +0200 Subject: [PATCH 19/24] Bugfix: removed demo code --- lib/pesieve.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/pesieve.py b/lib/pesieve.py index 8cbab3d6..6c45ee38 100644 --- a/lib/pesieve.py +++ b/lib/pesieve.py @@ -62,8 +62,6 @@ def scan(self, pid): # Debug output results_raw = json.loads(output) results = results_raw["scanned"]["modified"] - if pid == 360: - results["implanted"] = 1 if self.logger.debug: print results except Exception as e: From 0049f74e729584457857c2674faf016f51d67eff Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Sat, 14 Apr 2018 09:12:14 +0200 Subject: [PATCH 20/24] Fixed version number --- lib/lokilogger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/lokilogger.py b/lib/lokilogger.py index 8330e2a1..8ea4e91f 100644 --- a/lib/lokilogger.py +++ b/lib/lokilogger.py @@ -14,7 +14,7 @@ import socket from helpers import removeNonAsciiDrop -__version__ = '0.27.2' +__version__ = '0.27.5' # Logger Class ----------------------------------------------------------------- class LokiLogger(): From d756ee39323c3968b8ff5680264989abf07896e4 Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Sat, 14 Apr 2018 12:40:22 +0200 Subject: [PATCH 21/24] Exclude LOKI's processes from checks --- loki.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/loki.py b/loki.py index d0d67d24..bebb8ae1 100644 --- a/loki.py +++ b/loki.py @@ -559,6 +559,10 @@ def scan_processes(self): # LSASS Counter lsass_count = 0 + # LOKI's processes + loki_pid = os.getpid() + loki_ppid = psutil.Process(os.getpid()).ppid() # safer way to do this - os.ppid() fails in some envs + for process in processes: try: @@ -608,7 +612,7 @@ def scan_processes(self): continue # Skip own process ---------------------------------------------------- - if os.getpid() == pid: + if loki_pid == pid or loki_ppid == pid: logger.log("INFO", "ProcessScan", "Skipping LOKI Process %s" % process_info) continue From fc2f405354d9af72e20b52901f9abcb52236ef1f Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Sat, 14 Apr 2018 12:41:25 +0200 Subject: [PATCH 22/24] Don't show every rule during startup but only a count (use --debug to see them) --- loki.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/loki.py b/loki.py index bebb8ae1..a5e5b939 100644 --- a/loki.py +++ b/loki.py @@ -1036,6 +1036,7 @@ def initialize_yara_rules(self): if not os.path.exists(yara_rule_directory): continue logger.log("INFO", "Init", "Processing YARA rules folder {0}".format(yara_rule_directory)) + rule_count = 0 for root, directories, files in os.walk(yara_rule_directory, onerror=walk_error, followlinks=False): for file in files: try: @@ -1059,7 +1060,8 @@ def initialize_yara_rules(self): 'filetype': dummy, 'md5': dummy, }) - logger.log("INFO", "Init", "Initializing Yara rule %s" % file) + logger.log("DEBUG", "Init", "Initializing Yara rule %s" % file) + rule_count += 1 except Exception, e: logger.log("ERROR", "Init", "Error while initializing Yara rule %s" % file) traceback.print_exc() @@ -1089,7 +1091,7 @@ def initialize_yara_rules(self): 'filetype': dummy, 'md5': dummy }) - logger.log("INFO", "Init", "Initialized all Yara rules at once") + logger.log("INFO", "Init", "Initialized %d Yara rules" % rule_count) except Exception, e: traceback.print_exc() logger.log("ERROR", "Init", "Error during YARA rule compilation - please fix the issue in the rule set") From 6c68ad4a38f5cbe81402544f8b2976f01a0c0e0a Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Sat, 14 Apr 2018 12:42:17 +0200 Subject: [PATCH 23/24] LOKI upgrader allows a signature clean-up (--clean) --- loki-upgrader.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/loki-upgrader.py b/loki-upgrader.py index 09316d2b..09a69c88 100644 --- a/loki-upgrader.py +++ b/loki-upgrader.py @@ -55,7 +55,7 @@ def __init__(self, debug, logger, application_path): self.logger = logger self.application_path = application_path - def update_signatures(self): + def update_signatures(self, clean=False): try: for sig_url in self.UPDATE_URL_SIGS: # Downloading current repository @@ -72,6 +72,9 @@ def update_signatures(self): # Preparations try: sigDir = os.path.join(self.application_path, './signature-base/') + if clean: + self.logger.log("INFO", "Upgrader", "Cleaning directory '%s'" % sigDir) + shutil.rmtree(sigDir) for outDir in ['', 'iocs', 'yara', 'misc']: fullOutDir = os.path.join(sigDir, outDir) if not os.path.exists(fullOutDir): @@ -208,6 +211,8 @@ def get_application_path(): parser.add_argument('--progonly', action='store_true', help='Update the program files only', default=False) parser.add_argument('--nolog', action='store_true', help='Don\'t write a local log file', default=False) parser.add_argument('--debug', action='store_true', default=False, help='Debug output') + parser.add_argument('--clean', action='store_true', default=False, help='Clean up the signature directory and get ' + 'a fresh set') parser.add_argument('--detached', action='store_true', default=False, help=argparse.SUPPRESS) args = parser.parse_args() @@ -221,16 +226,15 @@ def get_application_path(): # Logger logger = LokiLogger(args.nolog, args.l, t_hostname, '', '', False, False, args.debug, platform=platform, caller='upgrader') - # Update Loki + # Update LOKI updater = LOKIUpdater(args.debug, logger, get_application_path()) - # Updating LOKI if not args.sigsonly: logger.log("INFO", "Upgrader", "Updating LOKI ...") updater.update_loki() if not args.progonly: logger.log("INFO", "Upgrader", "Updating Signatures ...") - updater.update_signatures() + updater.update_signatures(args.clean) logger.log("INFO", "Upgrader", "Update complete") From a29da1f72ea6c21b9dc836b5e440b729a9a9c6bc Mon Sep 17 00:00:00 2001 From: Florian Roth Date: Sat, 14 Apr 2018 12:42:47 +0200 Subject: [PATCH 24/24] Error fix in loki-upgrader --- lib/lokilogger.py | 2 +- loki-upgrader.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/lokilogger.py b/lib/lokilogger.py index 8ea4e91f..6512050a 100644 --- a/lib/lokilogger.py +++ b/lib/lokilogger.py @@ -14,7 +14,7 @@ import socket from helpers import removeNonAsciiDrop -__version__ = '0.27.5' +__version__ = '0.28.0' # Logger Class ----------------------------------------------------------------- class LokiLogger(): diff --git a/loki-upgrader.py b/loki-upgrader.py index 09a69c88..8bbe1408 100644 --- a/loki-upgrader.py +++ b/loki-upgrader.py @@ -160,13 +160,22 @@ def update_loki(self): try: # Create file if not present - os.makedirs(os.path.basename(targetFile)) + if not os.path.exists(os.path.dirname(targetFile)): + os.makedirs(os.path.dirname(targetFile)) + except Exception as e: + if self.debug: + self.logger.log("DEBUG", "Upgrader", "Cannot create dir name '%s'" % os.path.dirname(targetFile)) + traceback.print_exc() + + try: # Create target file target = file(targetFile, "wb") with source, target: - shutil.copyfileobj(source, target) + shutil.copyfileobj(source, target) + if self.debug: + self.logger.log("DEBUG", "Upgrader", "Successfully extracted '%s'" % targetFile) except Exception as e: - self.logger.log("ERROR", "Upgrader", "Cannot extract %s" % targetFile) + self.logger.log("ERROR", "Upgrader", "Cannot extract '%s'" % targetFile) if self.debug: traceback.print_exc()