sync internal repo

This commit is contained in:
Andre Pawlowski 2022-01-05 20:16:57 +01:00
parent 0b7ed4177a
commit 0f79ef527f
20 changed files with 623 additions and 1032 deletions

View file

@ -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) |

View file

@ -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

View file

@ -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.

View file

@ -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

46
scripts/lib/state.py Normal file
View file

@ -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)

54
scripts/lib/step_state.py Normal file
View file

@ -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)

85
scripts/lib/util.py Normal file
View file

@ -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)

99
scripts/lib/util_file.py Normal file
View file

@ -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

View file

@ -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__':

View file

@ -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__':

View file

@ -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()

View file

@ -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__':

View file

@ -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__':

View file

@ -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__':

144
scripts/search_hidden_exe.py Executable file
View file

@ -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()

View file

@ -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__':

View file

@ -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__':

View file

@ -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__':

View file

@ -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)

View file

@ -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__':