commit 0f7f25054a4d0e749c6d561a978d787b73f647a7 Author: Robert Date: Sat May 17 18:35:12 2025 -0400 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..876543f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +execguard diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..094a1be --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2025 Robert Strutts + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ab1bc3 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# ExecGuard +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. +Please look at the go code, etc... + +### About --init +This will initialize the /etc/execguard/allowed.db SQLite3 Database. +It is in Leaning mode... All program will run as normal. + +## Install +``` +cd execgaurd +sudo mkdir -p /etc/execguard/ +cp config.json.example /etc/execguard/config.json +go build -o execguard +sudo mv execguard /usr/local/bin/ +sudo execguard --update $(pwd)/update_bins.sh +sudo ./update_bins.sh +sudo execguard --init +``` +Ctrl+C to exit from execgaurd when done loading programs to allow. + +# Run a Service +Kind of Dangerious!!: +``` +sudo cp execguard.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now execguard +sudo service execguard status +``` +Reboot, to have all Boot programs, load into learning mode. +Make sure that --init is running on the service file. + +## Check the Logs! +``` +sudo tail /var/log/execguard.log +``` +Look out for - Found unauthorized executable: /path/to/program + +# Update allowed list +``` +sudo execguard --update /path/to/program +REPLACE /path/to/program with that found in the Log file. +``` + +# Once done initializing the System: +``` +sudo nano /etc/systemd/system/execguard.service +[Service] +ExecStart=/usr/local/bin/execguard --init + +REMOVE the --init from ExecStart command +``` +Reboot. diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..6c5ea04 --- /dev/null +++ b/config.json.example @@ -0,0 +1,5 @@ +{ + "protected_dirs": ["/home"], + "skip_dirs": [".cache",".git"], + "alert_email": "" +} diff --git a/execguard.go b/execguard.go new file mode 100644 index 0000000..77e31e3 --- /dev/null +++ b/execguard.go @@ -0,0 +1,272 @@ +package main + +import ( + "crypto/sha512" + "database/sql" + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "io/fs" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + "unsafe" + + _ "github.com/mattn/go-sqlite3" + "golang.org/x/sys/unix" +) + +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"` +} + +var initMode bool +var updateFile string +var config *Config + +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.Parse() + + if os.Geteuid() != 0 { + log.Fatal("This program must be run as root") + os.Exit(1) // Exit with status code 1 + } + + logf, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Fatalf("Error opening log file: %v", err) + } + defer logf.Close() + log.SetOutput(logf) + + db, err := sql.Open("sqlite3", dbFile) + if err != nil { + log.Fatalf("Error opening database: %v", err) + os.Exit(2) // Exit with status code 2 + } + defer db.Close() + + createTable(db) + + if updateFile != "" { + absPath, err := filepath.Abs(updateFile) + if err != nil { + log.Fatalf("Invalid update file path: %v", err) + os.Exit(1) // Exit with status code 1 + } + addToAllowed(db, absPath) + 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 + } + + go func() { + defer func() { + if r := recover(); r != nil { + log.Printf("Recovered from scan panic: %v", r) + } + }() + periodicScan(config.ProtectedDirs, db) + }() + + if err := monitorExecutions(db); err != nil { + log.Fatalf("Execution monitoring failed: %v", err) + os.Exit(4) // Exit with status code 4 + } +} + +func loadConfig() (*Config, error) { + data, err := os.ReadFile(configFile) + if err != nil { + return nil, err + } + var cfg Config + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} + +func createTable(db *sql.DB) { + query := `CREATE TABLE IF NOT EXISTS allowed ( + path TEXT PRIMARY KEY, + hash TEXT + )` + _, err := db.Exec(query) + if err != nil { + log.Fatalf("Failed to create table: %v", err) + os.Exit(5) // Exit with status code 5 + } +} + +func isAllowed(db *sql.DB, path string) bool { + var storedHash string + hash := computeHash(path) + if hash == "" { + return false + } + err := db.QueryRow("SELECT hash FROM allowed WHERE path = ?", path).Scan(&storedHash) + return err == nil && storedHash == hash +} + +func addToAllowed(db *sql.DB, path string) { + hash := "" + if initMode || updateFile != "" { + hash = computeHash(path) + } + _, err := db.Exec("INSERT OR REPLACE INTO allowed(path, hash) VALUES(?, ?)", path, hash) + if err != nil { + log.Printf("Error inserting allowed entry: %v", err) + } +} + +func computeHash(path string) string { + data, err := os.ReadFile(path) + if err != nil { + return "" + } + sha := sha512.Sum512(data) + return hex.EncodeToString(sha[:]) +} + +func periodicScan(dirs []string, db *sql.DB) { + skipSet := make(map[string]struct{}) + for _, skip := range config.SkipDirs { + if abs, err := filepath.Abs(skip); err == nil { + skipSet[abs] = struct{}{} + } + } + + for { + for _, dir := range dirs { + filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return nil + } + + absPath, err := filepath.Abs(path) + if err != nil { + return nil + } + + // Skip if in any of the SkipDirs + for skipDir := range skipSet { + if strings.HasPrefix(absPath, skipDir) { + return filepath.SkipDir + } + } + + if d.Type().IsRegular() { + info, err := d.Info() + if err != nil || (info.Mode().Perm()&0111 == 0) { + return nil + } + absPath, _ = filepath.EvalSymlinks(absPath) + + if initMode { + addToAllowed(db, absPath) + } else if !isAllowed(db, absPath) { + log.Printf("Found unauthorized executable: %s", absPath) + os.Chmod(absPath, info.Mode()&^0111) + sendAlert(fmt.Sprintf("Unauthorized executable found and blocked: %s", absPath)) + } + } + return nil + }) + } + time.Sleep(scanInterval) + } +} + +func monitorExecutions(db *sql.DB) error { + fd, err := unix.FanotifyInit(unix.FAN_CLOEXEC|unix.FAN_CLASS_CONTENT, unix.O_RDONLY|unix.O_LARGEFILE) + if err != nil { + return err + } + defer unix.Close(fd) + + for _, dir := range config.ProtectedDirs { + if err := unix.FanotifyMark(fd, unix.FAN_MARK_ADD|unix.FAN_MARK_MOUNT, + unix.FAN_OPEN_EXEC_PERM, unix.AT_FDCWD, dir); err != nil { + log.Printf("Failed to mark %s: %v", dir, err) + } + } + + buf := make([]byte, 4096) + for { + n, err := unix.Read(fd, buf) + if err != nil { + return err + } + for offset := 0; offset < n; { + meta := (*unix.FanotifyEventMetadata)(unsafe.Pointer(&buf[offset])) + if meta.Event_len == 0 { + break + } + + resp := unix.FanotifyResponse{Fd: meta.Fd, Response: unix.FAN_ALLOW} + defer unix.Close(int(meta.Fd)) + + if meta.Mask&unix.FAN_OPEN_EXEC_PERM != 0 { + fdpath := fmt.Sprintf("/proc/self/fd/%d", meta.Fd) + path, err := os.Readlink(fdpath) + if err == nil { + absPath, _ := filepath.Abs(path) + absPath, _ = filepath.EvalSymlinks(absPath) + info, statErr := os.Stat(absPath) + if statErr == nil && info.Mode().IsRegular() && (info.Mode().Perm()&0111 != 0) { + if initMode { + addToAllowed(db, absPath) + } else if !isAllowed(db, absPath) { + log.Printf("Blocked execution attempt: %s", absPath) + sendAlert(fmt.Sprintf("Unauthorized execution attempt blocked: %s", absPath)) + resp.Response = unix.FAN_DENY + } + } + } + } + + b := (*[unsafe.Sizeof(resp)]byte)(unsafe.Pointer(&resp))[:] + if _, err := unix.Write(fd, b); err != nil { + log.Printf("Fanotify response write error: %v", err) + } + + offset += int(meta.Event_len) + } + } +} + +func sendAlert(message string) { + if config.AlertEmail == "" { + return + } + if _, err := exec.LookPath("mail"); err != nil { + log.Printf("Mail command not found: %v", err) + return + } + cmd := exec.Command("mail", "-s", "ExecGuard Alert", config.AlertEmail) + cmd.Stdin = strings.NewReader(message) + if err := cmd.Run(); err != nil { + log.Printf("Failed to send alert: %v", err) + } +} diff --git a/execguard.service b/execguard.service new file mode 100644 index 0000000..46b1b81 --- /dev/null +++ b/execguard.service @@ -0,0 +1,10 @@ +[Unit] +Description=Executable Guardian +After=network.target + +[Service] +ExecStart=/usr/local/bin/execguard --init +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4e0c462 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module execguard + +go 1.23.6 + +require ( + github.com/mattn/go-sqlite3 v1.14.28 // indirect + golang.org/x/sys v0.33.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0dc66ed --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +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= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/update_bins.sh b/update_bins.sh new file mode 100755 index 0000000..9ccb161 --- /dev/null +++ b/update_bins.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Directories to search for executables +DIRS=("/usr/bin" "/usr/sbin" "/usr/local/bin") + +# Process each directory +for dir in "${DIRS[@]}"; do + # Check if directory exists + if [[ -d "$dir" ]]; then + echo "Processing directory: $dir" + + # Find all executable files in the directory + find "$dir" -maxdepth 1 -type f -executable | while read -r program; do + # Get just the program name without path: prog_name=$(basename "$program") + # Run execguard --update on the program + echo "Updating execguard for: $program" + execguard --update "$program" + done + else + echo "Directory not found: $dir" >&2 + fi +done + +# custom files here: +if [ -x /usr/local/maldetect/maldet ]; then + execguard --update /usr/local/maldetect/maldet +fi +sudo execguard --update /usr/lib/update-notifier/package-data-downloader +echo "Finished processing all directories"