├── README.md ├── contrib ├── scripts │ ├── alert.sh │ ├── libnotify.sh │ └── mail.sh └── systemd │ ├── README.md │ └── cryptostalker@.service ├── cryptostalker.go └── setup_workspace.sh /README.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | The goal of cryptostalker is to detect crypto ransomware. The mechanism it uses to do this is by recognizing new files that are created on the file system and trying to ascertain whether they are encrypted. 3 | 4 | This project is a port of the original [randumb](github.com/unixist/randumb) project that was written in python for linux using inotify. 5 | 6 | # How it works 7 | When cryptostalker runs, it places a recursive file system watch on the path specified with the ```--path``` command line flag. 8 | 9 | Whenever a new file is created, it is inspected for randomness via the [randumb](github.com/unixist/randumb) library. If it is deemed random, and within the ```--window``` and ```--count``` parameters, a message will be output saying that a suspicious file is found. This is possibly indicative of a newly-placed encrypted file somewhere on the filesystem under the ```--path``` directory. 10 | 11 | If the ```--stopAge``` command line flag is specified, any new process created within ```stopAge``` seconds of an encrypted file being detected will be terminated. The idea is to stop processes that might be responsible for performing the file encryption. This is a powerful, yet dangerous feature. 12 | 13 | Ideally, suspicious processes will be issued an interrupt, so they'd just be paused, while the user or system log is notified and you can recover any legitimate processes. Due to a limitation in golang for Windows, an interrupt can't be sent to processes; only a kill may be sent. When ```stopAge``` is implemented for other operating systems, it will be implemented with the interrupt functionality, not kill. 14 | 15 | # Setup 16 | These steps will set up a temporary workspace and install cryptostalker to it 17 | 18 | #### With repo cloned 19 | 20 | `$ source /path/to/repo/setup_workspace.sh` 21 | 22 | #### Without repo cloned 23 | Copy and paste these commands: 24 | 25 | ```bash 26 | path="$HOME/workspace.$RANDOM" 27 | export GOPATH=$path 28 | export GOBIN=$path/bin 29 | mkdir -p $path/src 30 | cd $path/src 31 | go get github.com/unixist/cryptostalker 32 | go install github.com/unixist/cryptostalker 33 | echo -e 'Now you can run:\n $GOBIN/cryptostalker --path=/tmp' 34 | ``` 35 | 36 | # Example 37 | ```bash 38 | # This will print out a line if even one encrypted file is seen anywhere under $HOME 39 | $ cryptostalker --path=$HOME 40 | 41 | # This will kill processes seen starting up 60 seconds before the encrypted file(s) are seen 42 | $ cryptostalker --path=$HOME --stopAge=60 43 | 44 | # For performance reasons, sleep for 100 ms after checking each file for randomness 45 | $ cryptostalker --path=$HOME --sleep=100 46 | 47 | # This will call a script (see contrib/scripts directory) when an encrypted file is seen anywhere under $HOME 48 | $ cryptostalker --path=$HOME --script=/usr/local/bin/alert.sh 49 | ``` 50 | 51 | # Tested systems 52 | * Linux 53 | * OSX 54 | * Windows 55 | 56 | # Tested samples 57 | * [jigsaw](https://malwr.com/analysis/MTI0NjVkYzNlMzkyNDdiZGEwZGFhZTkyNDhkMGUxZmI/) 58 | * Sample was detected encrypting files and terminated with the --stopAge=60 59 | * Need more tests... 60 | 61 | # Test your setup 62 | 63 | ## Example: GPG 64 | 65 | ### Prerequisites 66 | 67 | * use your existing GPG key or create a new one 68 | * cryptostalker watches a directory (e.g. ```/tmp```) 69 | 70 | 71 | ```bash 72 | $ for i in {1..200}; do dmesg > /tmp/$i.txt; done # fill data into some files 73 | $ for i in {1..200}; do gpg --out /tmp/$i.crypt --recipient $gpg-key-id --encrypt /tmp/$i.txt; done 74 | ``` 75 | 76 | This should result in something like: 77 | 78 | ``` 79 | YYYY/MM/DD HH:MM:SS Suspicious file: /tmp/test/70.crypt 80 | YYYY/MM/DD HH:MM:SS Suspicious file: /tmp/test/131.crypt 81 | YYYY/MM/DD HH:MM:SS Suspicious file: /tmp/test/165.crypt 82 | ... 83 | ``` 84 | 85 | # Details 86 | The file notification mechanism is Google's [fsnotify](https://github.com/fsnotify/fsnotify). Since it doesn't use the linux-specific [inotify](https://en.wikipedia.org/wiki/Inotify), cryptostalker currently relies on notifications of new files. So random/encrypted files will only be detected if they belong to new inodes. This means it wont catch the following case: a file is opened, truncated, and only then filled in with encrypted content. Fortunately, this is not how most malware works. 87 | 88 | # Bugs 89 | There are no known bugs. But there are design choices that render the current version of cryptostalker circumventable if the malware author knows what to look for. If you're interested in discussing bypasses, we can chat directly. I'm not interested in making it easier to discover than it already is :) 90 | -------------------------------------------------------------------------------- /contrib/scripts/alert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | logger -t cryptostalker "Suspicious file: $1" 4 | -------------------------------------------------------------------------------- /contrib/scripts/libnotify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## install libnotify and notify binary 4 | ## 5 | ## Debian/Ubuntu: apt-get install libnotify-bin 6 | ## 7 | 8 | notify-send --urgency=critical "[cryptostalker] Suspicious file: $1" 9 | -------------------------------------------------------------------------------- /contrib/scripts/mail.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "$1" | mail -s "cryptostalker alert!" $mail 4 | -------------------------------------------------------------------------------- /contrib/systemd/README.md: -------------------------------------------------------------------------------- 1 | # Systemd unit template 2 | 3 | This unit template allows dynamic instances of cryptostalker watching different directories (e.g. when using zfs datasets, different mount points, ...). It is useful on file servers (e.g. ```samba```). 4 | 5 | ## Installation 6 | 7 | Just copy to ```/lib/systemd/system/``` and adjust the paths or copy the binary into ```/usr/local/bin/```. 8 | 9 | ## Usage 10 | 11 | Watchout for some bogus systemd escapings (```systemd-escape``` can be useful for testing)! 12 | 13 | * ```-``` becomes ```/``` 14 | 15 | This enables cryptostalker for ```/share/invoice```: 16 | 17 | ```bash 18 | systemctl enable cryptostalker@-share-invoice.service 19 | systemctl start cryptostalker@-share-invoice.service 20 | ``` 21 | 22 | If your folder already contains```-```, you'll need ```\x2d``` (e.g. ```/internal-share/files/payslip```): 23 | 24 | ```bash 25 | systemctl enable cryptostalker@-internal\x2dshare-files-payslip.service 26 | systemctl start cryptostalker@-internal\\x2dshare-files-payslip.service # <-- mind the shell-escaped backslash! 27 | ``` 28 | -------------------------------------------------------------------------------- /contrib/systemd/cryptostalker@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=cryptostalker - detect crypto ransomware on %I 3 | 4 | [Service] 5 | ExecStart=-/usr/local/bin/cryptostalker --script=/usr/local/bin/cryptostalker.sh --path=%I 6 | ExecReload=/bin/kill -HUP $MAINPID 7 | Restart=on-failure 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /cryptostalker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "time" 12 | 13 | "github.com/rjeczalik/notify" 14 | "github.com/unixist/go-ps" 15 | "github.com/unixist/randumb" 16 | ) 17 | 18 | type options struct { 19 | path *string 20 | count *int 21 | sleep *int 22 | stopAge *int 23 | window *int 24 | script *string 25 | } 26 | 27 | func stopProcsYoungerThan(secs int) { 28 | age, _ := time.ParseDuration(fmt.Sprintf("%ds", secs)) 29 | for _, proc := range procsYoungerThan(age) { 30 | if err := stopProc(proc); err != nil { 31 | fmt.Printf("Failed to stop process: %d", proc) 32 | } 33 | } 34 | } 35 | 36 | func stopProc(pid int) error { 37 | if os.Getpid() == pid { 38 | return nil 39 | } 40 | p, err := os.FindProcess(pid) 41 | if err != nil { 42 | return err 43 | } 44 | if err := p.Signal(os.Kill); err != nil { 45 | return err 46 | } 47 | return nil 48 | } 49 | 50 | func procsYoungerThan(age time.Duration) []int { 51 | procs, _ := ps.Processes() 52 | ret := []int{} 53 | for _, i := range procs { 54 | if time.Since(i.CreationTime()) < age { 55 | ret = append(ret, i.Pid()) 56 | } 57 | } 58 | return ret 59 | } 60 | 61 | func isFileRandom(filename string) bool { 62 | s, err := os.Stat(filename) 63 | if err != nil { 64 | // File no longer exists. Either it was a temporary file or it was removed. 65 | return false 66 | } else if !s.Mode().IsRegular() { 67 | // File is a directory/socket/device, anything other than a regular file. 68 | return false 69 | } 70 | // TODO: process the file in pieces, not as a whole. This will thrash memory 71 | // if the file we're inspecting is too big. Suggestion: read data in PAGE_SIZE 72 | // bytes, and call randumb.IsRandom() size/PAGE_SIZE number of times. If N 73 | // pages are random, then return true. 74 | // Processing a file in this way also has the side effect of protecting against 75 | // ransomware evading detection by encoding non-random data inside the file along 76 | // with the encrypted data--and then removing the non-random cruft data later. 77 | data, err := ioutil.ReadFile(filename) 78 | if err != nil { 79 | // Don't output an error if it is permission related 80 | if !os.IsPermission(err) { 81 | log.Printf("Error reading file: %s: %v\n", filename, err) 82 | } 83 | return false 84 | } 85 | return randumb.IsRandom(data) 86 | } 87 | 88 | func Stalk(opts options) { 89 | c := make(chan notify.EventInfo, 1) 90 | // Make path recursive 91 | rpath := filepath.Join(*opts.path, "...") 92 | if err := notify.Watch(rpath, c, notify.Create); err != nil { 93 | log.Fatal(err) 94 | } 95 | defer notify.Stop(c) 96 | 97 | // Ingest events forever 98 | for ei := range c { 99 | path := ei.Path() 100 | go func() { 101 | if isFileRandom(path) { 102 | log.Printf("Suspicious file: %s", path) 103 | if *opts.stopAge != 0 { 104 | stopProcsYoungerThan(*opts.stopAge) 105 | } 106 | if *opts.script != "" { 107 | exec.Command(*opts.script,path).Start() 108 | } 109 | } 110 | }() 111 | if *opts.sleep != 0 { 112 | time.Sleep(time.Duration(*opts.sleep) * time.Millisecond) 113 | } 114 | } 115 | } 116 | 117 | func flags() options { 118 | opts := options{ 119 | count: flag.Int("count", 10, "The number of random files required to be seen within "), 120 | path: flag.String("path", "", "The path to watch"), 121 | script: flag.String("script", "", "Script to call (first parameter is the path of suspicious file) when something happens"), 122 | // Since the randomness check is expensive, it may make sense to sleep after 123 | // each check on systems that create lots of files. 124 | sleep: flag.Int("sleep", 10, "The time in milliseconds to sleep before processing each new file. Adjust higher if performance is an issue."), 125 | stopAge: flag.Int("stopAge", 0, "Stop all processes created within the last N seconds. Default is off."), 126 | window: flag.Int("window", 60, "The number of seconds within which random files must be observed"), 127 | } 128 | flag.Parse() 129 | if *opts.path == "" { 130 | log.Fatal("Please provide a --path") 131 | } 132 | return opts 133 | } 134 | 135 | func main() { 136 | Stalk(flags()) 137 | } 138 | -------------------------------------------------------------------------------- /setup_workspace.sh: -------------------------------------------------------------------------------- 1 | path="$HOME/workspace.$RANDOM" 2 | export GOPATH=$path 3 | export GOBIN=$path/bin 4 | mkdir -p $path/src 5 | cd $path/src 6 | go get github.com/unixist/cryptostalker 7 | go install github.com/unixist/cryptostalker 8 | echo -e 'Now you can run:\n $GOBIN/cryptostalker --path=/tmp' 9 | --------------------------------------------------------------------------------