|
|
|
@ -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) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|