Robert 7 months ago
commit 0f7f25054a
  1. 1
      .gitignore
  2. 22
      LICENSE
  3. 55
      README.md
  4. 5
      config.json.example
  5. 272
      execguard.go
  6. 10
      execguard.service
  7. 8
      go.mod
  8. 4
      go.sum
  9. 29
      update_bins.sh

1
.gitignore vendored

@ -0,0 +1 @@
execguard

@ -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.

@ -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.

@ -0,0 +1,5 @@
{
"protected_dirs": ["/home"],
"skip_dirs": [".cache",".git"],
"alert_email": ""
}

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

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

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

@ -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=

@ -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"
Loading…
Cancel
Save