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