diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 6f6272b84e5d..566c861c4aac 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -73,6 +73,7 @@ env: DNS_DATA_SOURCE WRITABLE_DATA_SOURCE SET_FS_PWD + CHECK_SYSCALL_SOURCE jobs: # # DOC VERIFICATION diff --git a/docs/docs/events/builtin/extra/check_syscall_source.md b/docs/docs/events/builtin/extra/check_syscall_source.md new file mode 100644 index 000000000000..612ff6289929 --- /dev/null +++ b/docs/docs/events/builtin/extra/check_syscall_source.md @@ -0,0 +1,49 @@ +# check_syscall_source + +## Intro + +check_syscall_source - An event reporting a syscall that was invoked from an unusual code location. + +## Description + +In most cases, all code running in a process is placed in dedicated code regions (VMAs, or Virtual Memory Areas) that are mapped from executable files that contain the code. Thus, the locations that syscalls are invoked from should be in one of these code regions. + +When a syscall is invoked from an unusual location, this event is triggered. This may happen in the following scenarios: + +- A shellcode is executed from the stack, the heap or an anonymous (non-file-backed) memory region. + +- A packed program is executed, and is either statically linked or it calls syscalls directly (instead of using libc wrappers). + +This event relies on an event filter to specify which syscalls should be monitored, to reduce overhead. An example command line usage of this event: + +`tracee --events check_syscall_source.args.syscall=open,openat`. + +To reduce noise in cases where code with significant syscall activity is being detected, any unique combination of process, syscall and VMA that contains the invoking code will be submitted as an event only once. + +## Arguments + +* `syscall`:`int`[K] - the syscall which was invoked from an unusual location. The syscall name is parsed if the `parse-arguments` option is specified. +* `ip`:`void *`[K] - the address from which the syscall was invoked (instruction pointer of the instruction following the syscall instruction). +* `is_stack`:`bool`[K] - whether the syscall was invoked from the stack. Mutually exclusive with `is_heap` and `is_anon_vma`. +* `is_heap`:`bool`[K] - whether the syscall was invoked from the heap. Mutually exclusive with `is_stack` and `is_anon_vma`. +* `is_anon_vma`:`bool`[K] - whether the syscall was invoked from an anonymous (non-file-backed) VMA. Mutually exclusive with `is_stack` and `is_heap`. + +## Hooks + +### sys_enter + +#### Type + +tracepoint + +#### Purpose + +Utilizes a tail call from the existing tracepoint on `sys_enter`. The called function analyzes the location from which the syscall was invoked. The analysis occurs only if a policy has selected this syscall as a filter for this event. + +## Example Use Case + +Detect shellcodes. + +## Issues + +Unwanted events may occur in scenarios where legitimate programs run code from unusual locations. This may happen in the case of JITs that write code to anonymous VMAs. Although such code is not expected to invoke syscalls directly (instead relying on some runtime that is mapped from an executable file), exceptions may exist. diff --git a/go.mod b/go.mod index 9868b2e641a9..3373426b20ad 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/IBM/fluent-forward-go v0.2.2 github.com/Masterminds/sprig/v3 v3.2.3 github.com/aquasecurity/libbpfgo v0.7.0-libbpf-1.4 + github.com/aquasecurity/libbpfgo/helpers v0.4.5 github.com/aquasecurity/tracee/api v0.0.0-20240613134034-89d2d4fc7689 github.com/aquasecurity/tracee/signatures/helpers v0.0.0-20240607205742-90c301111aee github.com/aquasecurity/tracee/types v0.0.0-20240607205742-90c301111aee diff --git a/go.sum b/go.sum index 7ecda3529cf3..5f942860dc30 100644 --- a/go.sum +++ b/go.sum @@ -406,6 +406,8 @@ github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVb github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aquasecurity/libbpfgo v0.7.0-libbpf-1.4 h1:rQ94U12Xlz2tncE8Rxnw3vpp/9hgUIEu3/Lv0/XQM0Q= github.com/aquasecurity/libbpfgo v0.7.0-libbpf-1.4/go.mod h1:iI7QCIZ3kXG0MR+FHsDZck6cYs1y1HyZP3sMObBg0sk= +github.com/aquasecurity/libbpfgo/helpers v0.4.5 h1:eCoLclL3yqv4N9jqGL3T/ckrLPms2r13C4V2xtU75yc= +github.com/aquasecurity/libbpfgo/helpers v0.4.5/go.mod h1:j/TQLmsZpOIdF3CnJODzYngG4yu1YoDCoRMELxkQSSA= github.com/aquasecurity/tracee/api v0.0.0-20240613134034-89d2d4fc7689 h1:mAOehSHrqAZ4lvn3AYgDxn+aDTKrv81ghNnGlteDB00= github.com/aquasecurity/tracee/api v0.0.0-20240613134034-89d2d4fc7689/go.mod h1:km0QNkaoOVxU/IYF/Pw/ju/2SO1mYn+HJOIyMDtnfkE= github.com/aquasecurity/tracee/signatures/helpers v0.0.0-20240607205742-90c301111aee h1:1KJy6Z2bSpmKQVPShU7hhbXgGVOgMwvzf9rjoWMTYEg= diff --git a/pkg/ebpf/c/common/common.h b/pkg/ebpf/c/common/common.h index a101ea92b8eb..e73edf6a13e4 100644 --- a/pkg/ebpf/c/common/common.h +++ b/pkg/ebpf/c/common/common.h @@ -62,7 +62,7 @@ static __inline int has_prefix(char *prefix, char *str, int n) } // prefix is too long - return 0; + return 1; } #endif diff --git a/pkg/ebpf/c/common/kconfig.h b/pkg/ebpf/c/common/kconfig.h index d28893496d1d..e0e53f5bbe43 100644 --- a/pkg/ebpf/c/common/kconfig.h +++ b/pkg/ebpf/c/common/kconfig.h @@ -9,7 +9,8 @@ enum kconfig_key_e { - ARCH_HAS_SYSCALL_WRAPPER = 1000U + ARCH_HAS_SYSCALL_WRAPPER = 1000U, + MMU = 1001U }; // PROTOTYPES diff --git a/pkg/ebpf/c/common/memory.h b/pkg/ebpf/c/common/memory.h index e91a2afa9287..491617f2a5fe 100644 --- a/pkg/ebpf/c/common/memory.h +++ b/pkg/ebpf/c/common/memory.h @@ -4,6 +4,7 @@ #include #include +#include // PROTOTYPES @@ -13,6 +14,12 @@ statfunc unsigned long get_arg_end_from_mm(struct mm_struct *); statfunc unsigned long get_env_start_from_mm(struct mm_struct *); statfunc unsigned long get_env_end_from_mm(struct mm_struct *); statfunc unsigned long get_vma_flags(struct vm_area_struct *); +statfunc unsigned long get_vma_start(struct vm_area_struct *); +statfunc struct vm_area_struct *find_vma(struct task_struct *task, u64 addr); +statfunc bool vma_is_stack(struct vm_area_struct *vma); +statfunc bool vma_is_heap(struct vm_area_struct *vma); +statfunc bool vma_is_anon(struct vm_area_struct *vma); +statfunc bool vma_is_vdso(struct vm_area_struct *vma); // FUNCTIONS @@ -51,4 +58,117 @@ statfunc struct mount *real_mount(struct vfsmount *mnt) return container_of(mnt, struct mount, mnt); } +statfunc unsigned long get_vma_start(struct vm_area_struct *vma) +{ + return BPF_CORE_READ(vma, vm_start); +} + +/** + * A busy process can have somewhere in the ballpark of 1000 VMAs. + * In an ideally balanced tree, this means that the max depth is ~10. + * A poorly balanced tree can have a leaf node that is up to twice as deep + * as another leaf node, which in the worst case scenario places its depth + * at 2*10 = 20. + * To be extra safe and accomodate for VMA counts higher than 1000, + * we define the max traversal depth as 25. + */ +#define MAX_VMA_RB_TREE_DEPTH 25 + +// Given a task, find the first VMA which contains the given address. +statfunc struct vm_area_struct *find_vma(struct task_struct *task, u64 addr) +{ + /** + * TODO: from kernel version 6.1, the data structure with which VMAs + * are managed changed from an RB tree to a maple tree. + * We currently don't support finding VMAs on such systems. + */ + struct mm_struct *mm = BPF_CORE_READ(task, mm); + if (!bpf_core_field_exists(mm->mm_rb)) + return NULL; + + // TODO: we don't support NOMMU systems yet (looking up VMAs on them requires walking the VMA + // linked list) + if (!get_kconfig(MMU)) { + return NULL; + } + + struct vm_area_struct *vma = NULL; + struct rb_node *rb_node = BPF_CORE_READ(mm, mm_rb.rb_node); + +#pragma unroll + for (int i = 0; i < MAX_VMA_RB_TREE_DEPTH; i++) { + barrier(); // without this, the compiler refuses to unroll the loop + + if (rb_node == NULL) + break; + + struct vm_area_struct *tmp = container_of(rb_node, struct vm_area_struct, vm_rb); + unsigned long vm_start = BPF_CORE_READ(tmp, vm_start); + unsigned long vm_end = BPF_CORE_READ(tmp, vm_end); + + if (vm_end > addr) { + vma = tmp; + if (vm_start <= addr) + break; + rb_node = BPF_CORE_READ(rb_node, rb_left); + } else + rb_node = BPF_CORE_READ(rb_node, rb_right); + } + + return vma; +} + +statfunc bool vma_is_stack(struct vm_area_struct *vma) +{ + struct mm_struct *vm_mm = BPF_CORE_READ(vma, vm_mm); + if (vm_mm == NULL) + return false; + + u64 vm_start = BPF_CORE_READ(vma, vm_start); + u64 vm_end = BPF_CORE_READ(vma, vm_end); + u64 start_stack = BPF_CORE_READ(vm_mm, start_stack); + + // logic taken from include/linux/mm.h (vma_is_initial_stack) + if (vm_start <= start_stack && start_stack <= vm_end) + return true; + + return false; +} + +statfunc bool vma_is_heap(struct vm_area_struct *vma) +{ + struct mm_struct *vm_mm = BPF_CORE_READ(vma, vm_mm); + if (vm_mm == NULL) + return false; + + u64 vm_start = BPF_CORE_READ(vma, vm_start); + u64 vm_end = BPF_CORE_READ(vma, vm_end); + u64 start_brk = BPF_CORE_READ(vm_mm, start_brk); + u64 brk = BPF_CORE_READ(vm_mm, brk); + + // logic taken from include/linux/mm.h (vma_is_initial_heap) + if (vm_start < brk && start_brk < vm_end) + return true; + + return false; +} + +statfunc bool vma_is_anon(struct vm_area_struct *vma) +{ + return BPF_CORE_READ(vma, vm_file) == NULL; +} + +statfunc bool vma_is_vdso(struct vm_area_struct *vma) +{ + struct vm_special_mapping *special_mapping = + (struct vm_special_mapping *) BPF_CORE_READ(vma, vm_private_data); + if (special_mapping == NULL) + return false; + + // read only 6 characters (7 with NULL terminator), enough to compare with "[vdso]" + char mapping_name[7]; + bpf_probe_read_str(&mapping_name, 7, BPF_CORE_READ(special_mapping, name)); + return has_prefix("[vdso]", mapping_name, 6); +} + #endif diff --git a/pkg/ebpf/c/maps.h b/pkg/ebpf/c/maps.h index 8ed74c1abaa7..56f153bb27ac 100644 --- a/pkg/ebpf/c/maps.h +++ b/pkg/ebpf/c/maps.h @@ -246,6 +246,22 @@ struct sys_exit_init_tail { typedef struct sys_exit_init_tail sys_exit_init_tail_t; +// store program for performing syscall checking logic +struct check_syscall_source_tail { + __uint(type, BPF_MAP_TYPE_PROG_ARRAY); + __uint(max_entries, MAX_EVENT_ID); + __type(key, u32); + __type(value, u32); +} check_syscall_source_tail SEC(".maps"); + +// store syscalls with abnormal source per VMA per process +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __uint(max_entries, 4096); + __type(key, syscall_source_key_t); + __type(value, bool); +} syscall_source_map SEC(".maps"); + // store stack traces #define MAX_STACK_ADDRESSES 1024 // max amount of diff stack trace addrs to buffer diff --git a/pkg/ebpf/c/tracee.bpf.c b/pkg/ebpf/c/tracee.bpf.c index fc9ba29cb9f0..174095b7e437 100644 --- a/pkg/ebpf/c/tracee.bpf.c +++ b/pkg/ebpf/c/tracee.bpf.c @@ -56,6 +56,11 @@ int tracepoint__raw_syscalls__sys_enter(struct bpf_raw_tracepoint_args *ctx) id = *id_64; } + + // Call syscall checker if registered for this syscall. + // If so, it will make sure the following tail is called. + bpf_tail_call(ctx, &check_syscall_source_tail, id); + bpf_tail_call(ctx, &sys_enter_init_tail, id); return 0; } @@ -5130,6 +5135,102 @@ int BPF_KPROBE(trace_security_task_setrlimit) return events_perf_submit(&p, 0); } +enum vma_type +{ + VMA_STACK, + VMA_HEAP, + VMA_ANON, + VMA_OTHER +}; + +statfunc enum vma_type get_vma_type(struct vm_area_struct *vma) +{ + if (vma_is_stack(vma)) + return VMA_STACK; + + if (vma_is_heap(vma)) + return VMA_HEAP; + + if (vma_is_anon(vma) && !vma_is_vdso(vma)) { + return VMA_ANON; + } + + return VMA_OTHER; +} + +SEC("raw_tracepoint/check_syscall_source") +int check_syscall_source(struct bpf_raw_tracepoint_args *ctx) +{ + // Get syscall ID. + // NOTE: this must happen first before any logic that may fail, + // because we must know the syscall ID for the tail call we preceded. + struct task_struct *task = (struct task_struct *) bpf_get_current_task(); + u32 id = ctx->args[1]; + if (is_compat(task)) { + // Translate 32bit syscalls to 64bit syscalls + u32 *id_64 = bpf_map_lookup_elem(&sys_32_to_64_map, &id); + if (id_64 == 0) + return 0; + id = *id_64; + } + + program_data_t p = {}; + if (!init_program_data(&p, ctx, CHECK_SYSCALL_SOURCE)) + goto out; + + if (!evaluate_scope_filters(&p)) + goto out; + + // Get instruction pointer + struct pt_regs *regs = (struct pt_regs *) ctx->args[0]; +#if defined(bpf_target_x86) + u64 ip = BPF_CORE_READ(regs, ip); +#elif defined(bpf_target_arm64) + u64 ip = BPF_CORE_READ(regs, pc); +#endif + + // Find VMA which contains the instruction pointer + struct vm_area_struct *vma = find_vma(task, ip); + if (vma == NULL) + goto out; + + // Get VMA type and make sure it's abnormal (stack/heap/anonymous VMA) + enum vma_type vma_type = get_vma_type(vma); + if (vma_type == VMA_OTHER) + goto out; + + // Build a key that identifies the combination of syscall, + // source VMA and process so we don't submit it multiple times + syscall_source_key_t key = {.syscall = id, + .tgid = get_task_ns_tgid(task), + .tgid_start_time = get_task_start_time(get_leader_task(task)), + .vma_addr = get_vma_start(vma)}; + bool val = true; + + // Try updating the map with the requirement that this key does not exist yet + if ((int) bpf_map_update_elem(&syscall_source_map, &key, &val, BPF_NOEXIST) == -17 /* EEXIST */) + // This key already exists, no need to submit the same syscall-vma-process combination again + goto out; + + bool is_stack = vma_type == VMA_STACK; + bool is_heap = vma_type == VMA_HEAP; + bool is_anon = vma_type == VMA_ANON; + + save_to_submit_buf(&p.event->args_buf, &id, sizeof(id), 0); + save_to_submit_buf(&p.event->args_buf, &ip, sizeof(ip), 1); + save_to_submit_buf(&p.event->args_buf, &is_stack, sizeof(is_stack), 2); + save_to_submit_buf(&p.event->args_buf, &is_heap, sizeof(is_heap), 3); + save_to_submit_buf(&p.event->args_buf, &is_anon, sizeof(is_anon), 4); + + events_perf_submit(&p, 0); + +out: + // Call sys_enter_init_tail which we preceded + bpf_tail_call(ctx, &sys_enter_init_tail, id); + + return 0; +} + // clang-format off // Network Packets (works from ~5.2 and beyond) diff --git a/pkg/ebpf/c/types.h b/pkg/ebpf/c/types.h index 13c6c07ac4e1..a9e8c71e31ad 100644 --- a/pkg/ebpf/c/types.h +++ b/pkg/ebpf/c/types.h @@ -123,6 +123,7 @@ enum event_id_e PROCESS_EXECUTION_FAILED, SECURITY_PATH_NOTIFY, SET_FS_PWD, + CHECK_SYSCALL_SOURCE, HIDDEN_KERNEL_MODULE_SEEKER, MODULE_LOAD, MODULE_FREE, @@ -561,4 +562,12 @@ struct sys_exit_tracepoint_args { long ret; }; +// key for the syscall source map +typedef struct { + u32 syscall; + u32 tgid; + u64 tgid_start_time; + u64 vma_addr; +} syscall_source_key_t; + #endif diff --git a/pkg/ebpf/c/vmlinux.h b/pkg/ebpf/c/vmlinux.h index b55c444db1cf..4ed3e0e99dd5 100644 --- a/pkg/ebpf/c/vmlinux.h +++ b/pkg/ebpf/c/vmlinux.h @@ -284,9 +284,34 @@ struct signal_struct { atomic_t live; }; +struct rb_node { + struct rb_node *rb_right; + struct rb_node *rb_left; +} __attribute__((aligned(sizeof(long)))); + +struct vm_area_struct; + +struct vm_operations_struct { + const char *(*name)(struct vm_area_struct *vma); +}; + +struct vm_special_mapping { + const char *name; +}; + struct vm_area_struct { + union { + struct { + unsigned long vm_start; + unsigned long vm_end; + }; + }; + struct rb_node vm_rb; + struct mm_struct *vm_mm; long unsigned int vm_flags; + const struct vm_operations_struct *vm_ops; struct file *vm_file; + void *vm_private_data; }; typedef unsigned int __kernel_gid32_t; @@ -634,8 +659,17 @@ struct super_block { unsigned long s_magic; }; +struct rb_root { + struct rb_node *rb_node; +}; + struct mm_struct { struct { + struct rb_root mm_rb; + long unsigned int stack_vm; + long unsigned int start_brk; + long unsigned int brk; + long unsigned int start_stack; long unsigned int arg_start; long unsigned int arg_end; long unsigned int env_start; @@ -677,6 +711,7 @@ enum bpf_func_id BPF_FUNC_sk_storage_get = 107, BPF_FUNC_copy_from_user = 148, BPF_FUNC_for_each_map_elem = 164, + BPF_FUNC_find_vma = 180, }; #define MODULE_NAME_LEN (64 - sizeof(unsigned long)) @@ -717,19 +752,10 @@ struct module { struct module_memory mem[MOD_MEM_NUM_TYPES]; // kernel versions >= 6.4 }; -struct rb_node { - struct rb_node *rb_right; - struct rb_node *rb_left; -} __attribute__((aligned(sizeof(long)))); - struct latch_tree_node { struct rb_node node[2]; }; -struct rb_root { - struct rb_node *rb_node; -}; - typedef struct seqcount { unsigned sequence; } seqcount_t; diff --git a/pkg/ebpf/event_filters.go b/pkg/ebpf/event_filters.go new file mode 100644 index 000000000000..a829fd76a188 --- /dev/null +++ b/pkg/ebpf/event_filters.go @@ -0,0 +1,89 @@ +package ebpf + +import ( + "maps" + "strconv" + "unsafe" + + bpf "github.com/aquasecurity/libbpfgo" + + "github.com/aquasecurity/tracee/pkg/errfmt" + "github.com/aquasecurity/tracee/pkg/events" + "github.com/aquasecurity/tracee/pkg/filters" + "github.com/aquasecurity/tracee/pkg/logger" +) + +type eventFilterHandler func(eventFilters map[string]filters.Filter[*filters.StringFilter], bpfModule *bpf.Module) error + +var eventFilterHandlers = map[events.ID]eventFilterHandler{ + events.CheckSyscallSource: populateMapsCheckSyscallSource, +} + +// populateEventFilterMaps populates maps with data from special event filters +func (t *Tracee) populateEventFilterMaps() error { + // Iterate through registerd event filter handlers + for eventID, handler := range eventFilterHandlers { + // Construct filters for this event + eventFilters := map[string]filters.Filter[*filters.StringFilter]{} + for it := t.config.Policies.CreateAllIterator(); it.HasNext(); { + p := it.Next() + f := p.DataFilter.GetEventFilters(eventID) + if len(f) == 0 { + continue + } + maps.Copy(eventFilters, f) + } + if len(eventFilters) == 0 { + continue + } + + // Call handler + err := handler(eventFilters, t.bpfModule) + if err != nil { + logger.Errorw("Failed to handle event filter for event " + events.Core.GetDefinitionByID(eventID).GetName() + ", err: " + err.Error()) + err = t.eventsDependencies.RemoveEvent(eventID) + if err != nil { + return err + } + } + } + return nil +} + +func populateMapsCheckSyscallSource(eventFilters map[string]filters.Filter[*filters.StringFilter], bpfModule *bpf.Module) error { + // Get syscalls to trace + syscallsFilter, ok := eventFilters["syscall"].(*filters.StringFilter) + if !ok { + return nil + } + syscalls := syscallsFilter.Equal() + + // Get map and program for check_syscall_source tailcall + checkSyscallSourceTail, err := bpfModule.GetMap("check_syscall_source_tail") + if err != nil { + return errfmt.Errorf("could not get BPF map \"check_syscall_source_tail\": %v", err) + } + checkSyscallSourceProg, err := bpfModule.GetProgram("check_syscall_source") + if err != nil { + return errfmt.Errorf("could not get BPF program \"check_syscall_source\": %v", err) + } + checkSyscallSourceProgFD := checkSyscallSourceProg.FileDescriptor() + if checkSyscallSourceProgFD < 0 { + return errfmt.Errorf("could not get BPF program FD for \"check_syscall_source\": %v", err) + } + + // Add each syscall to the tail call map + for _, syscall := range syscalls { + syscallID, err := strconv.Atoi(syscall) + if err != nil { + return errfmt.WrapError(err) + } + + err = checkSyscallSourceTail.Update(unsafe.Pointer(&syscallID), unsafe.Pointer(&checkSyscallSourceProgFD)) + if err != nil { + return errfmt.WrapError(err) + } + } + + return nil +} diff --git a/pkg/ebpf/initialization/kconfig.go b/pkg/ebpf/initialization/kconfig.go index b0efbc8c4f12..d86e06d9016c 100644 --- a/pkg/ebpf/initialization/kconfig.go +++ b/pkg/ebpf/initialization/kconfig.go @@ -10,10 +10,12 @@ import ( // Add here all kconfig variables used within tracee.bpf.c const ( CONFIG_ARCH_HAS_SYSCALL_WRAPPER environment.KernelConfigOption = iota + environment.CUSTOM_OPTION_START + CONFIG_MMU environment.KernelConfigOption = iota + environment.CUSTOM_OPTION_START ) var kconfigUsed = map[environment.KernelConfigOption]string{ CONFIG_ARCH_HAS_SYSCALL_WRAPPER: "CONFIG_ARCH_HAS_SYSCALL_WRAPPER", + CONFIG_MMU: "CONFIG_MMU", } // LoadKconfigValues load all kconfig variables used within tracee.bpf.c @@ -33,6 +35,7 @@ func LoadKconfigValues(kc *environment.KernelConfig) (map[environment.KernelConf values[key] = environment.UNDEFINED } values[CONFIG_ARCH_HAS_SYSCALL_WRAPPER] = environment.BUILTIN // assume CONFIG_ARCH_HAS_SYSCALL_WRAPPER is a BUILTIN option + values[CONFIG_MMU] = environment.BUILTIN // assume CONFIG_MMU is a BUILTIN option } else { for key := range kconfigUsed { values[key] = kc.GetValue(key) // undefined, builtin OR module diff --git a/pkg/ebpf/tracee.go b/pkg/ebpf/tracee.go index f8967835fa2d..812c742c56c8 100644 --- a/pkg/ebpf/tracee.go +++ b/pkg/ebpf/tracee.go @@ -1212,6 +1212,12 @@ func (t *Tracee) populateBPFMaps() error { } } + // Populate maps according to BPF-level event argument filters + err = t.populateEventFilterMaps() + if err != nil { + return errfmt.WrapError(err) + } + return nil } diff --git a/pkg/events/core.go b/pkg/events/core.go index 1345e77539c4..97f1330e0c57 100644 --- a/pkg/events/core.go +++ b/pkg/events/core.go @@ -105,6 +105,7 @@ const ( ProcessExecuteFailed SecurityPathNotify SetFsPwd + CheckSyscallSource HiddenKernelModuleSeeker ModuleLoad ModuleFree @@ -13072,6 +13073,27 @@ var CoreEvents = map[ID]Definition{ {Type: "u64", Name: "new_rlim_max"}, }, }, + CheckSyscallSource: { + id: CheckSyscallSource, + id32Bit: Sys32Undefined, + name: "check_syscall_source", + dependencies: Dependencies{ + probes: []Probe{ + {handle: probes.SyscallEnter__Internal, required: true}, + }, + tailCalls: []TailCall{ + {"check_syscall_source_tail", "check_syscall_source", []uint32{ /* Map will be populated at runtime according to event filter */ }}, + }, + }, + sets: []string{}, + params: []trace.ArgMeta{ + {Type: "int", Name: "syscall"}, + {Type: "void*", Name: "ip"}, + {Type: "bool", Name: "is_stack"}, + {Type: "bool", Name: "is_heap"}, + {Type: "bool", Name: "is_anon_vma"}, + }, + }, // // Begin of Signal Events (Control Plane) // diff --git a/pkg/events/parse_args.go b/pkg/events/parse_args.go index c646d211fd05..5e17a30d50a0 100644 --- a/pkg/events/parse_args.go +++ b/pkg/events/parse_args.go @@ -293,6 +293,18 @@ func ParseArgs(event *trace.Event) error { parseOrEmptyString(objTypeArg, objTypeArgument, err) } } + case CheckSyscallSource: + if syscallArg := GetArg(event, "syscall"); syscallArg != nil { + if id, isInt32 := syscallArg.Value.(int32); isInt32 { + if Core.IsDefined(ID(id)) { + eventDefinition := Core.GetDefinitionByID(ID(id)) + if eventDefinition.IsSyscall() { + syscallArg.Value = eventDefinition.GetName() + syscallArg.Type = "string" + } + } + } + } } return nil diff --git a/pkg/filters/data.go b/pkg/filters/data.go index 2a54620245cf..379cfbf436f7 100644 --- a/pkg/filters/data.go +++ b/pkg/filters/data.go @@ -122,7 +122,8 @@ func (af *DataFilter) Parse(filterName string, operatorAndValues string, eventsN valueHandler := func(val string) (string, error) { switch id { case events.SysEnter, - events.SysExit: + events.SysExit, + events.CheckSyscallSource: if dataName == "syscall" { // handle either syscall name or syscall id _, err := strconv.Atoi(val) if err != nil { diff --git a/tests/e2e-inst-signatures/e2e-check_syscall_source.go b/tests/e2e-inst-signatures/e2e-check_syscall_source.go new file mode 100644 index 000000000000..3a198754ee89 --- /dev/null +++ b/tests/e2e-inst-signatures/e2e-check_syscall_source.go @@ -0,0 +1,133 @@ +package main + +import ( + "fmt" + + libbpfgo "github.com/aquasecurity/libbpfgo/helpers" + + "github.com/aquasecurity/tracee/signatures/helpers" + "github.com/aquasecurity/tracee/types/detect" + "github.com/aquasecurity/tracee/types/protocol" + "github.com/aquasecurity/tracee/types/trace" +) + +type e2eCheckSyscallSource struct { + cb detect.SignatureHandler + hasMapleTree bool + foundStack bool + foundHeap bool + foundAnonVma bool +} + +func (sig *e2eCheckSyscallSource) Init(ctx detect.SignatureContext) error { + sig.cb = ctx.Callback + + // Find if this system uses maple trees to manage VMAs. + // If so we don't expect any check_syscall_source event to be submitted. + ksyms, err := libbpfgo.NewKernelSymbolsMap() + if err != nil { + return err + } + _, err = ksyms.GetSymbolByName("system", "mt_find") + if err != nil { + sig.hasMapleTree = false + } else { + sig.hasMapleTree = true + } + + return nil +} + +func (sig *e2eCheckSyscallSource) GetMetadata() (detect.SignatureMetadata, error) { + return detect.SignatureMetadata{ + ID: "CHECK_SYSCALL_SOURCE", + EventName: "CHECK_SYSCALL_SOURCE", + Version: "0.1.0", + Name: "Check Syscall Source Test", + Description: "Instrumentation events E2E Tests: Check Syscall Source", + Tags: []string{"e2e", "instrumentation"}, + }, nil +} + +func (sig *e2eCheckSyscallSource) GetSelectedEvents() ([]detect.SignatureEventSelector, error) { + return []detect.SignatureEventSelector{ + {Source: "tracee", Name: "check_syscall_source"}, + {Source: "tracee", Name: "init_namespaces"}, // This event always happens so we can pass the test on unsupported kernels + }, nil +} + +func (sig *e2eCheckSyscallSource) OnEvent(event protocol.Event) error { + eventObj, ok := event.Payload.(trace.Event) + if !ok { + return fmt.Errorf("failed to cast event's payload") + } + + switch eventObj.EventName { + case "init_namespaces": + // If the system uses maple trees we won't get any check_syscall_source events, pass the test + if sig.hasMapleTree { + m, _ := sig.GetMetadata() + + sig.cb(&detect.Finding{ + SigMetadata: m, + Event: event, + Data: map[string]interface{}{}, + }) + + return nil + } + case "check_syscall_source": + syscall, err := helpers.GetTraceeStringArgumentByName(eventObj, "syscall") + if err != nil { + return err + } + isStack, err := helpers.ArgVal[bool](eventObj.Args, "is_stack") + if err != nil { + return err + } + isHeap, err := helpers.ArgVal[bool](eventObj.Args, "is_heap") + if err != nil { + return err + } + isAnonVma, err := helpers.ArgVal[bool](eventObj.Args, "is_anon_vma") + if err != nil { + return err + } + + // check expected values from test for detection + + if syscall != "exit" { + return nil + } + + if isStack { + sig.foundStack = true + } else if isHeap { + sig.foundHeap = true + } else if isAnonVma { + sig.foundAnonVma = true + } else { + return nil + } + + if !sig.foundStack || !sig.foundHeap || !sig.foundAnonVma { + return nil + } + + m, _ := sig.GetMetadata() + + sig.cb(&detect.Finding{ + SigMetadata: m, + Event: event, + Data: map[string]interface{}{}, + }) + } + + return nil +} + +func (sig *e2eCheckSyscallSource) OnSignal(s detect.Signal) error { + return nil +} + +func (sig *e2eCheckSyscallSource) Close() {} diff --git a/tests/e2e-inst-signatures/export.go b/tests/e2e-inst-signatures/export.go index df459de39c5f..6b88fc4c2069 100644 --- a/tests/e2e-inst-signatures/export.go +++ b/tests/e2e-inst-signatures/export.go @@ -22,6 +22,7 @@ var ExportedSignatures = []detect.Signature{ &e2eSecurityPathNotify{}, &e2eSetFsPwd{}, &e2eFtraceHook{}, + &e2eCheckSyscallSource{}, } var ExportedDataSources = []detect.DataSource{ diff --git a/tests/e2e-inst-signatures/scripts/check_syscall_source.sh b/tests/e2e-inst-signatures/scripts/check_syscall_source.sh new file mode 100755 index 000000000000..af27ae8b72f7 --- /dev/null +++ b/tests/e2e-inst-signatures/scripts/check_syscall_source.sh @@ -0,0 +1,14 @@ +#!/usr/bin/bash + +exit_err() { + echo -n "ERROR: " + echo "$@" + exit 1 +} + +prog=sys_src_tester +dir=tests/e2e-inst-signatures/scripts +gcc $dir/$prog.c -o $dir/$prog -z execstack || exit_err "could not compile $prog.c" +./$dir/$prog stack 2>&1 > /tmp/$prog.log || exit_err "could not run $prog" +./$dir/$prog heap 2>&1 > /tmp/$prog.log || exit_err "could not run $prog" +./$dir/$prog mmap 2>&1 > /tmp/$prog.log || exit_err "could not run $prog" \ No newline at end of file diff --git a/tests/e2e-inst-signatures/scripts/sys_src_tester.c b/tests/e2e-inst-signatures/scripts/sys_src_tester.c new file mode 100644 index 000000000000..a34d981d0f8d --- /dev/null +++ b/tests/e2e-inst-signatures/scripts/sys_src_tester.c @@ -0,0 +1,91 @@ +// gcc -o syscall_source_tester -z execstack syscall_source_tester.c + +#include +#include +#include +#include +#include +#include + +// exit(0); +#if defined(__x86_64__) +#define SHELLCODE \ + "\x48\x31\xFF" /* xor rdi, rdi */ \ + "\x48\xC7\xC0\x3C\x00\x00\x00" /* mov rax, 60 ; __NR_exit */ \ + "\x0F\x05" /* syscall */ +#elif defined(__aarch64__) +#define SHELLCODE \ + "\x00\x00\x80\xD2" /* mov x0, 0 */ \ + "\xA8\x0B\x80\xD2" /* mov x8, #93 ; __NR_exit */ \ + "\x01\x00\x00\xD4" /* svc #0 */ +#else +#error Invalid architecture +#endif + +char shellcode[] = SHELLCODE; + +int main(int argc, char *argv[]) +{ + if (argc != 2) + goto usage; + + if (strcmp(argv[1], "stack") == 0) { + char shellcode_stack[] = SHELLCODE; +#if defined(__aarch64__) + __builtin___clear_cache (&shellcode_stack, &shellcode_stack + sizeof(shellcode)); +#endif + ((void (*)(void))shellcode_stack)(); + // cannot be reached + goto fail; + } + + if (strcmp(argv[1], "heap") == 0) { + void *shellcode_heap = malloc(sizeof(shellcode)); + if (shellcode_heap == NULL) { + perror("malloc failed"); + goto fail; + } + + memcpy(shellcode_heap, shellcode, sizeof(shellcode)); + + // set the heap memory as executable + if (mprotect((void *)((unsigned long long)shellcode_heap & ~(sysconf(_SC_PAGE_SIZE) - 1)), 2 * sysconf(_SC_PAGE_SIZE), PROT_READ | PROT_WRITE | PROT_EXEC) == -1) { + perror("mprotect failed"); + goto fail; + } + + // jump to the shellcode +#if defined(__aarch64__) + __builtin___clear_cache (&shellcode_heap, &shellcode_heap + sizeof(shellcode)); +#endif + ((void (*)(void))shellcode_heap)(); + + // cannot be reached + goto fail; + } + + if (strcmp(argv[1], "mmap") == 0) { + // create an anonymous mapping for the shellcode + void *shellcode_mmap = mmap(NULL, sizeof(shellcode), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (shellcode_mmap == MAP_FAILED) { + perror("mmap failed"); + goto fail; + } + + memcpy(shellcode_mmap, shellcode, sizeof(shellcode)); + + // jump to the shellcode +#if defined(__aarch64__) + __builtin___clear_cache(&shellcode_mmap, &shellcode_mmap + sizeof(shellcode)); +#endif + ((void (*)(void))shellcode_mmap)(); + + // cannot be reached + goto fail; + } + +usage: + printf("usage: ./syscall_source_tester [stack|heap|mmap]\n"); +fail: + exit(EXIT_FAILURE); +} diff --git a/tests/e2e-inst-test.sh b/tests/e2e-inst-test.sh index a1fc7f1e7fc6..2942f38c3308 100755 --- a/tests/e2e-inst-test.sh +++ b/tests/e2e-inst-test.sh @@ -129,20 +129,27 @@ for TEST in $TESTS; do rm -f $SCRIPT_TMP_DIR/build-$$ rm -f $SCRIPT_TMP_DIR/tracee-log-$$ - ./dist/tracee \ - --install-path $TRACEE_TMP_DIR \ - --cache cache-type=mem \ - --cache mem-cache-size=512 \ - --proctree source=both \ - --output option:sort-events \ - --output json:$SCRIPT_TMP_DIR/build-$$ \ - --output option:parse-arguments \ - --log file:$SCRIPT_TMP_DIR/tracee-log-$$ \ - --signatures-dir "$SIG_DIR" \ - --scope comm=echo,mv,ls,tracee,proctreetester,ping,ds_writer,fsnotify_tester,process_execute,tracee-ebpf,writev,set_fs_pwd.sh \ - --dnscache enable \ - --grpc-listen-addr unix:/tmp/tracee.sock \ - --events "$TEST" & + tracee_command="./dist/tracee \ + --install-path $TRACEE_TMP_DIR \ + --cache cache-type=mem \ + --cache mem-cache-size=512 \ + --proctree source=both \ + --output option:sort-events \ + --output json:$SCRIPT_TMP_DIR/build-$$ \ + --output option:parse-arguments \ + --log file:$SCRIPT_TMP_DIR/tracee-log-$$ \ + --signatures-dir "$SIG_DIR" \ + --scope comm=echo,mv,ls,tracee,proctreetester,ping,ds_writer,fsnotify_tester,process_execute,tracee-ebpf,writev,set_fs_pwd.sh,sys_src_tester \ + --dnscache enable \ + --grpc-listen-addr unix:/tmp/tracee.sock \ + --events "$TEST"" + + # Some tests might need event filters + if [ "$TEST" = "CHECK_SYSCALL_SOURCE" ]; then + tracee_command="$tracee_command --events check_syscall_source.args.syscall=exit" + fi + + $tracee_command & # Wait tracee to start