diff --git a/README.md b/README.md index fcffefe..b2416e6 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,47 @@ Blocks UnKnown or Changed Programs from running. Please do not run on PROD!!! Do a Full Backup before installing! This for educational use ONLY. Not fit for any real world system. +Beaware it is possible to lock your self out of your own system with this program, if not used right! Please look at the go code, etc... -### About --init +### About execgaurd --init This will initialize the /etc/execguard/allowed.db SQLite3 Database. It is in Leaning mode... All program will run as normal. +## How it works: +NOTE: All executables are blocked that are not in the allowed.db, so the protected_dirs config does not matter! To add a program to this allowed.db Database: sudo execguard --update /THEPATH/TO/PROGRAM_GOES_HERE Beacreful when updating/add to the allowed Database as the whole point is to Block Bad Programs, However, your systen need to run things, so be wise... You should monitor the output of the log file: tail -F /var/log/execguard.log + +## Make a key for xxtea +This will generate a new key phrase for you that is safe in size...to be placed inside of your config.json file. Do this before you go live. +``` +execguard --newKey +``` + +## /etc/execgaurd/config.json +scan_interval is the number of minutes to delay before scanning the protected_dirs for executables that are not allowed to run, it will chmod -x those programs. If 0, disables the scan for executables to remove the executution (x) bit. DO NOT ADD system bin paths to the Protected Dirs!!! As your system will fail to Boot!! skip_dirs are directories to skip inside of the protected_dirs. alert_email is where to send alerts besides the /var/log/execgaurd.log file. If the alert_email is an empty string, that will not send any emails... hash_encryption takes one of the following: none, xor, or xxtea. Passphrase is used on xor or xxtea to provide security against people injecting hashes into the database to make a bad program run. hash_type is either sha256, or sha512. Sha512 is better for security and sha256 is better on perforance, maybe... +``` +{ + "scan_interval": 0, + "protected_dirs": ["/home"], + "skip_dirs": [".cache",".git"], + "alert_email": "root@loalhost", + "passphrase": "cdzTE1Gk6/VuDlnU", + "hash_encryption": "xxtea", + "hash_type": "sha512" +} +``` ## Install +Be sure to update your config.json file to have a passphrase that was generated by execgaurd --newKey... ``` cd execgaurd sudo mkdir -p /etc/execguard/ -cp config.json.example /etc/execguard/config.json go build -o execguard +./execguard --newKey +## Copy the passphrase key into your clipboard. +sudo cp config.json.example /etc/execguard/config.json +sudo nano /etc/execguard/config.json +## Paste your passphrase into the config.json file and save. + sudo mv execguard /usr/local/bin/ sudo execguard --update $(pwd)/update_bins.sh sudo execguard --update $(pwd)/sys_update.sh @@ -68,3 +97,13 @@ sudo apt remove unattended-upgrades ``` ./sys_update.sh ``` +# Migrations +Changes made to passwords, hashes on system with existing data on allowed.db database...need to be migrated. +``` +sudo service execguard stop +sudo ./execguard --migrate +# Test afterword: +sudo ./execgaurd +# If successful: +sudo service execguard start +``` diff --git a/config.json.example b/config.json.example index 6c5ea04..ea34054 100644 --- a/config.json.example +++ b/config.json.example @@ -1,5 +1,9 @@ { + "scan_interval": 0, "protected_dirs": ["/home"], "skip_dirs": [".cache",".git"], "alert_email": "" + "passphrase": "cdzTE1Gk6/VuDlnU", + "hash_encryption": "xxtea", + "hash_type": "sha512" } diff --git a/execguard.go b/execguard.go index 77e31e3..bbff0d6 100644 --- a/execguard.go +++ b/execguard.go @@ -1,12 +1,18 @@ package main import ( + "bufio" + "sync" + "encoding/base64" + "crypto/rand" + "crypto/sha256" "crypto/sha512" "database/sql" "encoding/hex" "encoding/json" "flag" "fmt" + "io" "io/fs" "log" "os" @@ -18,29 +24,48 @@ import ( _ "github.com/mattn/go-sqlite3" "golang.org/x/sys/unix" + "github.com/yang3yen/xxtea-go/xxtea" ) const ( configFile = "/etc/execguard/config.json" dbFile = "/etc/execguard/allowed.db" logFile = "/var/log/execguard.log" - scanInterval = 9 * time.Minute ) type Config struct { ProtectedDirs []string `json:"protected_dirs"` AlertEmail string `json:"alert_email"` SkipDirs []string `json:"skip_dirs"` + ScanInterval int `json:"scan_interval"` // in minutes, 0 disables scan + Passphrase string `json:"passphrase"` // optional hash encryption key + HashEncryption string `json:"hash_encryption"` // "none", "xor", or "xxtea" + HashType string `json:"hash_type"` // "sha256" or "sha512" } var initMode bool var updateFile string +var migrateMode bool +var newKey bool var config *Config +var dbMutex sync.Mutex func main() { flag.BoolVar(&initMode, "init", false, "initialize and populate allowed executable database") flag.StringVar(&updateFile, "update", "", "add specified file to allowed database with hash") + flag.BoolVar(&migrateMode, "migrate", false, "recompute hashes of all allowed paths using current settings") + flag.BoolVar(&newKey, "newKey", false, "generate a new XXTEA-compatible encryption key") flag.Parse() + + if newKey { + // XXTEA key should be 16 bytes total...base64 will padd it... + key := make([]byte, 12) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + log.Fatalf("Failed to generate key: %v", err) + } + fmt.Printf("Generated XXTEA key (base64): %s\n", base64.StdEncoding.EncodeToString(key)) + return + } if os.Geteuid() != 0 { log.Fatal("This program must be run as root") @@ -61,6 +86,12 @@ func main() { } defer db.Close() + config, err = loadConfig() + if err != nil { + log.Fatalf("Error loading config: %v", err) + os.Exit(3) // Exit with status code 3 + } + createTable(db) if updateFile != "" { @@ -73,11 +104,10 @@ func main() { log.Printf("Added to allowed list: %s", absPath) return } - - config, err = loadConfig() - if err != nil { - log.Fatalf("Error loading config: %v", err) - os.Exit(3) // Exit with status code 3 + + if migrateMode { + runMigration(db) + return } go func() { @@ -95,6 +125,10 @@ func main() { } } +func randReader() io.Reader { + return rand.Reader +} + func loadConfig() (*Config, error) { data, err := os.ReadFile(configFile) if err != nil { @@ -119,6 +153,52 @@ func createTable(db *sql.DB) { } } +func runMigration(db *sql.DB) { + tempFile := "Migrate" + + f, err := os.CreateTemp("", tempFile) + if err != nil { + log.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(f.Name()) + + rows, err := db.Query("SELECT path FROM allowed") + if err != nil { + log.Fatalf("Failed to query allowed paths: %v", err) + } + defer rows.Close() + + for rows.Next() { + var path string + if err := rows.Scan(&path); err != nil { + log.Printf("Failed to read row: %v", err) + continue + } + _, _ = fmt.Fprintln(f, path) + } + f.Close() // make sure it can be read next + + // Reopen to read + input, err := os.Open(f.Name()) + if err != nil { + log.Fatalf("Failed to open temp file: %v", err) + } + defer input.Close() + + scanner := bufio.NewScanner(input) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line != "" { + time.Sleep(time.Duration(1) * 100 * time.Millisecond) + addToAllowed(db, line) + log.Printf("Migrated path: %s", line) + } + } + if err := scanner.Err(); err != nil { + log.Printf("Error reading Migrate file: %v", err) + } +} + func isAllowed(db *sql.DB, path string) bool { var storedHash string hash := computeHash(path) @@ -130,8 +210,11 @@ func isAllowed(db *sql.DB, path string) bool { } func addToAllowed(db *sql.DB, path string) { + dbMutex.Lock() + defer dbMutex.Unlock() + hash := "" - if initMode || updateFile != "" { + if initMode || updateFile != "" || migrateMode { hash = computeHash(path) } _, err := db.Exec("INSERT OR REPLACE INTO allowed(path, hash) VALUES(?, ?)", path, hash) @@ -140,23 +223,94 @@ func addToAllowed(db *sql.DB, path string) { } } +func normalizeXXTEAKey(key []byte) []byte { + switch { + case len(key) == 16: + return key + case len(key) > 16: + hash := sha256.Sum256(key) + return hash[:16] + default: // len(key) < 16 + padded := make([]byte, 16) + copy(padded, key) + // Simple padding with repeated key pattern + for i := len(key); i < 16; i++ { + padded[i] = key[i%len(key)] + } + return padded + } +} + func computeHash(path string) string { data, err := os.ReadFile(path) if err != nil { return "" } - sha := sha512.Sum512(data) - return hex.EncodeToString(sha[:]) + + var hashBytes []byte + switch strings.ToLower(config.HashType) { + case "sha256": + sum := sha256.Sum256(data) + hashBytes = sum[:] + case "sha512", "": + sum := sha512.Sum512(data) + hashBytes = sum[:] + default: + log.Printf("Unknown hash_type '%s', defaulting to sha512.", config.HashType) + sum := sha512.Sum512(data) + hashBytes = sum[:] + } + + switch strings.ToLower(config.HashEncryption) { + case "none": + return hex.EncodeToString(hashBytes) + + case "xor": + if config.Passphrase == "" { + log.Println("XOR encryption selected but no passphrase provided.") + return hex.EncodeToString(hashBytes) + } + key := []byte(config.Passphrase) + enc := make([]byte, len(hashBytes)) + for i := 0; i < len(hashBytes); i++ { + enc[i] = hashBytes[i] ^ key[i%len(key)] + } + return hex.EncodeToString(enc) + + case "xxtea": + if config.Passphrase == "" { + log.Println("XXTEA encryption selected but no passphrase provided.") + return hex.EncodeToString(hashBytes) + } + + key := normalizeXXTEAKey([]byte(config.Passphrase)) + enc, err := xxtea.Encrypt(hashBytes, key, false, 0) + if err != nil { + log.Println("XXTEA encryption KEY error???") + return hex.EncodeToString(hashBytes) + } + return base64.StdEncoding.EncodeToString(enc) + + default: + log.Printf("Unknown hash_encryption type: %s. Using plain hash.", config.HashEncryption) + return hex.EncodeToString(hashBytes) + } } func periodicScan(dirs []string, db *sql.DB) { + if config.ScanInterval == 0 { + // log.Println("Periodic scanning is disabled by configuration.") + return + } + skipSet := make(map[string]struct{}) for _, skip := range config.SkipDirs { if abs, err := filepath.Abs(skip); err == nil { skipSet[abs] = struct{}{} } } - + interval := time.Duration(config.ScanInterval) * time.Minute + // log.Printf("Starting periodic scan every %v...", interval) for { for _, dir := range dirs { filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { @@ -194,7 +348,7 @@ func periodicScan(dirs []string, db *sql.DB) { return nil }) } - time.Sleep(scanInterval) + time.Sleep(interval) } } diff --git a/go.mod b/go.mod index 4e0c462..a68cc14 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,6 @@ go 1.23.6 require ( github.com/mattn/go-sqlite3 v1.14.28 // indirect + github.com/yang3yen/xxtea-go v1.0.3 // indirect golang.org/x/sys v0.33.0 // indirect ) diff --git a/go.sum b/go.sum index 0dc66ed..92daa3c 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/yang3yen/xxtea-go v1.0.3 h1:C7yBcDRb909v39llhqx+QjAerOeWB+Oyqt/Z7yC7TBk= +github.com/yang3yen/xxtea-go v1.0.3/go.mod h1:baa5JUNAgCuVCNqYuWSSNNGTmmDyNMTtSSlNMqfli9M= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=