diff --git a/.gitignore b/.gitignore index db4561e..f2e69d9 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,5 @@ docs/_build/ # PyBuilder target/ + +.idea diff --git a/README.md b/README.md index e32c05b..ce33400 100644 --- a/README.md +++ b/README.md @@ -2,40 +2,53 @@ « usbkill » is an anti-forensic kill-switch that waits for a change on your USB ports and then immediately shuts down your computer. -> The project is still under development but it does work and is effective. - To run: ```shell sudo python usbkill.py ``` +or +```shell +sudo python3 usbkill.py +``` + +Related project; same idea, but implemented as a Linux driver: https://github.com/NateBrune/silk-guardian + ### Why? -There are 3 reasons (maybe more?) to use this tool: +Some reasons to use this tool: -- In case the police or other thugs come busting in (or steal your laptop from you when you are at a public library as happened to Ross). The police commonly uses a « [mouse jiggler](http://www.amazon.com/Cru-dataport-Jiggler-Automatic-keyboard-Activity/dp/B00MTZY7Y4/ref=pd_bxgy_pc_text_y/190-3944818-7671348) » to keep the screensaver and sleep mode from activating. -- You don't want someone to install backdoors or malware on your computer or to retrieve documents from your computer via USB. -- You want to improve the security of your (Full Disk Encrypted) home or corporate server (e.g. Your Raspberry). +- In case the police or other thugs come busting in (or steal your laptop from you when you are at a public library, as happened to Ross). The police commonly uses a « [mouse jiggler](http://www.amazon.com/Cru-dataport-Jiggler-Automatic-keyboard-Activity/dp/B00MTZY7Y4/ref=pd_bxgy_pc_text_y/190-3944818-7671348) » to keep the screensaver and sleep mode from activating. +- You don’t want someone to add or copy documents to or from your computer via USB. +- You want to improve the security of your (encrypted) home or corporate server (e.g. Your Raspberry). -> **[!] Important**: Make sure to use full disk encryption! Otherwise they will get in anyway. +> **[!] Important**: Make sure to use disk encryption for all folders that contain information you want to be private. Otherwise they will get it anyway. Full disk encryption is the easiest and surest option if available > **Tip**: Additionally, you may use a cord to attach a USB key to your wrist. Then insert the key into your computer and start usbkill. If they steal your computer, the USB will be removed and the computer shuts down immediately. ### Feature List - -- Compability with Linux, *BSD and OS X -- Shutdown the computer when there is USB activity -- Ability to whitelist an USB device -- Ability to change the check interval (default: 0.5) -- Work perfectly in sleep mode (OS X) -- Low memory consumption -- No dependency except Python - -and more to come! Custom commands for when a USB change is observed will be implemented. +(version 1.0-rc.4) +- Compatible with Linux, *BSD and OS X. +- Shutdown the computer when there is USB activity. +- Customizable. Define which commands should be executed just before shut down. +- Ability to whitelist a USB device. +- Ability to change the check interval (default: 250ms). +- Ability to melt the program on shut down. +- RAM and swap wiping. +- Works with sleep mode (OS X). +- No dependency except secure-delete iff you want usbkill to delete files/folders for you or if you want to wipe RAM or swap. ```sudo apt-get install secure-delete``` +- Sensible defaults + + +### Supported command line arguments (partially for devs): + +- -h or --help: show help message, exit. +- --version: show version of the program, exit. +- --no-shut-down: if a malicious change on the USB ports is detected, execute all the (destructive) commands you defined in settings.ini, but don’t turn off the computer. +- --cs: Copy program folder settings.ini to /etc/usbkill/settings.ini ### Contact [hephaestos@riseup.net](mailto:hephaestos@riseup.net) - PGP/GPG Fingerprint: 8764 EF6F D5C1 7838 8D10 E061 CF84 9CE5 42D0 B12B - diff --git a/install/usbkill b/install/usbkill new file mode 100644 index 0000000..5c37412 --- /dev/null +++ b/install/usbkill @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +# _ _ _ _ _ +# | | | | (_) | | +# _ _ ___| |__ | | _ _| | | +# | | | |/___) _ \| |_/ ) | | | +# | |_| |___ | |_) ) _ (| | | | +# |____/(___/|____/|_| \_)_|\_)_) +# +# +# Hephaestos - 8764 EF6F D5C1 7838 8D10 E061 CF84 9CE5 42D0 B12B +# +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import os + +# Check if program is run as root, else exit. +# Root is needed to power off the computer. +if not os.geteuid() == 0: + import sys + sys.exit("\n[ERROR] This program needs to run as root.\n") + +import usbkill +usbkill.go() diff --git a/install/usbkill.ini b/install/usbkill.ini new file mode 100644 index 0000000..8750432 --- /dev/null +++ b/install/usbkill.ini @@ -0,0 +1,153 @@ +# _ _ _ _ _ +# | | | | (_) | | +# _ _ ___| |__ | | _ _| | | +# | | | |/___) _ \| |_/ ) | | | +# | |_| |___ | |_) ) _ (| | | | +# |____/(___/|____/|_| \_)_|\_)_) +# +# +# Hephaestos - 8764 EF6F D5C1 7838 8D10 E061 CF84 9CE5 42D0 B12B +# +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +[config] + +######################## ######################## +# First are settings about usbkill behavior, then about the kill/destruction commands +######################## ######################## + +######################## +# Usbkill behavior commands: + +# whitelist +# sleep +# log_file +######################## + +# Whitelist command lists the USB ids that you want whitelisted +# How to get the correct usbid for your trusted USB device? +# BSD/Linux: run "lsusb", the usbid will looks like this: 0123:9abc +# Mac OS X: run "system_profiler SPUSBDataType" in the terminal and find the Vendor/Product ID, it will looks like this: +# > Product ID: 0x8403 +# > Vendor ID: 0x05ac (Apple Inc.) +# Take the 4 characters after the 0x and merge them (Vendor ID first), it will look like: 05ac:8403 +# Be warned! Other parties can copy your trusted usbid to another usb device! +# use whitelist command and single space separation as follows: +# whitelist = ["4c2a:d2b0", "0b2d:a2c4"] +# To allow multiple (2 and 4) USBs with same id: [ {"4c2a:d2b0":2}, {"0b2d:a2c4":4}] or +# [ "4c2a:d2b0","4c2a:d2b0", "0b2d:a2c4", ..., "0b2d:a2c4" ] (be consistent) +whitelist = [] + +# allow for a certain amount of sleep time between checks, e.g. 0.25 seconds: +sleep = 0.25 + +# Log file location: +log_file = /var/log/usbkill/usbkill.log + + +######################## +# Usbkill destruction commands: +# N.B: all these commands are executed in the following order, except that ram and swap-wipe are in parallel. + +# remove_file_cmd +# files_to_remove +# folders_to_remove +# melt_usbkill + +# kill_commands +# do_sync + +# wipe_ram +# wipe_ram_cmd +# wipe_swap +# wipe_swap_cmd +######################## + +########## +# Remove commands: + +# remove_file_cmd +# files_to_remove +# folders_to_remove +# melt_usbkill +########## + +# use srm to remove files. +# Check srm --help for available options +remove_file_cmd = srm -l + +# What files should be removed upon a kill? +# Provide absolute paths to the files (paths that start with '/' or '~'). +# Use " not ' to define the strings, e.g.: +# files_to_remove = ["~/Desktop/contacts.txt", "~/Desktop/dpr_journal.txt"] +files_to_remove = [] + +# What folders should be removed upon a kill? +# Provide absolute paths to the files (paths that start with '/' or '~'). +# Content in folders will be removed recursively +# Use " not ' to define the strings, e.g.: +# folders_to_remove = ["~/Desktop/sensitive/", "~/Desktop/dpr_journal_entries/"] +folders_to_remove = [ ] + +# Remove log (folder) and settings (folder) and usbkill program (folder) upon kill? +# This might be usefull if you only encrypt portions of your disk (home folder or volumes). +# Make sure to sync the system (using do_sync=True) if this is a critical feature for you. +# (True/False) +melt_usbkill = False + + +########## +# Custom commands: + +# kill_commands +# do_sync +########## + +# Custom kill commands that can not be specified using above described mechanisms. +# This is where you want to release volumes, etc. +# These commands will run in order and as root, as the last commands. +# Sync should be activated once more if you want to sync +# Use " not ' to define the strings, e.g.: +# kill_commands = [ "bash ~/scripts/destroy.sh", "sync" ] +kill_commands = [ ] + +# Should usbkill sync the file system for you? +# This should not be a problem on most computers. +# Sync will save some of your work to disk before killing your computer. +do_sync = True + +########## +# Wipe commands: +# Note: if both are set, the commands are excecuted in parallel. +# These commands will take a long time to complete and also make the device unresponsive! +# These commands will greatly stall the halting of your system! + +# do_wipe_swap +# wipe_swap_cmd +# do_wipe_ram +# wipe_ram_cmd +########## + + +# Set do_wipe_ram to True in order clean the ram. +do_wipe_ram = False +# Check sdmem --help for available options +wipe_ram_cmd = sdmem -fll + +# Set do_wipe_swap to True in order clean the swap. +do_wipe_swap = False +# Check sswap --help for available options +wipe_swap_cmd = sswap -l + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1daa7a7 --- /dev/null +++ b/setup.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python + +# _ _ _ _ _ +# | | | | (_) | | +# _ _ ___| |__ | | _ _| | | +# | | | |/___) _ \| |_/ ) | | | +# | |_| |___ | |_) ) _ (| | | | +# |____/(___/|____/|_| \_)_|\_)_) +# +# +# Hephaestos - 8764 EF6F D5C1 7838 8D10 E061 CF84 9CE5 42D0 B12B +# +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from distutils.core import setup +from os import path + +DIRNAME = path.dirname(path.realpath(__file__)) + +name = lambda x : path.join(DIRNAME, x) + +setup(name='usbkill', + version='1.0-rc.4', + description='usbkill is an anti-forensic kill-switch that waits for a change on your USB ports and then immediately shuts down your computer.', + author='Hephaestos', + author_email='hephaestos@riseup.net', + license='GPLv3', + url='https://github.com/hephaest0s/usbkill', + + packages=['usbkill'], + scripts=[name('install/usbkill')], + data_files=[ ('/etc/', [ name('install/usbkill.ini') ]) ] + ) + diff --git a/usbkill.py b/usbkill.py deleted file mode 100644 index 5b32eef..0000000 --- a/usbkill.py +++ /dev/null @@ -1,260 +0,0 @@ -# _ _ _ _ _ -# | | | | (_) | | -# _ _ ___| |__ | | _ _| | | -# | | | |/___) _ \| |_/ ) | | | -# | |_| |___ | |_) ) _ (| | | | -# |____/(___/|____/|_| \_)_|\_)_) -# -# -# Hephaestos - 8764 EF6F D5C1 7838 8D10 E061 CF84 9CE5 42D0 B12B -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import re -import subprocess -import platform -import os, sys, signal -from time import time, sleep - -# Get the current platform -CURRENT_PLATFORM = platform.system().upper() - -# Darwin specific library -if CURRENT_PLATFORM.startswith("DARWIN"): - import plistlib - -# We compile this function beforehand for efficiency. -DEVICE_RE = [ re.compile(".+ID\s(?P\w+:\w+)"), re.compile("0x([0-9a-z]{4})") ] - -# Set the settings filename here -SETTINGS_FILE = '/etc/usbkill/settings'; - - -help_message = """ -usbkill is a simple program with one goal: quickly shutdown the computer when a usb is inserted or removed. -It logs to /var/log/usbkill/kills.log -You can configure a whitelist of USB ids that are acceptable to insert and the remove. -The USB id can be found by running the command 'lsusb'. -Settings can be changed in /etc/usbkill/settings -In order to be able to shutdown the computer, this program needs to run as root. -""" - -def log(msg): - logfile = "/var/log/usbkill/usbkill.log" - with open(logfile, 'a+') as log: - contents = '\n{0} {1}\nCurrent state:'.format(str(time()), msg) - log.write(contents) - - # Log current USB state - if CURRENT_PLATFORM.startswith("DARWIN"): - os.system("system_profiler SPUSBDataType >> " + logfile) - else: - os.system("lsusb >> " + logfile) - -def kill_computer(): - # Log what is happening: - log("Detected a USB change. Dumping the list of connected devices and killing the computer...") - - # Sync the filesystem so that the recent log entry does not get lost. - os.system("sync") - - # Poweroff computer immediately - if CURRENT_PLATFORM.startswith("DARWIN"): - # OS X (Darwin) - Will halt ungracefully, without signaling apps - os.system("killall Finder && killall loginwindow && halt -q") - elif CURRENT_PLATFORM.endswith("BSD"): - # BSD-based systems - Will shutdown - os.system("shutdown -h now") - else: - # Linux-based systems - Will shutdown - os.system("poweroff -f") - -def lsusb(): - # A Python version of the command 'lsusb' that returns a list of connected usbids - if CURRENT_PLATFORM.startswith("DARWIN"): - # Use OS X system_profiler (native and 60% faster than lsusb port) - df = subprocess.check_output("system_profiler SPUSBDataType -xml -detailLevel mini", shell=True) - if sys.version_info[0] == 2: - df = plistlib.readPlistFromString(df) - elif sys.version_info[0] == 3: - df = plistlib.loads(df) - - devices = [] - def check_inside(result): - """ - I suspect this function can become more readable. - Function currently depends on a side effect, which is not necessary. - """ - # Do not take devices with Built-in_Device=Yes - try: - result["Built-in_Device"] - except KeyError: - - # Check if vendor_id/product_id is available for this one - try: - assert "vendor_id" in result and "product_id" in result - # Append to the list of devices - devices.append(DEVICE_RE[1].findall(result["vendor_id"])[0] + ':' + DEVICE_RE[1].findall(result["product_id"])[0]) - # debug: devices.append(result["vendor_id"] + ':' + result["product_id"]) - except AssertionError: {} - - # Check if there is items inside - try: - # Looks like, do the while again - for result_deep in result["_items"]: - # Check what's inside the _items array - check_inside(result_deep) - - except KeyError: {} - - # Run the loop - for result in df[0]["_items"]: - check_inside(result) - - return devices - else: - # Use lsusb on linux and bsd - return DEVICE_RE[0].findall(subprocess.check_output("lsusb", shell=True).decode('utf-8').strip()) - -def settings_template(filename): - # Make sure there is the settings folder - if not os.path.isdir("/etc/usbkill/"): - os.mkdir("/etc/usbkill/") - - # Make sure there is a settings file - if not os.path.isfile(filename): - # Pre-populate the settings file if it does not exist yet - with open(filename, 'w') as f: - f.write("# whitelist command lists the usb ids that you want whitelisted\n") - f.write("# find the correct usbid for your trusted usb using the command 'lsusb'\n") - f.write("# usbid looks something line 0123:9abc\n") - f.write("# Be warned! other parties can copy your trusted usbid to another usb device!\n") - f.write("# use whitelist command and single space separation as follows:\n") - f.write("# whitelist usbid1 usbid2 etc\n") - f.write("whitelist \n\n") - f.write("# allow for a certain amount of sleep time between checks, e.g. 0.25 seconds:\n") - f.write("sleep 0.25\n") - -def load_settings(filename): - # read all lines of settings file - with open(filename, 'r') as f: - lines = f.readlines() - - # Find the only two supported settings - devices = None - sleep_time = None - for line in lines: - if line[:10] == "whitelist ": - devices = line.replace("\n","").replace(" "," ").split(" ")[1:] - if line[:6] == "sleep ": - sleep_time = float(line.replace("\n","").replace(" "," ").split(" ").pop()) - - assert not None in [devices, sleep_time], "[ERROR] Please set the 'sleep' and 'whitelist' parameters in '/etc/usbkill/settings'!" - assert sleep_time > 0.0, "[ERROR] Please allow for positive non-zero 'sleep' delay between USB checks!" - return devices, sleep_time - -def loop(whitelisted_devices, sleep_time, killer): - # Main loop that checks every 'sleep_time' seconds if computer should be killed. - # Allows only whitelisted usb devices to connect! - # Does not allow usb device that was present during program start to disconnect! - start_devices = lsusb() - acceptable_devices = set(start_devices + whitelisted_devices) - - # Write to logs that loop is starting: - msg = "[INFO] Started patrolling the USB ports every " + str(sleep_time) + " seconds..." - log(msg) - print(msg) - - # Main loop - while True: - # List the current usb devices - current_devices = lsusb() - - # Check that no usbids are connected twice. - # Two devices with same usbid implied a usbid copy attack - if not len(current_devices) == len(set(current_devices)): - killer() - - # Check that all current devices are in the set of acceptable devices - for device in current_devices: - if device not in acceptable_devices: - killer() - - # Check that all start devices are still present in current devices - # Prevent multiple devices with the same Vendor/Product ID to be connected - for device in start_devices: - if device not in current_devices: - killer() - - sleep(sleep_time) - -def exit_handler(signum, frame): - print("\n[INFO] Exiting because exit signal was received\n") - log("[INFO] Exiting because exit signal was received") - sys.exit(0) - -if __name__=="__main__": - # Splash - print(" _ _ _ _ _ \n" + - " | | | | (_) | | \n" + - " _ _ ___| |__ | | _ _| | | \n" + - " | | | |/___) _ \| |_/ ) | | | \n" + - " | |_| |___ | |_) ) _ (| | | | \n" + - " |____/(___/|____/|_| \_)_|\_)_)\n") - - # Check arguments - args = sys.argv[1:] - - # Check for help - if '-h' in args or '--help' in args: - sys.exit(help_message) - - # Check if dev mode - killer = kill_computer - if '--dev' in args: - print("[NOTICE] Running in dev-mode.") - killer = lambda : sys.exit("Dev-mode, kill overwritten and exiting.") - args.remove('--dev') - - # Check all other args - if len(args) > 0: - sys.exit("\n[ERROR] Argument not understood. Can only understand -h\n") - - # Check if program is run as root, else exit. - # Root is needed to power off the computer. - if not os.geteuid() == 0: - sys.exit("\n[ERROR] This program needs to run as root.\n") - - # Warn the user if he does not have FileVault - if CURRENT_PLATFORM.startswith("DARWIN"): - if subprocess.check_output("fdesetup isactive", shell=True).strip() != "true": - print("[NOTICE] FileVault is disabled. Sensitive data SHOULD be encrypted.") - - # Make sure there is a logging folder - if not os.path.isdir("/var/log/usbkill/"): - os.mkdir("/var/log/usbkill/") - - # Register handlers for clean exit of loop - for sig in [signal.SIGINT, signal.SIGTERM, signal.SIGQUIT, ]: - signal.signal(sig, exit_handler) - - # Make sure settings file is available - settings_template(SETTINGS_FILE) - - # Load settings - whitelisted_devices, sleep_time = load_settings(SETTINGS_FILE) - - # Start main loop - loop(whitelisted_devices, sleep_time, killer) diff --git a/usbkill/__init__.py b/usbkill/__init__.py new file mode 100644 index 0000000..898adc0 --- /dev/null +++ b/usbkill/__init__.py @@ -0,0 +1 @@ +from .usbkill import go diff --git a/usbkill/usbkill.py b/usbkill/usbkill.py new file mode 100644 index 0000000..1b85d26 --- /dev/null +++ b/usbkill/usbkill.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python + +# _ _ _ _ _ +# | | | | (_) | | +# _ _ ___| |__ | | _ _| | | +# | | | |/___) _ \| |_/ ) | | | +# | |_| |___ | |_) ) _ (| | | | +# |____/(___/|____/|_| \_)_|\_)_) +# +# +# Hephaestos - 8764 EF6F D5C1 7838 8D10 E061 CF84 9CE5 42D0 B12B +# +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__version__ = "1.0-rc.4" + +import re +import subprocess +import platform +import os, sys, signal +from time import sleep +from datetime import datetime + +# Get the current platform +CURRENT_PLATFORM = platform.system().upper() + +# Darwin specific library +if CURRENT_PLATFORM.startswith("DARWIN"): + import plistlib + +# We compile this function beforehand for efficiency. +DEVICE_RE = [ re.compile(".+ID\s(?P\w+:\w+)"), re.compile("0x([0-9a-z]{4})") ] + +# Set the settings filename here +SETTINGS_FILE = '/etc/usbkill.ini' + +help_message = """ +usbkill is a simple program with one goal: quickly shutdown the computer when a USB is inserted or removed. +Events are logged in /var/log/usbkill/kills.log +You can configure a whitelist of USB ids that are acceptable to insert and the remove. +The USB id can be found by running the command 'lsusb'. +Settings can be changed in /etc/usbkill.ini +In order to be able to shutdown the computer, this program needs to run as root. + +Options: + -h --help: Show this help + --version: Print usbkill version and exit + --cs: Copy program folder usbkill.ini to /etc/usbkill/usbkill.ini + --no-shut-down: Execute all the (destructive) commands you defined in usbkill.ini, + but don't turn off the computer +""" + +class DeviceCountSet(dict): + # Warning: this class has behavior you may not expect. + # This is because the overloaded __add__ is a mixture of add and overwrite + def __init__(self, list): + count = dict() + for i in list: + if type(i) == dict: + count[i.keys()[0]] = i.values()[0] + elif i in count: + count[i] += 1 + else: + count[i] = 1 + super(DeviceCountSet,self).__init__(count) + + def __add__(self, other): + newdic = dict(self) + if type(other) in ['tuple', 'list']: + for k in other: + newdic[k] = 1 + else: + for k,v in other.items(): + if k in newdic: + if newdic[k] < v: + newdic[k] = v + else: + newdic[k] = v + return newdic + +def log(settings, msg): + log_file = settings['log_file'] + + contents = '\n{0} {1}\nCurrent state:\n'.format(str(datetime.now()), msg) + with open(log_file, 'a+') as log: + log.write(contents) + + # Log current USB state + if CURRENT_PLATFORM.startswith("DARWIN"): + os.system("system_profiler SPUSBDataType >> " + log_file) + else: + os.system("lsusb >> " + log_file) + +def shred(settings): + shredder = settings['remove_file_cmd'] + + # List logs and settings to be removed + if settings['melt_usbkill']: + settings['folders_to_remove'].append(os.path.dirname(settings['log_file'])) + settings['folders_to_remove'].append(os.path.dirname(SETTINGS_FILE)) + usbkill_folder = os.path.dirname(os.path.realpath(__file__)) + if usbkill_folder.upper().startswith('USB'): + settings['folders_to_remove'].append(usbkill_folder) + else: + settings['files_to_remove'].append(os.path.realpath(__file__)) + settings['files_to_remove'].append(usbkill_folder + "/usbkill.ini") + + # Remove files and folders + for _file in settings['files_to_remove'] + settings['folders_to_remove']: + os.system(shredder + _file ) + +def kill_computer(settings): + # Log what is happening: + if not settings['melt_usbkill']: # No need to spend time on logging if logs will be removed + log(settings, "Detected a USB change. Dumping the list of connected devices and killing the computer...") + + # Shred as specified in settings + shred(settings) + + # Execute kill commands in order. + for command in settings['kill_commands']: + os.system(command) + + if settings['do_sync']: + # Sync the filesystem to save recent changes + os.system("sync") + else: + # If syncing is risky because it might take too long, then sleep for 5ms. + # This will still allow for syncing in most cases. + sleep(0.05) + + # Wipe ram and/or swap + if settings['do_wipe_ram'] and settings['do_wipe_swap']: + os.system(settings['wipe_ram_cmd'] + " & " + settings['wipe_swap_cmd']) + elif settings['do_wipe_ram']: + os.system(settings['wipe_ram_cmd']) + elif settings['do_wipe_swap']: + os.system(settings['wipe_swap_cmd']) + + if settings['shut_down']: # (Use argument --no-shut-down to prevent a shutdown.) + # Finally poweroff computer immediately + if CURRENT_PLATFORM.startswith("DARWIN"): + # OS X (Darwin) - Will halt ungracefully, without signaling apps + os.system("killall Finder ; killall loginwindow ; halt -q") + elif CURRENT_PLATFORM.endswith("BSD"): + # BSD-based systems - Will shutdown + os.system("shutdown -h now") + else: + # Linux-based systems - Will shutdown + os.system("poweroff -f") + + # Exit the process to prevent executing twice (or more) all commands + sys.exit(0) + +def lsusb_darwin(): + # Use OS X system_profiler (native and 60% faster than lsusb port) + df = subprocess.check_output("system_profiler SPUSBDataType -xml -detailLevel mini", shell=True) + if sys.version_info[0] == 2: + df = plistlib.readPlistFromString(df) + elif sys.version_info[0] == 3: + df = plistlib.loads(df) + + def check_inside(result, devices): + """ + I suspect this function can become more readable. + """ + # Do not take devices with Built-in_Device=Yes + try: + result["Built-in_Device"] + except KeyError: + + # Check if vendor_id/product_id is available for this one + try: + # Ensure vendor_id and product_id are present + assert "vendor_id" in result and "product_id" in result + + try: + vendor_id = DEVICE_RE[1].findall(result["vendor_id"])[0] + except IndexError: + # Assume this is not an standard vendor_id (probably apple_vendor_id) + vendor_id = result["vendor_id"]; + + try: + product_id = DEVICE_RE[1].findall(result["product_id"])[0] + except IndexError: + # Assume this is not an standard product_id (probably apple_vendor_id) + product_id = result["product_id"]; + + # Append to the list of devices + devices.append(vendor_id + ':' + product_id) + + except AssertionError: {} + + # Check if there is items inside + try: + # Looks like, do the while again + for result_deep in result["_items"]: + # Check what's inside the _items array + check_inside(result_deep, devices) + + except KeyError: {} + + # Run the loop + devices = [] + for result in df[0]["_items"]: + check_inside(result, devices) + return devices + +def lsusb(): + # A Python version of the command 'lsusb' that returns a list of connected usbids + if CURRENT_PLATFORM.startswith("DARWIN"): + # Use OS X system_profiler (native, 60% faster, and doesn't need the lsusb port) + return DeviceCountSet(lsusb_darwin()) + else: + # Use lsusb on linux and bsd + return DeviceCountSet(DEVICE_RE[0].findall(subprocess.check_output("lsusb", shell=True).decode('utf-8').strip())) + +def program_present(program): + if sys.version_info[0] == 3: + # Python3 + from shutil import which + return which(program) != None + + else: + """ + Test if an executable exist in Python2 + -> http://stackoverflow.com/a/377028 + """ + def is_exe(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + + fpath, fname = os.path.split(program) + if fpath and is_exe(program): + return True + else: + for path in os.environ["PATH"].split(os.pathsep): + path = path.strip('"') + exe_file = os.path.join(path, program) + if is_exe(exe_file): + return True + return False + +def load_settings(filename): + # Libraries that are only needed in this function: + from json import loads as jsonloads + if sys.version_info[0] == 3: + # Python3 + import configparser + def get_setting(name, gtype=''): + """ + configparser: Compatibility layer for Python 2/3 + Function currently depends on a side effect, which is not necessary. + """ + section = config['config'] + if gtype == 'FLOAT': + return section.getfloat(name) + elif gtype == 'INT': + return section.getint(name) + elif gtype == 'BOOL': + return section.getboolean(name) + return section[name] + else: + #Python2 + import ConfigParser as configparser + def get_setting(name, gtype=''): + if gtype == 'FLOAT': + return config.getfloat('config', name) + elif gtype == 'INT': + return config.getint('config', name) + elif gtype == 'BOOL': + return config.getboolean('config', name) + return config.get('config', name) + + config = configparser.ConfigParser() + + # Read all lines of settings file + config.read(filename) + + # Build settings + settings = dict({ + 'sleep_time' : get_setting('sleep', 'FLOAT'), + 'whitelist': DeviceCountSet(jsonloads(get_setting('whitelist').strip())), + 'log_file': get_setting('log_file'), + 'melt_usbkill' : get_setting('melt_usbkill', 'BOOL'), + 'remove_file_cmd' : get_setting('remove_file_cmd') + " ", + 'files_to_remove' : jsonloads(get_setting('files_to_remove').strip()), + 'folders_to_remove' : jsonloads(get_setting('folders_to_remove').strip()), + 'do_sync' : get_setting('do_sync', 'BOOL'), + 'kill_commands': jsonloads(get_setting('kill_commands').strip()) + }) + + settings['do_wipe_ram'] = False + if get_setting('do_wipe_ram', 'BOOL'): + settings['do_wipe_ram'] = True + settings['wipe_ram_cmd'] = get_setting('wipe_ram_cmd') + " " + + settings['do_wipe_swap'] = False + if get_setting('do_wipe_swap', 'BOOL'): + settings['do_wipe_swap'] = True + settings['wipe_swap_cmd'] = get_setting('wipe_swap_cmd') + " " + + return settings + +def loop(settings): + # Main loop that checks every 'sleep_time' seconds if computer should be killed. + # Allows only whitelisted usb devices to connect! + # Does not allow usb device that was present during program start to disconnect! + start_devices = lsusb() + acceptable_devices = start_devices + settings['whitelist'] + + # Write to logs that loop is starting: + msg = "[INFO] Started patrolling the USB ports every " + str(settings['sleep_time']) + " seconds..." + log(settings, msg) + print(msg) + + # Main loop + while True: + # List the current usb devices + current_devices = lsusb() + + # Check that all current devices are in the set of acceptable devices + # and their cardinality is less than or equal to what is allowed + for device, count in current_devices.items(): + if device not in acceptable_devices: + # A device with unknown usbid detected + kill_computer(settings) + if count > acceptable_devices[device]: + # Count of a usbid is larger than what is acceptable (too many devices sharing usbid) + kill_computer(settings) + + # Check that all start devices are still present in current devices + # and their cardinality still the same + for device, count in start_devices.items(): + if device not in current_devices: + # A usbid has disappeared completely + kill_computer(settings) + if count > current_devices[device]: + # Count of a usbid device is lower than at program start (not enough devices for given usbid) + kill_computer(settings) + + sleep(settings['sleep_time']) + +def startup_checks(): + # Splash + print(" _ _ _ _ _ \n" + + " | | | | (_) | | \n" + + " _ _ ___| |__ | | _ _| | | \n" + + " | | | |/___) _ \| |_/ ) | | | \n" + + " | |_| |___ | |_) ) _ (| | | | \n" + + " |____/(___/|____/|_| \_)_|\_)_)\n") + + # Check arguments + args = sys.argv[1:] + + # Check for help + if '-h' in args or '--help' in args: + sys.exit(help_message) + + if '--version' in args: + print('usbkill', __version__) + sys.exit(0) + + copy_settings = False + if '--cs' in args: + args.remove('--cs') + copy_settings = True + + shut_down = True + if '--no-shut-down' in args: + print("[NOTICE] Ready to execute all the (potentially destructive) commands, but NOT shut down the computer.") + args.remove('--no-shut-down') + shut_down = False + + # Check all other args + if len(args) > 0: + sys.exit("\n[ERROR] Argument not understood. Can only understand -h\n") + + # Check if program is run as root, else exit. + # Root is needed to power off the computer. + if not os.geteuid() == 0: + sys.exit("\n[ERROR] This program needs to run as root.\n") + + # Warn the user if he does not have FileVault + if CURRENT_PLATFORM.startswith("DARWIN"): + try: + # fdesetup return exit code 0 when true and 1 when false + subprocess.check_output(["/usr/bin/fdesetup", "isactive"]) + except subprocess.CalledProcessError: + print("[NOTICE] FileVault is disabled. Sensitive data SHOULD be encrypted.") + + # On first time use copy usbkill.ini to /etc/usebkill.ini + # If dev-mode, always copy and don't remove old settings + if not os.path.isfile(SETTINGS_FILE) or copy_settings: + sources_path = os.path.dirname(os.path.realpath(__file__)) + if not os.path.isfile(os.path.join(sources_path, "install/usbkill.ini")): + sys.exit("\n[ERROR] You have lost your settings file. Get a new copy of the usbkill.ini and place it in /etc/ or in " + sources_path + "/\n") + print("[NOTICE] Copying install/setting.ini to " + SETTINGS_FILE ) + os.system("cp " + sources_path + "install/usbkill.ini " + SETTINGS_FILE) + + # Load settings + settings = load_settings(SETTINGS_FILE) + settings['shut_down'] = shut_down + + # Make sure no spaces a present in paths to be wiped. + for name in settings['folders_to_remove'] + settings['files_to_remove']: + if ' ' in name: + msg += "[ERROR][WARNING] '" + name + "'as specified in your usbkill.ini contains a space.\n" + sys.exit(msg) + + # Make sure srm is present if it will be used. + if settings['melt_usbkill'] or len(settings['folders_to_remove'] + settings['files_to_remove']) > 0: + if not program_present('srm'): + sys.exit("[ERROR] usbkill configured to destroy data, but srm not installed.\n") + if not settings['remove_file_cmd'].startswith('srm'): + sys.exit("[ERROR] remove_file_command should start with `srm'. srm should be used for automated data overwrite.\n") + # Make sure sdmem is present if it will be used. + if settings['do_wipe_ram']: + if not program_present('sdmem'): + sys.exit("[ERROR] usbkill configured to destroy data, but srm not installed.\n") + if not settings['wipe_ram_cmd'].startswith('sdmem'): + sys.exit("[ERROR] wipe_ram_cmd should start with `sdmem'. sdmem should be used for automated data overwrite.\n") + # Make sure sswap is present if it will be used. + if settings['do_wipe_swap']: + if not program_present('sswap'): + sys.exit("[ERROR] usbkill configured to destroy data, but srm not installed.\n") + if not settings['wipe_swap_cmd'].startswith('sswap'): + sys.exit("[ERROR] wipe_swap_cmd should start with `sswap'. sswap should be used for automated data overwrite.\n") + + # Make sure there is a logging folder + log_folder = os.path.dirname(settings['log_file']) + if not os.path.isdir(log_folder): + os.mkdir(log_folder) + + return settings + +def go(): + # Run startup checks and load settings + settings = startup_checks() + + # Define exit handler now that settings are loaded... + def exit_handler(signum, frame): + print("\n[INFO] Exiting because exit signal was received\n") + log(settings, "[INFO] Exiting because exit signal was received") + sys.exit(0) + + # Register handlers for clean exit of program + for sig in [signal.SIGINT, signal.SIGTERM, signal.SIGQUIT, ]: + signal.signal(sig, exit_handler) + + # Start main loop + loop(settings) + +if __name__=="__main__": + go() + +