diff --git a/README.md b/README.md index 7105e70..7b245ab 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Linux Security and Monitoring Scripts -These are a collection of security and monitoring scripts you can use to monitor your Linux installation for security-related events or for an investigation. Each script works on its own and is independent from other scripts. The scripts can be set up to either print out their results, send them to you via mail, or using [AlertR](https://github.com/sqall01/alertR) as notification channel. +These are a collection of security and monitoring scripts you can use to monitor your Linux installation for security-related events or for an investigation. Each script works on its own and is independent of other scripts. The scripts can be set up to either print out their results, send them to you via mail, or using [AlertR](https://github.com/sqall01/alertR) as notification channel. ## Repository Structure @@ -28,10 +28,12 @@ Finally, you can run all configured scripts by executing `start_search.py` (whic | Monitoring /etc/ld.so.preload file | [monitor_ld_preload.py](scripts/monitor_ld_preload.py) | | Monitoring /etc/passwd file | [monitor_passwd.py](scripts/monitor_passwd.py) | | Monitoring SSH authorized_keys files | [monitor_ssh_authorized_keys.py](scripts/monitor_ssh_authorized_keys.py) | -| Search for running deleted programs | [search_deleted_exe.py](scripts/search_deleted_exe.py) | -| Search for executables in /dev/shm | [search_dev_shm.py](scripts/search_dev_shm.py) | +| Search executables in /dev/shm | [search_dev_shm.py](scripts/search_dev_shm.py) | +| Search fileless programs (memfd_create) | [search_memfd_create.py](scripts/search_memfd_create.py) | +| Search hidden ELF files | [search_hidden_exe.py](scripts/search_hidden_exe.py) | | Search immutable files | [search_immutable_files.py](scripts/search_immutable_files.py) | -| Search for fileless programs (memfd_create) | [search_memfd_create.py](scripts/search_memfd_create.py) | -| Search for kernel thread impersonations | [search_non_kthreads.py](scripts/search_non_kthreads.py) | +| Search kernel thread impersonations | [search_non_kthreads.py](scripts/search_non_kthreads.py) | +| Search running deleted programs | [search_deleted_exe.py](scripts/search_deleted_exe.py) | +| Search fileless programs (memfd_create) | [search_memfd_create.py](scripts/search_memfd_create.py) | | Test script to check if alerting works | [test_alert.py](scripts/test_alert.py) | | Verify integrity of installed .deb packages | [verify_deb_packages.py](scripts/verify_deb_packages.py) | diff --git a/scripts/config/search_hidden_exe.py b/scripts/config/search_hidden_exe.py new file mode 100644 index 0000000..6a94916 --- /dev/null +++ b/scripts/config/search_hidden_exe.py @@ -0,0 +1,22 @@ +from typing import List + +# List of directories to search for hidden ELF files. Defaults to "/". +SEARCH_LOCATIONS = [] # type: List[str] + +# To prevent a timeout if this script is run regularly for monitoring, +# the search can be done in steps for each location given in SEARCH_LOCATIONS. +# Steps mean if you have location_A, the first execution of this script will +# process location_A non-recursively and terminates, +# the second execution will process location_A/subdir_A recursively and terminates, +# the third execution will process location_A/subdir_B recursively and terminates and so on. +# After all subdirectories where processed, the subsequent execution will begin again with location_A non-recursively. +SEARCH_IN_STEPS = False + +# List of directories to ignore. +HIDDEN_EXE_DIRECTORY_WHITELIST = [] # type: List[str] + +# List of hidden ELF files to ignore. +HIDDEN_EXE_FILE_WHITELIST = [] # type: List[str] + +# Is the script allowed to run or not? +ACTIVATED = True diff --git a/scripts/config/search_immutable_files.py b/scripts/config/search_immutable_files.py index b11ec0e..edf3d52 100755 --- a/scripts/config/search_immutable_files.py +++ b/scripts/config/search_immutable_files.py @@ -9,6 +9,7 @@ SEARCH_LOCATIONS = [] # type: List[str] # process location_A non-recursively and terminates, # the second execution will process location_A/subdir_A recursively and terminates, # the third execution will process location_A/subdir_B recursively and terminates and so on. +# After all subdirectories where processed, the subsequent execution will begin again with location_A non-recursively. SEARCH_IN_STEPS = False # List of directories to ignore. diff --git a/scripts/lib/alerts.py b/scripts/lib/alerts.py index 8e51ffb..9309125 100644 --- a/scripts/lib/alerts.py +++ b/scripts/lib/alerts.py @@ -28,7 +28,7 @@ def raise_alert_alertr(alertr_fifo: str, os.write(fd, json.dumps(msg_dict).encode("ascii")) os.close(fd) # Give AlertR sensor time to process the data. - # Otherwise a parsing error might occur on the FIFO sensor when multiple messages were mixed. + # Otherwise, a parsing error might occur on the FIFO sensor when multiple messages were mixed. time.sleep(2) break diff --git a/scripts/lib/state.py b/scripts/lib/state.py new file mode 100644 index 0000000..6ca8fe4 --- /dev/null +++ b/scripts/lib/state.py @@ -0,0 +1,46 @@ +import json +import os +import stat +from typing import Dict, Any + + +class StateException(Exception): + def __init__(self, msg: str): + self._msg = msg + + def __str__(self): + return self._msg + + +def load_state(state_dir: str) -> Dict[str, Any]: + state_file = os.path.join(state_dir, "state") + state_data = {} + if os.path.isfile(state_file): + data = None + try: + with open(state_file, 'rt') as fp: + data = fp.read() + if data is None: + raise StateException("Read state data is None.") + + state_data = json.loads(data) + + except Exception as e: + raise StateException("State data: '%s'; Exception: '%s'" % (str(data), str(e))) + + return state_data + + +def store_state(state_dir: str, state_data: Dict[str, Any]): + # Create state dir if it does not exist. + if not os.path.exists(state_dir): + os.makedirs(state_dir) + + state_file = os.path.join(state_dir, "state") + + with open(state_file, 'wt') as fp: + fp.write(json.dumps(state_data)) + + os.chmod(state_file, stat.S_IREAD | stat.S_IWRITE) + + diff --git a/scripts/lib/step_state.py b/scripts/lib/step_state.py new file mode 100644 index 0000000..c5418af --- /dev/null +++ b/scripts/lib/step_state.py @@ -0,0 +1,54 @@ +import os +import json +import stat +from typing import Dict, Any + +from .state import StateException +from .util_file import FileLocation + + +class StepLocation(FileLocation): + def __init__(self, location: str, search_recursive: bool): + super().__init__(location) + self._search_recursive = search_recursive + + @property + def search_recursive(self) -> bool: + return self._search_recursive + + +class StepStateException(StateException): + def __init__(self, msg: str): + super().__init__(msg) + + +def load_step_state(state_dir: str) -> Dict[str, Any]: + state_file = os.path.join(state_dir, "step_state") + state_data = {"next_step": 0} + if os.path.isfile(state_file): + data = None + try: + with open(state_file, 'rt') as fp: + data = fp.read() + if data is None: + raise StepStateException("Read state data is None.") + + state_data = json.loads(data) + + except Exception as e: + raise StepStateException("State data: '%s'; Exception: '%s'" % (str(data), str(e))) + + return state_data + + +def store_step_state(state_dir: str, state_data: Dict[str, Any]): + # Create state dir if it does not exist. + if not os.path.exists(state_dir): + os.makedirs(state_dir) + + state_file = os.path.join(state_dir, "step_state") + + with open(state_file, 'wt') as fp: + fp.write(json.dumps(state_data)) + + os.chmod(state_file, stat.S_IREAD | stat.S_IWRITE) diff --git a/scripts/lib/util.py b/scripts/lib/util.py new file mode 100644 index 0000000..0835d47 --- /dev/null +++ b/scripts/lib/util.py @@ -0,0 +1,85 @@ +import os +import socket + +from .alerts import raise_alert_alertr, raise_alert_mail + +try: + from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR, STATE_DIR + +except: + ALERTR_FIFO = None + FROM_ADDR = None + TO_ADDR = None + + +def output_error(file_name: str, msg: str): + + base_name = os.path.basename(file_name) + + # Decide where to output results. + print_output = False + if ALERTR_FIFO is None and FROM_ADDR is None and TO_ADDR is None: + print_output = True + + if print_output: + message = "#" * 80 + message += "\nError in '%s':\n%s" % (base_name, msg) + print(message) + + else: + hostname = socket.gethostname() + message = "Error in '%s' on host '%s':\n%s" \ + % (base_name, hostname, msg) + + if ALERTR_FIFO: + optional_data = dict() + optional_data["error"] = True + optional_data["script"] = base_name + optional_data["message"] = message + + raise_alert_alertr(ALERTR_FIFO, + optional_data) + + if FROM_ADDR is not None and TO_ADDR is not None: + mail_subject = "[Security] Error in '%s' on host '%s'" % (base_name, socket.gethostname()) + raise_alert_mail(FROM_ADDR, + TO_ADDR, + mail_subject, + message) + + +def output_finding(file_name: str, msg: str): + + base_name = os.path.basename(file_name) + + # Decide where to output results. + print_output = False + if ALERTR_FIFO is None and FROM_ADDR is None and TO_ADDR is None: + print_output = True + + if print_output: + message = "#" * 80 + message += "\nFinding in '%s':\n%s" % (base_name, msg) + + print(message) + + else: + hostname = socket.gethostname() + message = "Finding in '%s' on host '%s':\n%s" \ + % (base_name, hostname, msg) + + if ALERTR_FIFO: + optional_data = dict() + optional_data["finding"] = True + optional_data["script"] = base_name + optional_data["message"] = message + + raise_alert_alertr(ALERTR_FIFO, + optional_data) + + if FROM_ADDR is not None and TO_ADDR is not None: + mail_subject = "[Security] Finding in '%s' on host '%s'" % (base_name, socket.gethostname()) + raise_alert_mail(FROM_ADDR, + TO_ADDR, + mail_subject, + message) diff --git a/scripts/lib/util_file.py b/scripts/lib/util_file.py new file mode 100644 index 0000000..7e0b4b3 --- /dev/null +++ b/scripts/lib/util_file.py @@ -0,0 +1,99 @@ +import os + +from typing import List + + +class FileLocation: + """ + Class that stores a location of a file or directory. + """ + + def __init__(self, location: str): + self._location = location + + @property + def location(self) -> str: + return self._location + + +def apply_directory_whitelist(dir_whitelist: List[FileLocation], files: List[FileLocation]) -> List[FileLocation]: + """ + Applies a whitelist containing directories to the given file list. The whitelist contains directories + that are considered whitelisted. If the whitelist contains the directory "/home" then all files + stored in "/home" are removed from the result (e.g., "/home/user/test.txt"). + + :param dir_whitelist: + :param files: + :return: list of files that do not match whitelist + """ + if not dir_whitelist: + return files + + # Extract the components of the whitelist paths (pre-process it to reduces processing steps). + whitelist_path_components_list = [] + for whitelist_entry in dir_whitelist: + whitelist_path = os.path.normpath(whitelist_entry.location) + whitelist_path_components = [] + while True: + whitelist_path, component = os.path.split(whitelist_path) + if not component: + break + whitelist_path_components.insert(0, component) + whitelist_path_components_list.append(whitelist_path_components) + + new_files = [] + for file in files: + is_whitelisted = False + + # Extract the components of the path to the file. + path = os.path.dirname(os.path.normpath(file.location)) + path_components = [] + while True: + path, component = os.path.split(path) + if not component: + break + path_components.insert(0, component) + + for whitelist_path_components in whitelist_path_components_list: + + # Skip case such as "whitelist: /usr/local/bin" and "file path: /usr" + if len(whitelist_path_components) > len(path_components): + continue + + # NOTE: this check also works if "/" is whitelisted, since the whitelist components are empty and + # thus the file is counted as whitelisted. + is_whitelisted = True + for i in range(len(whitelist_path_components)): + if whitelist_path_components[i] != path_components[i]: + is_whitelisted = False + if is_whitelisted: + break + + if not is_whitelisted: + new_files.append(file) + return new_files + + +def apply_file_whitelist(file_whitelist: List[FileLocation], files: List[FileLocation]) -> List[FileLocation]: + """ + Applies a whitelist containing files to the given file list. The whitelist contains files + that are considered whitelisted. If the whitelist contains the file "/home/user/test.txt" than all occurrences of + this file in the file list will be removed. + + :param file_whitelist: + :param files: + :return: list of files that do not match whitelist + """ + if not file_whitelist: + return files + + new_files = [] + for file in files: + is_whitelisted = False + for whitelist_file in file_whitelist: + if os.path.samefile(file.location, whitelist_file.location): + is_whitelisted = True + break + if not is_whitelisted: + new_files.append(file) + return new_files diff --git a/scripts/monitor_hosts_file.py b/scripts/monitor_hosts_file.py index 969135f..3887516 100755 --- a/scripts/monitor_hosts_file.py +++ b/scripts/monitor_hosts_file.py @@ -16,14 +16,12 @@ None """ import os -import json -import socket -import stat from typing import Dict, Set -from lib.alerts import raise_alert_alertr, raise_alert_mail +from lib.state import load_state, store_state +from lib.util import output_error, output_finding -# Read configuration and library functions. +# Read configuration. try: from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR, STATE_DIR from config.monitor_hosts_file import ACTIVATED @@ -35,8 +33,6 @@ except: ACTIVATED = True STATE_DIR = os.path.join("/tmp", os.path.basename(__file__)) -MAIL_SUBJECT = "[Security] Monitoring /etc/hosts on host '%s'" % socket.gethostname() - class MonitorHostsException(Exception): def __init__(self, msg: str): @@ -76,77 +72,6 @@ def _get_hosts() -> Dict[str, Set[str]]: return hosts_data -def _load_hosts_data() -> Dict[str, Set[str]]: - state_file = os.path.join(STATE_DIR, "state") - hosts_data = {} - if os.path.isfile(state_file): - data = None - try: - with open(state_file, 'rt') as fp: - data = fp.read() - if data is None: - raise MonitorHostsException("Read state data is None.") - - temp = json.loads(data) - - # Convert list to set. - for k,v in temp.items(): - hosts_data[k] = set(v) - - except Exception as e: - raise MonitorHostsException("State data: '%s'; Exception: '%s'" % (str(data), str(e))) - - return hosts_data - - -def _output_error(msg: str): - - # Decide where to output results. - print_output = False - if ALERTR_FIFO is None and FROM_ADDR is None and TO_ADDR is None: - print_output = True - - if print_output: - print(msg) - - else: - hostname = socket.gethostname() - message = "Error monitoring /etc/hosts on host '%s': %s" \ - % (hostname, msg) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["error"] = True - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) - - -def _store_hosts_data(hosts_data: Dict[str, Set[str]]): - # Create state dir if it does not exist. - if not os.path.exists(STATE_DIR): - os.makedirs(STATE_DIR) - - state_file = os.path.join(STATE_DIR, "state") - - # Convert set to list. - temp = {} - for k, v in hosts_data.items(): - temp[k] = list(v) - - with open(state_file, 'wt') as fp: - fp.write(json.dumps(temp)) - - os.chmod(state_file, stat.S_IREAD | stat.S_IWRITE) - - def monitor_hosts(): # Decide where to output results. @@ -161,10 +86,14 @@ def monitor_hosts(): stored_hosts_data = {} try: - stored_hosts_data =_load_hosts_data() + state_data = load_state(STATE_DIR) + + # Convert list to set. + for k, v in state_data.items(): + stored_hosts_data[k] = set(v) except Exception as e: - _output_error(str(e)) + output_error(__file__, str(e)) return curr_hosts_data = {} @@ -172,7 +101,7 @@ def monitor_hosts(): curr_hosts_data = _get_hosts() except Exception as e: - _output_error(str(e)) + output_error(__file__, str(e)) return # Compare stored data with current one. @@ -180,121 +109,49 @@ def monitor_hosts(): # Extract current entry belonging to the same ip. if stored_entry_ip not in curr_hosts_data.keys(): - hostname = socket.gethostname() - message = "Host name for IP '%s' was deleted on host '%s'." \ - % (stored_entry_ip, hostname) + message = "Host name for IP '%s' was deleted." % stored_entry_ip - if print_output: - print(message) - print("#" * 80) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["ip"] = stored_entry_ip - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + output_finding(__file__, message) continue # Check host entry was removed. for host in stored_hosts_data[stored_entry_ip]: if host not in curr_hosts_data[stored_entry_ip]: - hostname = socket.gethostname() - message = "Host name entry for IP '%s' was removed on host '%s'.\n\n" % (stored_entry_ip, hostname) + message = "Host name entry for IP '%s' was removed.\n\n" % stored_entry_ip message += "Entry: %s" % host - if print_output: - print(message) - print("#" * 80) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["ip"] = stored_entry_ip - optional_data["host_entry"] = host - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + output_finding(__file__, message) # Check host entry was added. for host in curr_hosts_data[stored_entry_ip]: if host not in stored_hosts_data[stored_entry_ip]: - hostname = socket.gethostname() - message = "Host name entry for IP '%s' was added on host '%s'.\n\n" % (stored_entry_ip, hostname) + message = "Host name entry for IP '%s' was added.\n\n" % stored_entry_ip message += "Entry: %s" % host - if print_output: - print(message) - print("#" * 80) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["ip"] = stored_entry_ip - optional_data["host_entry"] = host - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + output_finding(__file__, message) # Check new data was added. for curr_entry_ip in curr_hosts_data.keys(): if curr_entry_ip not in stored_hosts_data.keys(): - hostname = socket.gethostname() - message = "New host name was added for IP '%s' on host '%s'.\n\n" \ - % (curr_entry_ip, hostname) + message = "New host name was added for IP '%s'.\n\n" % curr_entry_ip message += "Entries:\n" for host in curr_hosts_data[curr_entry_ip]: message += host message += "\n" - if print_output: - print(message) - print("#" * 80) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["ip"] = curr_entry_ip - optional_data["hosts"] = list(curr_hosts_data[curr_entry_ip]) - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + output_finding(__file__, message) try: - _store_hosts_data(curr_hosts_data) + # Convert set to list. + state_data = {} + for k, v in curr_hosts_data.items(): + state_data[k] = list(v) + + store_state(STATE_DIR, state_data) except Exception as e: - _output_error(str(e)) + output_error(__file__, str(e)) if __name__ == '__main__': diff --git a/scripts/monitor_ld_preload.py b/scripts/monitor_ld_preload.py index 6f51ee7..d3c860d 100755 --- a/scripts/monitor_ld_preload.py +++ b/scripts/monitor_ld_preload.py @@ -16,14 +16,12 @@ None """ import os -import json -import socket -import stat from typing import Set -from lib.alerts import raise_alert_alertr, raise_alert_mail +from lib.state import load_state, store_state +from lib.util import output_error, output_finding -# Read configuration and library functions. +# Read configuration. try: from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR, STATE_DIR from config.monitor_ld_preload import ACTIVATED @@ -35,16 +33,6 @@ except: ACTIVATED = True STATE_DIR = os.path.join("/tmp", os.path.basename(__file__)) -MAIL_SUBJECT = "[Security] Monitoring /etc/ld.so.preload on host '%s'" % socket.gethostname() - - -class MonitorLdPreloadException(Exception): - def __init__(self, msg: str): - self._msg = msg - - def __str__(self): - return self._msg - def _get_ld_preload() -> Set[str]: path = "/etc/ld.so.preload" @@ -61,72 +49,6 @@ def _get_ld_preload() -> Set[str]: return ld_data -def _load_ld_preload_data() -> Set[str]: - state_file = os.path.join(STATE_DIR, "state") - ld_data = set() - if os.path.isfile(state_file): - data = None - try: - with open(state_file, 'rt') as fp: - data = fp.read() - if data is None: - raise MonitorLdPreloadException("Read state data is None.") - - temp = json.loads(data) - ld_data = set(temp) - - except Exception as e: - raise MonitorLdPreloadException("State data: '%s'; Exception: '%s'" % (str(data), str(e))) - - return ld_data - - -def _output_error(msg: str): - - # Decide where to output results. - print_output = False - if ALERTR_FIFO is None and FROM_ADDR is None and TO_ADDR is None: - print_output = True - - if print_output: - print(msg) - - else: - hostname = socket.gethostname() - message = "Error monitoring /etc/ld.so.preload on host '%s': %s" \ - % (hostname, msg) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["error"] = True - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) - - -def _store_ld_preload_data(ld_data: Set[str]): - # Create state dir if it does not exist. - if not os.path.exists(STATE_DIR): - os.makedirs(STATE_DIR) - - state_file = os.path.join(STATE_DIR, "state") - - # Convert set to list. - temp = list(ld_data) - - with open(state_file, 'wt') as fp: - fp.write(json.dumps(temp)) - - os.chmod(state_file, stat.S_IREAD | stat.S_IWRITE) - - def monitor_ld_preload(): # Decide where to output results. @@ -141,10 +63,14 @@ def monitor_ld_preload(): stored_ld_data = set() try: - stored_ld_data =_load_ld_preload_data() + state_data = load_state(STATE_DIR) + + # Convert list to set. + if "ld_data" in state_data.keys(): + stored_ld_data = set(state_data["ld_data"]) except Exception as e: - _output_error(str(e)) + output_error(__file__, str(e)) return curr_ld_data = set() @@ -152,66 +78,33 @@ def monitor_ld_preload(): curr_ld_data = _get_ld_preload() except Exception as e: - _output_error(str(e)) + output_error(__file__, str(e)) return # Compare stored data with current one. for stored_entry in stored_ld_data: if stored_entry not in curr_ld_data: - hostname = socket.gethostname() - message = "LD_PRELOAD entry '%s' was deleted on host '%s'." \ - % (stored_entry, hostname) + message = "LD_PRELOAD entry '%s' was deleted." % stored_entry - if print_output: - print(message) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["entry"] = stored_entry - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + output_finding(__file__, message) continue # Check new data was added. for curr_entry in curr_ld_data: if curr_entry not in stored_ld_data: - hostname = socket.gethostname() - message = "LD_PRELOAD entry '%s' was added on host '%s'.\n\n" \ - % (curr_entry, hostname) + message = "LD_PRELOAD entry '%s' was added." % curr_entry - if print_output: - print(message) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["entry"] = curr_entry - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + output_finding(__file__, message) try: - _store_ld_preload_data(curr_ld_data) + # Convert set to list. + state_data = {"ld_data": list(curr_ld_data)} + + store_state(STATE_DIR, state_data) except Exception as e: - _output_error(str(e)) + output_error(__file__, str(e)) if __name__ == '__main__': diff --git a/scripts/monitor_passwd.py b/scripts/monitor_passwd.py index ea18470..41f6996 100755 --- a/scripts/monitor_passwd.py +++ b/scripts/monitor_passwd.py @@ -16,14 +16,12 @@ None """ import os -import json -import socket -import stat from typing import Dict -from lib.alerts import raise_alert_alertr, raise_alert_mail +from lib.state import load_state, store_state +from lib.util import output_error, output_finding -# Read configuration and library functions. +# Read configuration. try: from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR, STATE_DIR from config.monitor_passwd import ACTIVATED @@ -35,16 +33,6 @@ except: ACTIVATED = True STATE_DIR = os.path.join("/tmp", os.path.basename(__file__)) -MAIL_SUBJECT = "[Security] Monitoring /etc/passwd on host '%s'" % socket.gethostname() - - -class MonitorPasswdException(Exception): - def __init__(self, msg: str): - self._msg = msg - - def __str__(self): - return self._msg - def _get_passwd() -> Dict[str, str]: @@ -64,69 +52,7 @@ def _get_passwd() -> Dict[str, str]: return passwd_data -def _load_passwd_data() -> Dict[str, str]: - state_file = os.path.join(STATE_DIR, "state") - passwd_data = {} - if os.path.isfile(state_file): - data = None - try: - with open(state_file, 'rt') as fp: - data = fp.read() - if data is None: - raise MonitorPasswdException("Read state data is None.") - - passwd_data = json.loads(data) - - except Exception as e: - raise MonitorPasswdException("State data: '%s'; Exception: '%s'" % (str(data), str(e))) - - return passwd_data - - -def _output_error(msg: str): - - # Decide where to output results. - print_output = False - if ALERTR_FIFO is None and FROM_ADDR is None and TO_ADDR is None: - print_output = True - - if print_output: - print(msg) - - else: - hostname = socket.gethostname() - message = "Error monitoring /etc/passwd on host '%s': %s" \ - % (hostname, msg) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["error"] = True - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) - - -def _store_passwd_data(passwd_data: Dict[str, str]): - # Create state dir if it does not exist. - if not os.path.exists(STATE_DIR): - os.makedirs(STATE_DIR) - - state_file = os.path.join(STATE_DIR, "state") - - with open(state_file, 'wt') as fp: - fp.write(json.dumps(passwd_data)) - - os.chmod(state_file, stat.S_IREAD | stat.S_IWRITE) - - -def monitor_hosts(): +def monitor_passwd(): # Decide where to output results. print_output = False @@ -140,10 +66,10 @@ def monitor_hosts(): stored_passwd_data = {} try: - stored_passwd_data =_load_passwd_data() + stored_passwd_data = load_state(STATE_DIR) except Exception as e: - _output_error(str(e)) + output_error(__file__, str(e)) return curr_passwd_data = {} @@ -151,7 +77,7 @@ def monitor_hosts(): curr_passwd_data = _get_passwd() except Exception as e: - _output_error(str(e)) + output_error(__file__, str(e)) return # Compare stored data with current one. @@ -159,93 +85,34 @@ def monitor_hosts(): # Extract current entry belonging to the same user. if stored_entry_user not in curr_passwd_data.keys(): - hostname = socket.gethostname() - message = "User '%s' was deleted on host '%s'." \ - % (stored_entry_user, hostname) + message = "User '%s' was deleted." % stored_entry_user - if print_output: - print(message) - print("#" * 80) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["user"] = stored_entry_user - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + output_finding(__file__, message) continue # Check entry was modified. if stored_passwd_data[stored_entry_user] != curr_passwd_data[stored_entry_user]: - hostname = socket.gethostname() - message = "Passwd entry for user '%s' was modified on host '%s'.\n\n" % (stored_entry_user, hostname) + message = "Passwd entry for user '%s' was modified.\n\n" % stored_entry_user message += "Old entry: %s\n" % stored_passwd_data[stored_entry_user] message += "New entry: %s" % curr_passwd_data[stored_entry_user] - if print_output: - print(message) - print("#" * 80) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["user"] = stored_entry_user - optional_data["old_entry"] = stored_passwd_data[stored_entry_user] - optional_data["new_entry"] = curr_passwd_data[stored_entry_user] - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + output_finding(__file__, message) # Check new data was added. for curr_entry_user in curr_passwd_data.keys(): if curr_entry_user not in stored_passwd_data.keys(): - hostname = socket.gethostname() - message = "User '%s' was added on host '%s'.\n\n" \ - % (curr_entry_user, hostname) + message = "User '%s' was added.\n\n" % curr_entry_user message += "Entry: %s" % curr_passwd_data[curr_entry_user] - if print_output: - print(message) - print("#"*80) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["user"] = curr_entry_user - optional_data["entry"] = curr_passwd_data[curr_entry_user] - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + output_finding(__file__, message) try: - _store_passwd_data(curr_passwd_data) + store_state(STATE_DIR, curr_passwd_data) except Exception as e: - _output_error(str(e)) + output_error(__file__, str(e)) if __name__ == '__main__': - monitor_hosts() + monitor_passwd() diff --git a/scripts/monitor_ssh_authorized_keys.py b/scripts/monitor_ssh_authorized_keys.py index 3ca8898..acb6fbd 100755 --- a/scripts/monitor_ssh_authorized_keys.py +++ b/scripts/monitor_ssh_authorized_keys.py @@ -16,14 +16,13 @@ None """ import os -import json import stat -import socket from typing import List, Tuple, Dict, Any -from lib.alerts import raise_alert_alertr, raise_alert_mail +from lib.state import load_state, store_state +from lib.util import output_error, output_finding -# Read configuration and library functions. +# Read configuration. try: from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR, STATE_DIR from config.monitor_ssh_authorized_keys import ACTIVATED @@ -35,8 +34,6 @@ except: ACTIVATED = True STATE_DIR = os.path.join("/tmp", os.path.basename(__file__)) -MAIL_SUBJECT = "[Security] Monitoring SSH authorized_keys on host '%s'" % socket.gethostname() - class MonitorSSHException(Exception): def __init__(self, msg: str): @@ -76,55 +73,6 @@ def _get_system_ssh_data() -> List[Dict[str, Any]]: return ssh_data -def _load_ssh_data() -> List[Dict[str, Any]]: - state_file = os.path.join(STATE_DIR, "state") - ssh_data = [] - if os.path.isfile(state_file): - data = None - try: - with open(state_file, 'rt') as fp: - data = fp.read() - if data is None: - raise MonitorSSHException("Read state data is None.") - - ssh_data = json.loads(data) - - except Exception as e: - raise MonitorSSHException("State data: '%s'; Exception: '%s'" % (str(data), str(e))) - - return ssh_data - - -def _output_error(msg: str): - - # Decide where to output results. - print_output = False - if ALERTR_FIFO is None and FROM_ADDR is None and TO_ADDR is None: - print_output = True - - if print_output: - print(msg) - - else: - hostname = socket.gethostname() - message = "Error monitoring SSH authorized_keys on host '%s': %s" \ - % (hostname, msg) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["error"] = True - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) - - def _parse_authorized_keys_file(authorized_keys_file: str) -> List[str]: entries = set() try: @@ -138,18 +86,6 @@ def _parse_authorized_keys_file(authorized_keys_file: str) -> List[str]: return list(entries) -def _store_ssh_data(ssh_data: List[Dict[str, Any]]): - # Create state dir if it does not exist. - if not os.path.exists(STATE_DIR): - os.makedirs(STATE_DIR) - - state_file = os.path.join(STATE_DIR, "state") - with open(state_file, 'wt') as fp: - fp.write(json.dumps(ssh_data)) - - os.chmod(state_file, stat.S_IREAD | stat.S_IWRITE) - - def monitor_ssh_authorized_keys(): # Decide where to output results. @@ -165,11 +101,13 @@ def monitor_ssh_authorized_keys(): stored_ssh_data = [] curr_ssh_data = [] try: - stored_ssh_data = _load_ssh_data() + state_data = load_state(STATE_DIR) + if "ssh_data" in state_data.keys(): + stored_ssh_data = state_data["ssh_data"] curr_ssh_data = _get_system_ssh_data() except Exception as e: - _output_error(str(e)) + output_error(__file__, str(e)) return # Check if any authorized_keys file is world writable. @@ -177,28 +115,9 @@ def monitor_ssh_authorized_keys(): authorized_keys_file = curr_entry["authorized_keys_file"] file_stat = os.stat(authorized_keys_file) if file_stat.st_mode & stat.S_IWOTH: - hostname = socket.gethostname() - message = "SSH authorized_keys file for user '%s' is world writable on host '%s'." \ - % (curr_entry["user"], hostname) + message = "SSH authorized_keys file for user '%s' is world writable." % curr_entry["user"] - if print_output: - print(message) - print("#" * 80) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["username"] = curr_entry["user"] - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + output_finding(__file__, message) # Compare stored data with current one. for stored_entry in stored_ssh_data: @@ -210,114 +129,35 @@ def monitor_ssh_authorized_keys(): curr_user_entry = curr_entry break if curr_user_entry is None: - hostname = socket.gethostname() - message = "SSH authorized_keys file for user '%s' was deleted on host '%s'." \ - % (stored_entry["user"], hostname) - - if print_output: - print(message) - print("#" * 80) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["username"] = stored_entry["user"] - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + message = "SSH authorized_keys file for user '%s' was deleted." % stored_entry["user"] + output_finding(__file__, message) continue # Check authorized_keys path has changed. if stored_entry["authorized_keys_file"] != curr_user_entry["authorized_keys_file"]: - hostname = socket.gethostname() - message = "SSH authorized_keys location for user '%s' changed from '%s' to '%s' on host '%s'." \ + message = "SSH authorized_keys location for user '%s' changed from '%s' to '%s'." \ % (stored_entry["user"], stored_entry["authorized_keys_file"], - curr_user_entry["authorized_keys_file"], - hostname) + curr_user_entry["authorized_keys_file"]) - if print_output: - print(message) - print("#" * 80) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["username"] = stored_entry["user"] - optional_data["from"] = stored_entry["authorized_keys_file"] - optional_data["to"] = curr_user_entry["authorized_keys_file"] - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + output_finding(__file__, message) # Check authorized_key was removed. for authorized_key in stored_entry["authorized_keys_entries"]: if authorized_key not in curr_user_entry["authorized_keys_entries"]: - hostname = socket.gethostname() - message = "SSH authorized_keys entry was removed on host '%s'.\n\n" % hostname + message = "SSH authorized_keys entry was removed.\n\n" message += "Entry: %s" % authorized_key - if print_output: - print(message) - print("#" * 80) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["username"] = stored_entry["user"] - optional_data["authorized_keys_entry"] = authorized_key - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + output_finding(__file__, message) # Check authorized_key was added. for authorized_key in curr_user_entry["authorized_keys_entries"]: if authorized_key not in stored_entry["authorized_keys_entries"]: - hostname = socket.gethostname() - message = "SSH authorized_keys entry was added on host '%s'.\n\n" % hostname + message = "SSH authorized_keys entry was added.\n\n" message += "Entry: %s" % authorized_key - if print_output: - print(message) - print("#" * 80) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["username"] = stored_entry["user"] - optional_data["authorized_keys_entry"] = authorized_key - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + output_finding(__file__, message) for curr_entry in curr_ssh_data: found = False @@ -326,39 +166,20 @@ def monitor_ssh_authorized_keys(): found = True break if not found: - hostname = socket.gethostname() - message = "New authorized_keys file was added for user '%s' on host '%s'.\n\n" \ - % (curr_entry["user"], hostname) + message = "New authorized_keys file was added for user '%s'.\n\n" % curr_entry["user"] message += "Entries:\n" for authorized_key in curr_entry["authorized_keys_entries"]: message += authorized_key message += "\n" - if print_output: - print(message) - print("#" * 80) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["username"] = curr_entry["user"] - optional_data["authorized_keys_entries"] = curr_entry["authorized_keys_entries"] - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + output_finding(__file__, message) try: - _store_ssh_data(curr_ssh_data) + state_data["ssh_data"] = curr_ssh_data + store_state(STATE_DIR, state_data) except Exception as e: - _output_error(str(e)) + output_error(__file__, str(e)) if __name__ == '__main__': diff --git a/scripts/search_deleted_exe.py b/scripts/search_deleted_exe.py index 0e7753d..49b4830 100755 --- a/scripts/search_deleted_exe.py +++ b/scripts/search_deleted_exe.py @@ -16,13 +16,13 @@ None """ import os -import socket -# Read configuration and library functions. +from lib.util import output_finding + +# Read configuration. try: from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR from config.search_deleted_exe import ACTIVATED - from lib.alerts import raise_alert_alertr, raise_alert_mail except: ALERTR_FIFO = None FROM_ADDR = None @@ -43,7 +43,7 @@ def search_deleted_exe_files(): return # Get all suspicious ELF files. - fd = os.popen("ls -laR /proc/*/exe 2> /dev/null | grep -v memfd: | grep \(deleted\)") + fd = os.popen("ls -laR /proc/*/exe 2> /dev/null | grep -v memfd: | grep \\(deleted\\)") suspicious_exe_raw = fd.read().strip() fd.close() @@ -51,38 +51,11 @@ def search_deleted_exe_files(): if suspicious_exe_raw.strip(): suspicious_exes.extend(suspicious_exe_raw.strip().split("\n")) - for suspicious_exe in suspicious_exes: + if suspicious_exes: + message = "Deleted executable file(s) found:\n\n" + message += "\n".join(suspicious_exes) - if print_output: - print("SUSPICIOUS") - print(suspicious_exe) - print("") - - else: - if ALERTR_FIFO is not None: - - hostname = socket.gethostname() - optional_data = dict() - optional_data["suspicious_exe"] = suspicious_exe - optional_data["hostname"] = hostname - message = "Deleted executable file on host '%s' found.\n\n" % hostname - message += suspicious_exe - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - - hostname = socket.gethostname() - subject = "[Security] Deleted executable file on '%s'" % hostname - message = "Deleted executable file on host '%s' found.\n\n" % hostname - message += suspicious_exe - - raise_alert_mail(FROM_ADDR, - TO_ADDR, - subject, - message) + output_finding(__file__, message) if __name__ == '__main__': diff --git a/scripts/search_dev_shm.py b/scripts/search_dev_shm.py index 060c18e..eea952d 100755 --- a/scripts/search_dev_shm.py +++ b/scripts/search_dev_shm.py @@ -21,13 +21,13 @@ https://twitter.com/CraigHRowland/status/1269196509079166976 """ import os -import socket -# Read configuration and library functions. +from lib.util import output_finding + +# Read configuration. try: from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR from config.search_dev_shm import ACTIVATED - from lib.alerts import raise_alert_alertr, raise_alert_mail except: ALERTR_FIFO = None FROM_ADDR = None @@ -63,38 +63,11 @@ def search_suspicious_files(): if script_raw.strip(): suspicious_files.extend(script_raw.strip().split("\n")) - for suspicious_file in suspicious_files: + if suspicious_files: + message = "File(s) in /dev/shm suspicious:\n\n" + message += "\n".join(suspicious_files) - if print_output: - print("SUSPICIOUS") - print(suspicious_file) - print("") - - else: - if ALERTR_FIFO is not None: - - hostname = socket.gethostname() - optional_data = dict() - optional_data["suspicious_file"] = suspicious_file - optional_data["hostname"] = hostname - message = "File in /dev/shm on host '%s' suspicious.\n\n" % hostname - message += suspicious_file - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - - hostname = socket.gethostname() - subject = "[Security] Suspicious file found on '%s'" % hostname - message = "File in /dev/shm on host '%s' suspicious.\n\n" % hostname - message += suspicious_file - - raise_alert_mail(FROM_ADDR, - TO_ADDR, - subject, - message) + output_finding(__file__, message) if __name__ == '__main__': diff --git a/scripts/search_hidden_exe.py b/scripts/search_hidden_exe.py new file mode 100755 index 0000000..0f10dfc --- /dev/null +++ b/scripts/search_hidden_exe.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 + +# written by sqall +# twitter: https://twitter.com/sqall01 +# blog: https://h4des.org +# github: https://github.com/sqall01 +# +# Licensed under the MIT License. + +""" +Short summary: +Searches for hidden ELF files in the filesystem. Usually, ELF binaries are not hidden in a Linux environment. + +Requirements: +None +""" + +import os +from typing import List + +from lib.step_state import StepLocation, load_step_state, store_step_state +from lib.util import output_error, output_finding +from lib.util_file import FileLocation, apply_directory_whitelist, apply_file_whitelist + +# Read configuration. +try: + from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR, STATE_DIR + from config.search_hidden_exe import ACTIVATED, SEARCH_IN_STEPS, SEARCH_LOCATIONS, \ + HIDDEN_EXE_DIRECTORY_WHITELIST, HIDDEN_EXE_FILE_WHITELIST + + STATE_DIR = os.path.join(os.path.dirname(__file__), STATE_DIR, os.path.basename(__file__)) +except: + ALERTR_FIFO = None + FROM_ADDR = None + TO_ADDR = None + ACTIVATED = True + SEARCH_IN_STEPS = False + SEARCH_LOCATIONS = ["/"] + HIDDEN_EXE_DIRECTORY_WHITELIST = [] + HIDDEN_EXE_FILE_WHITELIST = [] + STATE_DIR = os.path.join("/tmp", os.path.basename(__file__)) + + +def search_hidden_exe_files(): + # Decide where to output results. + print_output = False + if ALERTR_FIFO is None and FROM_ADDR is None and TO_ADDR is None: + print_output = True + + if not ACTIVATED: + if print_output: + print("Module deactivated.") + return + + step_state_data = {} + try: + step_state_data = load_step_state(STATE_DIR) + + except Exception as e: + output_error(__file__, str(e)) + return + + # Reset step if we do not search in steps but everything. + if not SEARCH_IN_STEPS: + step_state_data["next_step"] = 0 + + if not SEARCH_LOCATIONS: + SEARCH_LOCATIONS.append("/") + + # Gather all search locations. + search_locations = [] # type: List[StepLocation] + # If SEARCH_IN_STEPS is active, build a list of directories to search in + if SEARCH_IN_STEPS: + for search_location in SEARCH_LOCATIONS: + + # Add parent directory as non-recursive search location in order to search in it without going deeper. + search_locations.append(StepLocation(search_location, False)) + + # Add all containing subdirectories as recursive search locations. + elements = os.listdir(search_location) + elements.sort() + for element in elements: + path = os.path.join(search_location, element) + if os.path.isdir(path): + search_locations.append(StepLocation(path, True)) + + # If we do not search in separated steps, just add each directory as a recursive search location. + else: + for search_location in SEARCH_LOCATIONS: + search_locations.append(StepLocation(search_location, True)) + + # Reset index if it is outside the search locations. + if step_state_data["next_step"] >= len(search_locations): + step_state_data["next_step"] = 0 + + while True: + search_location_obj = search_locations[step_state_data["next_step"]] + + # Get all hidden ELF files. + if search_location_obj.search_recursive: + fd = os.popen("find %s -type f -iname \".*\" -exec echo -n \"{} \" \\; -exec head -c 4 {} \\; -exec echo \"\" \\; | grep -P \"\\x7fELF\"" + % search_location_obj.location) + + else: + fd = os.popen("find %s -maxdepth 1 -type f -iname \".*\" -exec echo -n \"{} \" \\; -exec head -c 4 {} \\; -exec echo \"\" \\; | grep -P \"\\x7fELF\"" + % search_location_obj.location) + output_raw = fd.read().strip() + fd.close() + + if output_raw != "": + + hidden_files = [] # type: List[FileLocation] + output_list = output_raw.split("\n") + for output_entry in output_list: + file_location = output_entry[:-5] + hidden_files.append(FileLocation(file_location)) + + dir_whitelist = [FileLocation(x) for x in HIDDEN_EXE_DIRECTORY_WHITELIST] + file_whitelist = [FileLocation(x) for x in HIDDEN_EXE_FILE_WHITELIST] + + hidden_files = apply_directory_whitelist(dir_whitelist, hidden_files) + hidden_files = apply_file_whitelist(file_whitelist, hidden_files) + + if hidden_files: + message = "Hidden ELF file(s) found:\n\n" + message += "\n".join(["File: %s" % x.location for x in hidden_files]) + + output_finding(__file__, message) + + step_state_data["next_step"] += 1 + + # Stop search if we are finished. + if SEARCH_IN_STEPS or step_state_data["next_step"] >= len(search_locations): + break + + try: + store_step_state(STATE_DIR, step_state_data) + + except Exception as e: + output_error(__file__, str(e)) + + +if __name__ == '__main__': + search_hidden_exe_files() diff --git a/scripts/search_immutable_files.py b/scripts/search_immutable_files.py index ded6bdd..a8d046e 100755 --- a/scripts/search_immutable_files.py +++ b/scripts/search_immutable_files.py @@ -16,14 +16,13 @@ None """ import os -import json -import socket -import stat -from typing import Dict, Any, List, Tuple +from typing import List, cast -from lib.alerts import raise_alert_alertr, raise_alert_mail +from lib.step_state import StepLocation, load_step_state, store_step_state +from lib.util import output_error, output_finding +from lib.util_file import FileLocation, apply_directory_whitelist, apply_file_whitelist -# Read configuration and library functions. +# Read configuration. try: from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR, STATE_DIR from config.search_immutable_files import ACTIVATED, SEARCH_IN_STEPS, SEARCH_LOCATIONS, \ @@ -41,141 +40,15 @@ except: IMMUTABLE_FILE_WHITELIST = [] STATE_DIR = os.path.join("/tmp", os.path.basename(__file__)) -MAIL_SUBJECT = "[Security] Searching immutable files on host '%s'" % socket.gethostname() +class ImmutableFile(FileLocation): + def __init__(self, location: str, attribute: str): + super().__init__(location) + self._attribute = attribute -class SearchImmutableException(Exception): - def __init__(self, msg: str): - self._msg = msg - - def __str__(self): - return self._msg - - -def _load_state() -> Dict[str, Any]: - state_file = os.path.join(STATE_DIR, "state") - state_data = {"next_step": 0} - if os.path.isfile(state_file): - data = None - try: - with open(state_file, 'rt') as fp: - data = fp.read() - if data is None: - raise SearchImmutableException("Read state data is None.") - - state_data = json.loads(data) - - except Exception as e: - raise SearchImmutableException("State data: '%s'; Exception: '%s'" % (str(data), str(e))) - - return state_data - - -def _output_error(msg: str): - # Decide where to output results. - print_output = False - if ALERTR_FIFO is None and FROM_ADDR is None and TO_ADDR is None: - print_output = True - - if print_output: - print(msg) - - else: - hostname = socket.gethostname() - message = "Error searching immutable files on host on host '%s': %s" \ - % (hostname, msg) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["error"] = True - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) - - -def _process_directory_whitelist(immutable_files: List[Tuple[str, str]]) -> List[Tuple[str, str]]: - if not IMMUTABLE_DIRECTORY_WHITELIST: - return immutable_files - - # Extract the components of the whitelist paths (pre-process it to reduces processing steps). - whitelist_path_components_list = [] - for whitelist_entry in IMMUTABLE_DIRECTORY_WHITELIST: - whitelist_path = os.path.normpath(whitelist_entry) - whitelist_path_components = [] - while True: - whitelist_path, component = os.path.split(whitelist_path) - if not component: - break - whitelist_path_components.insert(0, component) - whitelist_path_components_list.append(whitelist_path_components) - - new_immutable_files = [] - for immutable_file in immutable_files: - is_whitelisted = False - - # Extract the components of the path to the immutable file. - immutable_path = os.path.dirname(os.path.normpath(immutable_file[0])) - immutable_path_components = [] - while True: - immutable_path, component = os.path.split(immutable_path) - if not component: - break - immutable_path_components.insert(0, component) - - for whitelist_path_components in whitelist_path_components_list: - - # Skip case such as "whitelist: /usr/local/bin" and "immutable path: /usr" - if len(whitelist_path_components) > len(immutable_path_components): - continue - - # NOTE: this check also works if "/" is whitelisted, since the whitelist components are empty and - # thus the file is counted as whitelisted. - is_whitelisted = True - for i in range(len(whitelist_path_components)): - if whitelist_path_components[i] != immutable_path_components[i]: - is_whitelisted = False - if is_whitelisted: - break - - if not is_whitelisted: - new_immutable_files.append(immutable_file) - return new_immutable_files - - -def _process_file_whitelist(immutable_files: List[Tuple[str, str]]) -> List[Tuple[str, str]]: - if not IMMUTABLE_FILE_WHITELIST: - return immutable_files - - new_immutable_files = [] - for immutable_file in immutable_files: - is_whitelisted = False - for whitelist_entry in IMMUTABLE_FILE_WHITELIST: - if os.path.samefile(immutable_file[0], whitelist_entry): - is_whitelisted = True - break - if not is_whitelisted: - new_immutable_files.append(immutable_file) - return new_immutable_files - - -def _store_state(state_data: Dict[str, Any]): - # Create state dir if it does not exist. - if not os.path.exists(STATE_DIR): - os.makedirs(STATE_DIR) - - state_file = os.path.join(STATE_DIR, "state") - - with open(state_file, 'wt') as fp: - fp.write(json.dumps(state_data)) - - os.chmod(state_file, stat.S_IREAD | stat.S_IWRITE) + @property + def attribute(self) -> str: + return self._attribute def search_immutable_files(): @@ -189,117 +62,100 @@ def search_immutable_files(): print("Module deactivated.") return - state_data = {} + step_state_data = {} try: - state_data = _load_state() + step_state_data = load_step_state(STATE_DIR) except Exception as e: - _output_error(str(e)) + output_error(__file__, str(e)) return # Reset step if we do not search in steps but everything. if not SEARCH_IN_STEPS: - state_data["next_step"] = 0 + step_state_data["next_step"] = 0 if not SEARCH_LOCATIONS: SEARCH_LOCATIONS.append("/") # Gather all search locations. - search_locations = [] + search_locations = [] # type: List[StepLocation] # If SEARCH_IN_STEPS is active, build a list of directories to search in if SEARCH_IN_STEPS: for search_location in SEARCH_LOCATIONS: # Add parent directory as non-recursive search location in order to search in it without going deeper. - # Tuple with directory as first element and recursive search as second element. - search_locations.append((search_location, False)) + search_locations.append(StepLocation(search_location, False)) - # Add all containing sub-directories as recursive search locations. + # Add all containing subdirectories as recursive search locations. elements = os.listdir(search_location) elements.sort() for element in elements: path = os.path.join(search_location, element) if os.path.isdir(path): - # Tuple with directory as first element and recursive search as second element. - search_locations.append((path, True)) + search_locations.append(StepLocation(path, True)) # If we do not search in separated steps, just add each directory as a recursive search location. else: for search_location in SEARCH_LOCATIONS: - # Tuple with directory as first element and recursive search as second element. - search_locations.append((search_location, True)) + search_locations.append(StepLocation(search_location, True)) # Reset index if it is outside the search locations. - if state_data["next_step"] >= len(search_locations): - state_data["next_step"] = 0 + if step_state_data["next_step"] >= len(search_locations): + step_state_data["next_step"] = 0 while True: - search_location, is_recursive = search_locations[state_data["next_step"]] + search_location_obj = search_locations[step_state_data["next_step"]] # Get all immutable files. - if is_recursive: + if search_location_obj.search_recursive: fd = os.popen("lsattr -R -a %s 2> /dev/null | sed -rn '/^[aAcCdDeijPsStTu\\-]{4}i/p'" - % search_location) + % search_location_obj.location) else: fd = os.popen("lsattr -a %s 2> /dev/null | sed -rn '/^[aAcCdDeijPsStTu\\-]{4}i/p'" - % search_location) + % search_location_obj.location) output_raw = fd.read().strip() fd.close() if output_raw != "": - immutable_files = [] + immutable_files = [] # type: List[ImmutableFile] output_list = output_raw.split("\n") for output_entry in output_list: output_entry_list = output_entry.split(" ") # Notify and skip line if sanity check fails. if len(output_entry_list) != 2: - _output_error("Unable to process line '%s'" % output_entry) + output_error(__file__, "Unable to process line '%s'" % output_entry) continue attributes = output_entry_list[0] file_location = output_entry_list[1] - immutable_files.append((file_location, attributes)) + immutable_files.append(ImmutableFile(file_location, attributes)) - immutable_files = _process_directory_whitelist(immutable_files) - immutable_files = _process_file_whitelist(immutable_files) + dir_whitelist = [FileLocation(x) for x in IMMUTABLE_DIRECTORY_WHITELIST] + file_whitelist = [FileLocation(x) for x in IMMUTABLE_FILE_WHITELIST] - hostname = socket.gethostname() - message = "Immutable files found on host '%s'.\n\n" % hostname - message += "\n".join(["File: %s; Attributes: %s" % (x[0], x[1]) for x in immutable_files]) + immutable_files = cast(List[ImmutableFile], apply_directory_whitelist(dir_whitelist, immutable_files)) + immutable_files = cast(List[ImmutableFile], apply_file_whitelist(file_whitelist, immutable_files)) - if print_output: - print(message) - print("#" * 80) + if immutable_files: + message = "Immutable file(s) found:\n\n" + message += "\n".join(["File: %s; Attributes: %s" % (x.location, x.attribute) for x in immutable_files]) - if ALERTR_FIFO: - optional_data = dict() - optional_data["immutable_files"] = output_raw.split("\n") - optional_data["hostname"] = hostname - optional_data["message"] = message + output_finding(__file__, message) - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) - - state_data["next_step"] += 1 + step_state_data["next_step"] += 1 # Stop search if we are finished. - if SEARCH_IN_STEPS or state_data["next_step"] >= len(search_locations): + if SEARCH_IN_STEPS or step_state_data["next_step"] >= len(search_locations): break try: - _store_state(state_data) + store_step_state(STATE_DIR, step_state_data) except Exception as e: - _output_error(str(e)) + output_error(__file__, str(e)) if __name__ == '__main__': diff --git a/scripts/search_memfd_create.py b/scripts/search_memfd_create.py index 51619bc..ea3ba67 100755 --- a/scripts/search_memfd_create.py +++ b/scripts/search_memfd_create.py @@ -19,13 +19,13 @@ https://www.sandflysecurity.com/blog/detecting-linux-memfd_create-fileless-malwa """ import os -import socket -# Read configuration and library functions. +from lib.util import output_finding + +# Read configuration. try: from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR from config.search_memfd_create import ACTIVATED - from lib.alerts import raise_alert_alertr, raise_alert_mail except: ALERTR_FIFO = None FROM_ADDR = None @@ -46,7 +46,7 @@ def search_deleted_memfd_files(): return # Get all suspicious ELF files. - fd = os.popen("ls -laR /proc/*/exe 2> /dev/null | grep memfd:.*\(deleted\)") + fd = os.popen("ls -laR /proc/*/exe 2> /dev/null | grep memfd:.*\\(deleted\\)") suspicious_exe_raw = fd.read().strip() fd.close() @@ -54,38 +54,11 @@ def search_deleted_memfd_files(): if suspicious_exe_raw.strip(): suspicious_exes.extend(suspicious_exe_raw.strip().split("\n")) - for suspicious_exe in suspicious_exes: + if suspicious_exes: + message = "Deleted memfd file(s) found:\n\n" + message += "\n".join(suspicious_exes) - if print_output: - print("SUSPICIOUS") - print(suspicious_exe) - print("") - - else: - if ALERTR_FIFO is not None: - - hostname = socket.gethostname() - optional_data = dict() - optional_data["suspicious_exe"] = suspicious_exe - optional_data["hostname"] = hostname - message = "Deleted memfd file on host '%s' found.\n\n" % hostname - message += suspicious_exe - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - - hostname = socket.gethostname() - subject = "[Security] Deleted memfd file on '%s'" % hostname - message = "Deleted memfd file on host '%s' found.\n\n" % hostname - message += suspicious_exe - - raise_alert_mail(FROM_ADDR, - TO_ADDR, - subject, - message) + output_finding(__file__, message) if __name__ == '__main__': diff --git a/scripts/search_non_kthreads.py b/scripts/search_non_kthreads.py index 20e93b8..f17f573 100755 --- a/scripts/search_non_kthreads.py +++ b/scripts/search_non_kthreads.py @@ -13,7 +13,7 @@ Malware will name itself with [brackets] to impersonate a Linux kernel thread. Any Linux process that looks like a [kernel thread] should have an empty maps file. Site note: -when using ps auxwf | grep "\[" they are children of [kthreadd] +when using ps auxwf | grep "\\[" they are children of [kthreadd] Requirements: None @@ -24,13 +24,13 @@ https://www.sandflysecurity.com/blog/detecting-linux-kernel-process-masquerading """ import os -import socket -# Read configuration and library functions. +from lib.util import output_error, output_finding + +# Read configuration. try: from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR from config.search_non_kthreads import NON_KTHREAD_WHITELIST, ACTIVATED - from lib.alerts import raise_alert_alertr, raise_alert_mail except: ALERTR_FIFO = None FROM_ADDR = None @@ -80,56 +80,20 @@ def search_suspicious_process(): # (e.g., "avahi-daemon: running [towelie.local]"" does not) elif process_name.startswith("["): - if print_output: - print("Checking pid: %s (%s) - " % (pid, process_name), - end="") - file_path = "/proc/%s/maps" % pid try: with open(file_path, 'rt') as fp: data = fp.read() if data == "": - if print_output: - print("ok") continue except Exception as e: - print("Exception") - print(e) - print("") + output_error(__file__, str(e)) continue - if print_output: - print("SUSPICIOUS") - print(ps_output) - print("") - - else: - if ALERTR_FIFO is not None: - - hostname = socket.gethostname() - optional_data = dict() - optional_data["pid"] = pid - optional_data["ps_output"] = ps_output - optional_data["hostname"] = hostname - message = "Process with pid '%s' on host '%s' suspicious.\n\n" % (pid, hostname) - message += ps_output - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - - hostname = socket.gethostname() - subject = "[Security] Suspicious process found on '%s'" % hostname - message = "Process with pid '%s' on host '%s' suspicious.\n\n" % (pid, hostname) - message += ps_output - - raise_alert_mail(FROM_ADDR, - TO_ADDR, - subject, - message) + message = "Process with pid '%s' suspicious.\n\n" % pid + message += ps_output + output_finding(__file__, message) if __name__ == '__main__': diff --git a/scripts/test_alert.py b/scripts/test_alert.py index 8561840..aeaf315 100755 --- a/scripts/test_alert.py +++ b/scripts/test_alert.py @@ -15,13 +15,12 @@ Requirements: None """ -import socket +from lib.util import output_finding -# Read configuration and and library functions. +# Read configuration. try: from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR from config.test_alert import ACTIVATED - from lib.alerts import raise_alert_alertr, raise_alert_mail except: ALERTR_FIFO = None FROM_ADDR = None @@ -31,22 +30,5 @@ except: if __name__ == '__main__': if ACTIVATED: - if ALERTR_FIFO is not None: - hostname = socket.gethostname() - optional_data = dict() - optional_data["hostname"] = hostname - message = "Alert test on host '%s'." % hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - hostname = socket.gethostname() - subject = "[Security] Alert test on '%s'" % hostname - message = "Alert test on host '%s'." % hostname - - raise_alert_mail(FROM_ADDR, - TO_ADDR, - subject, - message) + message = "Alert test." + output_finding(__file__, message) diff --git a/scripts/verify_deb_packages.py b/scripts/verify_deb_packages.py index b777a82..a2712fb 100755 --- a/scripts/verify_deb_packages.py +++ b/scripts/verify_deb_packages.py @@ -19,15 +19,14 @@ https://www.sandflysecurity.com/blog/detecting-linux-binary-file-poisoning/ """ import os -import socket from typing import List +from lib.util import output_finding -# Read configuration and library functions. +# Read configuration. try: from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR from config.verify_deb_packages import ACTIVATED, DEBSUMS_EXE, FILE_WHITELIST - from lib.alerts import raise_alert_alertr, raise_alert_mail except: ALERTR_FIFO = None FROM_ADDR = None @@ -36,8 +35,6 @@ except: FILE_WHITELIST = [] ACTIVATED = True -MAIL_SUBJECT = "[Security] Verifying deb package files on host '%s'" % socket.gethostname() - def _process_whitelist(changed_files: List[str]) -> List[str]: if not FILE_WHITELIST: @@ -74,28 +71,10 @@ def verify_deb_packages(): changed_files = _process_whitelist(changed_files) if changed_files: - hostname = socket.gethostname() - message = "Changed deb package files found on host '%s'.\n\n" % hostname + message = "Changed deb package files found.\n\n" message += "\n".join(["File: %s" % x for x in changed_files]) - if print_output: - print(message) - print("#" * 80) - - if ALERTR_FIFO: - optional_data = dict() - optional_data["immutable_files"] = output_raw.split("\n") - optional_data["hostname"] = hostname - optional_data["message"] = message - - raise_alert_alertr(ALERTR_FIFO, - optional_data) - - if FROM_ADDR is not None and TO_ADDR is not None: - raise_alert_mail(FROM_ADDR, - TO_ADDR, - MAIL_SUBJECT, - message) + output_finding(__file__, message) if __name__ == '__main__':