diff --git a/cmd/aurora/agent/agent.go b/cmd/aurora/agent/agent.go index 7ad1cb1..5cb92c0 100644 --- a/cmd/aurora/agent/agent.go +++ b/cmd/aurora/agent/agent.go @@ -147,6 +147,7 @@ func (a *Agent) Run() error { _ = a.listener.AddSource(ebpfprovider.SourceProcessExec) _ = a.listener.AddSource(ebpfprovider.SourceFileCreate) _ = a.listener.AddSource(ebpfprovider.SourceNetConnect) + _ = a.listener.AddSource(ebpfprovider.SourceBpfEvent) if err := a.listener.Initialize(); err != nil { return fmt.Errorf("initializing eBPF listener: %w", err) diff --git a/lib/consumer/sigma/sigmaconsumer.go b/lib/consumer/sigma/sigmaconsumer.go index 9f351bb..2a4c246 100644 --- a/lib/consumer/sigma/sigmaconsumer.go +++ b/lib/consumer/sigma/sigmaconsumer.go @@ -8,8 +8,8 @@ import ( "sync/atomic" "time" - sigma "github.com/markuskont/go-sigma-rule-engine" "github.com/Nextron-Labs/aurora-linux/lib/provider" + sigma "github.com/markuskont/go-sigma-rule-engine" log "github.com/sirupsen/logrus" "golang.org/x/time/rate" ) @@ -106,6 +106,7 @@ func (s *SigmaConsumer) InitializeWithRules(ruleDirs []string) error { NoCollapseWS: s.noCollapse, }) if err != nil { + log.WithError(err).Error("SigmaConsumer: failed to create sigma ruleset") return fmt.Errorf("creating sigma ruleset: %w", err) } @@ -303,6 +304,10 @@ type sigmaEventWrapper struct { // Select implements sigma.Selector — performs key-value lookup for structured data. func (w *sigmaEventWrapper) Select(key string) (interface{}, bool) { + // EventID lives on the event identifier, not in the data fields. + if key == "EventID" { + return int(w.event.ID().EventID), true + } v := w.event.Value(key) if !v.Valid { return nil, false diff --git a/lib/provider/ebpf/bpf/bpf_monitor.c b/lib/provider/ebpf/bpf/bpf_monitor.c new file mode 100644 index 0000000..d76e541 --- /dev/null +++ b/lib/provider/ebpf/bpf/bpf_monitor.c @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0 +// bpf_monitor.c — BPF programs for tracepoint/syscalls/sys_{enter,exit}_bpf +// +// Traces bpf() syscall invocations by pairing sys_enter_bpf (capture command, +// program type, and program name) with sys_exit_bpf (check return value). +// Only successful calls are emitted. For BPF_PROG_LOAD, the program type and +// name are read from the user-space bpf_attr union. + +//go:build ignore + +#include "vmlinux.h" +#include +#include + +#define BPF_OBJ_NAME_LEN 16 + +// Offsets within the bpf_attr union for BPF_PROG_LOAD: +// prog_type at offset 0 (u32) +// prog_name at offset 48 (char[16]) +#define ATTR_PROG_TYPE_OFF 0 +#define ATTR_PROG_NAME_OFF 48 + +// Temporary storage for in-flight bpf() calls, keyed by pid_tgid. +struct bpf_call_args { + __u32 cmd; + __u32 prog_type; + char prog_name[BPF_OBJ_NAME_LEN]; +}; + +struct bpf_event { + __u64 timestamp_ns; + __u32 pid; + __u32 uid; + __u32 cmd; + __u32 prog_type; + __s64 ret_val; // fd on success, negative errno on failure + char prog_name[BPF_OBJ_NAME_LEN]; +}; + +// Per-CPU hash to correlate enter/exit +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 10240); + __type(key, __u64); + __type(value, struct bpf_call_args); +} bpf_args SEC(".maps"); + +// Ring buffer for bpf events +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 2 * 1024 * 1024); // 2 MB +} bpf_events SEC(".maps"); + +// Lost event counter +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 1); + __type(key, __u32); + __type(value, __u64); +} bpf_lost_events SEC(".maps"); + +// PIDs that should be excluded from telemetry (Aurora itself). +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 64); + __type(key, __u32); + __type(value, __u8); +} self_pids SEC(".maps"); + +SEC("tracepoint/syscalls/sys_enter_bpf") +int trace_sys_enter_bpf(struct trace_event_raw_sys_enter *ctx) { + __u64 pid_tgid = bpf_get_current_pid_tgid(); + __u32 pid = pid_tgid >> 32; + if (bpf_map_lookup_elem(&self_pids, &pid)) + return 0; + + // args: cmd(0), attr(1), size(2) + __u32 cmd = (__u32)ctx->args[0]; + const void *uattr = (const void *)ctx->args[1]; + + struct bpf_call_args args = {}; + args.cmd = cmd; + + // For BPF_PROG_LOAD (cmd=5), read program type and name from uattr + if (cmd == 5 && uattr) { + bpf_probe_read_user(&args.prog_type, sizeof(args.prog_type), + uattr + ATTR_PROG_TYPE_OFF); + bpf_probe_read_user_str(args.prog_name, sizeof(args.prog_name), + uattr + ATTR_PROG_NAME_OFF); + } + + bpf_map_update_elem(&bpf_args, &pid_tgid, &args, BPF_ANY); + return 0; +} + +SEC("tracepoint/syscalls/sys_exit_bpf") +int trace_sys_exit_bpf(struct trace_event_raw_sys_exit *ctx) { + __u64 pid_tgid = bpf_get_current_pid_tgid(); + __u32 pid = pid_tgid >> 32; + if (bpf_map_lookup_elem(&self_pids, &pid)) + return 0; + + struct bpf_call_args *args = bpf_map_lookup_elem(&bpf_args, &pid_tgid); + if (!args) + return 0; + + // Clean up immediately + struct bpf_call_args saved = *args; + bpf_map_delete_elem(&bpf_args, &pid_tgid); + + long retval = ctx->ret; + + // Only emit successful calls (retval >= 0) + if (retval < 0) + return 0; + + struct bpf_event *evt = bpf_ringbuf_reserve(&bpf_events, sizeof(*evt), 0); + if (!evt) { + __u32 key = 0; + __u64 *count = bpf_map_lookup_elem(&bpf_lost_events, &key); + if (count) + __sync_fetch_and_add(count, 1); + return 0; + } + + evt->timestamp_ns = bpf_ktime_get_ns(); + evt->pid = pid; + + __u64 uid_gid = bpf_get_current_uid_gid(); + evt->uid = uid_gid & 0xFFFFFFFF; + + evt->cmd = saved.cmd; + evt->prog_type = saved.prog_type; + evt->ret_val = retval; + + __builtin_memcpy(evt->prog_name, saved.prog_name, BPF_OBJ_NAME_LEN); + + bpf_ringbuf_submit(evt, 0); + return 0; +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/lib/provider/ebpf/bpf_stubs.go b/lib/provider/ebpf/bpf_stubs.go index a5b0292..aff2d27 100644 --- a/lib/provider/ebpf/bpf_stubs.go +++ b/lib/provider/ebpf/bpf_stubs.go @@ -58,3 +58,18 @@ func (o *netMonitorObjects) Close() error { return nil } func loadNetMonitorObjects(objs *netMonitorObjects, opts *ciliumebpf.CollectionOptions) error { return fmt.Errorf("BPF programs are only available on Linux; run go generate on a Linux host") } + +// bpfMonitorObjects holds the BPF objects for the bpf syscall monitor. +type bpfMonitorObjects struct { + TraceSysEnterBpf *ciliumebpf.Program + TraceSysExitBpf *ciliumebpf.Program + BpfEvents *ciliumebpf.Map + BpfLostEvents *ciliumebpf.Map + SelfPids *ciliumebpf.Map +} + +func (o *bpfMonitorObjects) Close() error { return nil } + +func loadBpfMonitorObjects(objs *bpfMonitorObjects, opts *ciliumebpf.CollectionOptions) error { + return fmt.Errorf("BPF programs are only available on Linux; run go generate on a Linux host") +} diff --git a/lib/provider/ebpf/event.go b/lib/provider/ebpf/event.go index 5f176d7..cf84b86 100644 --- a/lib/provider/ebpf/event.go +++ b/lib/provider/ebpf/event.go @@ -14,6 +14,7 @@ const ( EventIDProcessCreation uint16 = 1 EventIDNetworkConnection uint16 = 3 EventIDFileEvent uint16 = 11 + EventIDBpfEvent uint16 = 100 ) // ebpfEvent is the concrete Event implementation for events from the eBPF provider. diff --git a/lib/provider/ebpf/fieldmap.go b/lib/provider/ebpf/fieldmap.go index 2add3f0..e6e9a92 100644 --- a/lib/provider/ebpf/fieldmap.go +++ b/lib/provider/ebpf/fieldmap.go @@ -110,6 +110,124 @@ func buildNetFieldsMap( return fields } +// BPF command names aligned with Sysmon output. +var bpfCmdNames = map[uint32]string{ + 0: "BPF_MAP_CREATE", + 1: "BPF_MAP_LOOKUP_ELEM", + 2: "BPF_MAP_UPDATE_ELEM", + 3: "BPF_MAP_DELETE_ELEM", + 4: "BPF_MAP_GET_NEXT_KEY", + 5: "BPF_PROG_LOAD", + 6: "BPF_OBJ_PIN", + 7: "BPF_OBJ_GET", + 8: "BPF_PROG_ATTACH", + 9: "BPF_PROG_DETACH", + 10: "BPF_PROG_TEST_RUN", + 11: "BPF_PROG_GET_NEXT_ID", + 12: "BPF_MAP_GET_NEXT_ID", + 13: "BPF_PROG_GET_FD_BY_ID", + 14: "BPF_MAP_GET_FD_BY_ID", + 15: "BPF_OBJ_GET_INFO_BY_FD", + 16: "BPF_PROG_QUERY", + 17: "BPF_RAW_TRACEPOINT_OPEN", + 18: "BPF_BTF_LOAD", + 19: "BPF_BTF_GET_FD_BY_ID", + 20: "BPF_TASK_FD_QUERY", + 21: "BPF_MAP_LOOKUP_AND_DELETE_ELEM", + 22: "BPF_MAP_FREEZE", + 23: "BPF_BTF_GET_NEXT_ID", + 24: "BPF_MAP_LOOKUP_BATCH", + 25: "BPF_MAP_LOOKUP_AND_DELETE_BATCH", + 26: "BPF_MAP_UPDATE_BATCH", + 27: "BPF_MAP_DELETE_BATCH", + 28: "BPF_LINK_CREATE", + 29: "BPF_LINK_UPDATE", + 30: "BPF_LINK_GET_FD_BY_ID", + 31: "BPF_LINK_GET_NEXT_ID", + 32: "BPF_ENABLE_STATS", + 33: "BPF_ITER_CREATE", + 34: "BPF_LINK_DETACH", + 35: "BPF_PROG_BIND_MAP", +} + +// BPF program type names aligned with Sysmon output. +var bpfProgTypeNames = map[uint32]string{ + 0: "UNSPEC", + 1: "SOCKET_FILTER", + 2: "KPROBE", + 3: "SCHED_CLS", + 4: "SCHED_ACT", + 5: "TRACEPOINT", + 6: "XDP", + 7: "PERF_EVENT", + 8: "CGROUP_SKB", + 9: "CGROUP_SOCK", + 10: "LWT_IN", + 11: "LWT_OUT", + 12: "LWT_XMIT", + 13: "SOCK_OPS", + 14: "SK_SKB", + 15: "CGROUP_DEVICE", + 16: "SK_MSG", + 17: "RAW_TRACEPOINT", + 18: "CGROUP_SOCK_ADDR", + 19: "LWT_SEG6LOCAL", + 20: "LIRC_MODE2", + 21: "SK_REUSEPORT", + 22: "FLOW_DISSECTOR", + 23: "CGROUP_SYSCTL", + 24: "RAW_TRACEPOINT_WRITABLE", + 25: "CGROUP_SOCKOPT", + 26: "TRACING", + 27: "STRUCT_OPS", + 28: "EXT", + 29: "LSM", + 30: "SK_LOOKUP", + 31: "SYSCALL", +} + +func bpfCmdName(cmd uint32) string { + if name, ok := bpfCmdNames[cmd]; ok { + return name + } + return strconv.FormatUint(uint64(cmd), 10) +} + +func bpfProgTypeName(pt uint32) string { + if name, ok := bpfProgTypeNames[pt]; ok { + return name + } + return strconv.FormatUint(uint64(pt), 10) +} + +// buildBpfFieldsMap constructs the DataFieldsMap for a bpf_event. +func buildBpfFieldsMap( + pid, uid uint32, + image string, + username string, + cmd uint32, + progType uint32, + retVal int64, + progName string, +) enrichment.DataFieldsMap { + fields := make(enrichment.DataFieldsMap, 8) + + fields.AddField("Image", image) + fields.AddField("User", username) + fields.AddField("ProcessId", strconv.FormatUint(uint64(pid), 10)) + fields.AddField("BpfCommand", bpfCmdName(cmd)) + fields.AddField("BpfProgramType", bpfProgTypeName(progType)) + fields.AddField("BpfProgramId", strconv.FormatInt(retVal, 10)) + + if progName == "" { + fields.AddField("BpfProgramName", "-") + } else { + fields.AddField("BpfProgramName", progName) + } + + return fields +} + // formatIPv4 formats a v4-mapped-v6 address (bytes 12-15) as dotted decimal. func formatIPv4(addr [16]byte) string { return strconv.Itoa(int(addr[12])) + "." + diff --git a/lib/provider/ebpf/generate.go b/lib/provider/ebpf/generate.go index 612c2a6..30b67fe 100644 --- a/lib/provider/ebpf/generate.go +++ b/lib/provider/ebpf/generate.go @@ -11,3 +11,4 @@ package ebpf //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpfel -cc clang execMonitor bpf/exec_monitor.c -- -I/usr/include -I./bpf/headers //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpfel -cc clang fileMonitor bpf/file_monitor.c -- -I/usr/include -I./bpf/headers //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpfel -cc clang netMonitor bpf/net_monitor.c -- -I/usr/include -I./bpf/headers +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpfel -cc clang bpfMonitor bpf/bpf_monitor.c -- -I/usr/include -I./bpf/headers diff --git a/lib/provider/ebpf/listener.go b/lib/provider/ebpf/listener.go index 88bb93f..bf3608e 100644 --- a/lib/provider/ebpf/listener.go +++ b/lib/provider/ebpf/listener.go @@ -23,6 +23,7 @@ const ( SourceProcessExec = "LinuxEBPF:ProcessExec" SourceFileCreate = "LinuxEBPF:FileCreate" SourceNetConnect = "LinuxEBPF:NetConnect" + SourceBpfEvent = "LinuxEBPF:BpfEvent" readErrorBackoff = 100 * time.Millisecond ) @@ -33,20 +34,25 @@ type Listener struct { enableExec bool enableFile bool enableNet bool + enableBpf bool // eBPF objects and links execObjs *execMonitorObjects fileObjs *fileMonitorObjects netObjs *netMonitorObjects + bpfObjs *bpfMonitorObjects execLink link.Link fileEnter link.Link fileExit link.Link netLink link.Link + bpfEnter link.Link + bpfExit link.Link // Ring buffer readers execReader *ringbuf.Reader fileReader *ringbuf.Reader netReader *ringbuf.Reader + bpfReader *ringbuf.Reader // Correlation and caches correlator *enrichment.Correlator @@ -62,6 +68,7 @@ type Listener struct { initExecFn func() error initFileFn func() error initNetFn func() error + initBpfFn func() error } // NewListener creates a new eBPF listener with the given correlator. @@ -83,6 +90,8 @@ func (l *Listener) AddSource(source string) error { l.enableFile = true case SourceNetConnect: l.enableNet = true + case SourceBpfEvent: + l.enableBpf = true default: return fmt.Errorf("unknown source: %s", source) } @@ -142,6 +151,16 @@ func (l *Listener) Initialize() error { initialized++ } } + if l.enableBpf { + requested++ + if err := l.initBpfSource(); err != nil { + l.enableBpf = false + initErrs = append(initErrs, fmt.Errorf("bpf monitor: %w", err)) + log.WithError(err).Warn("Failed to initialize bpf monitor; source disabled") + } else { + initialized++ + } + } if requested > 0 && initialized == 0 { return fmt.Errorf("failed to initialize any eBPF monitor: %w", errors.Join(initErrs...)) @@ -178,6 +197,13 @@ func (l *Listener) initNetSource() error { return l.initNet() } +func (l *Listener) initBpfSource() error { + if l.initBpfFn != nil { + return l.initBpfFn() + } + return l.initBpf() +} + // initExec loads the exec monitor BPF program and attaches to sched_process_exec. func (l *Listener) initExec() error { objs := &execMonitorObjects{} @@ -280,6 +306,46 @@ func (l *Listener) initNet() error { return nil } +// initBpf loads the bpf monitor BPF program and attaches to bpf enter/exit. +func (l *Listener) initBpf() error { + objs := &bpfMonitorObjects{} + if err := loadBpfMonitorObjects(objs, nil); err != nil { + return classifyBPFError(err, "bpf_monitor") + } + if err := l.registerSelfPID(objs.SelfPids, "bpf"); err != nil { + objs.Close() + return err + } + + enter, err := link.Tracepoint("syscalls", "sys_enter_bpf", objs.TraceSysEnterBpf, nil) + if err != nil { + objs.Close() + return fmt.Errorf("attaching sys_enter_bpf: %w", err) + } + + exit, err := link.Tracepoint("syscalls", "sys_exit_bpf", objs.TraceSysExitBpf, nil) + if err != nil { + enter.Close() + objs.Close() + return fmt.Errorf("attaching sys_exit_bpf: %w", err) + } + + rd, err := ringbuf.NewReader(objs.BpfEvents) + if err != nil { + exit.Close() + enter.Close() + objs.Close() + return fmt.Errorf("creating bpf ring buffer reader: %w", err) + } + + l.bpfObjs = objs + l.bpfEnter = enter + l.bpfExit = exit + l.bpfReader = rd + + return nil +} + func (l *Listener) registerSelfPID(m *ebpf.Map, source string) error { if m == nil { return fmt.Errorf("registering self PID for %s monitor: self_pids map is nil", source) @@ -308,6 +374,10 @@ func (l *Listener) SendEvents(callback func(event provider.Event)) { l.wg.Add(1) go l.readNetEvents(callback) } + if l.enableBpf && l.bpfReader != nil { + l.wg.Add(1) + go l.readBpfEvents(callback) + } l.wg.Wait() } @@ -399,6 +469,35 @@ func (l *Listener) readNetEvents(callback func(event provider.Event)) { } } +// readBpfEvents reads from the bpf ring buffer and processes events. +func (l *Listener) readBpfEvents(callback func(event provider.Event)) { + defer l.wg.Done() + + var record ringbuf.Record + for { + err := l.bpfReader.ReadInto(&record) + if err != nil { + if errors.Is(err, ringbuf.ErrClosed) { + return + } + log.WithError(err).Error("Reading bpf ring buffer") + if l.closed.Load() { + return + } + time.Sleep(readErrorBackoff) + continue + } + + evt, err := l.parseBpfEvent(record.RawSample) + if err != nil { + log.WithError(err).Debug("Parsing bpf event") + continue + } + + callback(evt) + } +} + // BPF binary structs for parsing ring buffer records. type bpfExecEvent struct { @@ -435,6 +534,16 @@ type bpfNetEvent struct { Pad uint16 } +type bpfBpfEvent struct { + TimestampNs uint64 + Pid uint32 + Uid uint32 + Cmd uint32 + ProgType uint32 + RetVal int64 + ProgName [16]byte +} + // parseExecEvent parses a raw exec event from the ring buffer and reconstructs // all fields from /proc. func (l *Listener) parseExecEvent(data []byte) (*ebpfEvent, error) { @@ -607,6 +716,44 @@ func (l *Listener) parseNetEvent(data []byte) (*ebpfEvent, error) { }, nil } +// parseBpfEvent parses a raw bpf syscall event and reconstructs fields. +func (l *Listener) parseBpfEvent(data []byte) (*ebpfEvent, error) { + var raw bpfBpfEvent + if err := binary.Read(bytes.NewReader(data), binary.LittleEndian, &raw); err != nil { + return nil, fmt.Errorf("decoding bpf event: %w", err) + } + + pid := raw.Pid + uid := raw.Uid + + // Resolve Image + image, _ := readExeLink(pid) + if image == "" && l.correlator != nil { + if info := l.correlator.Lookup(pid); info != nil { + image = info.Image + } + } + + username := l.userCache.Lookup(uid) + + fields := buildBpfFieldsMap( + pid, uid, image, username, + raw.Cmd, raw.ProgType, raw.RetVal, + nullTermStr(raw.ProgName[:], len(raw.ProgName)), + ) + + return &ebpfEvent{ + id: provider.EventIdentifier{ + ProviderName: ProviderName, + EventID: EventIDBpfEvent, + }, + pid: pid, + source: SourceBpfEvent, + ts: l.ktimeToWall(raw.TimestampNs), + fields: fields, + }, nil +} + // LostEvents returns the total number of lost events across all enabled sources. func (l *Listener) LostEvents() uint64 { var total uint64 @@ -620,6 +767,9 @@ func (l *Listener) LostEvents() uint64 { if l.netObjs != nil { total += readLostCounter(l.netObjs.NetLostEvents) } + if l.bpfObjs != nil { + total += readLostCounter(l.bpfObjs.BpfLostEvents) + } return total } @@ -648,6 +798,11 @@ func (l *Listener) Close() error { closeErrs = append(closeErrs, fmt.Errorf("closing net reader: %w", err)) } } + if l.bpfReader != nil { + if err := l.bpfReader.Close(); err != nil { + closeErrs = append(closeErrs, fmt.Errorf("closing bpf reader: %w", err)) + } + } // Detach tracepoints if l.execLink != nil { @@ -670,6 +825,16 @@ func (l *Listener) Close() error { closeErrs = append(closeErrs, fmt.Errorf("closing net link: %w", err)) } } + if l.bpfEnter != nil { + if err := l.bpfEnter.Close(); err != nil { + closeErrs = append(closeErrs, fmt.Errorf("closing bpf enter link: %w", err)) + } + } + if l.bpfExit != nil { + if err := l.bpfExit.Close(); err != nil { + closeErrs = append(closeErrs, fmt.Errorf("closing bpf exit link: %w", err)) + } + } // Close BPF objects if l.execObjs != nil { @@ -687,6 +852,11 @@ func (l *Listener) Close() error { closeErrs = append(closeErrs, fmt.Errorf("closing net objects: %w", err)) } } + if l.bpfObjs != nil { + if err := l.bpfObjs.Close(); err != nil { + closeErrs = append(closeErrs, fmt.Errorf("closing bpf objects: %w", err)) + } + } return errors.Join(closeErrs...) } diff --git a/lib/provider/replay/replay.go b/lib/provider/replay/replay.go index 3818dc0..6054fbe 100644 --- a/lib/provider/replay/replay.go +++ b/lib/provider/replay/replay.go @@ -228,6 +228,8 @@ func defaultSourceForEvent(providerName string, eventID uint16) string { return "LinuxEBPF:NetConnect" case 11: return "LinuxEBPF:FileCreate" + case 100: + return "LinuxEBPF:BpfEvent" default: return "" }