commit
0f7f25054a
@ -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…
Reference in new issue