Push initial working version
This commit is contained in:
175
main.go
175
main.go
@@ -4,65 +4,123 @@ import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/cilium/ebpf/link"
|
||||
"github.com/cilium/ebpf/perf"
|
||||
"github.com/cilium/ebpf/rlimit"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// event matches the C struct in your BPF program
|
||||
type Config struct {
|
||||
MonitoredFiles []string `yaml:"monitored_files"`
|
||||
IgnoreActions []string `yaml:"ignore_actions"`
|
||||
IgnoreUsers []string `yaml:"ignore_users"`
|
||||
}
|
||||
|
||||
type event struct {
|
||||
Pid uint32
|
||||
Uid uint32
|
||||
Comm [16]byte
|
||||
Filename [256]byte
|
||||
Pid uint32
|
||||
Uid uint32
|
||||
Euid uint32
|
||||
Loginuid uint32
|
||||
FilenameHash uint32
|
||||
Comm [16]byte
|
||||
Filename [256]byte
|
||||
Flags uint32
|
||||
}
|
||||
|
||||
// simpleHash implements the same DJB2-like hash as the eBPF C code.
|
||||
func simpleHash(filename string) uint32 {
|
||||
hash := uint32(5381)
|
||||
for _, c := range []byte(filename) {
|
||||
hash = ((hash << 5) + hash) + uint32(c) // hash * 33 + c
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
func loadConfig(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading config file: %w", err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parsing config: %w", err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func parseUint32(s string) (uint32, error) {
|
||||
var n uint32
|
||||
_, err := fmt.Sscanf(s, "%d", &n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func isReadOperation(flags uint32) bool {
|
||||
return (flags & (syscall.O_WRONLY | syscall.O_RDWR)) == 0
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Allow the current process to lock memory for eBPF resources
|
||||
cfg, err := loadConfig("config.yaml")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
if err := rlimit.RemoveMemlock(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Load the pre-compiled BPF program
|
||||
objs := tracepointObjects{}
|
||||
if err := loadTracepointObjects(&objs, nil); err != nil {
|
||||
log.Fatalf("loading objects: %v", err)
|
||||
log.Fatalf("Loading objects: %v", err)
|
||||
}
|
||||
defer objs.Close()
|
||||
|
||||
// Populate the target filename map (critical part)
|
||||
targetFilename := "/tmp/testfile\x00" // Null-terminated
|
||||
var filenameBuf [256]byte
|
||||
copy(filenameBuf[:], targetFilename)
|
||||
|
||||
key := uint32(0)
|
||||
if err := objs.TargetFilenameMap.Put(key, filenameBuf); err != nil {
|
||||
log.Fatalf("putting target filename in map: %v", err)
|
||||
// Populate filename hashes
|
||||
for _, file := range cfg.MonitoredFiles {
|
||||
hash := simpleHash(file)
|
||||
val := uint32(1) // Just a marker
|
||||
if err := objs.TargetHashesMap.Put(hash, val); err != nil {
|
||||
log.Fatalf("Failed to add hash for %s: %v", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Populate ignored UIDs
|
||||
for _, username := range cfg.IgnoreUsers {
|
||||
u, err := user.Lookup(username)
|
||||
if err != nil {
|
||||
log.Printf("Warning: user %s not found", username)
|
||||
continue
|
||||
}
|
||||
uid, err := parseUint32(u.Uid)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse UID for %s: %v", u.Uid, err)
|
||||
}
|
||||
|
||||
if err := objs.IgnoreUidsMap.Put(uid, uint32(1)); err != nil {
|
||||
log.Printf("Failed to ignore UID %d: %v", uid, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Attach the tracepoint
|
||||
tp, err := link.Tracepoint("syscalls", "sys_enter_openat", objs.TraceOpenat, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("attaching tracepoint: %v", err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer tp.Close()
|
||||
|
||||
// Set up perf event reader
|
||||
rd, err := perf.NewReader(objs.Events, os.Getpagesize())
|
||||
if err != nil {
|
||||
log.Fatalf("creating perf event reader: %v", err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer rd.Close()
|
||||
|
||||
log.Println("Monitoring for openat() syscalls to /tmp/testfile...")
|
||||
|
||||
// Graceful shutdown
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
@@ -73,30 +131,75 @@ func main() {
|
||||
if errors.Is(err, perf.ErrClosed) {
|
||||
return
|
||||
}
|
||||
log.Printf("reading from perf reader: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(record.RawSample) < int(unsafe.Sizeof(event{})) {
|
||||
log.Printf("invalid sample size: %d", len(record.RawSample))
|
||||
log.Printf("Reading event: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var e event
|
||||
if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &e); err != nil {
|
||||
log.Printf("parsing event: %v", err)
|
||||
log.Printf("Decoding event: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert byte arrays to strings
|
||||
comm := string(bytes.TrimRight(e.Comm[:], "\x00"))
|
||||
filename := string(bytes.TrimRight(e.Filename[:], "\x00"))
|
||||
// Final verification in userspace
|
||||
matched := false
|
||||
currentFile := string(bytes.TrimRight(e.Filename[:], "\x00"))
|
||||
for _, file := range cfg.MonitoredFiles {
|
||||
if currentFile == file {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("PID: %d, UID: %d, CMD: %s, FILE: %s",
|
||||
e.Pid, e.Uid, comm, filename)
|
||||
// Skip ignored actions
|
||||
if isReadOperation(e.Flags) && contains(cfg.IgnoreActions, "read") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get username from UID
|
||||
currentUsername := "unknown"
|
||||
if e.Uid == 0 {
|
||||
currentUsername = "root"
|
||||
} else if user, err := user.LookupId(fmt.Sprintf("%d", e.Uid)); err == nil {
|
||||
currentUsername = user.Username
|
||||
}
|
||||
|
||||
// With this more robust version:
|
||||
userInfo := fmt.Sprintf("%d (%s)", e.Uid, currentUsername)
|
||||
if e.Loginuid != 0 && e.Uid != e.Loginuid {
|
||||
loginUser, err := user.LookupId(fmt.Sprintf("%d", e.Loginuid))
|
||||
loginUsername := "unknown"
|
||||
if err == nil {
|
||||
loginUsername = loginUser.Username
|
||||
}
|
||||
userInfo = fmt.Sprintf("%d (%s) [Login: %d (%s)]",
|
||||
e.Uid, currentUsername, e.Loginuid, loginUsername)
|
||||
}
|
||||
|
||||
log.Printf("Event: PID=%d UID=%d (%s) CMD=%s FILE=%s FLAGS=%08x",
|
||||
e.Pid,
|
||||
e.Uid,
|
||||
userInfo,
|
||||
string(bytes.TrimRight(e.Comm[:], "\x00")),
|
||||
string(bytes.TrimRight(e.Filename[:], "\x00")),
|
||||
e.Flags,
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Println("Monitoring started. Ctrl+C to exit.")
|
||||
<-sig
|
||||
log.Println("Shutting down...")
|
||||
}
|
||||
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user