Refactor userspace detector.
This commit is contained in:
parent
dba2deb1e5
commit
e942206b12
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
|
||||
__pycache__/
|
||||
*.pdf
|
||||
/docs/research/
|
||||
/build/
|
||||
|
||||
@ -4,43 +4,41 @@ import os
|
||||
CONFIG_FILE = 'config.json'
|
||||
|
||||
def load_config():
|
||||
"""
|
||||
Load the configuration from the JSON file or create a new one if it doesn't exist
|
||||
|
||||
Returns:
|
||||
dict: The configuration data
|
||||
"""
|
||||
config = {}
|
||||
|
||||
# Check if the configuration file exists
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r') as file:
|
||||
config = json.load(file)
|
||||
except (IOError, json.JSONDecodeError) as e:
|
||||
print(f"Error loading configuration: {e}")
|
||||
except:
|
||||
print("[-] Error: Failed to load config file")
|
||||
else:
|
||||
config = {
|
||||
'white_listed_programs': [],
|
||||
'auto_kill_programs': [],
|
||||
'kbd_names': ['kbd']
|
||||
}
|
||||
save_config(config)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def save_config(config):
|
||||
"""
|
||||
Save the configuration to the JSON file
|
||||
|
||||
Args:
|
||||
config (dict): The configuration data
|
||||
"""
|
||||
try:
|
||||
with open(CONFIG_FILE, 'w') as file:
|
||||
json.dump(config, file, indent=4)
|
||||
except IOError as e:
|
||||
print(f"Error saving configuration: {e}")
|
||||
|
||||
# Load the configuration
|
||||
config_data = load_config()
|
||||
|
||||
# Access and modify the settings
|
||||
whitelist = config_data.get('whitelist', [])
|
||||
autokill_list = config_data.get('autokill_list', [])
|
||||
other_setting = config_data.get('other_setting')
|
||||
|
||||
# Add a process to the whitelist
|
||||
whitelist.append(9999)
|
||||
|
||||
# Remove a process from the autokill list
|
||||
if 1234 in autokill_list:
|
||||
autokill_list.remove(1234)
|
||||
|
||||
# Modify the other_setting value
|
||||
config_data['other_setting'] = 'new_value'
|
||||
|
||||
# Save the modified configuration back to the JSON file
|
||||
save_config(config_data)
|
||||
except:
|
||||
print("[-] Error: Failed to save config file")
|
||||
|
||||
|
||||
240
src/keylogger_detector.py
Executable file
240
src/keylogger_detector.py
Executable file
@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
from config import CONFIG_FILE, load_config, save_config
|
||||
from utils import (
|
||||
check_root,
|
||||
check_packages,
|
||||
get_keyboard_device_files,
|
||||
get_real_path,
|
||||
get_pids_using_file,
|
||||
get_process_name,
|
||||
kill_processes
|
||||
)
|
||||
|
||||
# Global variables
|
||||
|
||||
auto_kill_option = False
|
||||
verbose_option = False
|
||||
safe_option = False
|
||||
|
||||
# Functions
|
||||
|
||||
def print_help():
|
||||
print('Usage: python3 keylogger_detector.py [OPTIONS]')
|
||||
print('Options:')
|
||||
print(' -h, --help\t\t\tPrint this help message')
|
||||
print(' -v, --verbose\t\t\tVerbose mode. Will cause additional information to be printed during execution')
|
||||
print(' -a, --auto-kill\t\tAutomatically kill blacklisted processes')
|
||||
print(' -s, --safe\t\t\tSafe mode. Asked to confirm before killing a process')
|
||||
|
||||
def set_input_options():
|
||||
"""
|
||||
Set input options based on command line arguments
|
||||
|
||||
Invalid arguments are ignored
|
||||
|
||||
Raises:
|
||||
SystemExit: If -h or --help is passed as an argument, the help message is printed and the program exits
|
||||
"""
|
||||
|
||||
global auto_kill_option, verbose_option, safe_option
|
||||
if len(sys.argv) > 1:
|
||||
for arg in sys.argv[1:]:
|
||||
if arg == '-h' or arg == '--help':
|
||||
print_help()
|
||||
sys.exit(0)
|
||||
elif arg == '-v' or arg == '--verbose':
|
||||
verbose_option = True
|
||||
elif arg == '-a' or arg == '--auto-kill':
|
||||
auto_kill_option = True
|
||||
elif arg == '-s' or arg == '--safe':
|
||||
safe_option = True
|
||||
|
||||
|
||||
def confirm_kill_procces(process_name, times=0):
|
||||
"""
|
||||
Prompt the user to confirm to kill a process.
|
||||
Should be used only in safe mode.
|
||||
|
||||
Args:
|
||||
process_name (str): Name of the process to kill
|
||||
times (int) : Number of times prompt has been displayed but neither y nor n where given. Defaults to 0.
|
||||
Use to limit promt attempts.
|
||||
|
||||
Returns:
|
||||
bool: True if user confirms the kill, False otherwise.
|
||||
|
||||
Raises:
|
||||
SystemExit: If the user has given invalid input more than 5 times, the program exits.
|
||||
"""
|
||||
if times > 5:
|
||||
print('Too many invalid inputs. Exiting.')
|
||||
sys.exit(1)
|
||||
if times > 0:
|
||||
print('Invalid input. Please enter y or n.')
|
||||
print(f'Do you want to kill {process_name}? (y/n)')
|
||||
answer = input()
|
||||
if answer == 'y':
|
||||
return True
|
||||
elif answer == 'n':
|
||||
return False
|
||||
else:
|
||||
return confirm_kill_procces(process_name, times+1)
|
||||
|
||||
def detect_keyloggers():
|
||||
"""
|
||||
Detect (userland) keylogger processes based on which processes have a keyboard file open (/dev/input/event*)
|
||||
|
||||
The main function of the program.
|
||||
Will attempt to detect keyloggers based on the config file, command line arguments and user input.
|
||||
Here the control flow and logic of the program are defined.
|
||||
"""
|
||||
|
||||
############################
|
||||
# 1. Setup and initialization
|
||||
############################
|
||||
global auto_kill_option, verbose_option, safe_option
|
||||
global CONFIG_FILE
|
||||
set_input_options()
|
||||
if verbose_option:
|
||||
print('[Verbose] Input options set')
|
||||
|
||||
check_root()
|
||||
if verbose_option:
|
||||
print('[Verbose] Root access checked')
|
||||
|
||||
check_packages()
|
||||
if verbose_option:
|
||||
print('[Verbose] Packages checked')
|
||||
|
||||
config = load_config()
|
||||
if verbose_option:
|
||||
print('[Verbose] Config file loaded')
|
||||
|
||||
white_listed_programs = config['white_listed_programs']
|
||||
auto_kill_programs = config['auto_kill_programs']
|
||||
kbd_names = config['kbd_names']
|
||||
if verbose_option:
|
||||
print('[Verbose] Config file parsed')
|
||||
|
||||
############################
|
||||
# 2. Get device files mapped to keyboard
|
||||
############################
|
||||
keyboard_device_files = get_keyboard_device_files(kbd_names)
|
||||
if verbose_option:
|
||||
print('[Verbose] Keyboard device files found:', keyboard_device_files)
|
||||
|
||||
############################
|
||||
# 3. Get pids using keyboard device files
|
||||
############################
|
||||
pids = []
|
||||
for device_file in keyboard_device_files:
|
||||
pids.append(get_pids_using_file(device_file))
|
||||
pids = sorted(list(set(pids)))
|
||||
if verbose_option:
|
||||
print('[Verbose] Process IDs using keyboard device files:', pids)
|
||||
|
||||
############################
|
||||
# 4. Get process names using pids
|
||||
############################
|
||||
process_names = []
|
||||
name_pid_dict = {}
|
||||
for pid in pids:
|
||||
name = get_process_name(pid)
|
||||
process_names.append(name)
|
||||
name_pid_dict[name].add(pid)
|
||||
process_names = sorted(list(set(process_names)))
|
||||
if verbose_option:
|
||||
print('[Verbose] Process names using keyboard device files:', process_names)
|
||||
|
||||
############################
|
||||
# 5.If auto_kill option is set, kill auto-killable processes
|
||||
############################
|
||||
if auto_kill_option:
|
||||
for name in process_names:
|
||||
if name in auto_kill_programs:
|
||||
if verbose_option:
|
||||
print('[Verbose] Auto-killable process found:', name)
|
||||
if safe_option:
|
||||
if confirm_kill_procces(name):
|
||||
kill_process(name_pid_dict[name])
|
||||
else:
|
||||
kill_process(name_pid_dict[name])
|
||||
|
||||
############################
|
||||
# 6. Identify suspicious processes, i.e. those not whitelisted
|
||||
############################
|
||||
suspicious_processes = []
|
||||
for name in process_names:
|
||||
if name not in white_listed_programs:
|
||||
suspicious_processes.append(name)
|
||||
if verbose_option:
|
||||
print('[Verbose] Suspicious processes found:', suspicious_processes)
|
||||
print('[Verbose] Suspicious processes not killed:', [name for name in suspicious_processes if name not in auto_kill_programs])
|
||||
print('[Verbose] Suspicious processes killed:', [name for name in suspicious_processes if name in auto_kill_programs])
|
||||
|
||||
|
||||
############################
|
||||
# 6.1 If no suspicious processes are found, exit
|
||||
############################
|
||||
if len(suspicious_processes) == 0:
|
||||
print("[+] No suspicious processes found")
|
||||
sys.exit(0)
|
||||
|
||||
############################
|
||||
# 7. Prompt user to chose which processes (not covered by auto kill if set) to kill
|
||||
############################
|
||||
print('[-]The following suspicious processes were found:')
|
||||
for name in suspicious_processes:
|
||||
print(f'\t{name}')
|
||||
print('Please enter the names of the processes to kill, separated by a space.')
|
||||
print('To not kill any just hit enter.')
|
||||
if safe_option:
|
||||
print('[Info] You are in safe mode. In safe mode you will be asked to confirm each kill.')
|
||||
else:
|
||||
print('[Info] Please be aware that killing an important process may cause your system to crash.')
|
||||
|
||||
to_kill = input().split()
|
||||
if len(to_kill) == 0:
|
||||
print('[+] No processes killed.')
|
||||
sys.exit(0)
|
||||
|
||||
if verbose_option:
|
||||
print('[Verbose] Processes to kill:', to_kill)
|
||||
if safe_option:
|
||||
for name in to_kill:
|
||||
for pid in name_pid_dict[name]:
|
||||
if confirm_kill_procces(name):
|
||||
kill_process(id)
|
||||
if verbose_option:
|
||||
print('[Verbose] Process killed:', name)
|
||||
else:
|
||||
for name in to_kill:
|
||||
for pid in name_pid_dict[name]:
|
||||
kill_process(id)
|
||||
if verbose_option:
|
||||
print('[Verbose] Process killed:', name)
|
||||
|
||||
|
||||
to_kill = list(set(to_kill))
|
||||
auto_kill_programs = list(set(auto_kill_programs)).append(to_kill)
|
||||
config['auto_kill_programs'] = auto_kill_programs
|
||||
white_listed_programs = list(set(white_listed_programs))
|
||||
config['white_listed_programs'] = white_listed_programs
|
||||
kbd_names = list(set(kbd_names))
|
||||
config['kbd_names'] = kbd_names
|
||||
save_config(config, CONFIG_FILE)
|
||||
if verbose_option:
|
||||
print('[Verbose] Config file saved')
|
||||
|
||||
if __name__ == '__main__':
|
||||
detect_keyloggers()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -13,37 +13,6 @@ auto_kill_option = False
|
||||
verbose_option = False
|
||||
safe_option = False
|
||||
|
||||
|
||||
# Load Configurations
|
||||
def load_config():
|
||||
|
||||
config = {}
|
||||
|
||||
# Check if file exists
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r') as file:
|
||||
config = json.load(file)
|
||||
except:
|
||||
print("[-] Error: Failed to load config file")
|
||||
else:
|
||||
config = {
|
||||
'white_listed_programs': [],
|
||||
'auto_kill_programs': [],
|
||||
'kbd_names': ['kbd']
|
||||
}
|
||||
save_config(config) # Save the default configuration
|
||||
|
||||
return config
|
||||
|
||||
# Save new configurations to json file
|
||||
def save_config(config):
|
||||
try:
|
||||
with open(CONFIG_FILE, 'w') as file:
|
||||
json.dump(config, file)
|
||||
except IOError as e:
|
||||
print(f"[-] Error! Failed to save config file: {e}")
|
||||
|
||||
# Check if the user is in sudo mode
|
||||
def check_sudo():
|
||||
if os.geteuid() != 0:
|
||||
113
src/utils.py
Executable file
113
src/utils.py
Executable file
@ -0,0 +1,113 @@
|
||||
import os # for path operations, getuid, kill
|
||||
import subprocess # for executing shell commands
|
||||
import signal # for sending signals to processes
|
||||
import sys # for exit
|
||||
|
||||
def check_root():
|
||||
"""
|
||||
Check if script is run as root(sudo).
|
||||
|
||||
Raises:
|
||||
SystemExit: If not run as root.
|
||||
"""
|
||||
if os.getuid() != 0:
|
||||
print("[-] Please run as root.")
|
||||
sys.exit(1)
|
||||
|
||||
def check_packages():
|
||||
"""
|
||||
Check if all required packages are installed.
|
||||
|
||||
Raises:
|
||||
SystemExit: If any packges is missing.
|
||||
"""
|
||||
packages = ['fuser', 'which']
|
||||
missing_packages = []
|
||||
|
||||
for package in packages:
|
||||
if subprocess.call(['which', package]) != 0:
|
||||
missing_packages.append(package)
|
||||
if len(missing_packages) > 0:
|
||||
print("[-] Missing packages: {}".format(', '.join(missing_packages)))
|
||||
sys.exit(1)
|
||||
|
||||
def get_keyboard_device_files(names):
|
||||
"""
|
||||
Get paths corresponding to keyboard device files by searching /dev/input/by-path.
|
||||
Uses get_real_path() to resolve symlinks.
|
||||
|
||||
Args:
|
||||
names (list): List of strings to use for searching. e.g.['kbd']
|
||||
|
||||
Returns:
|
||||
str: Path to keyboard device file.
|
||||
|
||||
"""
|
||||
keyboard_device_files = []
|
||||
for root, dirs, files in os.walk('/dev/input/by-path'):
|
||||
for file in files:
|
||||
if any(name in files for name in names):
|
||||
keyboard_device_files.append(get_real_path(os.path.join(root, file)))
|
||||
return keyboard_device_files
|
||||
|
||||
def get_real_path(path):
|
||||
"""
|
||||
Resolve a path of a file.
|
||||
Args:
|
||||
path (str): Path to a file. Possibly a symlink.
|
||||
|
||||
Returns:
|
||||
str: The resolved (real) path.
|
||||
"""
|
||||
if os.path.islink(path):
|
||||
return os.path.realpath(path)
|
||||
else:
|
||||
return path
|
||||
|
||||
def get_pids_using_file(path):
|
||||
"""
|
||||
Get all process IDs using a file. (Essentially a wrapper for fuser.)
|
||||
|
||||
Args:
|
||||
path (str): Path to a file. Usually /dev/input/eventX.
|
||||
|
||||
Returns:
|
||||
list: List of process IDs.
|
||||
|
||||
Raises:
|
||||
SystemExit: If fuser fails to run.
|
||||
"""
|
||||
try:
|
||||
pids = subprocess.check_output(['fuser', path]).decode('utf-8').split()
|
||||
except subprocess.CalledProcessError:
|
||||
print("[-] Error: fuser failed to run on", path)
|
||||
sys.exit(1)
|
||||
return pids
|
||||
|
||||
def get_process_name(pid):
|
||||
"""
|
||||
Get the name of a process.
|
||||
|
||||
Args:
|
||||
pid (int): Process ID.
|
||||
|
||||
Returns:
|
||||
str: Name of the process.
|
||||
"""
|
||||
with open('/proc/{}/comm'.format(pid)) as f:
|
||||
return f.read().strip()
|
||||
|
||||
def kill_processes(pids):
|
||||
"""
|
||||
Kill processes.
|
||||
|
||||
Args:
|
||||
pids (list): List of process IDs.
|
||||
"""
|
||||
|
||||
for pid in pids:
|
||||
try:
|
||||
os.kill(int(pid), signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
print("[-] Process {} not found.".format(pid))
|
||||
|
||||
Reference in New Issue
Block a user