diff --git a/.gitignore b/.gitignore index ab888e404..d95337c47 100755 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ SUSE/docker-compose/config/traefik/traefik.toml SUSE/docker-compose/config/velociraptor/* SUSE/docker-compose/data/velociraptor/* SUSE/docker-compose/logs/velociraptor/* +vql/linux/bpf/*/*.bpf.o diff --git a/artifacts/definitions/Linux/Events/ProcessExecutions.yaml b/artifacts/definitions/Linux/Events/ProcessExecutions.yaml index da3c1053c..2b02b574d 100644 --- a/artifacts/definitions/Linux/Events/ProcessExecutions.yaml +++ b/artifacts/definitions/Linux/Events/ProcessExecutions.yaml @@ -1,15 +1,21 @@ name: Linux.Events.ProcessExecutions description: | - This artifact collects process execution events from the Linux audit system. - -precondition: SELECT OS From info() where OS = 'linux' + This artifact collects process execution events using the execsnoop eBPF plugin + if the client binary supports it and the kernel is 5.8+. Otherwise, the audit + plugin is used. type: CLIENT_EVENT sources: - - query: | + - precondition: | + SELECT OS, KernelVersion, + parse_string_with_regex(string=KernelVersion, regex='^(?P[0-9]+.[0-9]+)') AS parsed + FROM info() + WHERE OS = 'linux' + AND (version(plugin='execsnoop') = Null OR parse_float(string=parsed.kernel_ver) < 5.8) + query: | LET proc_exec_rules = ("-a always,exit -F arch=b64 -S execve -k vrr_procmon", "-a always,exit -F arch=b32 -S execve -k vrr_procmon") LET exec_log = SELECT timestamp(string=Timestamp) AS Time, Sequence, @@ -39,3 +45,31 @@ sources: hashes.SHA1 AS SHA1, hashes.SHA256 AS SHA256 FROM hash_log + + - precondition: | + SELECT OS, KernelVersion, + parse_string_with_regex(string=KernelVersion, regex='^(?P[0-9]+.[0-9]+)') AS parsed + FROM info() + WHERE OS = 'linux' + AND version(plugin='execsnoop') != Null + AND parse_float(string=parsed.kernel_ver) >= 5.8 + + query: | + LET exec_log = SELECT * FROM execsnoop() + + LET hash_log = SELECT *, + hash(path=Exe, hashselect=['SHA1', 'SHA256']) AS hashes + FROM exec_log + + // Cache Uid -> Username mapping. + LET users <= SELECT User, atoi(string=Uid) AS UserID + FROM Artifact.Linux.Sys.Users() + + SELECT Time, Pid, Ppid, Uid, + { SELECT User from users WHERE UserID = Uid } AS User, + Argv AS CmdLine, + Exe, + Cwd AS CWD, + hashes.SHA1 AS SHA1, + hashes.SHA256 AS SHA256 + FROM hash_log diff --git a/artifacts/definitions/SUSE/Linux/Events/ProcessStatuses.yaml b/artifacts/definitions/SUSE/Linux/Events/ProcessStatuses.yaml index 063bc01b5..dc538dc9c 100644 --- a/artifacts/definitions/SUSE/Linux/Events/ProcessStatuses.yaml +++ b/artifacts/definitions/SUSE/Linux/Events/ProcessStatuses.yaml @@ -1,15 +1,21 @@ name: SUSE.Linux.Events.ProcessStatuses -description: | - This artifact collects new processes created by non-root users from - the Linux kernel audit events. -precondition: SELECT OS From info() where OS = 'linux' +description: | + This artifact collects new processes created by non-root users using the + execsnoop plugin if client binary supports it and the kernel is 5.8+. + Otherwise, the audit plugin is used. type: CLIENT_EVENT sources: - - query: | + - precondition: | + SELECT OS, KernelVersion, + parse_string_with_regex(string=KernelVersion, regex='^(?P[0-9]+.[0-9]+)') AS parsed + FROM info() + WHERE OS = 'linux' + AND (version(plugin='execsnoop') = Null OR parse_float(string=parsed.kernel_ver) < 5.8) + query: | LET proc_stat_rules = ("-a always,exit -F arch=b64 -S execve -k vrr_procmon", "-a always,exit -F arch=b32 -S execve -k vrr_procmon") LET proc_exec_log = SELECT timestamp(string=Timestamp) AS Time, Sequence, @@ -37,3 +43,34 @@ sources: State,CmdLine, CWD, Exe AS ImagePath, Hash.SHA256 AS Hash_Sha256, Hash.SHA1 AS Hash_Sha1 FROM proc_exec_log + + + - precondition: | + SELECT OS, KernelVersion, + parse_string_with_regex(string=KernelVersion, regex='^(?P[0-9]+.[0-9]+)') AS parsed + FROM info() + WHERE OS = 'linux' + AND version(plugin='execsnoop') != Null + AND parse_float(string=parsed.kernel_ver) >= 5.8 + + query: | + LET exec_log = SELECT * FROM execsnoop() WHERE Uid != 0 + + LET hash_log = SELECT *, + hash(path=Exe, hashselect=['SHA1', 'SHA256']) AS hashes + FROM exec_log + + // Cache Uid -> Username mapping. + LET usrs <= SELECT User, atoi(string=Uid) AS UserID + FROM Artifact.Linux.Sys.Users() + + SELECT Time, Pid, Ppid, + Uid AS UserID, + { SELECT User from usrs WHERE UserID = Uid } AS User, + "n/a" AS State, + Argv AS CmdLine, + Cwd AS CWD, + Exe AS ImagePath, + hashes.SHA256 AS Hash_Sha256, + hashes.SHA1 AS Hash_Sha1 + FROM hash_log diff --git a/docs/references/vql.yaml b/docs/references/vql.yaml index cd46fe7c3..82b40b772 100644 --- a/docs/references/vql.yaml +++ b/docs/references/vql.yaml @@ -1371,6 +1371,11 @@ per row repeated: true category: plugin +- name: execsnoop + description: Report execve system calls on linux + type: Plugin + metadata: + permissions: MACHINE_STATE - name: execve description: | This plugin launches an external command and captures its STDERR, diff --git a/vql/linux/bpf/execsnoop/ebpf.go b/vql/linux/bpf/execsnoop/ebpf.go new file mode 100644 index 000000000..98133df61 --- /dev/null +++ b/vql/linux/bpf/execsnoop/ebpf.go @@ -0,0 +1,36 @@ +//go:build linux + +package bpf + +import ( + _ "embed" + + libbpf "github.com/aquasecurity/libbpfgo" + "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/vql/linux/bpf" +) + +//go:generate make -C .. ${PWD}/execsnoop.bpf.o +//go:embed execsnoop.bpf.o +var bpfCode []byte + +func initBpf(logger *logging.LogContext) (*libbpf.Module, error) { + bpf.SetLoggerCallback(logger) + + bpfModule, err := bpf.LoadBpfModule("execsnoop", bpfCode, nil) + if err != nil { + return nil, err + } + + prog, err := bpfModule.GetProgram("tracepoint__sched__sched_process_exec") + if err != nil { + return nil, err + } + + _, err = prog.AttachTracepoint("sched", "sched_process_exec") + if err != nil { + return nil, err + } + + return bpfModule, nil +} diff --git a/vql/linux/bpf/execsnoop/execsnoop.bpf.c b/vql/linux/bpf/execsnoop/execsnoop.bpf.c new file mode 100644 index 000000000..258c7359c --- /dev/null +++ b/vql/linux/bpf/execsnoop/execsnoop.bpf.c @@ -0,0 +1,155 @@ +// +build ignore + +#include "vmlinux.h" +#include +#include +#include + +#define NAME_MAX 255 // limits.h +#define FILE_MAX 4096 // limits.h +#define CWD_MAX 4096 +#define ARGV_MAX 4096 // limits.h has ARG_MAX 128KB which also includes environ +#define MAX_PATH_COMPONENTS 16 +#define BUF_MAX (ARGV_MAX + FILE_MAX + CWD_MAX) + +struct event_t { + u32 pid; + u32 ppid; + u32 uid; + u32 exe_len; + u32 argv_len; + u32 cwd_len; + u8 buf[BUF_MAX]; // holds variable length fields: argv, exe filename and cwd +}; + +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 16 * 1024); +} rb_map SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); + __uint(max_entries, 1); + __type(key, int); + __type(value, struct event_t); +} heap SEC(".maps"); + +// Append dentry name to buf at buf_off +static __always_inline int +process_dentry(u8 *buf, int buf_off, struct dentry *dentry) { + struct qstr d_name = BPF_CORE_READ(dentry, d_name); + uint len = d_name.len; + + if (len > NAME_MAX) + return -1; + // also read the trailing \0 + int sz = bpf_probe_read_kernel_str(&buf[buf_off], len + 1, (void *)d_name.name); + if (sz < 0) + return -1; + + buf_off += len + 1; + return buf_off; +} + +// Walk path up to / appending each component to buf. +// The components will be in reverse order, e.g. dir2\0dir1\0mnt\0 +// Reversing and replacing the \0s with slashes will be done in userspace. +static __always_inline u32 +get_path_str(struct path *path, u8 *buf) { + struct dentry *dentry = BPF_CORE_READ(path, dentry); + struct vfsmount *vfsmnt = BPF_CORE_READ(path, mnt); + struct dentry *mnt_root = BPF_CORE_READ(vfsmnt, mnt_root); + struct mount *mnt_p = container_of(vfsmnt, struct mount, mnt); + struct mount *mnt_parent_p = BPF_CORE_READ(mnt_p, mnt_parent); + int buf_off = 0; + +#pragma unroll + for (int i = 0; i < MAX_PATH_COMPONENTS; i++) { + struct dentry *d_parent = BPF_CORE_READ(dentry, d_parent); + + if (dentry == mnt_root || dentry == d_parent) { + if (dentry != mnt_root) { + // We reached root, but not mount root - escaped? + break; + } + if (mnt_p != mnt_parent_p) { + // We reached root, but not global root - continue with mount point path + dentry = BPF_CORE_READ(mnt_p, mnt_mountpoint); + mnt_p = BPF_CORE_READ(mnt_p, mnt_parent); + mnt_parent_p = BPF_CORE_READ(mnt_p, mnt_parent); + vfsmnt = &mnt_p->mnt; + mnt_root = BPF_CORE_READ(vfsmnt, mnt_root); + continue; + } + // Global root - path fully parsed + break; + } + + buf_off = process_dentry(buf, buf_off, dentry); + if (buf_off < 0) + break; + + dentry = d_parent; + } + + return buf_off; +} + +SEC("tracepoint/sched/sched_process_exec") +int tracepoint__sched__sched_process_exec(struct trace_event_raw_sched_process_exec *ctx) { + const int zero = 0; + + struct event_t *event; + event = bpf_map_lookup_elem(&heap, &zero); + if (!event) { + return 0; + } + + struct task_struct *task = (struct task_struct *)bpf_get_current_task(); + + event->pid = bpf_get_current_pid_tgid() >> 32; + event->ppid = BPF_CORE_READ(task, real_parent, tgid); + event->uid = (u32)bpf_get_current_uid_gid(); + + // find argv and place at the start of event->buf + void *arg_start = (void *)BPF_CORE_READ(task, mm, arg_start); + void *arg_end = (void *)BPF_CORE_READ(task, mm, arg_end); + ulong arg_sz = arg_end - arg_start; + arg_sz = arg_sz < ARGV_MAX ? arg_sz : ARGV_MAX; + int arg_ret = bpf_probe_read_user(&event->buf, arg_sz, arg_start); + if (arg_ret < 0) { + return 0; + } + event->argv_len = arg_sz; + + // find the exe filename and append it to event->buf after argv. + struct file *filp = BPF_CORE_READ(task, mm, exe_file); + struct path *f_path = __builtin_preserve_access_index(&filp->f_path); + int file_sz = get_path_str(f_path, &event->buf[arg_sz]); + if (file_sz < 0) { + return 0; + } + event->exe_len = file_sz; + + // find the cwd path components and append to event->buf after the exe filename + struct fs_struct *fsp = BPF_CORE_READ(task, fs); + struct path *pwd = __builtin_preserve_access_index(&fsp->pwd); + uint cwd_start = arg_sz + file_sz; + if (cwd_start > ARGV_MAX + FILE_MAX) + return 0; + int cwd_sz = get_path_str(pwd, &event->buf[cwd_start]); + if (cwd_sz < 0) + return 0; + event->cwd_len = cwd_sz; + + // total bytes to send to userspace + uint total = sizeof(*event) - BUF_MAX + arg_sz + file_sz + cwd_sz; + if (total > sizeof(*event)) + return 0; + + bpf_ringbuf_output(&rb_map, event, total, 0); + + return 0; +} + +char _license[] SEC("license") = "GPL"; diff --git a/vql/linux/bpf/execsnoop/execsnoop.go b/vql/linux/bpf/execsnoop/execsnoop.go new file mode 100644 index 000000000..f97eb394d --- /dev/null +++ b/vql/linux/bpf/execsnoop/execsnoop.go @@ -0,0 +1,224 @@ +//go:build linux + +package bpf + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "sync" + "time" + "unsafe" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/artifacts" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/logging" + "www.velocidex.com/golang/velociraptor/utils" + "www.velocidex.com/golang/velociraptor/vql" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/linux/bpf" + "www.velocidex.com/golang/vfilter" +) + +const ( + EXECSNOOP = "execsnoop" + RINGBUF_MAP = "rb_map" +) + +type ExecsnoopPlugin struct{} + +func (self ExecsnoopPlugin) Info(scope vfilter.Scope, type_map *vfilter.TypeMap) *vfilter.PluginInfo { + return &vfilter.PluginInfo{ + Name: "execsnoop", + Doc: "Report execve system calls", + Metadata: vql.VQLMetadata().Permissions(acls.MACHINE_STATE).Build(), + } +} + +func (self ExecsnoopPlugin) Call( + ctx context.Context, scope vfilter.Scope, + args *ordereddict.Dict) <-chan vfilter.Row { + + outputCh := make(chan vfilter.Row) + + go func() { + defer close(outputCh) + err := vql_subsystem.CheckAccess(scope, acls.MACHINE_STATE) + if err != nil { + scope.Log("execsnoop: %s", err) + return + } + + clientCfg, ok := artifacts.GetConfig(scope) + if !ok { + scope.Log("execsnoop: unable to get config") + return + } + + cfgObj := &config_proto.Config{Client: clientCfg} + logger := logging.GetLogger(cfgObj, &logging.ClientComponent) + + subscriber := bpf.GetManager().Subscribe(EXECSNOOP, &publisher{logger: logger}) + defer bpf.GetManager().Unsubscribe(EXECSNOOP, subscriber) + + for { + select { + case <-ctx.Done(): + return + + case event := <-subscriber.EventCh: + outputCh <- event + + case err := <-subscriber.ErrorCh: + scope.Log("%v", err) + return + } + } + }() + + return outputCh +} + +// event received from bpf +type bpfEvent struct { + Pid uint32 + Ppid uint32 + Uid uint32 + ExeLen uint32 + ArgvLen uint32 + CwdLen uint32 + // note: corresponding field for `u8 buf[BUF_MAX]` omitted +} + +// event for sending to velociraptor +type Event struct { + Time time.Time + Pid uint32 + Ppid uint32 + Uid uint32 + Cwd string + Exe string + Argv string +} + +// pathFromParts returns the path in the normal form. The ebpf program +// provides the path components in reverse order and \0 delimited. +// e.g. given prog\0dir2\0dir1\0\mnt\0 return /mnt/dir1/dir2/prog +func pathFromParts(s []byte) string { + parts := bytes.Split(s, []byte{0x00}) + + for left, right := 0, len(parts)-1; left < right; left, right = left+1, right-1 { + parts[left], parts[right] = parts[right], parts[left] + } + + return string(bytes.Join(parts, []byte("/"))) +} + +func parseArgs(s []byte) string { + s = bytes.TrimSuffix(s, []byte{0x00}) + s = bytes.ReplaceAll(s, []byte{0x00}, []byte(" ")) + return string(s) +} + +func parseData(data []byte) (Event, error) { + var event bpfEvent + eventSize := uint32(unsafe.Sizeof(event)) + eventBuf := bytes.NewBuffer(data[:eventSize]) + err := binary.Read(eventBuf, utils.NativeEndian(), &event) + if err != nil { + return Event{}, err + } + + // extract the variable length fields: argv, exe filename + // and cwd that were passed by the bpf program in event->buf + argvEnd := eventSize + event.ArgvLen + argv := parseArgs(data[eventSize:argvEnd]) + + exeEnd := argvEnd + event.ExeLen + exe := pathFromParts(data[argvEnd:exeEnd]) + + cwd := "/" + if event.CwdLen > 0 { + cwdEnd := exeEnd + event.CwdLen + cwd = pathFromParts(data[exeEnd:cwdEnd]) + } + + return Event{ + Time: time.Now(), + Pid: event.Pid, + Ppid: event.Ppid, + Uid: event.Uid, + Cwd: cwd, + Exe: exe, + Argv: argv, + }, nil +} + +type publisher struct { + wg sync.WaitGroup + cancel func() + logger *logging.LogContext +} + +func (p *publisher) Start() { + var ctx context.Context + ctx, p.cancel = context.WithCancel(context.Background()) + bpfModuleLoadDoneCh := make(chan struct{}) + + p.wg.Add(1) + go func() { + defer p.wg.Done() + + bpfModule, err := initBpf(p.logger) + bpfModuleLoadDoneCh <- struct{}{} + if err != nil { + e := fmt.Errorf("execsnoop: initBpf: %s", err) + bpf.GetManager().PublishError(ctx, EXECSNOOP, e) + return + } + defer bpfModule.Close() + + eventsCh := make(chan []byte) + ringBuffer, err := bpfModule.InitRingBuf(RINGBUF_MAP, eventsCh) + if err != nil { + e := fmt.Errorf("execsnoop: InitRingBuf: %s", err) + bpf.GetManager().PublishError(ctx, EXECSNOOP, e) + return + } + ringBuffer.Poll(300) + + for { + select { + case <-ctx.Done(): + return + + case data, ok := <-eventsCh: + if !ok { + e := fmt.Errorf("execsnoop: events channel was closed") + bpf.GetManager().PublishError(ctx, EXECSNOOP, e) + return + } + event, err := parseData(data) + if err != nil { + p.logger.Warnf("execsnoop: failed to decode received data: %s", err) + continue + } + bpf.GetManager().PublishEvent(ctx, EXECSNOOP, event) + } + } + }() + + <-bpfModuleLoadDoneCh // wait until the BPF module is loaded +} + +func (p *publisher) Stop() { + p.cancel() + p.wg.Wait() +} + +func init() { + vql_subsystem.RegisterPlugin(&ExecsnoopPlugin{}) +} diff --git a/vql_plugins/plugins_bpf.go b/vql_plugins/plugins_bpf.go index a5745d8c9..522315865 100644 --- a/vql_plugins/plugins_bpf.go +++ b/vql_plugins/plugins_bpf.go @@ -23,4 +23,5 @@ import ( _ "www.velocidex.com/golang/velociraptor/vql/linux/bpf/tcpsnoop" _ "www.velocidex.com/golang/velociraptor/vql/linux/bpf/dnssnoop" _ "www.velocidex.com/golang/velociraptor/vql/linux/bpf/chattrsnoop" + _ "www.velocidex.com/golang/velociraptor/vql/linux/bpf/execsnoop" )