Patched email alerts, so it does not block everything...!

main
Robert 7 months ago
parent 8bde01627b
commit fba73cd158
  1. 6
      build.sh
  2. 196
      execguard.go
  3. 5
      update_bins.sh

@ -0,0 +1,6 @@
#!/bin/bash
go build -o execguard
sudo cp execguard /usr/local/bin/
sudo ./execguard --update /usr/local/bin/execguard
echo -e "Running execguard...Hit CTRL+C to end."
sudo execguard

@ -1,8 +1,11 @@
package main package main
import ( import (
"bytes"
"context"
"bufio" "bufio"
"sync" "sync"
"encoding/binary"
"encoding/base64" "encoding/base64"
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
@ -27,10 +30,13 @@ import (
"github.com/yang3yen/xxtea-go/xxtea" "github.com/yang3yen/xxtea-go/xxtea"
) )
const sizeofFanotifyEventMetadata = int(unsafe.Sizeof(unix.FanotifyEventMetadata{}))
const ( const (
configFile = "/etc/execguard/config.json" configFile = "/etc/execguard/config.json"
dbFile = "/etc/execguard/allowed.db" dbFile = "/etc/execguard/allowed.db"
logFile = "/var/log/execguard.log" logFile = "/var/log/execguard.log"
mailPath = "/usr/bin/mail"
) )
type Config struct { type Config struct {
@ -50,6 +56,7 @@ var migrateMode bool
var newKey bool var newKey bool
var config *Config var config *Config
var dbMutex sync.Mutex var dbMutex sync.Mutex
var alertCache sync.Map
func main() { func main() {
flag.BoolVar(&initMode, "init", false, "initialize and populate allowed executable database") flag.BoolVar(&initMode, "init", false, "initialize and populate allowed executable database")
@ -361,7 +368,13 @@ func periodicScan(dirs []string, db *sql.DB) {
} else if !isAllowed(db, absPath) { } else if !isAllowed(db, absPath) {
log.Printf("Found unauthorized executable: %s", absPath) log.Printf("Found unauthorized executable: %s", absPath)
os.Chmod(absPath, info.Mode()&^0111) os.Chmod(absPath, info.Mode()&^0111)
go sendAlert(fmt.Sprintf("Unauthorized executable found and blocked: %s", absPath))
if _, seen := alertCache.LoadOrStore(absPath, struct{}{}); !seen {
go sendAlert(fmt.Sprintf("Unauthorized execution attempt blocked: %s", absPath), db)
time.AfterFunc(10*time.Minute, func() {
alertCache.Delete(absPath)
})
}
} }
} }
return nil return nil
@ -372,75 +385,126 @@ func periodicScan(dirs []string, db *sql.DB) {
} }
func monitorExecutions(db *sql.DB) error { func monitorExecutions(db *sql.DB) error {
fd, err := unix.FanotifyInit(unix.FAN_CLOEXEC|unix.FAN_CLASS_CONTENT, unix.O_RDONLY|unix.O_LARGEFILE) fd, err := unix.FanotifyInit(unix.FAN_CLOEXEC|unix.FAN_CLASS_CONTENT, unix.O_RDONLY|unix.O_LARGEFILE)
if err != nil { if err != nil {
return err return fmt.Errorf("fanotify init failed: %w", err)
} }
defer unix.Close(fd) defer unix.Close(fd)
success := false
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)
} else {
success = true
}
}
if !success {
return fmt.Errorf("failed to mark any protected directories")
}
for _, dir := range config.ProtectedDirs { buf := make([]byte, 4096)
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) defer func() {
for { if r := recover(); r != nil {
n, err := unix.Read(fd, buf) log.Printf("Recovered from panic in monitorExecutions: %v", r)
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)
// To avoid locking up the Whole System...use go function on sendAlert!!!
go sendAlert(fmt.Sprintf("Unauthorized execution attempt blocked: %s", absPath))
resp.Response = unix.FAN_DENY
}
}
}
}
b := (*[unsafe.Sizeof(resp)]byte)(unsafe.Pointer(&resp))[:] for {
if _, err := unix.Write(fd, b); err != nil { n, err := unix.Read(fd, buf)
log.Printf("Fanotify response write error: %v", err) if err != nil {
} return fmt.Errorf("fanotify read failed: %w", err)
}
offset += int(meta.Event_len) for offset := 0; offset < n; {
} if n-offset < sizeofFanotifyEventMetadata {
} break
}
meta := (*unix.FanotifyEventMetadata)(unsafe.Pointer(&buf[offset]))
if meta.Event_len == 0 {
break
}
resp := unix.FanotifyResponse{Fd: meta.Fd, Response: unix.FAN_ALLOW}
shouldClose := true
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)
if info, statErr := os.Stat(absPath); statErr == nil &&
info.Mode().IsRegular() && (info.Mode().Perm()&0111 != 0) {
if initMode {
go func(p string, fd int) {
addToAllowed(db, p)
unix.Close(fd)
}(absPath, int(meta.Fd))
shouldClose = false
} else {
if !isAllowed(db, absPath) {
log.Printf("Blocked execution attempt: %s", absPath)
if _, seen := alertCache.LoadOrStore(absPath, struct{}{}); !seen {
go sendAlert(fmt.Sprintf("Unauthorized execution attempt blocked: %s", absPath), db)
time.AfterFunc(10*time.Minute, func() {
alertCache.Delete(absPath)
})
}
resp.Response = unix.FAN_DENY
}
}
}
}
}
var respBuf bytes.Buffer
if err := binary.Write(&respBuf, binary.LittleEndian, resp); err != nil {
log.Printf("Failed to encode fanotify response: %v", err)
} else if _, err := unix.Write(fd, respBuf.Bytes()); err != nil {
log.Printf("Fanotify response write error: %v", err)
}
if shouldClose {
unix.Close(int(meta.Fd))
}
offset += int(meta.Event_len)
}
}
} }
func sendAlert(message string) { func sendAlert(message string, db *sql.DB) {
if config.AlertEmail == "" { if config.AlertEmail == "" {
return return
} }
if _, err := exec.LookPath("mail"); err != nil { if !isAllowed(db, mailPath) {
log.Printf("Mail command not found: %v", err) log.Printf("%s not allowed...blocked email, sorry.", mailPath)
return return // Prevent system crash!
} }
cmd := exec.Command("mail", "-s", "ExecGuard Alert", config.AlertEmail) if _, err := os.Stat(mailPath); err != nil {
cmd.Stdin = strings.NewReader(message) log.Printf("Mail command not found: %v", err)
if err := cmd.Run(); err != nil { return
log.Printf("Failed to send alert: %v", err) }
} time.Sleep(time.Duration(300) * time.Millisecond) // Must give time for Block to get over with...
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel() // Ensure the context is canceled to avoid leaks
cmd := exec.CommandContext(ctx, mailPath, "-s", "ExecGuard Alert", config.AlertEmail)
cmd.Env = []string{"PATH=/usr/bin:/bin"} // Set a minimal PATH
cmd.Stdin = strings.NewReader(message)
output, err := cmd.CombinedOutput()
if ctx.Err() == context.DeadlineExceeded {
log.Printf("sendAlert timeout after 15s for message: %q", message)
}
if err != nil {
log.Printf("Failed to send alert: %v, output: %s", err, output)
}
} }

@ -14,7 +14,7 @@ for dir in "${DIRS[@]}"; do
# Get just the program name without path: prog_name=$(basename "$program") # Get just the program name without path: prog_name=$(basename "$program")
# Run execguard --update on the program # Run execguard --update on the program
echo "Updating execguard for: $program" echo "Updating execguard for: $program"
execguard --update "$program" sudo execguard --update "$program"
done done
else else
echo "Directory not found: $dir" >&2 echo "Directory not found: $dir" >&2
@ -22,8 +22,9 @@ for dir in "${DIRS[@]}"; do
done done
# custom files here: # custom files here:
sudo execguard --update /usr/bin/mail
if [ -x /usr/local/maldetect/maldet ]; then if [ -x /usr/local/maldetect/maldet ]; then
execguard --update /usr/local/maldetect/maldet sudo execguard --update /usr/local/maldetect/maldet
fi fi
sudo execguard --update /usr/lib/update-notifier/package-data-downloader sudo execguard --update /usr/lib/update-notifier/package-data-downloader
echo "Finished processing all directories" echo "Finished processing all directories"

Loading…
Cancel
Save