feat(go): advance validation sidecar toward production
This commit is contained in:
parent
6a06eba255
commit
f85c2e831a
92 changed files with 8881 additions and 91 deletions
84
go-agent/internal/ebpfsource/bpf/exec.bpf.c
Normal file
84
go-agent/internal/ebpfsource/bpf/exec.bpf.c
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#define SEC(name) __attribute__((section(name), used))
|
||||
#define __uint(name, value) int (*name)[value]
|
||||
|
||||
typedef unsigned int u32;
|
||||
typedef unsigned long long u64;
|
||||
|
||||
#define BPF_MAP_TYPE_PERF_EVENT_ARRAY 4
|
||||
#define BPF_F_CURRENT_CPU 0xffffffffULL
|
||||
|
||||
struct trace_event_raw_sys_enter {
|
||||
u64 _common;
|
||||
long id;
|
||||
unsigned long args[6];
|
||||
};
|
||||
|
||||
// Minimal CO-RE flavor of task_struct. The loader relocates these two fields
|
||||
// against the running kernel's BTF, avoiding a generated vmlinux.h dependency.
|
||||
struct task_struct {
|
||||
int tgid;
|
||||
struct task_struct *real_parent;
|
||||
} __attribute__((preserve_access_index));
|
||||
|
||||
struct exec_event {
|
||||
u32 pid;
|
||||
u32 ppid;
|
||||
u32 uid;
|
||||
char parent_comm[16];
|
||||
char filename[128];
|
||||
char arg1[128];
|
||||
char arg2[128];
|
||||
};
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
|
||||
__uint(key_size, sizeof(u32));
|
||||
__uint(value_size, sizeof(u32));
|
||||
} events SEC(".maps");
|
||||
|
||||
static u64 (*bpf_get_current_pid_tgid)(void) = (void *)14;
|
||||
static u64 (*bpf_get_current_uid_gid)(void) = (void *)15;
|
||||
static long (*bpf_get_current_comm)(void *buf, u32 size) = (void *)16;
|
||||
static long (*bpf_probe_read_user)(void *dst, u32 size, const void *unsafe_ptr) = (void *)112;
|
||||
static long (*bpf_probe_read_user_str)(void *dst, u32 size, const void *unsafe_ptr) = (void *)114;
|
||||
static long (*bpf_probe_read_kernel)(void *dst, u32 size, const void *unsafe_ptr) = (void *)113;
|
||||
static long (*bpf_perf_event_output)(void *ctx, void *map, u64 flags,
|
||||
void *data, u64 size) = (void *)25;
|
||||
static u64 (*bpf_get_current_task)(void) = (void *)35;
|
||||
|
||||
#define bpf_core_read(dst, size, src) \
|
||||
bpf_probe_read_kernel((dst), (size), \
|
||||
(const void *)__builtin_preserve_access_index(src))
|
||||
|
||||
SEC("tracepoint/syscalls/sys_enter_execve")
|
||||
int trace_execve(struct trace_event_raw_sys_enter *ctx)
|
||||
{
|
||||
struct exec_event event = {};
|
||||
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
|
||||
struct task_struct *parent = 0;
|
||||
const char *const *argv = (const char *const *)ctx->args[1];
|
||||
const char *argument = 0;
|
||||
|
||||
event.pid = bpf_get_current_pid_tgid() >> 32;
|
||||
event.uid = bpf_get_current_uid_gid();
|
||||
bpf_core_read(&parent, sizeof(parent), &task->real_parent);
|
||||
if (parent)
|
||||
bpf_core_read(&event.ppid, sizeof(event.ppid), &parent->tgid);
|
||||
bpf_get_current_comm(event.parent_comm, sizeof(event.parent_comm));
|
||||
bpf_probe_read_user_str(event.filename, sizeof(event.filename),
|
||||
(const void *)ctx->args[0]);
|
||||
bpf_probe_read_user(&argument, sizeof(argument), &argv[1]);
|
||||
if (argument)
|
||||
bpf_probe_read_user_str(event.arg1, sizeof(event.arg1), argument);
|
||||
argument = 0;
|
||||
bpf_probe_read_user(&argument, sizeof(argument), &argv[2]);
|
||||
if (argument)
|
||||
bpf_probe_read_user_str(event.arg2, sizeof(event.arg2), argument);
|
||||
|
||||
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
|
||||
return 0;
|
||||
}
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
212
go-agent/internal/ebpfsource/bpf/syscall.bpf.c
Normal file
212
go-agent/internal/ebpfsource/bpf/syscall.bpf.c
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#define SEC(name) __attribute__((section(name), used))
|
||||
#define __uint(name, value) int (*name)[value]
|
||||
|
||||
typedef unsigned int u32;
|
||||
typedef unsigned short u16;
|
||||
typedef unsigned long long u64;
|
||||
|
||||
#define BPF_MAP_TYPE_HASH 1
|
||||
#define BPF_MAP_TYPE_PERF_EVENT_ARRAY 4
|
||||
#define BPF_F_CURRENT_CPU 0xffffffffULL
|
||||
|
||||
struct trace_event_raw_sys_enter {
|
||||
u64 _common;
|
||||
long id;
|
||||
unsigned long args[6];
|
||||
};
|
||||
|
||||
struct trace_event_raw_sys_exit {
|
||||
u64 _common;
|
||||
long id;
|
||||
long ret;
|
||||
};
|
||||
|
||||
struct task_struct {
|
||||
int tgid;
|
||||
struct task_struct *real_parent;
|
||||
} __attribute__((preserve_access_index));
|
||||
|
||||
struct syscall_event {
|
||||
u32 pid;
|
||||
u32 ppid;
|
||||
u32 uid;
|
||||
u32 syscall_id;
|
||||
char comm[16];
|
||||
u64 args[6];
|
||||
char text[80];
|
||||
};
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_HASH);
|
||||
__uint(key_size, sizeof(u64));
|
||||
__uint(value_size, sizeof(u32));
|
||||
__uint(max_entries, 64);
|
||||
} monitored_syscalls SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_HASH);
|
||||
__uint(key_size, 16);
|
||||
__uint(value_size, sizeof(u32));
|
||||
__uint(max_entries, 64);
|
||||
} monitored_host_comms SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_HASH);
|
||||
__uint(key_size, sizeof(u64));
|
||||
__uint(value_size, sizeof(struct syscall_event));
|
||||
__uint(max_entries, 4096);
|
||||
} pending_returns SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
|
||||
__uint(key_size, sizeof(u32));
|
||||
__uint(value_size, sizeof(u32));
|
||||
} syscall_events SEC(".maps");
|
||||
|
||||
static u64 (*bpf_get_current_pid_tgid)(void) = (void *)14;
|
||||
static u64 (*bpf_get_current_uid_gid)(void) = (void *)15;
|
||||
static long (*bpf_get_current_comm)(void *buf, u32 size) = (void *)16;
|
||||
static void *(*bpf_map_lookup_elem)(void *map, const void *key) = (void *)1;
|
||||
static long (*bpf_map_update_elem)(void *map, const void *key, const void *value, u64 flags) = (void *)2;
|
||||
static long (*bpf_map_delete_elem)(void *map, const void *key) = (void *)3;
|
||||
static long (*bpf_probe_read_user_str)(void *dst, u32 size, const void *unsafe_ptr) = (void *)114;
|
||||
static long (*bpf_probe_read_user)(void *dst, u32 size, const void *unsafe_ptr) = (void *)112;
|
||||
static long (*bpf_probe_read_kernel)(void *dst, u32 size, const void *unsafe_ptr) = (void *)113;
|
||||
static long (*bpf_perf_event_output)(void *ctx, void *map, u64 flags,
|
||||
void *data, u64 size) = (void *)25;
|
||||
static u64 (*bpf_get_current_task)(void) = (void *)35;
|
||||
|
||||
#define bpf_core_read(dst, size, src) \
|
||||
bpf_probe_read_kernel((dst), (size), \
|
||||
(const void *)__builtin_preserve_access_index(src))
|
||||
|
||||
SEC("tracepoint/raw_syscalls/sys_enter")
|
||||
int trace_sys_enter(struct trace_event_raw_sys_enter *ctx)
|
||||
{
|
||||
u64 syscall_number = ctx->id;
|
||||
u32 *syscall_id = bpf_map_lookup_elem(&monitored_syscalls, &syscall_number);
|
||||
struct syscall_event event = {};
|
||||
struct task_struct *task;
|
||||
struct task_struct *parent = 0;
|
||||
|
||||
if (!syscall_id)
|
||||
return 0;
|
||||
|
||||
task = (struct task_struct *)bpf_get_current_task();
|
||||
event.pid = bpf_get_current_pid_tgid() >> 32;
|
||||
event.uid = bpf_get_current_uid_gid();
|
||||
event.syscall_id = *syscall_id;
|
||||
bpf_core_read(&parent, sizeof(parent), &task->real_parent);
|
||||
if (parent)
|
||||
bpf_core_read(&event.ppid, sizeof(event.ppid), &parent->tgid);
|
||||
bpf_get_current_comm(event.comm, sizeof(event.comm));
|
||||
|
||||
// Canonical ID 21 covers write/writev/pwrite variants. Keep the hot path
|
||||
// in-kernel and emit only for process names that can satisfy the
|
||||
// persistence-write host rule.
|
||||
if (*syscall_id >= 21 && *syscall_id <= 26 &&
|
||||
!bpf_map_lookup_elem(&monitored_host_comms, event.comm))
|
||||
return 0;
|
||||
|
||||
// Match the Python BCC transport's semantic argument layout rather than
|
||||
// exposing pointer-only raw ABI slots in alert keys and details.
|
||||
if (*syscall_id == 3) { // memfd_create(name, flags)
|
||||
event.args[0] = ctx->args[1];
|
||||
bpf_probe_read_user_str(event.text, sizeof(event.text),
|
||||
(const void *)ctx->args[0]);
|
||||
} else if (*syscall_id == 7 || *syscall_id == 8) { // process_vm_{readv,writev}
|
||||
event.args[0] = ctx->args[0]; // target pid
|
||||
event.args[1] = ctx->args[2]; // local iovec count
|
||||
event.args[2] = ctx->args[4]; // remote iovec count
|
||||
event.args[3] = ctx->args[5]; // flags
|
||||
} else if (*syscall_id == 14) { // chmod(path, mode)
|
||||
event.args[0] = ctx->args[1];
|
||||
event.args[2] = (u64)-100; // AT_FDCWD
|
||||
bpf_probe_read_user_str(event.text, sizeof(event.text),
|
||||
(const void *)ctx->args[0]);
|
||||
} else if (*syscall_id == 15) { // fchmodat(dirfd, path, mode)
|
||||
event.args[0] = ctx->args[2];
|
||||
event.args[2] = ctx->args[0];
|
||||
bpf_probe_read_user_str(event.text, sizeof(event.text),
|
||||
(const void *)ctx->args[1]);
|
||||
} else if (*syscall_id == 16 || *syscall_id == 17) { // chown/lchown(path, uid, gid)
|
||||
event.args[0] = ctx->args[1];
|
||||
event.args[1] = ctx->args[2];
|
||||
event.args[2] = (u64)-100; // AT_FDCWD
|
||||
bpf_probe_read_user_str(event.text, sizeof(event.text),
|
||||
(const void *)ctx->args[0]);
|
||||
} else if (*syscall_id == 18) { // fchownat(dirfd, path, uid, gid)
|
||||
event.args[0] = ctx->args[2];
|
||||
event.args[1] = ctx->args[3];
|
||||
event.args[2] = ctx->args[0];
|
||||
bpf_probe_read_user_str(event.text, sizeof(event.text),
|
||||
(const void *)ctx->args[1]);
|
||||
} else if (*syscall_id == 19) { // capset(header, data[2])
|
||||
u32 effective_low = 0;
|
||||
u32 effective_high = 0;
|
||||
const char *data = (const char *)ctx->args[1];
|
||||
bpf_probe_read_user(&effective_low, sizeof(effective_low), data);
|
||||
bpf_probe_read_user(&effective_high, sizeof(effective_high), data + 12);
|
||||
event.args[0] = effective_low | ((u64)effective_high << 32);
|
||||
} else if (*syscall_id == 22 || *syscall_id == 23) { // connect/bind(fd, sockaddr, len)
|
||||
const char *address = (const char *)ctx->args[1];
|
||||
u16 family = 0;
|
||||
u16 port = 0;
|
||||
u32 ipv4 = 0;
|
||||
bpf_probe_read_user(&family, sizeof(family), address);
|
||||
bpf_probe_read_user(&port, sizeof(port), address + 2);
|
||||
event.args[0] = family;
|
||||
event.args[3] = ctx->args[0];
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
event.args[1] = __builtin_bswap16(port);
|
||||
#else
|
||||
event.args[1] = port;
|
||||
#endif
|
||||
if (family == 2) {
|
||||
bpf_probe_read_user(&ipv4, sizeof(ipv4), address + 4);
|
||||
event.args[2] = ipv4;
|
||||
} else if (family == 10) {
|
||||
bpf_probe_read_user(event.text, 16, address + 8);
|
||||
}
|
||||
} else if (*syscall_id >= 24 && *syscall_id <= 26) { // listen/accept/accept4
|
||||
event.args[3] = ctx->args[0];
|
||||
} else {
|
||||
#pragma unroll
|
||||
for (int i = 0; i < 6; i++)
|
||||
event.args[i] = ctx->args[i];
|
||||
}
|
||||
|
||||
if (*syscall_id >= 21 && *syscall_id <= 26) {
|
||||
u64 pid_tgid = bpf_get_current_pid_tgid();
|
||||
bpf_map_update_elem(&pending_returns, &pid_tgid, &event, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bpf_perf_event_output(ctx, &syscall_events, BPF_F_CURRENT_CPU, &event, sizeof(event));
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("tracepoint/raw_syscalls/sys_exit")
|
||||
int trace_sys_exit(struct trace_event_raw_sys_exit *ctx)
|
||||
{
|
||||
u64 pid_tgid = bpf_get_current_pid_tgid();
|
||||
struct syscall_event *event = bpf_map_lookup_elem(&pending_returns, &pid_tgid);
|
||||
|
||||
if (!event)
|
||||
return 0;
|
||||
if (event->syscall_id == 25 || event->syscall_id == 26)
|
||||
event->args[3] = ctx->ret;
|
||||
// Every write-family syscall returns its byte count, so the shared
|
||||
// canonical event is confirmed only when at least one byte was written.
|
||||
if ((event->syscall_id == 21 && ctx->ret > 0) ||
|
||||
(event->syscall_id >= 22 && event->syscall_id <= 24 && ctx->ret == 0) ||
|
||||
((event->syscall_id == 25 || event->syscall_id == 26) && ctx->ret >= 0))
|
||||
bpf_perf_event_output(ctx, &syscall_events, BPF_F_CURRENT_CPU,
|
||||
event, sizeof(*event));
|
||||
bpf_map_delete_elem(&pending_returns, &pid_tgid);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
131
go-agent/internal/ebpfsource/exec.go
Normal file
131
go-agent/internal/ebpfsource/exec.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package ebpfsource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cilium/ebpf/link"
|
||||
"github.com/cilium/ebpf/perf"
|
||||
"github.com/cilium/ebpf/rlimit"
|
||||
|
||||
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/events"
|
||||
)
|
||||
|
||||
// ExecSource owns the Go-native exec tracepoint and perf reader. Load errors
|
||||
// are returned to the caller so startup can log and fall back to polling.
|
||||
type ExecSource struct {
|
||||
objects execObjects
|
||||
link link.Link
|
||||
reader *perf.Reader
|
||||
}
|
||||
|
||||
// execEventRaw mirrors struct exec_event in bpf/exec.bpf.c. Keeping the wire
|
||||
// layout explicit makes changes to the kernel/user-space ABI reviewable.
|
||||
type execEventRaw struct {
|
||||
PID uint32
|
||||
PPID uint32
|
||||
UID uint32
|
||||
ParentComm [16]int8
|
||||
Filename [128]int8
|
||||
Arg1 [128]int8
|
||||
Arg2 [128]int8
|
||||
}
|
||||
|
||||
// LostSamplesError reports perf-buffer pressure without making it impossible
|
||||
// for the caller to continue reading later events.
|
||||
type LostSamplesError struct {
|
||||
Count uint64
|
||||
}
|
||||
|
||||
func (e LostSamplesError) Error() string {
|
||||
return fmt.Sprintf("exec perf buffer lost %d samples", e.Count)
|
||||
}
|
||||
|
||||
func OpenExecSource() (*ExecSource, error) {
|
||||
// Required by older kernels. On newer memcg-accounted kernels this may fail
|
||||
// in an otherwise capable service sandbox, so attempt the load regardless
|
||||
// and preserve both errors only when loading also fails.
|
||||
memlockErr := rlimit.RemoveMemlock()
|
||||
var objects execObjects
|
||||
if err := loadExecObjects(&objects, nil); err != nil {
|
||||
if memlockErr != nil {
|
||||
return nil, fmt.Errorf("load exec BPF objects: %w (memlock adjustment: %v)", err, memlockErr)
|
||||
}
|
||||
return nil, fmt.Errorf("load exec BPF objects: %w", err)
|
||||
}
|
||||
attached, err := link.Tracepoint("syscalls", "sys_enter_execve", objects.TraceExecve, nil)
|
||||
if err != nil {
|
||||
objects.Close()
|
||||
return nil, fmt.Errorf("attach exec tracepoint: %w", err)
|
||||
}
|
||||
reader, err := perf.NewReader(objects.Events, 64*4096)
|
||||
if err != nil {
|
||||
attached.Close()
|
||||
objects.Close()
|
||||
return nil, fmt.Errorf("open exec perf reader: %w", err)
|
||||
}
|
||||
return &ExecSource{objects: objects, link: attached, reader: reader}, nil
|
||||
}
|
||||
|
||||
func (s *ExecSource) Read() (events.ExecEvent, error) {
|
||||
record, err := s.reader.Read()
|
||||
if err != nil {
|
||||
return events.ExecEvent{}, err
|
||||
}
|
||||
if record.LostSamples != 0 {
|
||||
return events.ExecEvent{}, LostSamplesError{Count: record.LostSamples}
|
||||
}
|
||||
return decodeExecSample(record.RawSample)
|
||||
}
|
||||
|
||||
func decodeExecSample(sample []byte) (events.ExecEvent, error) {
|
||||
var raw execEventRaw
|
||||
if len(sample) != binary.Size(raw) {
|
||||
return events.ExecEvent{}, fmt.Errorf("exec event size %d, want %d", len(sample), binary.Size(raw))
|
||||
}
|
||||
if err := binary.Read(bytes.NewReader(sample), binary.NativeEndian, &raw); err != nil {
|
||||
return events.ExecEvent{}, fmt.Errorf("decode exec event: %w", err)
|
||||
}
|
||||
argv := make([]string, 0, 2)
|
||||
if value := cString(raw.Arg1[:]); value != "" {
|
||||
argv = append(argv, value)
|
||||
}
|
||||
if value := cString(raw.Arg2[:]); value != "" {
|
||||
argv = append(argv, value)
|
||||
}
|
||||
return events.ExecEvent{
|
||||
PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
|
||||
ParentComm: cString(raw.ParentComm[:]), Filename: cString(raw.Filename[:]),
|
||||
Argv: argv,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ExecSource) Close() error {
|
||||
var failures []error
|
||||
if s.reader != nil {
|
||||
failures = append(failures, s.reader.Close())
|
||||
}
|
||||
if s.link != nil {
|
||||
failures = append(failures, s.link.Close())
|
||||
}
|
||||
failures = append(failures, s.objects.Close())
|
||||
return errors.Join(failures...)
|
||||
}
|
||||
|
||||
func cString(raw []int8) string {
|
||||
value := make([]byte, 0, len(raw))
|
||||
for _, char := range raw {
|
||||
if char == 0 {
|
||||
break
|
||||
}
|
||||
value = append(value, byte(char))
|
||||
}
|
||||
return string(value)
|
||||
}
|
||||
|
||||
var _ io.Closer = (*ExecSource)(nil)
|
||||
141
go-agent/internal/ebpfsource/exec_bpfeb.go
Normal file
141
go-agent/internal/ebpfsource/exec_bpfeb.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// Code generated by bpf2go; DO NOT EDIT.
|
||||
//go:build mips || mips64 || ppc64 || s390x
|
||||
|
||||
package ebpfsource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
)
|
||||
|
||||
// Names of all BPF objects in the ELF.
|
||||
//
|
||||
// Used for safe lookups in a Collection or CollectionSpec.
|
||||
const (
|
||||
execMapEvents = "events"
|
||||
execProgTraceExecve = "trace_execve"
|
||||
)
|
||||
|
||||
// loadExec returns the embedded CollectionSpec for exec.
|
||||
func loadExec() (*ebpf.CollectionSpec, error) {
|
||||
reader := bytes.NewReader(_ExecBytes)
|
||||
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't load exec: %w", err)
|
||||
}
|
||||
|
||||
return spec, err
|
||||
}
|
||||
|
||||
// loadExecObjects loads exec and converts it into a struct.
|
||||
//
|
||||
// The following types are suitable as obj argument:
|
||||
//
|
||||
// *execObjects
|
||||
// *execPrograms
|
||||
// *execMaps
|
||||
//
|
||||
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
|
||||
func loadExecObjects(obj any, opts *ebpf.CollectionOptions) error {
|
||||
spec, err := loadExec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return spec.LoadAndAssign(obj, opts)
|
||||
}
|
||||
|
||||
// execSpecs contains maps and programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type execSpecs struct {
|
||||
execProgramSpecs
|
||||
execMapSpecs
|
||||
execVariableSpecs
|
||||
}
|
||||
|
||||
// execProgramSpecs contains programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type execProgramSpecs struct {
|
||||
TraceExecve *ebpf.ProgramSpec `ebpf:"trace_execve"`
|
||||
}
|
||||
|
||||
// execMapSpecs contains maps before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type execMapSpecs struct {
|
||||
Events *ebpf.MapSpec `ebpf:"events"`
|
||||
}
|
||||
|
||||
// execVariableSpecs contains global variables before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type execVariableSpecs struct {
|
||||
}
|
||||
|
||||
// execObjects contains all objects after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type execObjects struct {
|
||||
execPrograms
|
||||
execMaps
|
||||
execVariables
|
||||
}
|
||||
|
||||
func (o *execObjects) Close() error {
|
||||
return _ExecClose(
|
||||
&o.execPrograms,
|
||||
&o.execMaps,
|
||||
)
|
||||
}
|
||||
|
||||
// execMaps contains all maps after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type execMaps struct {
|
||||
Events *ebpf.Map `ebpf:"events"`
|
||||
}
|
||||
|
||||
func (m *execMaps) Close() error {
|
||||
return _ExecClose(
|
||||
m.Events,
|
||||
)
|
||||
}
|
||||
|
||||
// execVariables contains all global variables after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type execVariables struct {
|
||||
}
|
||||
|
||||
// execPrograms contains all programs after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type execPrograms struct {
|
||||
TraceExecve *ebpf.Program `ebpf:"trace_execve"`
|
||||
}
|
||||
|
||||
func (p *execPrograms) Close() error {
|
||||
return _ExecClose(
|
||||
p.TraceExecve,
|
||||
)
|
||||
}
|
||||
|
||||
func _ExecClose(closers ...io.Closer) error {
|
||||
for _, closer := range closers {
|
||||
if err := closer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not access this directly.
|
||||
//
|
||||
//go:embed exec_bpfeb.o
|
||||
var _ExecBytes []byte
|
||||
BIN
go-agent/internal/ebpfsource/exec_bpfeb.o
Normal file
BIN
go-agent/internal/ebpfsource/exec_bpfeb.o
Normal file
Binary file not shown.
141
go-agent/internal/ebpfsource/exec_bpfel.go
Normal file
141
go-agent/internal/ebpfsource/exec_bpfel.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// Code generated by bpf2go; DO NOT EDIT.
|
||||
//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm
|
||||
|
||||
package ebpfsource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
)
|
||||
|
||||
// Names of all BPF objects in the ELF.
|
||||
//
|
||||
// Used for safe lookups in a Collection or CollectionSpec.
|
||||
const (
|
||||
execMapEvents = "events"
|
||||
execProgTraceExecve = "trace_execve"
|
||||
)
|
||||
|
||||
// loadExec returns the embedded CollectionSpec for exec.
|
||||
func loadExec() (*ebpf.CollectionSpec, error) {
|
||||
reader := bytes.NewReader(_ExecBytes)
|
||||
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't load exec: %w", err)
|
||||
}
|
||||
|
||||
return spec, err
|
||||
}
|
||||
|
||||
// loadExecObjects loads exec and converts it into a struct.
|
||||
//
|
||||
// The following types are suitable as obj argument:
|
||||
//
|
||||
// *execObjects
|
||||
// *execPrograms
|
||||
// *execMaps
|
||||
//
|
||||
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
|
||||
func loadExecObjects(obj any, opts *ebpf.CollectionOptions) error {
|
||||
spec, err := loadExec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return spec.LoadAndAssign(obj, opts)
|
||||
}
|
||||
|
||||
// execSpecs contains maps and programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type execSpecs struct {
|
||||
execProgramSpecs
|
||||
execMapSpecs
|
||||
execVariableSpecs
|
||||
}
|
||||
|
||||
// execProgramSpecs contains programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type execProgramSpecs struct {
|
||||
TraceExecve *ebpf.ProgramSpec `ebpf:"trace_execve"`
|
||||
}
|
||||
|
||||
// execMapSpecs contains maps before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type execMapSpecs struct {
|
||||
Events *ebpf.MapSpec `ebpf:"events"`
|
||||
}
|
||||
|
||||
// execVariableSpecs contains global variables before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type execVariableSpecs struct {
|
||||
}
|
||||
|
||||
// execObjects contains all objects after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type execObjects struct {
|
||||
execPrograms
|
||||
execMaps
|
||||
execVariables
|
||||
}
|
||||
|
||||
func (o *execObjects) Close() error {
|
||||
return _ExecClose(
|
||||
&o.execPrograms,
|
||||
&o.execMaps,
|
||||
)
|
||||
}
|
||||
|
||||
// execMaps contains all maps after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type execMaps struct {
|
||||
Events *ebpf.Map `ebpf:"events"`
|
||||
}
|
||||
|
||||
func (m *execMaps) Close() error {
|
||||
return _ExecClose(
|
||||
m.Events,
|
||||
)
|
||||
}
|
||||
|
||||
// execVariables contains all global variables after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type execVariables struct {
|
||||
}
|
||||
|
||||
// execPrograms contains all programs after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type execPrograms struct {
|
||||
TraceExecve *ebpf.Program `ebpf:"trace_execve"`
|
||||
}
|
||||
|
||||
func (p *execPrograms) Close() error {
|
||||
return _ExecClose(
|
||||
p.TraceExecve,
|
||||
)
|
||||
}
|
||||
|
||||
func _ExecClose(closers ...io.Closer) error {
|
||||
for _, closer := range closers {
|
||||
if err := closer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not access this directly.
|
||||
//
|
||||
//go:embed exec_bpfel.o
|
||||
var _ExecBytes []byte
|
||||
BIN
go-agent/internal/ebpfsource/exec_bpfel.o
Normal file
BIN
go-agent/internal/ebpfsource/exec_bpfel.o
Normal file
Binary file not shown.
52
go-agent/internal/ebpfsource/exec_test.go
Normal file
52
go-agent/internal/ebpfsource/exec_test.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package ebpfsource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecodeExecSample(t *testing.T) {
|
||||
raw := execEventRaw{PID: 42, PPID: 7, UID: 1000}
|
||||
copyCString(raw.ParentComm[:], "nginx")
|
||||
copyCString(raw.Filename[:], "/bin/bash")
|
||||
copyCString(raw.Arg1[:], "-c")
|
||||
copyCString(raw.Arg2[:], "id")
|
||||
var sample bytes.Buffer
|
||||
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
event, err := decodeExecSample(sample.Bytes())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if event.PID != 42 || event.PPID != 7 || event.UID != 1000 ||
|
||||
event.ParentComm != "nginx" || event.Filename != "/bin/bash" ||
|
||||
len(event.Argv) != 2 || event.Argv[0] != "-c" || event.Argv[1] != "id" {
|
||||
t.Fatalf("unexpected event: %#v", event)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeExecSampleRejectsWrongSize(t *testing.T) {
|
||||
if _, err := decodeExecSample([]byte{1, 2, 3}); err == nil {
|
||||
t.Fatal("expected size error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCStringStopsAtNUL(t *testing.T) {
|
||||
if got := cString([]int8{'a', 'b', 0, 'c'}); got != "ab" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func copyCString(destination []int8, value string) {
|
||||
for index, char := range []byte(value) {
|
||||
if index >= len(destination)-1 {
|
||||
break
|
||||
}
|
||||
destination[index] = int8(char)
|
||||
}
|
||||
}
|
||||
9
go-agent/internal/ebpfsource/generate.go
Normal file
9
go-agent/internal/ebpfsource/generate.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package ebpfsource
|
||||
|
||||
// Generate checked-in object bindings so normal builds need neither clang nor
|
||||
// kernel headers. The tracepoint ABI is stable and avoids host-specific
|
||||
// generated vmlinux.h while the loader remains pure Go.
|
||||
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go@v0.22.0 -cc clang -cflags "-O2 -g -Wall -Werror" exec bpf/exec.bpf.c
|
||||
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go@v0.22.0 -cc clang -cflags "-O2 -g -Wall -Werror" syscall bpf/syscall.bpf.c
|
||||
75
go-agent/internal/ebpfsource/spec_test.go
Normal file
75
go-agent/internal/ebpfsource/spec_test.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package ebpfsource
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
"github.com/cilium/ebpf/btf"
|
||||
)
|
||||
|
||||
func TestEmbeddedExecCollectionShape(t *testing.T) {
|
||||
specification, err := loadExec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
program := specification.Programs["trace_execve"]
|
||||
eventsMap := specification.Maps["events"]
|
||||
if program == nil || program.Type != ebpf.TracePoint || program.SectionName != "tracepoint/syscalls/sys_enter_execve" {
|
||||
t.Fatalf("unexpected exec program: %#v", program)
|
||||
}
|
||||
if eventsMap == nil || eventsMap.Type != ebpf.PerfEventArray {
|
||||
t.Fatalf("unexpected exec events map: %#v", eventsMap)
|
||||
}
|
||||
if countCORERelocations(program) < 2 {
|
||||
t.Fatal("exec program must retain parent task CO-RE relocations")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbeddedSyscallCollectionShape(t *testing.T) {
|
||||
specification, err := loadSyscall()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
program := specification.Programs["trace_sys_enter"]
|
||||
exitProgram := specification.Programs["trace_sys_exit"]
|
||||
filterMap := specification.Maps["monitored_syscalls"]
|
||||
writeCommsMap := specification.Maps["monitored_host_comms"]
|
||||
pendingWritesMap := specification.Maps["pending_returns"]
|
||||
eventsMap := specification.Maps["syscall_events"]
|
||||
if program == nil || program.Type != ebpf.TracePoint || program.SectionName != "tracepoint/raw_syscalls/sys_enter" {
|
||||
t.Fatalf("unexpected syscall program: %#v", program)
|
||||
}
|
||||
if exitProgram == nil || exitProgram.Type != ebpf.TracePoint || exitProgram.SectionName != "tracepoint/raw_syscalls/sys_exit" {
|
||||
t.Fatalf("unexpected syscall exit program: %#v", exitProgram)
|
||||
}
|
||||
if filterMap == nil || filterMap.Type != ebpf.Hash || filterMap.MaxEntries != 64 ||
|
||||
filterMap.KeySize != 8 || filterMap.ValueSize != 4 {
|
||||
t.Fatalf("unexpected syscall filter map: %#v", filterMap)
|
||||
}
|
||||
if writeCommsMap == nil || writeCommsMap.Type != ebpf.Hash || writeCommsMap.MaxEntries != 64 ||
|
||||
writeCommsMap.KeySize != 16 || writeCommsMap.ValueSize != 4 {
|
||||
t.Fatalf("unexpected write comm map: %#v", writeCommsMap)
|
||||
}
|
||||
if pendingWritesMap == nil || pendingWritesMap.Type != ebpf.Hash || pendingWritesMap.MaxEntries != 4096 ||
|
||||
pendingWritesMap.KeySize != 8 {
|
||||
t.Fatalf("unexpected pending writes map: %#v", pendingWritesMap)
|
||||
}
|
||||
if eventsMap == nil || eventsMap.Type != ebpf.PerfEventArray {
|
||||
t.Fatalf("unexpected syscall events map: %#v", eventsMap)
|
||||
}
|
||||
if countCORERelocations(program) < 2 {
|
||||
t.Fatal("syscall program must retain parent task CO-RE relocations")
|
||||
}
|
||||
}
|
||||
|
||||
func countCORERelocations(program *ebpf.ProgramSpec) int {
|
||||
count := 0
|
||||
for index := range program.Instructions {
|
||||
if btf.CORERelocationMetadata(&program.Instructions[index]) != nil {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
476
go-agent/internal/ebpfsource/syscall.go
Normal file
476
go-agent/internal/ebpfsource/syscall.go
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package ebpfsource
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
"github.com/cilium/ebpf/link"
|
||||
"github.com/cilium/ebpf/perf"
|
||||
"github.com/cilium/ebpf/rlimit"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/events"
|
||||
)
|
||||
|
||||
const (
|
||||
syscallMprotect uint32 = iota + 1
|
||||
syscallMmap
|
||||
syscallMemfdCreate
|
||||
syscallPtrace
|
||||
syscallPrctl
|
||||
syscallSeccomp
|
||||
syscallProcessVMReadv
|
||||
syscallProcessVMWritev
|
||||
syscallMlock
|
||||
syscallMlock2
|
||||
syscallMlockall
|
||||
hostSetuid
|
||||
hostSetgid
|
||||
hostChmod
|
||||
hostFchmodat
|
||||
hostChown
|
||||
hostLchown
|
||||
hostFchownat
|
||||
hostCapset
|
||||
hostFinitModule
|
||||
hostFileWrite
|
||||
hostTCPConnect
|
||||
hostBind
|
||||
hostListen
|
||||
hostAccept
|
||||
hostAccept4
|
||||
)
|
||||
|
||||
var syscallNames = map[uint32]string{
|
||||
syscallMprotect: "mprotect",
|
||||
syscallMmap: "mmap",
|
||||
syscallMemfdCreate: "memfd_create",
|
||||
syscallPtrace: "ptrace",
|
||||
syscallPrctl: "prctl",
|
||||
syscallSeccomp: "seccomp",
|
||||
syscallProcessVMReadv: "process_vm_readv",
|
||||
syscallProcessVMWritev: "process_vm_writev",
|
||||
syscallMlock: "mlock",
|
||||
syscallMlock2: "mlock2",
|
||||
syscallMlockall: "mlockall",
|
||||
}
|
||||
|
||||
type syscallEventRaw struct {
|
||||
PID uint32
|
||||
PPID uint32
|
||||
UID uint32
|
||||
SyscallID uint32
|
||||
Comm [16]int8
|
||||
Args [6]uint64
|
||||
Text [80]int8
|
||||
}
|
||||
|
||||
type SyscallLostSamplesError struct {
|
||||
Count uint64
|
||||
}
|
||||
|
||||
// SecurityEvent is the typed result of the shared filtered syscall transport.
|
||||
// Exactly one field is populated.
|
||||
type SecurityEvent struct {
|
||||
Syscall *events.SyscallEvent
|
||||
Host *events.HostEvent
|
||||
pathDirFD int64
|
||||
}
|
||||
|
||||
func (e SyscallLostSamplesError) Error() string {
|
||||
return fmt.Sprintf("syscall perf buffer lost %d samples", e.Count)
|
||||
}
|
||||
|
||||
// SyscallSource filters raw syscall entry events in-kernel using a map of
|
||||
// architecture-specific syscall numbers, then exposes transport-neutral
|
||||
// SyscallEvent values to the existing rule engine.
|
||||
type SyscallSource struct {
|
||||
objects syscallObjects
|
||||
links []link.Link
|
||||
reader *perf.Reader
|
||||
procRoot string
|
||||
}
|
||||
|
||||
func OpenSyscallSource(procRoot string) (*SyscallSource, error) {
|
||||
if procRoot == "" {
|
||||
procRoot = "/proc"
|
||||
}
|
||||
numbers := monitoredSyscalls()
|
||||
memlockErr := rlimit.RemoveMemlock()
|
||||
var objects syscallObjects
|
||||
if err := loadSyscallObjects(&objects, nil); err != nil {
|
||||
if memlockErr != nil {
|
||||
return nil, fmt.Errorf("load syscall BPF objects: %w (memlock adjustment: %v)", err, memlockErr)
|
||||
}
|
||||
return nil, fmt.Errorf("load syscall BPF objects: %w", err)
|
||||
}
|
||||
for number, identifier := range numbers {
|
||||
if err := objects.MonitoredSyscalls.Update(number, identifier, ebpf.UpdateAny); err != nil {
|
||||
objects.Close()
|
||||
return nil, fmt.Errorf("configure syscall %d: %w", number, err)
|
||||
}
|
||||
}
|
||||
for _, comm := range events.HostInterpreterComms() {
|
||||
var key [16]byte
|
||||
copy(key[:], comm)
|
||||
if err := objects.MonitoredHostComms.Update(key, uint32(1), ebpf.UpdateAny); err != nil {
|
||||
objects.Close()
|
||||
return nil, fmt.Errorf("configure write comm %q: %w", comm, err)
|
||||
}
|
||||
}
|
||||
enterLink, err := link.Tracepoint("raw_syscalls", "sys_enter", objects.TraceSysEnter, nil)
|
||||
if err != nil {
|
||||
objects.Close()
|
||||
return nil, fmt.Errorf("attach raw syscall tracepoint: %w", err)
|
||||
}
|
||||
exitLink, err := link.Tracepoint("raw_syscalls", "sys_exit", objects.TraceSysExit, nil)
|
||||
if err != nil {
|
||||
enterLink.Close()
|
||||
objects.Close()
|
||||
return nil, fmt.Errorf("attach raw syscall exit tracepoint: %w", err)
|
||||
}
|
||||
reader, err := perf.NewReader(objects.SyscallEvents, 64*4096)
|
||||
if err != nil {
|
||||
exitLink.Close()
|
||||
enterLink.Close()
|
||||
objects.Close()
|
||||
return nil, fmt.Errorf("open syscall perf reader: %w", err)
|
||||
}
|
||||
return &SyscallSource{objects: objects, links: []link.Link{enterLink, exitLink}, reader: reader, procRoot: procRoot}, nil
|
||||
}
|
||||
|
||||
func (s *SyscallSource) Read() (SecurityEvent, error) {
|
||||
record, err := s.reader.Read()
|
||||
if err != nil {
|
||||
return SecurityEvent{}, err
|
||||
}
|
||||
if record.LostSamples != 0 {
|
||||
return SecurityEvent{}, SyscallLostSamplesError{Count: record.LostSamples}
|
||||
}
|
||||
securityEvent, err := decodeSecuritySample(record.RawSample)
|
||||
if err == nil && securityEvent.Host != nil {
|
||||
if securityEvent.Host.Path != "" {
|
||||
securityEvent.Host.Path = resolveProcessPath(
|
||||
s.procRoot, securityEvent.Host.PID, securityEvent.pathDirFD,
|
||||
securityEvent.Host.Path,
|
||||
)
|
||||
} else if securityEvent.Host.Event == "module_load" || securityEvent.Host.Event == "file_write" {
|
||||
securityEvent.Host.Path = resolveProcessFD(s.procRoot, securityEvent.Host.PID, securityEvent.pathDirFD)
|
||||
if securityEvent.Host.Event == "module_load" && securityEvent.Host.Path != "" {
|
||||
securityEvent.Host.ModuleName = filepath.Base(strings.TrimSuffix(securityEvent.Host.Path, " (deleted)"))
|
||||
}
|
||||
}
|
||||
if securityEvent.Host.Event == "tcp_connect" &&
|
||||
!isTCPSocket(s.procRoot, securityEvent.Host.PID, securityEvent.pathDirFD) {
|
||||
securityEvent.Host = nil
|
||||
}
|
||||
if securityEvent.Host != nil &&
|
||||
(securityEvent.Host.Event == "listen" || securityEvent.Host.Event == "accept") {
|
||||
local, peer, ok := lookupTCPSocket(s.procRoot, securityEvent.Host.PID, securityEvent.pathDirFD)
|
||||
if !ok {
|
||||
securityEvent.Host = nil
|
||||
} else {
|
||||
securityEvent.Host.LocalIP, securityEvent.Host.LocalPort = local.IP, local.Port
|
||||
securityEvent.Host.PeerIP, securityEvent.Host.PeerPort = peer.IP, peer.Port
|
||||
}
|
||||
}
|
||||
}
|
||||
return securityEvent, err
|
||||
}
|
||||
|
||||
func decodeSecuritySample(sample []byte) (SecurityEvent, error) {
|
||||
var raw syscallEventRaw
|
||||
if len(sample) != binary.Size(raw) {
|
||||
return SecurityEvent{}, fmt.Errorf("syscall event size %d, want %d", len(sample), binary.Size(raw))
|
||||
}
|
||||
if err := binary.Read(bytes.NewReader(sample), binary.NativeEndian, &raw); err != nil {
|
||||
return SecurityEvent{}, fmt.Errorf("decode syscall event: %w", err)
|
||||
}
|
||||
if raw.SyscallID == hostSetuid || raw.SyscallID == hostSetgid {
|
||||
event := events.HostEvent{
|
||||
PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
|
||||
Comm: cString(raw.Comm[:]), TargetUID: -1, TargetGID: -1,
|
||||
}
|
||||
if raw.SyscallID == hostSetuid {
|
||||
event.Event = "setuid"
|
||||
event.TargetUID = int(raw.Args[0])
|
||||
} else {
|
||||
event.Event = "setgid"
|
||||
event.TargetGID = int(raw.Args[0])
|
||||
}
|
||||
return SecurityEvent{Host: &event}, nil
|
||||
}
|
||||
if raw.SyscallID >= hostChmod && raw.SyscallID <= hostFchownat {
|
||||
event := events.HostEvent{
|
||||
PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
|
||||
Comm: cString(raw.Comm[:]), Path: cString(raw.Text[:]),
|
||||
TargetUID: -1, TargetGID: -1,
|
||||
}
|
||||
switch raw.SyscallID {
|
||||
case hostChmod, hostFchmodat:
|
||||
event.Event = "chmod"
|
||||
event.Mode = int(raw.Args[0])
|
||||
case hostChown, hostLchown, hostFchownat:
|
||||
event.Event = "chown"
|
||||
event.TargetUID = int(raw.Args[0])
|
||||
event.TargetGID = int(raw.Args[1])
|
||||
}
|
||||
return SecurityEvent{Host: &event, pathDirFD: int64(raw.Args[2])}, nil
|
||||
}
|
||||
if raw.SyscallID == hostCapset {
|
||||
event := events.HostEvent{
|
||||
Event: "capset", PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
|
||||
Comm: cString(raw.Comm[:]), TargetUID: -1, TargetGID: -1,
|
||||
Capabilities: capabilityNames(raw.Args[0]),
|
||||
}
|
||||
return SecurityEvent{Host: &event}, nil
|
||||
}
|
||||
if raw.SyscallID == hostFinitModule {
|
||||
event := events.HostEvent{
|
||||
Event: "module_load", PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
|
||||
Comm: cString(raw.Comm[:]), TargetUID: -1, TargetGID: -1,
|
||||
}
|
||||
return SecurityEvent{Host: &event, pathDirFD: int64(raw.Args[0])}, nil
|
||||
}
|
||||
if raw.SyscallID == hostFileWrite {
|
||||
event := events.HostEvent{
|
||||
Event: "file_write", PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
|
||||
Comm: cString(raw.Comm[:]), TargetUID: -1, TargetGID: -1,
|
||||
}
|
||||
return SecurityEvent{Host: &event, pathDirFD: int64(raw.Args[0])}, nil
|
||||
}
|
||||
if raw.SyscallID == hostTCPConnect || raw.SyscallID == hostBind {
|
||||
event := events.HostEvent{
|
||||
PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
|
||||
Comm: cString(raw.Comm[:]), TargetUID: -1, TargetGID: -1,
|
||||
}
|
||||
address := socketAddress(raw)
|
||||
if raw.SyscallID == hostTCPConnect {
|
||||
event.Event, event.PeerIP, event.PeerPort = "tcp_connect", address, int(raw.Args[1])
|
||||
} else {
|
||||
event.Event, event.LocalIP, event.LocalPort = "bind", address, int(raw.Args[1])
|
||||
}
|
||||
return SecurityEvent{Host: &event, pathDirFD: int64(raw.Args[3])}, nil
|
||||
}
|
||||
if raw.SyscallID >= hostListen && raw.SyscallID <= hostAccept4 {
|
||||
eventName := "accept"
|
||||
if raw.SyscallID == hostListen {
|
||||
eventName = "listen"
|
||||
}
|
||||
event := events.HostEvent{
|
||||
Event: eventName, PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
|
||||
Comm: cString(raw.Comm[:]), TargetUID: -1, TargetGID: -1,
|
||||
}
|
||||
return SecurityEvent{Host: &event, pathDirFD: int64(raw.Args[3])}, nil
|
||||
}
|
||||
name, ok := syscallNames[raw.SyscallID]
|
||||
if !ok {
|
||||
return SecurityEvent{}, fmt.Errorf("unknown canonical syscall id %d", raw.SyscallID)
|
||||
}
|
||||
event := events.SyscallEvent{
|
||||
PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
|
||||
Comm: cString(raw.Comm[:]), Syscall: name, Args: raw.Args,
|
||||
Text: cString(raw.Text[:]),
|
||||
}
|
||||
return SecurityEvent{Syscall: &event}, nil
|
||||
}
|
||||
|
||||
func decodeSyscallSample(sample []byte) (events.SyscallEvent, error) {
|
||||
securityEvent, err := decodeSecuritySample(sample)
|
||||
if err != nil {
|
||||
return events.SyscallEvent{}, err
|
||||
}
|
||||
if securityEvent.Syscall == nil {
|
||||
return events.SyscallEvent{}, fmt.Errorf("sample is a typed host event")
|
||||
}
|
||||
return *securityEvent.Syscall, nil
|
||||
}
|
||||
|
||||
func (s *SyscallSource) Close() error {
|
||||
var failures []error
|
||||
if s.reader != nil {
|
||||
failures = append(failures, s.reader.Close())
|
||||
}
|
||||
for _, attached := range s.links {
|
||||
if attached != nil {
|
||||
failures = append(failures, attached.Close())
|
||||
}
|
||||
}
|
||||
failures = append(failures, s.objects.Close())
|
||||
return errors.Join(failures...)
|
||||
}
|
||||
|
||||
func monitoredSyscalls() map[uint64]uint32 {
|
||||
numbers := map[uint64]uint32{
|
||||
unix.SYS_MPROTECT: syscallMprotect,
|
||||
unix.SYS_MMAP: syscallMmap,
|
||||
unix.SYS_MEMFD_CREATE: syscallMemfdCreate,
|
||||
unix.SYS_PTRACE: syscallPtrace,
|
||||
unix.SYS_PRCTL: syscallPrctl,
|
||||
unix.SYS_SECCOMP: syscallSeccomp,
|
||||
unix.SYS_PROCESS_VM_READV: syscallProcessVMReadv,
|
||||
unix.SYS_PROCESS_VM_WRITEV: syscallProcessVMWritev,
|
||||
unix.SYS_MLOCK: syscallMlock,
|
||||
unix.SYS_MLOCK2: syscallMlock2,
|
||||
unix.SYS_MLOCKALL: syscallMlockall,
|
||||
unix.SYS_SETUID: hostSetuid,
|
||||
unix.SYS_SETGID: hostSetgid,
|
||||
unix.SYS_FCHMODAT: hostFchmodat,
|
||||
unix.SYS_FCHOWNAT: hostFchownat,
|
||||
unix.SYS_CAPSET: hostCapset,
|
||||
unix.SYS_FINIT_MODULE: hostFinitModule,
|
||||
unix.SYS_WRITE: hostFileWrite,
|
||||
unix.SYS_WRITEV: hostFileWrite,
|
||||
unix.SYS_PWRITE64: hostFileWrite,
|
||||
unix.SYS_PWRITEV: hostFileWrite,
|
||||
unix.SYS_PWRITEV2: hostFileWrite,
|
||||
unix.SYS_CONNECT: hostTCPConnect,
|
||||
unix.SYS_BIND: hostBind,
|
||||
unix.SYS_LISTEN: hostListen,
|
||||
unix.SYS_ACCEPT4: hostAccept4,
|
||||
}
|
||||
for number, identifier := range legacyPermissionSyscalls() {
|
||||
numbers[number] = identifier
|
||||
}
|
||||
for number, identifier := range legacyNetworkSyscalls() {
|
||||
numbers[number] = identifier
|
||||
}
|
||||
return numbers
|
||||
}
|
||||
|
||||
func socketAddress(raw syscallEventRaw) string {
|
||||
switch raw.Args[0] {
|
||||
case unix.AF_INET:
|
||||
bytes := make([]byte, 4)
|
||||
binary.NativeEndian.PutUint32(bytes, uint32(raw.Args[2]))
|
||||
return net.IP(bytes).String()
|
||||
case unix.AF_INET6:
|
||||
bytes := make([]byte, 16)
|
||||
for index := range bytes {
|
||||
bytes[index] = byte(raw.Text[index])
|
||||
}
|
||||
return net.IP(bytes).String()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func capabilityNames(effective uint64) []string {
|
||||
known := []struct {
|
||||
bit uint
|
||||
name string
|
||||
}{
|
||||
{0, "CAP_CHOWN"}, {1, "CAP_DAC_OVERRIDE"}, {2, "CAP_DAC_READ_SEARCH"},
|
||||
{12, "CAP_NET_ADMIN"}, {13, "CAP_NET_RAW"}, {16, "CAP_SYS_MODULE"},
|
||||
{19, "CAP_SYS_PTRACE"}, {21, "CAP_SYS_ADMIN"},
|
||||
}
|
||||
names := make([]string, 0)
|
||||
for _, capability := range known {
|
||||
if effective&(uint64(1)<<capability.bit) != 0 {
|
||||
names = append(names, capability.name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func resolveProcessPath(procRoot string, pid int, dirFD int64, path string) string {
|
||||
if filepath.IsAbs(path) {
|
||||
return filepath.Clean(path)
|
||||
}
|
||||
processRoot := filepath.Join(procRoot, fmt.Sprintf("%d", pid))
|
||||
baseLink := filepath.Join(processRoot, "cwd")
|
||||
if dirFD >= 0 {
|
||||
baseLink = filepath.Join(processRoot, "fd", fmt.Sprintf("%d", dirFD))
|
||||
}
|
||||
base, err := os.Readlink(baseLink)
|
||||
if err != nil || !filepath.IsAbs(base) {
|
||||
return path
|
||||
}
|
||||
return filepath.Clean(filepath.Join(base, path))
|
||||
}
|
||||
|
||||
func resolveProcessFD(procRoot string, pid int, fd int64) string {
|
||||
if fd < 0 {
|
||||
return ""
|
||||
}
|
||||
path, err := os.Readlink(filepath.Join(procRoot, fmt.Sprintf("%d", pid), "fd", fmt.Sprintf("%d", fd)))
|
||||
if err != nil || !filepath.IsAbs(path) {
|
||||
return ""
|
||||
}
|
||||
return filepath.Clean(path)
|
||||
}
|
||||
|
||||
func isTCPSocket(procRoot string, pid int, fd int64) bool {
|
||||
_, _, ok := lookupTCPSocket(procRoot, pid, fd)
|
||||
return ok
|
||||
}
|
||||
|
||||
type socketEndpoint struct {
|
||||
IP string
|
||||
Port int
|
||||
}
|
||||
|
||||
func lookupTCPSocket(procRoot string, pid int, fd int64) (socketEndpoint, socketEndpoint, bool) {
|
||||
target, err := os.Readlink(filepath.Join(procRoot, fmt.Sprintf("%d", pid), "fd", fmt.Sprintf("%d", fd)))
|
||||
if err != nil || !strings.HasPrefix(target, "socket:[") || !strings.HasSuffix(target, "]") {
|
||||
return socketEndpoint{}, socketEndpoint{}, false
|
||||
}
|
||||
inode := strings.TrimSuffix(strings.TrimPrefix(target, "socket:["), "]")
|
||||
for _, name := range []string{"tcp", "tcp6"} {
|
||||
file, err := os.Open(filepath.Join(procRoot, "net", name))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) > 9 && fields[9] == inode {
|
||||
local, localErr := decodeProcEndpoint(fields[1], name == "tcp6")
|
||||
peer, peerErr := decodeProcEndpoint(fields[2], name == "tcp6")
|
||||
file.Close()
|
||||
return local, peer, localErr == nil && peerErr == nil
|
||||
}
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
return socketEndpoint{}, socketEndpoint{}, false
|
||||
}
|
||||
|
||||
func decodeProcEndpoint(value string, ipv6 bool) (socketEndpoint, error) {
|
||||
addressHex, portHex, ok := strings.Cut(value, ":")
|
||||
if !ok {
|
||||
return socketEndpoint{}, fmt.Errorf("invalid proc endpoint %q", value)
|
||||
}
|
||||
address, err := hex.DecodeString(addressHex)
|
||||
if err != nil || len(address) != 4 && len(address) != 16 {
|
||||
return socketEndpoint{}, fmt.Errorf("invalid proc address %q", addressHex)
|
||||
}
|
||||
if ipv6 {
|
||||
for offset := 0; offset < len(address); offset += 4 {
|
||||
slices.Reverse(address[offset : offset+4])
|
||||
}
|
||||
} else {
|
||||
slices.Reverse(address)
|
||||
}
|
||||
port, err := strconv.ParseUint(portHex, 16, 16)
|
||||
if err != nil {
|
||||
return socketEndpoint{}, err
|
||||
}
|
||||
return socketEndpoint{IP: net.IP(address).String(), Port: int(port)}, nil
|
||||
}
|
||||
|
||||
var _ io.Closer = (*SyscallSource)(nil)
|
||||
157
go-agent/internal/ebpfsource/syscall_bpfeb.go
Normal file
157
go-agent/internal/ebpfsource/syscall_bpfeb.go
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
// Code generated by bpf2go; DO NOT EDIT.
|
||||
//go:build mips || mips64 || ppc64 || s390x
|
||||
|
||||
package ebpfsource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
)
|
||||
|
||||
// Names of all BPF objects in the ELF.
|
||||
//
|
||||
// Used for safe lookups in a Collection or CollectionSpec.
|
||||
const (
|
||||
syscallMapMonitoredHostComms = "monitored_host_comms"
|
||||
syscallMapMonitoredSyscalls = "monitored_syscalls"
|
||||
syscallMapPendingReturns = "pending_returns"
|
||||
syscallMapSyscallEvents = "syscall_events"
|
||||
syscallProgTraceSysEnter = "trace_sys_enter"
|
||||
syscallProgTraceSysExit = "trace_sys_exit"
|
||||
)
|
||||
|
||||
// loadSyscall returns the embedded CollectionSpec for syscall.
|
||||
func loadSyscall() (*ebpf.CollectionSpec, error) {
|
||||
reader := bytes.NewReader(_SyscallBytes)
|
||||
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't load syscall: %w", err)
|
||||
}
|
||||
|
||||
return spec, err
|
||||
}
|
||||
|
||||
// loadSyscallObjects loads syscall and converts it into a struct.
|
||||
//
|
||||
// The following types are suitable as obj argument:
|
||||
//
|
||||
// *syscallObjects
|
||||
// *syscallPrograms
|
||||
// *syscallMaps
|
||||
//
|
||||
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
|
||||
func loadSyscallObjects(obj any, opts *ebpf.CollectionOptions) error {
|
||||
spec, err := loadSyscall()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return spec.LoadAndAssign(obj, opts)
|
||||
}
|
||||
|
||||
// syscallSpecs contains maps and programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type syscallSpecs struct {
|
||||
syscallProgramSpecs
|
||||
syscallMapSpecs
|
||||
syscallVariableSpecs
|
||||
}
|
||||
|
||||
// syscallProgramSpecs contains programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type syscallProgramSpecs struct {
|
||||
TraceSysEnter *ebpf.ProgramSpec `ebpf:"trace_sys_enter"`
|
||||
TraceSysExit *ebpf.ProgramSpec `ebpf:"trace_sys_exit"`
|
||||
}
|
||||
|
||||
// syscallMapSpecs contains maps before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type syscallMapSpecs struct {
|
||||
MonitoredHostComms *ebpf.MapSpec `ebpf:"monitored_host_comms"`
|
||||
MonitoredSyscalls *ebpf.MapSpec `ebpf:"monitored_syscalls"`
|
||||
PendingReturns *ebpf.MapSpec `ebpf:"pending_returns"`
|
||||
SyscallEvents *ebpf.MapSpec `ebpf:"syscall_events"`
|
||||
}
|
||||
|
||||
// syscallVariableSpecs contains global variables before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type syscallVariableSpecs struct {
|
||||
}
|
||||
|
||||
// syscallObjects contains all objects after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type syscallObjects struct {
|
||||
syscallPrograms
|
||||
syscallMaps
|
||||
syscallVariables
|
||||
}
|
||||
|
||||
func (o *syscallObjects) Close() error {
|
||||
return _SyscallClose(
|
||||
&o.syscallPrograms,
|
||||
&o.syscallMaps,
|
||||
)
|
||||
}
|
||||
|
||||
// syscallMaps contains all maps after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type syscallMaps struct {
|
||||
MonitoredHostComms *ebpf.Map `ebpf:"monitored_host_comms"`
|
||||
MonitoredSyscalls *ebpf.Map `ebpf:"monitored_syscalls"`
|
||||
PendingReturns *ebpf.Map `ebpf:"pending_returns"`
|
||||
SyscallEvents *ebpf.Map `ebpf:"syscall_events"`
|
||||
}
|
||||
|
||||
func (m *syscallMaps) Close() error {
|
||||
return _SyscallClose(
|
||||
m.MonitoredHostComms,
|
||||
m.MonitoredSyscalls,
|
||||
m.PendingReturns,
|
||||
m.SyscallEvents,
|
||||
)
|
||||
}
|
||||
|
||||
// syscallVariables contains all global variables after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type syscallVariables struct {
|
||||
}
|
||||
|
||||
// syscallPrograms contains all programs after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type syscallPrograms struct {
|
||||
TraceSysEnter *ebpf.Program `ebpf:"trace_sys_enter"`
|
||||
TraceSysExit *ebpf.Program `ebpf:"trace_sys_exit"`
|
||||
}
|
||||
|
||||
func (p *syscallPrograms) Close() error {
|
||||
return _SyscallClose(
|
||||
p.TraceSysEnter,
|
||||
p.TraceSysExit,
|
||||
)
|
||||
}
|
||||
|
||||
func _SyscallClose(closers ...io.Closer) error {
|
||||
for _, closer := range closers {
|
||||
if err := closer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not access this directly.
|
||||
//
|
||||
//go:embed syscall_bpfeb.o
|
||||
var _SyscallBytes []byte
|
||||
BIN
go-agent/internal/ebpfsource/syscall_bpfeb.o
Normal file
BIN
go-agent/internal/ebpfsource/syscall_bpfeb.o
Normal file
Binary file not shown.
157
go-agent/internal/ebpfsource/syscall_bpfel.go
Normal file
157
go-agent/internal/ebpfsource/syscall_bpfel.go
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
// Code generated by bpf2go; DO NOT EDIT.
|
||||
//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm
|
||||
|
||||
package ebpfsource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
)
|
||||
|
||||
// Names of all BPF objects in the ELF.
|
||||
//
|
||||
// Used for safe lookups in a Collection or CollectionSpec.
|
||||
const (
|
||||
syscallMapMonitoredHostComms = "monitored_host_comms"
|
||||
syscallMapMonitoredSyscalls = "monitored_syscalls"
|
||||
syscallMapPendingReturns = "pending_returns"
|
||||
syscallMapSyscallEvents = "syscall_events"
|
||||
syscallProgTraceSysEnter = "trace_sys_enter"
|
||||
syscallProgTraceSysExit = "trace_sys_exit"
|
||||
)
|
||||
|
||||
// loadSyscall returns the embedded CollectionSpec for syscall.
|
||||
func loadSyscall() (*ebpf.CollectionSpec, error) {
|
||||
reader := bytes.NewReader(_SyscallBytes)
|
||||
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't load syscall: %w", err)
|
||||
}
|
||||
|
||||
return spec, err
|
||||
}
|
||||
|
||||
// loadSyscallObjects loads syscall and converts it into a struct.
|
||||
//
|
||||
// The following types are suitable as obj argument:
|
||||
//
|
||||
// *syscallObjects
|
||||
// *syscallPrograms
|
||||
// *syscallMaps
|
||||
//
|
||||
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
|
||||
func loadSyscallObjects(obj any, opts *ebpf.CollectionOptions) error {
|
||||
spec, err := loadSyscall()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return spec.LoadAndAssign(obj, opts)
|
||||
}
|
||||
|
||||
// syscallSpecs contains maps and programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type syscallSpecs struct {
|
||||
syscallProgramSpecs
|
||||
syscallMapSpecs
|
||||
syscallVariableSpecs
|
||||
}
|
||||
|
||||
// syscallProgramSpecs contains programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type syscallProgramSpecs struct {
|
||||
TraceSysEnter *ebpf.ProgramSpec `ebpf:"trace_sys_enter"`
|
||||
TraceSysExit *ebpf.ProgramSpec `ebpf:"trace_sys_exit"`
|
||||
}
|
||||
|
||||
// syscallMapSpecs contains maps before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type syscallMapSpecs struct {
|
||||
MonitoredHostComms *ebpf.MapSpec `ebpf:"monitored_host_comms"`
|
||||
MonitoredSyscalls *ebpf.MapSpec `ebpf:"monitored_syscalls"`
|
||||
PendingReturns *ebpf.MapSpec `ebpf:"pending_returns"`
|
||||
SyscallEvents *ebpf.MapSpec `ebpf:"syscall_events"`
|
||||
}
|
||||
|
||||
// syscallVariableSpecs contains global variables before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type syscallVariableSpecs struct {
|
||||
}
|
||||
|
||||
// syscallObjects contains all objects after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type syscallObjects struct {
|
||||
syscallPrograms
|
||||
syscallMaps
|
||||
syscallVariables
|
||||
}
|
||||
|
||||
func (o *syscallObjects) Close() error {
|
||||
return _SyscallClose(
|
||||
&o.syscallPrograms,
|
||||
&o.syscallMaps,
|
||||
)
|
||||
}
|
||||
|
||||
// syscallMaps contains all maps after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type syscallMaps struct {
|
||||
MonitoredHostComms *ebpf.Map `ebpf:"monitored_host_comms"`
|
||||
MonitoredSyscalls *ebpf.Map `ebpf:"monitored_syscalls"`
|
||||
PendingReturns *ebpf.Map `ebpf:"pending_returns"`
|
||||
SyscallEvents *ebpf.Map `ebpf:"syscall_events"`
|
||||
}
|
||||
|
||||
func (m *syscallMaps) Close() error {
|
||||
return _SyscallClose(
|
||||
m.MonitoredHostComms,
|
||||
m.MonitoredSyscalls,
|
||||
m.PendingReturns,
|
||||
m.SyscallEvents,
|
||||
)
|
||||
}
|
||||
|
||||
// syscallVariables contains all global variables after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type syscallVariables struct {
|
||||
}
|
||||
|
||||
// syscallPrograms contains all programs after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type syscallPrograms struct {
|
||||
TraceSysEnter *ebpf.Program `ebpf:"trace_sys_enter"`
|
||||
TraceSysExit *ebpf.Program `ebpf:"trace_sys_exit"`
|
||||
}
|
||||
|
||||
func (p *syscallPrograms) Close() error {
|
||||
return _SyscallClose(
|
||||
p.TraceSysEnter,
|
||||
p.TraceSysExit,
|
||||
)
|
||||
}
|
||||
|
||||
func _SyscallClose(closers ...io.Closer) error {
|
||||
for _, closer := range closers {
|
||||
if err := closer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not access this directly.
|
||||
//
|
||||
//go:embed syscall_bpfel.o
|
||||
var _SyscallBytes []byte
|
||||
BIN
go-agent/internal/ebpfsource/syscall_bpfel.o
Normal file
BIN
go-agent/internal/ebpfsource/syscall_bpfel.o
Normal file
Binary file not shown.
256
go-agent/internal/ebpfsource/syscall_test.go
Normal file
256
go-agent/internal/ebpfsource/syscall_test.go
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package ebpfsource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestDecodeSyscallSample(t *testing.T) {
|
||||
raw := syscallEventRaw{
|
||||
PID: 42, PPID: 7, UID: 1000, SyscallID: syscallMprotect,
|
||||
Args: [6]uint64{0x1000, 0x2000, 0x6},
|
||||
}
|
||||
copyCString(raw.Comm[:], "dropper")
|
||||
var sample bytes.Buffer
|
||||
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
event, err := decodeSyscallSample(sample.Bytes())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if event.PID != 42 || event.PPID != 7 || event.UID != 1000 ||
|
||||
event.Comm != "dropper" || event.Syscall != "mprotect" || event.Args[2] != 0x6 {
|
||||
t.Fatalf("unexpected event: %#v", event)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeSyscallSampleRejectsUnknownIDAndSize(t *testing.T) {
|
||||
if _, err := decodeSyscallSample([]byte{1}); err == nil {
|
||||
t.Fatal("expected size error")
|
||||
}
|
||||
raw := syscallEventRaw{SyscallID: 99}
|
||||
var sample bytes.Buffer
|
||||
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := decodeSyscallSample(sample.Bytes()); err == nil {
|
||||
t.Fatal("expected unknown ID error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeSecuritySampleProducesTypedHostTransition(t *testing.T) {
|
||||
raw := syscallEventRaw{
|
||||
PID: 42, PPID: 7, UID: 1000, SyscallID: hostSetuid,
|
||||
Args: [6]uint64{0},
|
||||
}
|
||||
copyCString(raw.Comm[:], "python3")
|
||||
var sample bytes.Buffer
|
||||
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
securityEvent, err := decodeSecuritySample(sample.Bytes())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if securityEvent.Syscall != nil || securityEvent.Host == nil ||
|
||||
securityEvent.Host.Event != "setuid" || securityEvent.Host.TargetUID != 0 ||
|
||||
securityEvent.Host.TargetGID != -1 || securityEvent.Host.Comm != "python3" {
|
||||
t.Fatalf("unexpected security event: %#v", securityEvent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeSecuritySampleProducesPermissionChange(t *testing.T) {
|
||||
raw := syscallEventRaw{
|
||||
PID: 42, PPID: 7, UID: 1000, SyscallID: hostFchownat,
|
||||
Args: [6]uint64{0, 0},
|
||||
}
|
||||
copyCString(raw.Comm[:], "python3")
|
||||
copyCString(raw.Text[:], "/etc/systemd/system/backdoor.service")
|
||||
var sample bytes.Buffer
|
||||
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
securityEvent, err := decodeSecuritySample(sample.Bytes())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if securityEvent.Host == nil || securityEvent.Host.Event != "chown" ||
|
||||
securityEvent.Host.Path != "/etc/systemd/system/backdoor.service" ||
|
||||
securityEvent.Host.TargetUID != 0 || securityEvent.Host.TargetGID != 0 {
|
||||
t.Fatalf("unexpected security event: %#v", securityEvent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeSecuritySampleProducesCapabilities(t *testing.T) {
|
||||
raw := syscallEventRaw{
|
||||
PID: 42, PPID: 7, UID: 1000, SyscallID: hostCapset,
|
||||
Args: [6]uint64{(1 << 2) | (1 << 21)},
|
||||
}
|
||||
copyCString(raw.Comm[:], "python3")
|
||||
var sample bytes.Buffer
|
||||
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
securityEvent, err := decodeSecuritySample(sample.Bytes())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{"CAP_DAC_READ_SEARCH", "CAP_SYS_ADMIN"}
|
||||
if securityEvent.Host == nil || securityEvent.Host.Event != "capset" ||
|
||||
!slices.Equal(securityEvent.Host.Capabilities, want) {
|
||||
t.Fatalf("unexpected security event: %#v", securityEvent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeSecuritySampleProducesModuleLoad(t *testing.T) {
|
||||
raw := syscallEventRaw{
|
||||
PID: 42, PPID: 7, UID: 0, SyscallID: hostFinitModule,
|
||||
Args: [6]uint64{5},
|
||||
}
|
||||
var sample bytes.Buffer
|
||||
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
securityEvent, err := decodeSecuritySample(sample.Bytes())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if securityEvent.Host == nil || securityEvent.Host.Event != "module_load" || securityEvent.pathDirFD != 5 {
|
||||
t.Fatalf("unexpected security event: %#v", securityEvent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeSecuritySampleProducesFileWrite(t *testing.T) {
|
||||
raw := syscallEventRaw{
|
||||
PID: 42, PPID: 7, UID: 1000, SyscallID: hostFileWrite,
|
||||
Args: [6]uint64{5, 128},
|
||||
}
|
||||
copyCString(raw.Comm[:], "python3")
|
||||
var sample bytes.Buffer
|
||||
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
securityEvent, err := decodeSecuritySample(sample.Bytes())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if securityEvent.Host == nil || securityEvent.Host.Event != "file_write" ||
|
||||
securityEvent.Host.Comm != "python3" || securityEvent.pathDirFD != 5 {
|
||||
t.Fatalf("unexpected security event: %#v", securityEvent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeSecuritySampleProducesNetworkEvents(t *testing.T) {
|
||||
raw := syscallEventRaw{
|
||||
PID: 42, PPID: 7, UID: 1000, SyscallID: hostTCPConnect,
|
||||
Args: [6]uint64{unix.AF_INET, 4444, 0, 5},
|
||||
}
|
||||
copyCString(raw.Comm[:], "python3")
|
||||
address := []byte{8, 8, 8, 8}
|
||||
raw.Args[2] = uint64(binary.NativeEndian.Uint32(address))
|
||||
var sample bytes.Buffer
|
||||
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
securityEvent, err := decodeSecuritySample(sample.Bytes())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if securityEvent.Host == nil || securityEvent.Host.Event != "tcp_connect" ||
|
||||
securityEvent.Host.PeerIP != "8.8.8.8" || securityEvent.Host.PeerPort != 4444 ||
|
||||
securityEvent.pathDirFD != 5 {
|
||||
t.Fatalf("unexpected network event: %#v", securityEvent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeSecuritySampleProducesAccept(t *testing.T) {
|
||||
raw := syscallEventRaw{
|
||||
PID: 42, PPID: 7, UID: 1000, SyscallID: hostAccept4,
|
||||
Args: [6]uint64{0, 0, 0, 6},
|
||||
}
|
||||
copyCString(raw.Comm[:], "python3")
|
||||
var sample bytes.Buffer
|
||||
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
securityEvent, err := decodeSecuritySample(sample.Bytes())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if securityEvent.Host == nil || securityEvent.Host.Event != "accept" || securityEvent.pathDirFD != 6 {
|
||||
t.Fatalf("unexpected accept event: %#v", securityEvent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitoredSyscallsUsesPlatformConstants(t *testing.T) {
|
||||
numbers := monitoredSyscalls()
|
||||
if len(numbers) != 26+len(legacyPermissionSyscalls())+len(legacyNetworkSyscalls()) ||
|
||||
numbers[unix.SYS_MEMFD_CREATE] != syscallMemfdCreate ||
|
||||
numbers[unix.SYS_MPROTECT] != syscallMprotect || numbers[unix.SYS_SETUID] != hostSetuid {
|
||||
t.Fatalf("unexpected syscall map: %#v", numbers)
|
||||
}
|
||||
for _, number := range []uint64{
|
||||
unix.SYS_WRITE, unix.SYS_WRITEV, unix.SYS_PWRITE64,
|
||||
unix.SYS_PWRITEV, unix.SYS_PWRITEV2,
|
||||
} {
|
||||
if numbers[number] != hostFileWrite {
|
||||
t.Fatalf("write syscall %d maps to %d", number, numbers[number])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveProcessPath(t *testing.T) {
|
||||
procRoot := t.TempDir()
|
||||
processDir := filepath.Join(procRoot, "42")
|
||||
if err := os.Mkdir(processDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink("/etc/systemd", filepath.Join(processDir, "cwd")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(processDir, "fd"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink("/etc", filepath.Join(processDir, "fd", "5")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink("socket:[12345]", filepath.Join(processDir, "fd", "6")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(procRoot, "net"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tcpTable := "sl local remote st tx rx tr tm retr uid timeout inode\n0: 0100007F:1F90 08080808:115C 01 0 0 0 0 0 12345\n"
|
||||
if err := os.WriteFile(filepath.Join(procRoot, "net", "tcp"), []byte(tcpTable), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := resolveProcessPath(procRoot, 42, -100, "system/agent.service"); got != "/etc/systemd/system/agent.service" {
|
||||
t.Fatalf("resolved path=%q", got)
|
||||
}
|
||||
if got := resolveProcessPath(procRoot, 42, 5, "cron.d/agent"); got != "/etc/cron.d/agent" {
|
||||
t.Fatalf("dirfd-resolved path=%q", got)
|
||||
}
|
||||
if got := resolveProcessFD(procRoot, 42, 5); got != "/etc" {
|
||||
t.Fatalf("fd-resolved path=%q", got)
|
||||
}
|
||||
if !isTCPSocket(procRoot, 42, 6) || isTCPSocket(procRoot, 42, 5) {
|
||||
t.Fatal("TCP socket classification mismatch")
|
||||
}
|
||||
local, peer, ok := lookupTCPSocket(procRoot, 42, 6)
|
||||
if !ok || local.IP != "127.0.0.1" || local.Port != 8080 || peer.IP != "8.8.8.8" || peer.Port != 4444 {
|
||||
t.Fatalf("unexpected endpoints: local=%#v peer=%#v ok=%v", local, peer, ok)
|
||||
}
|
||||
if got := resolveProcessPath(procRoot, 99, -100, "relative"); got != "relative" {
|
||||
t.Fatalf("unresolved path=%q", got)
|
||||
}
|
||||
}
|
||||
19
go-agent/internal/ebpfsource/syscalls_amd64.go
Normal file
19
go-agent/internal/ebpfsource/syscalls_amd64.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
//go:build amd64
|
||||
|
||||
package ebpfsource
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
func legacyPermissionSyscalls() map[uint64]uint32 {
|
||||
return map[uint64]uint32{
|
||||
unix.SYS_CHMOD: hostChmod,
|
||||
unix.SYS_CHOWN: hostChown,
|
||||
unix.SYS_LCHOWN: hostLchown,
|
||||
}
|
||||
}
|
||||
|
||||
func legacyNetworkSyscalls() map[uint64]uint32 {
|
||||
return map[uint64]uint32{unix.SYS_ACCEPT: hostAccept}
|
||||
}
|
||||
13
go-agent/internal/ebpfsource/syscalls_other.go
Normal file
13
go-agent/internal/ebpfsource/syscalls_other.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
//go:build !amd64
|
||||
|
||||
package ebpfsource
|
||||
|
||||
func legacyPermissionSyscalls() map[uint64]uint32 {
|
||||
return nil
|
||||
}
|
||||
|
||||
func legacyNetworkSyscalls() map[uint64]uint32 {
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue