From 0f7f25054a4d0e749c6d561a978d787b73f647a7 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 17 May 2025 18:35:12 -0400 Subject: [PATCH] init --- .gitignore | 1 + LICENSE | 22 ++++ README.md | 55 +++++++++ config.json.example | 5 + execguard.go | 272 ++++++++++++++++++++++++++++++++++++++++++++ execguard.service | 10 ++ go.mod | 8 ++ go.sum | 4 + update_bins.sh | 29 +++++ 9 files changed, 406 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config.json.example create mode 100644 execguard.go create mode 100644 execguard.service create mode 100644 go.mod create mode 100644 go.sum create mode 100755 update_bins.sh 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"