mirror of https://github.com/sqall01/LSMS.git
sync internal repo
This commit is contained in:
parent
8f8497f63e
commit
ad8617ebb1
77
README.md
77
README.md
|
@ -1,50 +1,77 @@
|
|||
# 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 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.
|
||||
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
|
||||
|
||||
The scripts are located in the directory `scripts/`. Each script contains a short summary in the header of the file with a description of what it is supposed to do, (if needed) dependencies that have to be installed and (if available) references to where the idea for this script stems from.
|
||||
The scripts are located in the directory `scripts/`.
|
||||
Each script contains a short summary in the header of the file with a description of what it is supposed to do,
|
||||
(if needed) dependencies that have to be installed and (if available) references to where the idea for
|
||||
this script stems from.
|
||||
|
||||
Each script has a configuration file in the `scripts/config/` directory to configure it. If the configuration file was not found during the execution of the script, the script will fall back to default settings and print out the results. Hence, it is not necessary to provide a configuration file.
|
||||
Each script has a configuration file in the `scripts/config/` directory to configure it.
|
||||
If the configuration file was not found during the execution of the script,
|
||||
the script will fall back to default settings and print out the results.
|
||||
Hence, it is not necessary to provide a configuration file.
|
||||
|
||||
The `scripts/lib/` directory contains code that is shared between different scripts.
|
||||
|
||||
Scripts using a `monitor_` prefix hold a state and are only useful for monitoring purposes. A single usage of them for an investigation will only result in showing the current state the Linux system and not changes that might be relevant for the system's security.
|
||||
Scripts using a `monitor_` prefix hold a state and are only useful for monitoring purposes.
|
||||
A single usage of them for an investigation will only result in showing the current state the
|
||||
Linux system and not changes that might be relevant for the system's security. If you want to
|
||||
establish the current state of your system as benign for these scripts, you can provide the `--init` argument.
|
||||
|
||||
## Usage
|
||||
|
||||
Take a look at the header of the script you want to execute. It contains a short description what this script is supposed to do and what requirements are needed (if any needed at all). If requirements are needed, install them before running the script.
|
||||
Take a look at the header of the script you want to execute. It contains a short description what this script
|
||||
is supposed to do and what requirements are needed (if any needed at all). If requirements are needed,
|
||||
install them before running the script.
|
||||
|
||||
The shared configuration file `scripts/config/config.py` contains settings that are used by all scripts. Furthermore, each script can be configured by using the corresponding configuration file in the `scripts/config/` directory. If no configuration file was found, a default setting is used and the results are printed out.
|
||||
The shared configuration file `scripts/config/config.py` contains settings that are used by all scripts.
|
||||
Furthermore, each script can be configured by using the corresponding configuration file in the `scripts/config/`
|
||||
directory. If no configuration file was found, a default setting is used and the results are printed out.
|
||||
|
||||
Finally, you can run all configured scripts by executing `start_search.py` (which is located in the main directory) or by executing each script manually. A Python3 interpreter is needed to run the scripts.
|
||||
Finally, you can run all configured scripts by executing `start_search.py` (which is located in the main directory)
|
||||
or by executing each script manually. A Python3 interpreter is needed to run the scripts.
|
||||
|
||||
### Monitoring
|
||||
|
||||
If you want to use the scripts to monitor your Linux system constantly, you have to perform the following steps:
|
||||
|
||||
1. Set up a notification channel that is supported by the scripts (currently printing out, mail, or [AlertR](https://github.com/sqall01/alertR)).
|
||||
1. Set up a notification channel that is supported by the scripts (currently printing out, mail,
|
||||
or [AlertR](https://github.com/sqall01/alertR)).
|
||||
|
||||
2. Configure the scripts that you want to run using the configuration files in the `scripts/config/` directory.
|
||||
|
||||
3. Set up a cron job as `root` user that executes `start_search.py` (e.g., `0 * * * * root /opt/LSMS/start_search.py` to start the search hourly).
|
||||
3. Execute `start_search.py` with the `--init` argument to initialize the scripts with the `monitor_` prefix and let
|
||||
them establish a state of your system. However, this assumes that your system is currently uncompromised.
|
||||
If you are unsure of this, you should verify its current state.
|
||||
|
||||
4. Set up a cron job as `root` user that executes `start_search.py`
|
||||
|
||||
5. (e.g., `0 * * * * root /opt/LSMS/start_search.py` to start the search hourly).
|
||||
|
||||
## List of Scripts
|
||||
|
||||
| Name | Script |
|
||||
|---------------------------------------------|--------------------------------------------------------------------------|
|
||||
| Monitoring cron files | [monitor_cron.py](scripts/monitor_cron.py) |
|
||||
| Monitoring /etc/hosts file | [monitor_hosts_file.py](scripts/monitor_hosts_file.py) |
|
||||
| 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 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 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) |
|
||||
| Name | Script |
|
||||
|----------------------------------------------------------------------|------------------------------------------------------------------------------|
|
||||
| Monitoring cron files | [monitor_cron.py](scripts/monitor_cron.py) |
|
||||
| Monitoring /etc/hosts file | [monitor_hosts_file.py](scripts/monitor_hosts_file.py) |
|
||||
| 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 modules | [monitor_modules.py](scripts/monitor_modules.py) |
|
||||
| Monitoring SSH authorized_keys files | [monitor_ssh_authorized_keys.py](scripts/monitor_ssh_authorized_keys.py) |
|
||||
| Monitoring systemd unit files | [monitor_systemd_units.py](scripts/monitor_systemd_units.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 kernel thread impersonations | [search_non_kthreads.py](scripts/search_non_kthreads.py) |
|
||||
| Search processes that were started by a now disconnected SSH session | [search_ssh_leftover_processes.py](scripts/search_ssh_leftover_processes.py) |
|
||||
| Search running deleted programs | [search_deleted_exe.py](scripts/search_deleted_exe.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) |
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
from typing import List
|
||||
|
||||
# List of modules that are loaded and should be ignored.
|
||||
MODULES_WHITELIST = [] # type: List[str]
|
||||
|
||||
# Is the script allowed to run or not?
|
||||
ACTIVATED = True
|
|
@ -0,0 +1,16 @@
|
|||
# Is the script allowed to run or not?
|
||||
ACTIVATED = True
|
||||
|
||||
# Directories in which systemd unit files can be placed. Following list are the defaults on Ubuntu/Debian.
|
||||
SYSTEMD_UNIT_DIRS = ["/etc/systemd/system",
|
||||
"/etc/systemd/user",
|
||||
"/etc/systemd/network",
|
||||
"/usr/lib/systemd/system",
|
||||
"/usr/lib/systemd/user",
|
||||
"/usr/lib/systemd/network",
|
||||
"/usr/local/lib/systemd/system",
|
||||
"/usr/local/lib/systemd/user"
|
||||
"/usr/local/lib/systemd/network",
|
||||
"/lib/systemd/system",
|
||||
"/lib/systemd/user",
|
||||
"/lib/systemd/network"] # type: List[str]
|
|
@ -0,0 +1,2 @@
|
|||
# Is the script allowed to run or not?
|
||||
ACTIVATED = True
|
|
@ -25,7 +25,7 @@ def raise_alert_alertr(alertr_fifo: str,
|
|||
try:
|
||||
# Will throw an exception if FIFO file does not have a reader instead of blocking.
|
||||
fd = os.open(alertr_fifo, os.O_WRONLY | os.O_NONBLOCK)
|
||||
os.write(fd, json.dumps(msg_dict).encode("ascii"))
|
||||
os.write(fd, (json.dumps(msg_dict) + "\n").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.
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
SUPPRESS_OUTPUT = False
|
|
@ -1,6 +1,9 @@
|
|||
import difflib
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
|
||||
from . import global_vars
|
||||
from .alerts import raise_alert_alertr, raise_alert_mail
|
||||
|
||||
try:
|
||||
|
@ -12,7 +15,17 @@ except:
|
|||
TO_ADDR = None
|
||||
|
||||
|
||||
def get_diff_per_line(name1: str, data1: str, name2: str, data2: str) -> str:
|
||||
# difflib function needs trailing newline for each element to build a usable output string
|
||||
temp1 = ["%s\n" % x for x in data1.split("\n")]
|
||||
temp2 = ["%s\n" % x for x in data2.split("\n")]
|
||||
return "".join(difflib.unified_diff(temp1, temp2, fromfile=name1, tofile=name2))
|
||||
|
||||
|
||||
def output_error(file_name: str, msg: str):
|
||||
# Suppresses output, for example, if an initialization run is performed.
|
||||
if global_vars.SUPPRESS_OUTPUT:
|
||||
return
|
||||
|
||||
base_name = os.path.basename(file_name)
|
||||
|
||||
|
@ -37,18 +50,21 @@ def output_error(file_name: str, msg: str):
|
|||
optional_data["script"] = base_name
|
||||
optional_data["message"] = message
|
||||
|
||||
raise_alert_alertr(ALERTR_FIFO,
|
||||
optional_data)
|
||||
threading.Thread(target=raise_alert_alertr,
|
||||
args=(ALERTR_FIFO, optional_data),
|
||||
daemon=False).start()
|
||||
|
||||
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)
|
||||
threading.Thread(target=raise_alert_mail,
|
||||
args=(FROM_ADDR, TO_ADDR, mail_subject, message),
|
||||
daemon=False).start()
|
||||
|
||||
|
||||
def output_finding(file_name: str, msg: str):
|
||||
# Suppresses output, for example, if an initialization run is performed.
|
||||
if global_vars.SUPPRESS_OUTPUT:
|
||||
return
|
||||
|
||||
base_name = os.path.basename(file_name)
|
||||
|
||||
|
|
|
@ -12,7 +12,11 @@ Short summary:
|
|||
Monitor /etc/crontab, /etc/cron.d/*, user specific crontab files and script files run by cron (e.g., script files in /etc/cron.hourly) for changes to detect attempts for attacker persistence.
|
||||
Additionally, check if crontab entries and user specific crontab files belong to existing system users.
|
||||
|
||||
NOTE: The first execution of this script will only show you the current state of the environment which should be acknowledged before monitoring for changes will become an effective security measure.
|
||||
NOTE: The first execution of this script should be done with the argument "--init".
|
||||
Otherwise, the script will only show you the current state of the environment since no state was established yet.
|
||||
However, this assumes that the system is uncompromised during the initial execution.
|
||||
Hence, if you are unsure this is the case you should verify the current state
|
||||
before monitoring for changes will become an effective security measure.
|
||||
|
||||
Requirements:
|
||||
None
|
||||
|
@ -21,8 +25,10 @@ None
|
|||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from typing import Dict, List, Set
|
||||
|
||||
import lib.global_vars
|
||||
from lib.state import load_state, store_state
|
||||
from lib.util import output_error, output_finding
|
||||
from lib.util_user import get_system_users
|
||||
|
@ -236,4 +242,8 @@ def monitor_cron():
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) == 2:
|
||||
# Suppress output in our initial execution to establish a state.
|
||||
if sys.argv[1] == "--init":
|
||||
lib.global_vars.SUPPRESS_OUTPUT = True
|
||||
monitor_cron()
|
||||
|
|
|
@ -11,15 +11,21 @@
|
|||
Short summary:
|
||||
Monitor /etc/hosts for changes to detect malicious attempts to divert traffic.
|
||||
|
||||
NOTE: The first execution of this script will only show you the current state of the environment which should be acknowledged before monitoring for changes will become an effective security measure.
|
||||
NOTE: The first execution of this script should be done with the argument "--init".
|
||||
Otherwise, the script will only show you the current state of the environment since no state was established yet.
|
||||
However, this assumes that the system is uncompromised during the initial execution.
|
||||
Hence, if you are unsure this is the case you should verify the current state
|
||||
before monitoring for changes will become an effective security measure.
|
||||
|
||||
Requirements:
|
||||
None
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Set
|
||||
|
||||
import lib.global_vars
|
||||
from lib.state import load_state, store_state
|
||||
from lib.util import output_error, output_finding
|
||||
|
||||
|
@ -153,4 +159,8 @@ def monitor_hosts():
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) == 2:
|
||||
# Suppress output in our initial execution to establish a state.
|
||||
if sys.argv[1] == "--init":
|
||||
lib.global_vars.SUPPRESS_OUTPUT = True
|
||||
monitor_hosts()
|
||||
|
|
|
@ -11,15 +11,21 @@
|
|||
Short summary:
|
||||
Monitor /etc/ld.so.preload for changes to detect malicious attempts to alter the control flow of binaries.
|
||||
|
||||
NOTE: The first execution of this script will only show you the current state of the environment which should be acknowledged before monitoring for changes will become an effective security measure.
|
||||
NOTE: The first execution of this script should be done with the argument "--init".
|
||||
Otherwise, the script will only show you the current state of the environment since no state was established yet.
|
||||
However, this assumes that the system is uncompromised during the initial execution.
|
||||
Hence, if you are unsure this is the case you should verify the current state
|
||||
before monitoring for changes will become an effective security measure.
|
||||
|
||||
Requirements:
|
||||
None
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Set
|
||||
|
||||
import lib.global_vars
|
||||
from lib.state import load_state, store_state
|
||||
from lib.util import output_error, output_finding
|
||||
|
||||
|
@ -110,4 +116,8 @@ def monitor_ld_preload():
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) == 2:
|
||||
# Suppress output in our initial execution to establish a state.
|
||||
if sys.argv[1] == "--init":
|
||||
lib.global_vars.SUPPRESS_OUTPUT = True
|
||||
monitor_ld_preload()
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
#!/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:
|
||||
Monitor /proc/modules for changes to detect if a malicious module was loaded. The script can run in two different modes:
|
||||
1) before running the script, whitelist every module that is allowed to be loaded on the host in the configuration file.
|
||||
2) assume all loaded modules are legitimate during the initial execution of the script with "--init" and monitor for changes.
|
||||
|
||||
If using 1), you get fewer false-positives due to the time you spend setting everything up.
|
||||
If using 2), you assume the host is uncompromised during the initial execution of the script.
|
||||
If you have a module that is loaded/unloaded frequently, you can still configure the whitelist additionally
|
||||
to prevent constant alerting.
|
||||
|
||||
Requirements:
|
||||
None
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import lib.global_vars
|
||||
from lib.state import load_state, store_state
|
||||
from lib.util import output_error, output_finding
|
||||
|
||||
# Read configuration.
|
||||
try:
|
||||
from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR, STATE_DIR
|
||||
from config.monitor_modules import ACTIVATED, MODULES_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
|
||||
MODULES_WHITELIST = []
|
||||
STATE_DIR = os.path.join("/tmp", os.path.basename(__file__))
|
||||
|
||||
|
||||
def _get_modules():
|
||||
"""
|
||||
Reads all currently loaded modules.
|
||||
:return: set of loaded modules
|
||||
"""
|
||||
loaded_modules = set()
|
||||
with open("/proc/modules", 'r') as fp:
|
||||
for line in fp:
|
||||
line_list = line.split(" ")
|
||||
loaded_modules.add(line_list[0])
|
||||
return loaded_modules
|
||||
|
||||
|
||||
def monitor_modules():
|
||||
# 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
|
||||
|
||||
stored_modules_data = set()
|
||||
try:
|
||||
state_data = load_state(STATE_DIR)
|
||||
|
||||
# Convert list to set.
|
||||
if "modules_data" in state_data.keys():
|
||||
stored_modules_data = set(state_data["modules_data"])
|
||||
|
||||
except Exception as e:
|
||||
output_error(__file__, str(e))
|
||||
return
|
||||
|
||||
current_modules = set()
|
||||
try:
|
||||
current_modules = _get_modules()
|
||||
|
||||
except Exception as e:
|
||||
output_error(__file__, str(e))
|
||||
return
|
||||
|
||||
# Remove whitelisted modules from the currently loaded modules set.
|
||||
current_modules = current_modules - set(MODULES_WHITELIST)
|
||||
|
||||
# Check for newly loaded modules.
|
||||
loaded_modules = current_modules - stored_modules_data
|
||||
if loaded_modules:
|
||||
message = "New modules loaded.\n\n"
|
||||
message += "Entries:\n"
|
||||
for module in loaded_modules:
|
||||
message += module
|
||||
message += "\n"
|
||||
|
||||
output_finding(__file__, message)
|
||||
|
||||
# Check for newly unloaded modules.
|
||||
unloaded_modules = stored_modules_data - current_modules
|
||||
if unloaded_modules:
|
||||
message = "Running modules unloaded.\n\n"
|
||||
message += "Entries:\n"
|
||||
for module in unloaded_modules:
|
||||
message += module
|
||||
message += "\n"
|
||||
|
||||
output_finding(__file__, message)
|
||||
|
||||
try:
|
||||
# Convert set to list.
|
||||
state_data = {"modules_data": list(current_modules)}
|
||||
|
||||
store_state(STATE_DIR, state_data)
|
||||
|
||||
except Exception as e:
|
||||
output_error(__file__, str(e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) == 2:
|
||||
# Suppress output in our initial execution to establish a state.
|
||||
if sys.argv[1] == "--init":
|
||||
lib.global_vars.SUPPRESS_OUTPUT = True
|
||||
monitor_modules()
|
|
@ -11,15 +11,21 @@
|
|||
Short summary:
|
||||
Monitor /etc/passwd for changes to detect malicious attempts to hijack/change users.
|
||||
|
||||
NOTE: The first execution of this script will only show you the current state of the environment which should be acknowledged before monitoring for changes will become an effective security measure.
|
||||
NOTE: The first execution of this script should be done with the argument "--init".
|
||||
Otherwise, the script will only show you the current state of the environment since no state was established yet.
|
||||
However, this assumes that the system is uncompromised during the initial execution.
|
||||
Hence, if you are unsure this is the case you should verify the current state
|
||||
before monitoring for changes will become an effective security measure.
|
||||
|
||||
Requirements:
|
||||
None
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict
|
||||
|
||||
import lib.global_vars
|
||||
from lib.state import load_state, store_state
|
||||
from lib.util import output_error, output_finding
|
||||
from lib.util_user import get_system_users
|
||||
|
@ -109,4 +115,8 @@ def monitor_passwd():
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) == 2:
|
||||
# Suppress output in our initial execution to establish a state.
|
||||
if sys.argv[1] == "--init":
|
||||
lib.global_vars.SUPPRESS_OUTPUT = True
|
||||
monitor_passwd()
|
||||
|
|
|
@ -11,7 +11,11 @@
|
|||
Short summary:
|
||||
Monitor ~/.ssh/authorized_keys for changes to detect malicious backdoor attempts.
|
||||
|
||||
NOTE: The first execution of this script will only show you the current state of the environment which should be acknowledged before monitoring for changes will become an effective security measure.
|
||||
NOTE: The first execution of this script should be done with the argument "--init".
|
||||
Otherwise, the script will only show you the current state of the environment since no state was established yet.
|
||||
However, this assumes that the system is uncompromised during the initial execution.
|
||||
Hence, if you are unsure this is the case you should verify the current state
|
||||
before monitoring for changes will become an effective security measure.
|
||||
|
||||
Requirements:
|
||||
None
|
||||
|
@ -19,8 +23,10 @@ None
|
|||
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
from typing import List, Tuple, Dict, Any
|
||||
|
||||
import lib.global_vars
|
||||
from lib.state import load_state, store_state
|
||||
from lib.util import output_error, output_finding
|
||||
from lib.util_user import get_system_users
|
||||
|
@ -172,4 +178,8 @@ def monitor_ssh_authorized_keys():
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) == 2:
|
||||
# Suppress output in our initial execution to establish a state.
|
||||
if sys.argv[1] == "--init":
|
||||
lib.global_vars.SUPPRESS_OUTPUT = True
|
||||
monitor_ssh_authorized_keys()
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
#!/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:
|
||||
Monitor systemd unit files to find ones that are used for malware persistence.
|
||||
|
||||
NOTE: The first execution of this script should be done with the argument "--init".
|
||||
Otherwise, the script will only show you the current state of the environment since no state was established yet.
|
||||
However, this assumes that the system is uncompromised during the initial execution.
|
||||
Hence, if you are unsure this is the case you should verify the current state
|
||||
before monitoring for changes will become an effective security measure.
|
||||
|
||||
Requirements:
|
||||
None
|
||||
|
||||
Reference:
|
||||
https://www.trendmicro.com/en_us/research/23/c/iron-tiger-sysupdate-adds-linux-targeting.html
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict
|
||||
|
||||
import lib.global_vars
|
||||
from lib.state import load_state, store_state
|
||||
from lib.util import get_diff_per_line, output_error, output_finding
|
||||
|
||||
# Read configuration.
|
||||
try:
|
||||
from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR, STATE_DIR
|
||||
from config.monitor_systemd_units import ACTIVATED, SYSTEMD_UNIT_DIRS
|
||||
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
|
||||
STATE_DIR = os.path.join("/tmp", os.path.basename(__file__))
|
||||
SYSTEMD_UNIT_DIRS = ["/etc/systemd/system",
|
||||
"/etc/systemd/user",
|
||||
"/etc/systemd/network",
|
||||
"/usr/lib/systemd/system",
|
||||
"/usr/lib/systemd/user",
|
||||
"/usr/lib/systemd/network",
|
||||
"/usr/local/lib/systemd/system",
|
||||
"/usr/local/lib/systemd/user"
|
||||
"/usr/local/lib/systemd/network",
|
||||
"/lib/systemd/system",
|
||||
"/lib/systemd/user",
|
||||
"/lib/systemd/network"]
|
||||
|
||||
|
||||
def _get_system_unit_files() -> Dict[str, str]:
|
||||
systemd_unit_files = dict()
|
||||
for systemd_unit_dir in SYSTEMD_UNIT_DIRS:
|
||||
for root, _, files in os.walk(systemd_unit_dir):
|
||||
for file in files:
|
||||
file_location = os.path.join(root, file)
|
||||
|
||||
# Some files are broken symlinks, hence, check if they exist
|
||||
if os.path.exists(file_location):
|
||||
with open(file_location, "rt") as fp:
|
||||
data = fp.read()
|
||||
|
||||
# Filter for systemd unit files that can execute commands
|
||||
if "[Unit]" in data and "[Service]" in data:
|
||||
# Since keys do not have to start at the beginning of the line, we go through each line,
|
||||
# remove whitespaces leading whitespaces and check if it starts with a key we are interested in
|
||||
for line in data.split("\n"):
|
||||
normalized_line = line.strip()
|
||||
if any(normalized_line.startswith(x) for x in ["ExecStart",
|
||||
"ExecStartPre",
|
||||
"ExecStartPost",
|
||||
"ExecReload",
|
||||
"ExecStop",
|
||||
"ExecStopPost"]):
|
||||
|
||||
# Store complete data of unit file. Even on a non-server system such as a
|
||||
# xubuntu 22.04 we only have around 700 unit files of interest. Calculating with
|
||||
# 1kB of data per file (which is way larger than a normal unit file has) we only
|
||||
# need a little over 700 kB memory for this. Even on a Raspberry Pi we have no
|
||||
# problem doing this. Further, it will prevent race-conditions when we already
|
||||
# have the data stored and do not read it afterwards from the file if we generate
|
||||
# alerts.
|
||||
systemd_unit_files[file_location] = data
|
||||
break
|
||||
|
||||
return systemd_unit_files
|
||||
|
||||
|
||||
def monitor_systemd_units():
|
||||
# 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
|
||||
|
||||
stored_systemd_units_data = {}
|
||||
try:
|
||||
stored_systemd_units_data = load_state(STATE_DIR)
|
||||
|
||||
except Exception as e:
|
||||
output_error(__file__, str(e))
|
||||
return
|
||||
|
||||
# Add units key in case we do not have any stored data yet.
|
||||
if "units" not in stored_systemd_units_data.keys():
|
||||
stored_systemd_units_data["units"] = {}
|
||||
|
||||
curr_systemd_units_data = {}
|
||||
try:
|
||||
curr_systemd_units_data = _get_system_unit_files()
|
||||
|
||||
except Exception as e:
|
||||
output_error(__file__, str(e))
|
||||
return
|
||||
|
||||
# Compare stored unit files data with current one.
|
||||
stored_units_data = stored_systemd_units_data["units"]
|
||||
for stored_unit_file, stored_unit_data in stored_units_data.items():
|
||||
|
||||
# Check if unit file was deleted.
|
||||
if stored_unit_file not in curr_systemd_units_data.keys():
|
||||
message = "Systemd unit file '%s' was deleted." % stored_unit_file
|
||||
output_finding(__file__, message)
|
||||
continue
|
||||
|
||||
# Check if unit file was modified.
|
||||
if stored_unit_data != curr_systemd_units_data[stored_unit_file]:
|
||||
|
||||
diff = get_diff_per_line("Old",
|
||||
stored_unit_data,
|
||||
"New",
|
||||
curr_systemd_units_data[stored_unit_file])
|
||||
|
||||
message = "Systemd unit file '%s' was modified:\n\nDiff:\n%s\n\nNew file:\n%s" % (stored_unit_file,
|
||||
diff,
|
||||
curr_systemd_units_data[stored_unit_file]) # noqa:E501
|
||||
|
||||
output_finding(__file__, message)
|
||||
|
||||
# Check new unit file added.
|
||||
for curr_unit_file in curr_systemd_units_data.keys():
|
||||
if curr_unit_file not in stored_units_data.keys():
|
||||
message = "Systemd unit file '%s' was added:\n\n%s" % (curr_unit_file,
|
||||
curr_systemd_units_data[curr_unit_file])
|
||||
output_finding(__file__, message)
|
||||
|
||||
try:
|
||||
store_state(STATE_DIR, {"units": curr_systemd_units_data})
|
||||
|
||||
except Exception as e:
|
||||
output_error(__file__, str(e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) == 2:
|
||||
# Suppress output in our initial execution to establish a state.
|
||||
if sys.argv[1] == "--init":
|
||||
lib.global_vars.SUPPRESS_OUTPUT = True
|
||||
monitor_systemd_units()
|
|
@ -16,6 +16,7 @@ None
|
|||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from lib.util import output_finding
|
||||
|
||||
|
@ -59,4 +60,11 @@ def search_deleted_exe_files():
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
search_deleted_exe_files()
|
||||
is_init_run = False
|
||||
if len(sys.argv) == 2:
|
||||
if sys.argv[1] == "--init":
|
||||
is_init_run = True
|
||||
|
||||
# Script does not need to establish a state.
|
||||
if not is_init_run:
|
||||
search_deleted_exe_files()
|
||||
|
|
|
@ -16,11 +16,12 @@ Requirements:
|
|||
None
|
||||
|
||||
Reference:
|
||||
https://twitter.com/CraigHRowland/status/1268863172825346050?s=20
|
||||
https://twitter.com/CraigHRowland/status/1268863172825346050
|
||||
https://twitter.com/CraigHRowland/status/1269196509079166976
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from lib.util import output_finding
|
||||
|
||||
|
@ -71,4 +72,11 @@ def search_suspicious_files():
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
search_suspicious_files()
|
||||
is_init_run = False
|
||||
if len(sys.argv) == 2:
|
||||
if sys.argv[1] == "--init":
|
||||
is_init_run = True
|
||||
|
||||
# Script does not need to establish a state.
|
||||
if not is_init_run:
|
||||
search_suspicious_files()
|
||||
|
|
|
@ -16,6 +16,7 @@ None
|
|||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
from lib.step_state import StepLocation, load_step_state, store_step_state
|
||||
|
@ -141,4 +142,11 @@ def search_hidden_exe_files():
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
search_hidden_exe_files()
|
||||
is_init_run = False
|
||||
if len(sys.argv) == 2:
|
||||
if sys.argv[1] == "--init":
|
||||
is_init_run = True
|
||||
|
||||
# Script does not need to establish a state.
|
||||
if not is_init_run:
|
||||
search_hidden_exe_files()
|
||||
|
|
|
@ -16,6 +16,7 @@ None
|
|||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import List, cast
|
||||
|
||||
from lib.step_state import StepLocation, load_step_state, store_step_state
|
||||
|
@ -159,4 +160,11 @@ def search_immutable_files():
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
search_immutable_files()
|
||||
is_init_run = False
|
||||
if len(sys.argv) == 2:
|
||||
if sys.argv[1] == "--init":
|
||||
is_init_run = True
|
||||
|
||||
# Script does not need to establish a state.
|
||||
if not is_init_run:
|
||||
search_immutable_files()
|
||||
|
|
|
@ -19,6 +19,7 @@ https://www.sandflysecurity.com/blog/detecting-linux-memfd_create-fileless-malwa
|
|||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from lib.util import output_finding
|
||||
|
||||
|
@ -62,4 +63,11 @@ def search_deleted_memfd_files():
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
search_deleted_memfd_files()
|
||||
is_init_run = False
|
||||
if len(sys.argv) == 2:
|
||||
if sys.argv[1] == "--init":
|
||||
is_init_run = True
|
||||
|
||||
# Script does not need to establish a state.
|
||||
if not is_init_run:
|
||||
search_deleted_memfd_files()
|
||||
|
|
|
@ -24,6 +24,7 @@ https://www.sandflysecurity.com/blog/detecting-linux-kernel-process-masquerading
|
|||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from lib.util import output_error, output_finding
|
||||
|
||||
|
@ -97,4 +98,11 @@ def search_suspicious_process():
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
search_suspicious_process()
|
||||
is_init_run = False
|
||||
if len(sys.argv) == 2:
|
||||
if sys.argv[1] == "--init":
|
||||
is_init_run = True
|
||||
|
||||
# Script does not need to establish a state.
|
||||
if not is_init_run:
|
||||
search_suspicious_process()
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
#!/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 processes that were started by an SSH session that is now disconnected.
|
||||
|
||||
Requirements:
|
||||
None
|
||||
|
||||
Reference:
|
||||
https://twitter.com/CraigHRowland/status/1579582776529281026
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from lib.util import output_error, output_finding
|
||||
|
||||
# Read configuration.
|
||||
try:
|
||||
from config.config import ALERTR_FIFO, FROM_ADDR, TO_ADDR
|
||||
from config.search_ssh_leftover_processes import ACTIVATED
|
||||
except:
|
||||
ALERTR_FIFO = None
|
||||
FROM_ADDR = None
|
||||
TO_ADDR = None
|
||||
ACTIVATED = True
|
||||
|
||||
|
||||
def search_leftover_ssh_process():
|
||||
|
||||
# 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
|
||||
|
||||
# Search for SSH_CONNECTION and SSH_CLIENT
|
||||
fd = os.popen("grep -l SSH_C /proc/*/environ 2> /dev/null")
|
||||
ssh_processes = fd.read().strip()
|
||||
fd.close()
|
||||
|
||||
for ssh_process in ssh_processes.split("\n"):
|
||||
# Example output: /proc/996/environ
|
||||
# noinspection RegExpRedundantEscape
|
||||
matches = re.search(r'proc/(\d*)/environ', ssh_process, re.IGNORECASE)
|
||||
if not matches:
|
||||
continue
|
||||
|
||||
pid = matches.group(1)
|
||||
|
||||
try:
|
||||
with open("/proc/" + str(pid) + "/status", "r") as fp:
|
||||
status_data = fp.read()
|
||||
|
||||
except FileNotFoundError: # Process got terminated while searching
|
||||
continue
|
||||
|
||||
ppid = None
|
||||
name = None
|
||||
for line in status_data.split("\n"):
|
||||
if line.startswith("PPid:"):
|
||||
line_split = line.split("\t")
|
||||
|
||||
try:
|
||||
ppid = int(line_split[-1])
|
||||
except Exception as e:
|
||||
output_error(__file__, "PPid not parsable for pid %d\n\n%s\n\n%s" % (pid, status_data, str(e)))
|
||||
break
|
||||
|
||||
elif line.startswith("Name:"):
|
||||
line_split = line.split("\t")
|
||||
name = line_split[-1]
|
||||
|
||||
if ppid is not None and name is not None:
|
||||
break
|
||||
|
||||
if ppid is not None and name is not None:
|
||||
if ppid == 1:
|
||||
|
||||
# Get executed file
|
||||
exe_link = "/proc/" + str(pid) + "/exe"
|
||||
fd = os.popen("ls -laR %s" % exe_link)
|
||||
exe_raw = fd.read().strip()
|
||||
fd.close()
|
||||
matches = re.search(r'/proc/\d*/exe -> (.*)', exe_raw, re.IGNORECASE)
|
||||
exe_file = exe_raw
|
||||
if matches:
|
||||
exe_file = matches.group(1)
|
||||
|
||||
message = "Leftover process of SSH session found.\n\n"
|
||||
message += "Name: %s\n" % name
|
||||
message += "Exe: %s\n" % exe_file
|
||||
message += "Pid: %s\n" % pid
|
||||
|
||||
output_finding(__file__, message)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
is_init_run = False
|
||||
if len(sys.argv) == 2:
|
||||
if sys.argv[1] == "--init":
|
||||
is_init_run = True
|
||||
|
||||
# Script does not need to establish a state.
|
||||
if not is_init_run:
|
||||
search_leftover_ssh_process()
|
|
@ -15,6 +15,7 @@ Requirements:
|
|||
None
|
||||
"""
|
||||
|
||||
import sys
|
||||
from lib.util import output_finding
|
||||
|
||||
# Read configuration.
|
||||
|
@ -29,6 +30,13 @@ except:
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if ACTIVATED:
|
||||
message = "Alert test."
|
||||
output_finding(__file__, message)
|
||||
is_init_run = False
|
||||
if len(sys.argv) == 2:
|
||||
if sys.argv[1] == "--init":
|
||||
is_init_run = True
|
||||
|
||||
# Script does not need to establish a state.
|
||||
if not is_init_run:
|
||||
if ACTIVATED:
|
||||
message = "Alert test."
|
||||
output_finding(__file__, message)
|
||||
|
|
|
@ -19,6 +19,7 @@ https://www.sandflysecurity.com/blog/detecting-linux-binary-file-poisoning/
|
|||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
from lib.util import output_finding
|
||||
|
@ -61,7 +62,7 @@ def verify_deb_packages():
|
|||
print("Module deactivated.")
|
||||
return
|
||||
|
||||
fd = os.popen("%s -c" % DEBSUMS_EXE)
|
||||
fd = os.popen("%s -c 2> /dev/null" % DEBSUMS_EXE)
|
||||
output_raw = fd.read().strip()
|
||||
fd.close()
|
||||
|
||||
|
@ -78,4 +79,11 @@ def verify_deb_packages():
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
verify_deb_packages()
|
||||
is_init_run = False
|
||||
if len(sys.argv) == 2:
|
||||
if sys.argv[1] == "--init":
|
||||
is_init_run = True
|
||||
|
||||
# Script does not need to establish a state.
|
||||
if not is_init_run:
|
||||
verify_deb_packages()
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
import os
|
||||
import subprocess
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
from scripts.config.config import START_PROCESS_TIMEOUT, TO_ADDR, FROM_ADDR, ALERTR_FIFO
|
||||
from scripts.lib.alerts import raise_alert_alertr, raise_alert_mail
|
||||
|
@ -29,7 +30,12 @@ if __name__ == '__main__':
|
|||
if print_output:
|
||||
print("Executing %s" % script)
|
||||
|
||||
to_execute = script_dir + script
|
||||
to_execute = [script_dir + script]
|
||||
|
||||
# Pass arguments to scripts.
|
||||
if len(sys.argv) > 1:
|
||||
to_execute.extend(sys.argv[1:])
|
||||
|
||||
process = None
|
||||
try:
|
||||
process = subprocess.Popen(to_execute,
|
||||
|
|
Loading…
Reference in New Issue