-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
1 addition
and
389 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,388 +0,0 @@ | ||
--- loki.py.original 2023-12-28 14:31:22.375121479 +0100 | ||
+++ loki.py 2024-01-03 03:56:32.048867818 +0100 | ||
@@ -48,6 +48,10 @@ | ||
from lib.doublepulsar import DoublePulsar | ||
from lib.vuln_checker import VulnChecker | ||
|
||
+# Daemon mode | ||
+import socket | ||
+from threading import Thread | ||
+ | ||
# Platform | ||
os_platform = "" | ||
|
||
@@ -113,6 +117,8 @@ | ||
fullExcludes = [] | ||
# Platform specific excludes (match the beginning of the full path) (not user-defined) | ||
startExcludes = [] | ||
+ # Excludes hash (md5, sha1 and sha256) | ||
+ excludes_hash = [] | ||
|
||
# File type magics | ||
filetype_magics = {} | ||
@@ -196,6 +202,14 @@ | ||
|
||
def scan_path(self, path): | ||
|
||
+ if os.path.isfile(path) and os_platform == "linux": | ||
+ logger.log("INFO", "Init", "Single file mode for " + os_platform) | ||
+ root = '' | ||
+ directories = '' | ||
+ files = [ path ] | ||
+ loki.scan_path_files(root, directories, files) | ||
+ return | ||
+ | ||
# Check if path exists | ||
if not os.path.exists(path): | ||
logger.log("ERROR", "FileScan", "None Existing Scanning Path %s ... " % path) | ||
@@ -210,9 +224,6 @@ | ||
"Skipping %s directory [fixed excludes] (try using --force, --allhds or --alldrives)" % skip) | ||
return | ||
|
||
- # Counter | ||
- c = 0 | ||
- | ||
for root, directories, files in os.walk(path, onerror=walk_error, followlinks=False): | ||
|
||
# Skip paths that start with .. | ||
@@ -233,6 +244,13 @@ | ||
newDirectories.append(dir) | ||
directories[:] = newDirectories | ||
|
||
+ loki.scan_path_files(root, directories, files) | ||
+ | ||
+ def scan_path_files(self, root, directories, files): | ||
+ | ||
+ # Counter | ||
+ c = 0 | ||
+ | ||
# Loop through files | ||
for filename in files: | ||
try: | ||
@@ -278,8 +296,12 @@ | ||
skipIt = True | ||
|
||
# File mode | ||
- mode = os.stat(filePath).st_mode | ||
- if stat.S_ISCHR(mode) or stat.S_ISBLK(mode) or stat.S_ISFIFO(mode) or stat.S_ISLNK(mode) or stat.S_ISSOCK(mode): | ||
+ try: | ||
+ mode = os.stat(filePath).st_mode | ||
+ if stat.S_ISCHR(mode) or stat.S_ISBLK(mode) or stat.S_ISFIFO(mode) or stat.S_ISLNK(mode) or stat.S_ISSOCK(mode): | ||
+ continue | ||
+ except: | ||
+ logger.log("DEBUG", "FileScan", "Skipping element %s does not exist or is a broken symlink" % (filePath)) | ||
continue | ||
|
||
# Skip | ||
@@ -397,6 +419,11 @@ | ||
if md5_num in self.false_hashes.keys() or sha1_num in self.false_hashes.keys() or sha256_num in self.false_hashes.keys(): | ||
continue | ||
|
||
+ # Skip exclude hash | ||
+ if md5 in self.excludes_hash or sha1 in self.excludes_hash or sha256 in self.excludes_hash: | ||
+ logger.log("DEBUG", "FileScan", "Skipping element %s excluded by hash" % (filePath)) | ||
+ continue | ||
+ | ||
# Malware Hash | ||
matchScore = 100 | ||
matchLevel = "Malware" | ||
@@ -475,10 +502,13 @@ | ||
# Now print the total result | ||
if total_score >= args.a: | ||
message_type = "ALERT" | ||
+ threading.current_thread().message = "ALERT" | ||
elif total_score >= args.w: | ||
message_type = "WARNING" | ||
+ threading.current_thread().message = "WARNING" | ||
elif total_score >= args.n: | ||
message_type = "NOTICE" | ||
+ threading.current_thread().message = "NOTICE" | ||
|
||
if total_score < args.n: | ||
continue | ||
@@ -590,6 +620,78 @@ | ||
owner.upper().startswith("LO") or | ||
owner.upper().startswith("SYSTEM")) | ||
|
||
+ def scan_processes_linux(self): | ||
+ | ||
+ processes = psutil.pids() | ||
+ | ||
+ loki_pid = os.getpid() | ||
+ loki_ppid = psutil.Process(os.getpid()).ppid() | ||
+ | ||
+ for process in processes: | ||
+ | ||
+ # Gather Process Information ------------------------------------- | ||
+ pid = process | ||
+ try: | ||
+ name = psutil.Process(process).name() | ||
+ except (psutil.NoSuchProcess): | ||
+ logger.log("DEBUG", "ProcessScan", "Skipping Process PID: %s as it just exited and no longer exists" % (str(pid))) | ||
+ continue | ||
+ owner = psutil.Process(process).username() | ||
+ status = psutil.Process(process).status() | ||
+ try: | ||
+ cmd = ' '.join(psutil.Process(process).cmdline()) | ||
+ except (psutil.NoSuchProcess, psutil.ZombieProcess): | ||
+ logger.log("WARNING", "ProcessScan", "Process PID: %s NAME: %s STATUS: %s" % (str(pid), name, status)) | ||
+ continue | ||
+ path = psutil.Process(process).cwd() | ||
+ bin = psutil.Process(process).exe() | ||
+ tty = psutil.Process(process).terminal() | ||
+ memory_maps = psutil.Process(process).memory_maps() | ||
+ ws_size = psutil.Process(process).memory_info().vms | ||
+ num_fds = psutil.Process(process).num_fds() | ||
+ open_files = psutil.Process(process).open_files() | ||
+ | ||
+ process_info = "PID: %s NAME: %s OWNER: %s STATUS: %s BIN: %s CMD: %s PATH: %s TTY: %s" % (str(pid), name, owner, status, bin, cmd, path, tty) | ||
+ | ||
+ # Print info ------------------------------------------------------- | ||
+ logger.log("INFO", "ProcessScan", "Process %s" % process_info) | ||
+ | ||
+ # Process Masquerading Detection ----------------------------------- | ||
+ | ||
+ if re.search('\[', cmd): | ||
+ maps = Popen('cat /proc/' + str(pid) + '/maps', shell=True, stdout=subprocess.PIPE) | ||
+ if(maps.stdout.read()): | ||
+ logger.log("WARNING", "ProcessScan", "Potentional Process Masquerading PID: %s CMD: %s Check /proc/%s/maps" % (str(pid), cmd, str(pid))) | ||
+ | ||
+ # File Name Checks ------------------------------------------------- | ||
+ for fioc in self.filename_iocs: | ||
+ match = fioc['regex'].search(cmd) | ||
+ if match: | ||
+ if int(fioc['score']) > 70: | ||
+ logger.log("ALERT", "ProcessScan", "File Name IOC matched PATTERN: %s DESC: %s MATCH: %s" % (fioc['regex'].pattern, fioc['description'], cmd)) | ||
+ elif int(fioc['score']) > 40: | ||
+ logger.log("WARNING", "ProcessScan", "File Name Suspicious IOC matched PATTERN: %s DESC: %s MATCH: %s" % (fioc['regex'].pattern, fioc['description'], cmd)) | ||
+ | ||
+ # Process connections ---------------------------------------------- | ||
+ if not args.nolisten: | ||
+ connections = psutil.Process(pid).connections() | ||
+ conn_count = 0 | ||
+ conn_limit = 20 | ||
+ for pconn in connections: | ||
+ conn_count += 1 | ||
+ if conn_count > conn_limit: | ||
+ logger.log("NOTICE", "ProcessScan", "Process PID: %s NAME: %s More connections detected. Showing only %s" % (str(pid), name, conn_limit)) | ||
+ break | ||
+ ip = pconn.laddr.ip | ||
+ status = pconn.status | ||
+ ext = pconn.raddr | ||
+ if(ext): | ||
+ ext_ip = pconn.raddr.ip | ||
+ ext_port = pconn.raddr.port | ||
+ logger.log("NOTICE", "ProcessScan", "Process PID: %s NAME: %s CONNECTION: %s <=> %s %s (%s)" % (str(pid), name, ip, ext_ip, ext_port, status)) | ||
+ else: | ||
+ logger.log("NOTICE", "ProcessScan", "Process PID: %s NAME: %s CONNECTION: %s (%s)" % (str(pid), name, ip, status)) | ||
+ | ||
def scan_processes(self, nopesieve, nolisten, excludeprocess, pesieveshellc): | ||
# WMI Handler | ||
c = wmi.WMI() | ||
@@ -1117,6 +1219,10 @@ | ||
# Full Path | ||
yaraRuleFile = os.path.join(root, file) | ||
|
||
+ if(file in args.disable_yara_files.split(",")): | ||
+ logger.log("NOTICE", "Init", "Disabled yara file: " + file) | ||
+ continue | ||
+ | ||
# Skip hidden, backup or system related files | ||
if file.startswith(".") or file.startswith("~") or file.startswith("_"): | ||
continue | ||
@@ -1290,6 +1396,7 @@ | ||
def initialize_excludes(self, excludes_file): | ||
try: | ||
excludes = [] | ||
+ excludes_hash = [] | ||
with open(excludes_file, 'r') as config: | ||
lines = config.read().splitlines() | ||
|
||
@@ -1297,14 +1404,23 @@ | ||
if re.search(r'^[\s]*#', line): | ||
continue | ||
try: | ||
+ # If the line contains md5sum | ||
+ if re.search(r'^md5sum:', line): | ||
+ excludes_hash.append(re.sub('(md5sum:|(\s\#|\#).*)','', line)) | ||
+ # If the line contains sha1sum | ||
+ elif re.search(r'^sha1sum:', line): | ||
+ excludes_hash.append(re.sub('(sha1sum:|(\s\#|\#).*)','', line)) | ||
+ elif re.search(r'^sha256sum:', line): | ||
+ excludes_hash.append(re.sub('(sha256sum:|(\s\#|\#).*)','', line)) | ||
# If the line contains something | ||
- if re.search(r'\w', line): | ||
+ elif re.search(r'\w', line): | ||
regex = re.compile(line, re.IGNORECASE) | ||
excludes.append(regex) | ||
except Exception: | ||
logger.log("ERROR", "Init", "Cannot compile regex: %s" % line) | ||
|
||
self.fullExcludes = excludes | ||
+ self.excludes_hash = excludes_hash | ||
|
||
except Exception: | ||
if logger.debug: | ||
@@ -1444,12 +1560,25 @@ | ||
|
||
|
||
# CTRL+C Handler -------------------------------------------------------------- | ||
+def remove_pidfile(): | ||
+ if(args.d): | ||
+ try: | ||
+ os.remove(args.pidfile) | ||
+ except Exception: | ||
+ pass | ||
+ | ||
def signal_handler(signal_name, frame): | ||
try: | ||
print("------------------------------------------------------------------------------\n") | ||
logger.log('INFO', 'Init', 'LOKI\'s work has been interrupted by a human. Returning to Asgard.') | ||
except Exception: | ||
print('LOKI\'s work has been interrupted by a human. Returning to Asgard.') | ||
+ remove_pidfile() | ||
+ sys.exit(0) | ||
+ | ||
+def signal_handler_term(signal_name, frame): | ||
+ remove_pidfile() | ||
+ print('LOKI\'s work has been interrupted by a SIGTERM. Returning to Asgard.') | ||
sys.exit(0) | ||
|
||
def main(): | ||
@@ -1468,6 +1597,12 @@ | ||
parser.add_argument('-a', help='Alert score', metavar='alert-level', default=100) | ||
parser.add_argument('-w', help='Warning score', metavar='warning-level', default=60) | ||
parser.add_argument('-n', help='Notice score', metavar='notice-level', default=40) | ||
+ parser.add_argument('-d', help='Run as a daemon', action='store_true', default=False) | ||
+ parser.add_argument('--pidfile', help='Pid file path (default: loki.pid)', default='loki.pid') | ||
+ parser.add_argument('--listen-host', help='Listen host for daemon mode (default: localhost)', default='localhost') | ||
+ parser.add_argument('--listen-port', help='Listen port for daemon mode (default: 1337)', type=int, default=1337) | ||
+ parser.add_argument('--auth', help='Auth key, only in daemon mode', default='') | ||
+ parser.add_argument('--disable-yara-files', help='Comma separated list of yara files to disable', default='') | ||
parser.add_argument('--allhds', action='store_true', help='Scan all local hard drives (Windows only)', default=False) | ||
parser.add_argument('--alldrives', action='store_true', help='Scan all drives (including network drives and removable media)', default=False) | ||
parser.add_argument('--printall', action='store_true', help='Print all files that are scanned', default=False) | ||
@@ -1532,9 +1667,25 @@ | ||
# Signal handler for CTRL+C | ||
signal_module.signal(signal_module.SIGINT, signal_handler) | ||
|
||
+ # Signal handler for SIGTERM | ||
+ signal_module.signal(signal_module.SIGTERM, signal_handler_term) | ||
+ | ||
# Argument parsing | ||
args = main() | ||
|
||
+ # Save pidfile | ||
+ if args.d is True: | ||
+ if(os.path.exists(args.pidfile)): | ||
+ fpid = open(args.pidfile, 'r') | ||
+ loki_pid = int(fpid.read()) | ||
+ fpid.close() | ||
+ if psutil.pid_exists(loki_pid): | ||
+ print("LOKI daemon already running. Returning to Asgard.") | ||
+ sys.exit(0) | ||
+ with open(args.pidfile, 'w', encoding='utf-8') as fpid: | ||
+ fpid.write(str(os.getpid())) | ||
+ fpid.close() | ||
+ | ||
# Remove old log file | ||
if os.path.exists(args.l): | ||
os.remove(args.l) | ||
@@ -1553,8 +1704,20 @@ | ||
updateLoki(sigsOnly=False) | ||
sys.exit(0) | ||
|
||
+ if os_platform == "linux": | ||
+ try: | ||
+ for key, val in platform.freedesktop_os_release().items(): | ||
+ if key == 'PRETTY_NAME': | ||
+ platform_pretty_name = val | ||
+ except Exception: | ||
+ platform_pretty_name = platform.system() | ||
+ platform_machine = platform.machine() | ||
+ platform_full = platform_pretty_name + " (" + platform_machine + ")" | ||
+ else: | ||
+ platform_full = getPlatformFull() | ||
+ | ||
logger.log("NOTICE", "Init", "Starting Loki Scan VERSION: {3} SYSTEM: {0} TIME: {1} PLATFORM: {2}".format( | ||
- getHostname(os_platform), getSyslogTimestamp(), getPlatformFull(), logger.version)) | ||
+ getHostname(os_platform), getSyslogTimestamp(), platform_full, logger.version)) | ||
|
||
# Loki | ||
loki = Loki(args.intense) | ||
@@ -1589,6 +1752,11 @@ | ||
|
||
# Scan Processes -------------------------------------------------- | ||
resultProc = False | ||
+ if not args.noprocscan and os_platform == "linux": | ||
+ if isAdmin: | ||
+ loki.scan_processes_linux() | ||
+ else: | ||
+ logger.log("NOTICE", "Init", "Skipping process memory check. User has no admin rights.") | ||
if not args.noprocscan and os_platform == "windows": | ||
if isAdmin: | ||
loki.scan_processes(args.nopesieve, args.nolisten, args.excludeprocess, args.pesieveshellc) | ||
@@ -1619,6 +1787,63 @@ | ||
loki.scan_path(defaultPath) | ||
|
||
# Linux & macOS | ||
+ elif args.d is True: | ||
+ logger.log("NOTICE", "Init", "Loki-daemonized (c) 2023 c0m4r") | ||
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | ||
+ server.bind((args.listen_host, args.listen_port)) | ||
+ server.listen(5) | ||
+ | ||
+ def handle_client(client_socket, address): | ||
+ size = 2048 | ||
+ while True: | ||
+ try: | ||
+ clientid = threading.current_thread().name | ||
+ threading.current_thread().message = '' | ||
+ data = client_socket.recv(size) | ||
+ scan_path = data.decode().split(" ")[0] | ||
+ if args.auth: | ||
+ server_authkey = args.auth | ||
+ try: | ||
+ client_authkey = data.decode().split(" ")[1] | ||
+ except: | ||
+ logger.log("NOTICE", "Auth", "Client " + str(address[0]) + ":" + str(address[1]) + " no valid authorization") | ||
+ client_socket.send('authorization required'.encode()) | ||
+ client_socket.close() | ||
+ return False | ||
+ | ||
+ if client_authkey == server_authkey: | ||
+ logger.log("NOTICE", "Auth", "Client " + str(address[0]) + ":" + str(address[1]) + " accepted") | ||
+ else: | ||
+ logger.log("NOTICE", "Auth", "Client " + str(address[0]) + ":" + str(address[1]) + " unauthorized") | ||
+ client_socket.send('unauthorized'.encode()) | ||
+ client_socket.close() | ||
+ return False | ||
+ logger.log("INFO", "Init", "Received: " + data.decode() + " from: " + str(address[0]) + ":" + str(address[1])) | ||
+ loki.scan_path(scan_path) | ||
+ # Result ---------------------------------------------------------- | ||
+ if threading.current_thread().message == 'ALERT': | ||
+ logger.log("RESULT", "Results", "Indicators detected! (Client: " + clientid + ")") | ||
+ client_socket.send('RESULT: Indicators detected!'.encode()) | ||
+ elif threading.current_thread().message == 'WARNING': | ||
+ logger.log("RESULT", "Results", "Suspicious objects detected! (Client: " + clientid + ")") | ||
+ client_socket.send('RESULT: Suspicious objects detected!'.encode()) | ||
+ else: | ||
+ logger.log("RESULT", "Results", "SYSTEM SEEMS TO BE CLEAN. (Client: " + clientid + ")") | ||
+ client_socket.send('RESULT: SYSTEM SEEMS TO BE CLEAN.'.encode()) | ||
+ | ||
+ logger.log("NOTICE", "Results", "Finished LOKI Scan CLIENT: %s SYSTEM: %s TIME: %s" % (clientid, getHostname(os_platform), getSyslogTimestamp())) | ||
+ client_socket.close() | ||
+ return False | ||
+ except socket.error: | ||
+ client_socket.close() | ||
+ return False | ||
+ | ||
+ logger.log("NOTICE", "Init", "Listening on " + args.listen_host + ":" + str(args.listen_port)) | ||
+ while True: | ||
+ client, addr = server.accept() | ||
+ Thread(target=handle_client, args=(client, addr), name=str(addr[0]) + ":" + str(addr[1])).start() | ||
+ | ||
else: | ||
loki.scan_path(defaultPath) | ||
|
||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters