Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First pull request with several commits to review. #20

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c4979ea
Adding FreeBSD support.
ozfive Mar 3, 2023
60e90a8
Fixing some text in README.md file.
ozfive Mar 3, 2023
6f97079
Removed Support for FreeBSD as a TODO item.
ozfive Mar 3, 2023
8b905d8
Refactored main() to reduce its Cognitive Complexity from 22 to 15.
ozfive Mar 3, 2023
91ff3cb
New Exempt Devices functionality in config file.
ozfive Mar 3, 2023
41feb6e
Expanding functionality of the checkExe method that checks if the pat…
ozfive Mar 3, 2023
ef040b2
Refactored enumerateDevices() to reduce its Cognitive Complexity.
ozfive Mar 3, 2023
693621e
Refactored shutdownSequence(conf *config) to reduce its Cognitive Com…
ozfive Mar 3, 2023
593c865
Calling Windows DLL CfgMgr32.dll to enumerate the devices in usb_enum…
ozfive Mar 3, 2023
0b0b66b
The DLL is now properly unloaded using a defer statement after loadin…
ozfive Mar 3, 2023
7257ff3
Add support for enumerating USB devices on macOS using the IOKit fram…
ozfive Mar 4, 2023
58ba675
Fixed comment word wrap.
ozfive Mar 4, 2023
00935bd
Device whiteliting removed as this feature was just added.
ozfive Mar 4, 2023
6d3e95e
Load kernel32.dll and initiate a forced shutdown via ExitWindowsEx fu…
ozfive Mar 4, 2023
b4e1408
Call the reboot system call directly using syscall package for Linux …
ozfive Mar 4, 2023
98b9d02
This commit adds the ability to initiate a system shutdown on FreeBSD…
ozfive Mar 4, 2023
e29c63a
Adding more clarity in a comment on what the Windows shutdownNow func…
ozfive Mar 4, 2023
1ad8c84
Adding more clarity in a comment on what enumerateDevices does in usb…
ozfive Mar 4, 2023
67163ad
Removed duplicate comment.
ozfive Mar 4, 2023
bbc98d1
Removed duplicate comment and added a comment to shutdown_freebsd.go …
ozfive Mar 4, 2023
1294802
Adding a lookup for the path of lsusb to send to stat. For some reaso…
ozfive Mar 7, 2023
f239bb4
fixing go.mod file.
ozfive Mar 7, 2023
b4b0ebf
Add .deepsource.toml
deepsource-io[bot] Mar 11, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .deepsource.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version = 1

[[analyzers]]
name = "go"

[analyzers.meta]
import_root = "github.com/ozfive/deadman"
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
![deadman](github/logo.png)

a usb-based dead man's switch for your computer.
A usb-based dead man's switch for your computer.

## Usage
```
Expand All @@ -14,15 +14,13 @@ deadman.exe
Can be killed with SIGINT (ctrl-c)

## Purpose
deadman is intended as an anti-forensics/compulsion tool that will prevent or limit access to your machine if you are compelled away from it, or a device is attached to it without your consent. While it is running, if any USB device is attached or removed, it will execute a forced shut down. Possible use cases could involve a usb fob attached to the user with a lanyard as a kill switch, or as a countermeasure to devices like mouse jigglers or programmable HID devices.
deadman is intended as an anti-forensics/compulsion tool that will prevent or limit access to your machine if you are compelled away from it, or a device is attached to it without your consent. While it is running, if any USB device is attached or removed, it will execute a forced shut-down. Possible use cases could involve a USB fob attached to the user with a lanyard as a kill switch, or as a countermeasure to devices like mouse jigglers or programmable HID devices.

## Inspiration
I recently came across [heaphaest0s](https://github.com/hephaest0s)' cool project, [usbkill](https://github.com/hephaest0s/usbkill), which is written in Python. I thought I might be able to improve it somewhat by making an alternative Go version that would have no external dependencies and also would work on Windows 7/8. It currently has feature parity with USBKill, though new features are being developed all the time.

## TODO
* Moar testing
* Moar docs
* Add support for FreeBSD
* Device whitelisting
* Better logging. You won't see much at the moment as it shuts down as soon as it can.
* Hook into system calls as much as possible. For now, linux and OSX systems parse ```lsusb``` or its equivalent every second. There is a branch in development for an event-based model, though this still requires polling in both OSX and Windows. On linux, it can receive udev events via a netlink socket. Whether a similar model is easily obtained in OSX or is even possible in Windows at all is being researched. In the meantime, a more efficient method of polling via WMI is being developed.
* Hook into system calls as much as possible. For now, Linux and OSX systems parse ```lsusb``` or its equivalent every second. There is a branch in development for an event-based model, though this still requires polling in both OSX and Windows. On Linux, it can receive udev events via a netlink socket. Whether a similar model is easily obtained in OSX or is even possible in Windows at all is being researched. In the meantime, a more efficient method of polling via WMI is being developed.
1 change: 1 addition & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type config struct {
PollInterval int
ShutdownTimeout int
Commands []string
ExemptDevices []device
//EmergencyFallback bool
//EmergencyInterval int
}
Expand Down
8 changes: 8 additions & 0 deletions deadman.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,11 @@ shutdownTimeout = 5000

#commands = [
#]

# [[ExemptDevices]]
# Name = "Device A"
# ID = "1234"

# [[ExemptDevices]]
# Name = "Device B"
# ID = "5678"
29 changes: 25 additions & 4 deletions filecheck.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
package main

import (
"os/exec"
"errors"
"os"
"path/filepath"
"runtime"
)

func checkExe(path string) error {
_, err := exec.LookPath(path)
return err
//TODO: check for executability
info, err := os.Stat(path)
if err != nil {
return err
}

if info.IsDir() {
return errors.New("Path is a directory")
}

if runtime.GOOS == "windows" {
if filepath.Ext(path) != ".exe" && filepath.Ext(path) != ".com" {
return errors.New("Path is not an executable")
}
} else {
if info.Mode()&0111 == 0 {

return errors.New("Path is not an executable")
}
}

return nil
}
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module deadman

go 1.19

require github.com/naoina/toml v0.1.1

require (
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/naoina/go-stringutil v0.1.0 // indirect
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks=
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
github.com/naoina/toml v0.1.1 h1:PT/lllxVVN0gzzSqSlHEmP8MJB4MY2U7STGxiouV4X8=
github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
121 changes: 73 additions & 48 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,75 +9,100 @@ import (
)

func main() {
configFile := ""
flag.StringVar(&configFile, "configfile", "", "The config file to use.")
flag.Parse()

//Load the config file
var configFile string
var conf *config
conf := getConfig(configFile)

flag.StringVar(&configFile, "configfile", "", "config file to use")
flag.Parse()
deviceMap, err := getInitialDeviceMap(conf)
if err != nil {
log.Fatal(err)
}

sigint := make(chan os.Signal, 1)
signal.Notify(sigint, os.Interrupt)

check := time.NewTicker(time.Duration(conf.PollInterval) * time.Millisecond)
log.Println("Deadman running (Press Ctrl-C to exit.) ...")

for {
select {
case <-check.C:
now := time.Now().UnixNano()
checkForNewDevices(deviceMap, now, conf)
checkForRemovedDevices(deviceMap, now)
case <-sigint:
log.Println("SIGINT was received, exiting.")
os.Exit(0)
}
}
}

func getConfig(configFile string) *config {
if configFile != "" {
var err error
conf, err = loadConfig(configFile)
conf, err := loadConfig(configFile)
if err != nil {
log.Fatal("failed to parse config file: ", err)
log.Fatal("Failed to parse config file: ", err)
}
return conf
} else {
log.Println("defaulting to immediate shutdown and 1 second polling interval")
conf = &config{Shutdown: true, PollInterval: 1000, ShutdownTimeout: 10000}
log.Println("Defaulting to immediate shutdown and 1 second polling interval.")
return &config{Shutdown: true, PollInterval: 1000, ShutdownTimeout: 10000}
}
}

func getInitialDeviceMap(conf *config) (map[device]int64, error) {
initialDevices, err := enumerateDevices()
if err != nil {
log.Fatal(err)
return nil, err
}

deviceMap := map[device]int64{}
if len(initialDevices) == 0 {
log.Fatal("No devices were found. Is the USB subsystem running?")
}

if len(initialDevices) > 0 {
now := time.Now().UnixNano()
for _, d := range initialDevices {
deviceMap := make(map[device]int64)
now := time.Now().UnixNano()
for _, d := range initialDevices {
if isExemptDevice(d, conf) {
deviceMap[d] = now
}
} else {
log.Fatal("No devices found")
}
return deviceMap, nil
}

sigint := make(chan os.Signal, 1)
signal.Notify(sigint, os.Interrupt)

check := time.NewTicker(time.Duration(conf.PollInterval) * time.Millisecond)
func isExemptDevice(d device, conf *config) bool {
for _, wd := range conf.ExemptDevices {
if wd.Name == d.Name && wd.ID == d.ID {
return true
}
}
return false
}

log.Println("deadman started (press ctrl-c to exit)")
func checkForNewDevices(deviceMap map[device]int64, now int64, conf *config) {
devices, err := enumerateDevices()
if err != nil {
log.Fatal(err)
}

for {
select {
case <-check.C:
now := time.Now().UnixNano()
devices, err := enumerateDevices()
if err != nil {
log.Fatal(err)
}
// Look to see if there are any new devicies
for _, d := range devices {
if _, ok := deviceMap[d]; !ok {
log.Printf("New device: %s [%s]\n", d.Name, d.ID)
shutdownSequence(conf)
}
deviceMap[d] = now
}
for _, d := range devices {
if _, ok := deviceMap[d]; !ok && isExemptDevice(d, conf) {
log.Printf("New device: %s [%s]\n", d.Name, d.ID)
shutdownNow()
deviceMap[d] = now
} else {
deviceMap[d] = now
}
}
}

// Look to see if any devices have been removed
for d, t := range deviceMap {
if t != now {
log.Printf("Device %s [%s] has been removed\n", d.Name, d.ID)
shutdownSequence(conf)
}
}
case <-sigint:
log.Println("SIGINT received")
os.Exit(0)
func checkForRemovedDevices(deviceMap map[device]int64, now int64) {
for d, t := range deviceMap {
if t != now {
log.Printf("Device %s [%s] has been removed\n", d.Name, d.ID)
shutdownNow()
}
}
}
70 changes: 32 additions & 38 deletions shutdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"log"
"os"
"os/exec"
"strings"
"sync"
Expand Down Expand Up @@ -43,51 +42,46 @@ func parallelExecute(cmd *exec.Cmd, wg *sync.WaitGroup) {
}

func shutdownSequence(conf *config) {
if conf.Commands == nil {
shutdownNow()
return
}

done := make(chan struct{})
cl := new(commandList)
wg := new(sync.WaitGroup)
t := time.NewTimer(time.Duration(conf.ShutdownTimeout) * time.Millisecond)

if conf.Commands != nil {
go func() {
select {
case <-done:
case <-t.C:
log.Println("timed out waiting for commands to finish")
cl.KillAll()
}
if conf.Shutdown {
log.Println("shutting down now")
if err := shutdownNow(); err != nil {
log.Fatal("error shutting down:", err)
}
} else {
log.Println("commands have finished")
os.Exit(0)
for _, command := range conf.Commands {
cmdParts := strings.Split(command, " ")
if string(cmdParts[0][0]) == "!" {
osCommand := exec.Command(cmdParts[0][1:], cmdParts[1:]...)
cl.Add(osCommand)
go parallelExecute(osCommand, wg)
} else {
osCommand := exec.Command(cmdParts[0], cmdParts[1:]...)
cl.Add(osCommand)
err := osCommand.Run()
if err != nil {
log.Printf("Error while running command %s: %s", osCommand.Path, err)
}
}()
}
}

for _, command := range conf.Commands {
cmdParts := strings.Split(command, " ")
if string(cmdParts[0][0]) == "!" {
osCommand := exec.Command(cmdParts[0][1:], cmdParts[1:]...)
cl.Add(osCommand)
go parallelExecute(osCommand, wg)
} else {
osCommand := exec.Command(cmdParts[0], cmdParts[1:]...)
cl.Add(osCommand)
err := osCommand.Run()
if err != nil {
log.Printf("Error while running command %s: %s", osCommand.Path, err)
}
go func() {
wg.Wait()
if conf.Shutdown {
if err := shutdownNow(); err != nil {
log.Fatal("error shutting down:", err)
}
}
done <- struct{}{}
}()

go func() {
wg.Wait()
done <- struct{}{}
}()
} else {
shutdownNow()
select {
case <-done:
log.Println("commands have finished")
case <-time.After(time.Duration(conf.ShutdownTimeout) * time.Millisecond):
log.Println("timed out waiting for commands to finish")
cl.KillAll()
}
}
31 changes: 31 additions & 0 deletions shutdown_freebsd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
This file contains a function called shutdownNow() that attempts to initiate
a forced shutdown of the FreeBSD OS using the syscall package.

The function first tries to call the Reboot system call with the
RB_POWERCYCLE command, which should initiate an immediate shutdown of the
system. If that fails, it tries to call the Reboot system call with the
RB_HALT command, which should initiate a system halt. If both of
these system calls fail, the function returns an error with the message
"Failed to initiate system shutdown."
*/
package main

import (
"syscall"
)

func shutdownNow() error {
// First, try to call the reboot system call with the appropriate arguments
if err := syscall.Reboot(syscall.RB_POWERCYCLE); err == nil {
return nil
}

// If that fails, try to call the halt system call with the appropriate arguments
if err := syscall.Reboot(syscall.RB_HALT); err == nil {
return nil
}

// If both system calls fail, return an error
return fmt.Errorf("Failed to initiate system shutdown.")
}
Loading