diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 6acfdeb7c83c..1298691ea25f 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -74,6 +74,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..2138ad244f63 --- /dev/null +++ b/docs/docs/events/builtin/extra/check_syscall_source.md @@ -0,0 +1,50 @@ +# 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 parameter 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. This argument is also used as a parameter to select which syscalls should be checked. +* `ip`:`void *`[K] - the address from which the syscall was invoked (instruction pointer of the instruction following the syscall instruction). +* `vma_type`:`char *`[K] - the type of the VMA which contains the code that triggered the syscall (one of *stack*/*heap*/*anonymous*) +* `vma_start`:`void *`[K] - the start address of the VMA which contains the code that triggered the syscall +* `vma_size`:`unsigned long`[K] - the size of the VMA which contains the code that triggered the syscall +* `vma_flags`:`unsigned long`[K] - the flags of the VMA which contains the code that triggered the syscall. The flag names are parsed if the `parse-arguments` option is specified. + +## Hooks + +### Individual syscalls + +#### Type + +kprobe + +#### Purpose + +A kprobe is placed on each syscall that was selected using a parameter for this event. The kprobe function analyzes the location from which the syscall was invoked. + +## 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 9d9c48c2f1dc..89007e038c1d 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.0.20240729111821-61d531acf4ca + github.com/aquasecurity/libbpfgo/helpers v0.4.5 github.com/aquasecurity/tracee/api v0.0.0-20240905132323-d1eaeef6a19f github.com/aquasecurity/tracee/signatures/helpers v0.0.0-20241009193135-0b23713fa9f9 github.com/aquasecurity/tracee/types v0.0.0-20241008181102-d40bc1f81863 diff --git a/go.sum b/go.sum index 9a7f30106857..8a7d6de82283 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.0.20240729111821-61d531acf4ca h1:OPbvwFFvR11c1bgOLhBq1R5Uk3hwUjHW2KfrdyJan9Y= github.com/aquasecurity/libbpfgo v0.7.0-libbpf-1.4.0.20240729111821-61d531acf4ca/go.mod h1:UpO6kTehEgAGGKR2twztBxvzjTiLiV/cb2xmlYb+TfE= +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-20240905132323-d1eaeef6a19f h1:O4UmMQViaaP1wKL1eXe7C6VylwrUmUB5mYM+roqnUZg= github.com/aquasecurity/tracee/api v0.0.0-20240905132323-d1eaeef6a19f/go.mod h1:Gn6xVkaBkVe1pOQ0++uuHl+lMMClv0TPY8mCQ6j88aA= github.com/aquasecurity/tracee/signatures/helpers v0.0.0-20241009193135-0b23713fa9f9 h1:sB84YYSDgUAYNSonXeMPweaN6dviCld8UNqcKDn1jBM= diff --git a/pkg/ebpf/bpf_log.go b/pkg/ebpf/bpf_log.go index e66c53c35f0e..de686eb42f39 100644 --- a/pkg/ebpf/bpf_log.go +++ b/pkg/ebpf/bpf_log.go @@ -31,6 +31,9 @@ const ( // hidden kernel module functions BPFLogIDHidKerMod + + // find vma not supported + BPFLogIDFindVMAUnsupported // BPF_LOG_FIND_VMA_UNSUPPORTED ) var stringMap = map[BPFLogType]string{ @@ -49,6 +52,9 @@ var stringMap = map[BPFLogType]string{ // hidden kernel module functions BPFLogIDHidKerMod: "BPF_LOG_ID_HID_KER_MOD", + + // find vma not supported + BPFLogIDFindVMAUnsupported: "BPF_LOG_FIND_VMA_UNSUPPORTED", } var errorMap = map[BPFLogType]string{ @@ -67,6 +73,9 @@ var errorMap = map[BPFLogType]string{ // hidden kernel module functions BPFLogIDHidKerMod: "Failure in hidden kernel module seeker logic", + + // find vma not supported + BPFLogIDFindVMAUnsupported: "Finding VMAs is not supported in this kernel", } func (b BPFLogType) String() string { diff --git a/pkg/ebpf/c/common/common.h b/pkg/ebpf/c/common/common.h index f35146d0e01d..3f3b4eb23b8c 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..ec533ed8dcb4 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,11 @@ 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 struct vm_area_struct *find_vma(void *ctx, 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 +57,123 @@ statfunc struct mount *real_mount(struct vfsmount *mnt) return container_of(mnt, struct mount, mnt); } +/** + * 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 + +static bool alerted_find_vma_unsupported = false; + +// Given a task, find the first VMA which contains the given address. +statfunc struct vm_area_struct *find_vma(void *ctx, 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)) { + if (!alerted_find_vma_unsupported) { + tracee_log(ctx, BPF_LOG_LVL_WARN, BPF_LOG_FIND_VMA_UNSUPPORTED, 0); + alerted_find_vma_unsupported = true; + } + 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)) { + if (!alerted_find_vma_unsupported) { + tracee_log(ctx, BPF_LOG_LVL_WARN, BPF_LOG_FIND_VMA_UNSUPPORTED, 0); + alerted_find_vma_unsupported = true; + } + 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 fe415e403484..17f36dae37bd 100644 --- a/pkg/ebpf/c/maps.h +++ b/pkg/ebpf/c/maps.h @@ -265,6 +265,14 @@ struct sys_exit_init_tail { typedef struct sys_exit_init_tail sys_exit_init_tail_t; +// 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 0e480035f656..eeb84879b3a3 100644 --- a/pkg/ebpf/c/tracee.bpf.c +++ b/pkg/ebpf/c/tracee.bpf.c @@ -5184,6 +5184,107 @@ int BPF_KPROBE(trace_chmod_common) 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("kprobe/check_syscall_source") +int BPF_KPROBE(check_syscall_source) +{ + program_data_t p = {}; + if (!init_program_data(&p, ctx, CHECK_SYSCALL_SOURCE)) + return 0; + + if (!evaluate_scope_filters(&p)) + return 0; + + // Get instruction pointer + struct pt_regs *regs = ctx; + if (get_kconfig(ARCH_HAS_SYSCALL_WRAPPER)) + regs = (struct pt_regs *) PT_REGS_PARM1(ctx); + u64 ip = PT_REGS_IP_CORE(regs); + + // Find VMA which contains the instruction pointer + struct task_struct *task = (struct task_struct *) bpf_get_current_task(); + if (unlikely(task == NULL)) + return 0; + struct vm_area_struct *vma = find_vma(ctx, task, ip); + if (vma == NULL) + return 0; + + // 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) + return 0; + + // Get syscall ID + u32 syscall = get_syscall_id_from_regs(regs); + + // 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 = syscall, + .tgid = get_task_host_tgid(task), + .tgid_start_time = get_task_start_time(get_leader_task(task)), + .vma_addr = BPF_CORE_READ(vma, vm_start)}; + 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) == -EEXIST) + // This key already exists, no need to submit the same syscall-vma-process combination again + return 0; + + char *vma_type_str; + + switch (vma_type) { + case VMA_STACK: + vma_type_str = "stack"; + break; + case VMA_HEAP: + vma_type_str = "heap"; + break; + case VMA_ANON: + vma_type_str = "anonymous"; + break; + // shouldn't happen + default: + return 0; + } + + unsigned long vma_start = BPF_CORE_READ(vma, vm_start); + unsigned long vma_size = BPF_CORE_READ(vma, vm_end) - vma_start; + unsigned long vma_flags = BPF_CORE_READ(vma, vm_flags); + + save_to_submit_buf(&p.event->args_buf, &syscall, sizeof(syscall), 0); + save_to_submit_buf(&p.event->args_buf, &ip, sizeof(ip), 1); + save_str_to_buf(&p.event->args_buf, vma_type_str, 2); + save_to_submit_buf(&p.event->args_buf, &vma_start, sizeof(vma_start), 3); + save_to_submit_buf(&p.event->args_buf, &vma_size, sizeof(vma_size), 4); + save_to_submit_buf(&p.event->args_buf, &vma_flags, sizeof(vma_flags), 5); + + events_perf_submit(&p, 0); + + 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 b6822b92add6..904b5ab3b45b 100644 --- a/pkg/ebpf/c/types.h +++ b/pkg/ebpf/c/types.h @@ -124,6 +124,7 @@ enum event_id_e PROCESS_EXECUTE_FAILED, SECURITY_PATH_NOTIFY, SET_FS_PWD, + CHECK_SYSCALL_SOURCE, HIDDEN_KERNEL_MODULE_SEEKER, MODULE_LOAD, MODULE_FREE, @@ -422,6 +423,9 @@ enum bpf_log_id // hidden kernel module functions BPF_LOG_ID_HID_KER_MOD, + + // find vma not supported + BPF_LOG_FIND_VMA_UNSUPPORTED, }; typedef struct bpf_log { @@ -566,4 +570,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 fbc020871ab5..3d8196187a08 100644 --- a/pkg/ebpf/c/vmlinux.h +++ b/pkg/ebpf/c/vmlinux.h @@ -64,6 +64,8 @@ enum true = 1, }; +#define EEXIST 17 + #if defined(__TARGET_ARCH_x86) struct thread_info { @@ -286,9 +288,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; @@ -641,8 +668,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; @@ -687,6 +723,7 @@ enum bpf_func_id BPF_FUNC_get_current_task_btf = 158, BPF_FUNC_for_each_map_elem = 164, BPF_FUNC_task_pt_regs = 175, + BPF_FUNC_find_vma = 180, }; #define MODULE_NAME_LEN (64 - sizeof(unsigned long)) @@ -727,19 +764,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_parameters.go b/pkg/ebpf/event_parameters.go new file mode 100644 index 000000000000..e22dbd1b2bd3 --- /dev/null +++ b/pkg/ebpf/event_parameters.go @@ -0,0 +1,97 @@ +package ebpf + +import ( + "fmt" + "strconv" + + "github.com/aquasecurity/tracee/pkg/ebpf/probes" + "github.com/aquasecurity/tracee/pkg/events" + "github.com/aquasecurity/tracee/pkg/filters" + "github.com/aquasecurity/tracee/pkg/logger" +) + +type eventParameterHandler func(t *Tracee, eventParams []map[string]filters.Filter[*filters.StringFilter]) error + +var eventParameterHandlers = map[events.ID]eventParameterHandler{ + events.CheckSyscallSource: attachCheckSyscallSourceProbes, +} + +// handleEventParameters performs initialization actions according to event parameters, +// specified using policies or the command line as event arguments. +// For example, an event can use one of its parameters to populate eBPF maps, +// or perhaps attach eBPF programs according to the filters. +func (t *Tracee) handleEventParameters() error { + // Iterate through registerd event parameter handlers + for eventID, handler := range eventParameterHandlers { + // Make sure this event is selected + if _, err := t.eventsDependencies.GetEvent(eventID); err != nil { + continue + } + // Get the event parameters for all policies + eventParams := make([]map[string]filters.Filter[*filters.StringFilter], 0) + for iterator := t.policyManager.CreateAllIterator(); iterator.HasNext(); { + policy := iterator.Next() + policyParams := policy.DataFilter.GetEventFilters(eventID) + if len(policyParams) == 0 { + continue + } + eventParams = append(eventParams, policyParams) + } + if len(eventParams) == 0 { + // No parameters for this event + continue + } + // Call handler + if err := handler(t, eventParams); err != nil { + if err := t.eventsDependencies.RemoveEvent(eventID); err != nil { + logger.Warnw("Failed to remove event from dependencies manager", "remove reason", "failed handling event parameters", "error", err) + } + return fmt.Errorf("failed to handle parameters for event %s: %v", events.Core.GetDefinitionByID(eventID).GetName(), err) + } + } + return nil +} + +func attachCheckSyscallSourceProbes(t *Tracee, eventParams []map[string]filters.Filter[*filters.StringFilter]) error { + // Get syscalls to trace + syscalls := make(map[string]struct{}, 0) + for _, policyParams := range eventParams { + syscallsParam, ok := policyParams["syscall"].(*filters.StringFilter) + if !ok { + return nil + } + for _, entry := range syscallsParam.Equal() { + syscallID, err := strconv.Atoi(entry) + if err != nil { + return err + } + if !events.Core.IsDefined(events.ID(syscallID)) { + return fmt.Errorf("syscall id %d is not defined", syscallID) + } + + syscallName := events.Core.GetDefinitionByID(events.ID(syscallID)).GetName() + syscalls[syscallName] = struct{}{} + } + } + + // Create probe group + probeMap := make(map[probes.Handle]probes.Probe) + i := 0 + for syscallName := range syscalls { + probeMap[probes.Handle(i)] = probes.NewTraceProbe(probes.SyscallEnter, syscallName, "check_syscall_source") + i++ + } + t.checkSyscallSourceProbes = probes.NewProbeGroup(t.bpfModule, probeMap) + + // Attach probes + i = 0 + for syscallName := range syscalls { + if err := t.checkSyscallSourceProbes.Attach(probes.Handle(i), t.kernelSymbols); err != nil { + // Report attachment errors but don't fail, because it may be a syscall that doesn't exist on this system + logger.Warnw("Failed to attach check_syscall_source kprobe", "syscall", syscallName, "error", err) + } + i++ + } + + 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 0945566e034e..bcd9b3bb5ffc 100644 --- a/pkg/ebpf/tracee.go +++ b/pkg/ebpf/tracee.go @@ -125,6 +125,8 @@ type Tracee struct { // This does not mean they are required for tracee to function. // TODO: remove this in favor of dependency manager nodes requiredKsyms []string + // Probes created for check_syscall_source event + checkSyscallSourceProbes *probes.ProbeGroup } func (t *Tracee) Stats() *metrics.Stats { @@ -518,6 +520,16 @@ func (t *Tracee) Init(ctx gocontext.Context) error { }, } + // Perform extra initializtion steps required by specific events according to their arguments + err = capabilities.GetInstance().EBPF( + func() error { + return t.handleEventParameters() + }, + ) + if err != nil { + return errfmt.WrapError(err) + } + return nil } @@ -1486,6 +1498,12 @@ func (t *Tracee) Close() { logger.Errorw("failed to detach probes when closing tracee", "err", err) } } + if t.checkSyscallSourceProbes != nil { + err := t.checkSyscallSourceProbes.DetachAll() + if err != nil { + logger.Errorw("failed to detach check_syscall_source probes when closing tracee", "err", err) + } + } if t.bpfModule != nil { t.bpfModule.Close() } diff --git a/pkg/events/core.go b/pkg/events/core.go index b0adc1f25e5d..23b0aa4f2d87 100644 --- a/pkg/events/core.go +++ b/pkg/events/core.go @@ -106,6 +106,7 @@ const ( ProcessExecuteFailed SecurityPathNotify SetFsPwd + CheckSyscallSource HiddenKernelModuleSeeker ModuleLoad ModuleFree @@ -13060,6 +13061,20 @@ var CoreEvents = map[ID]Definition{ }, }, }, + CheckSyscallSource: { + id: CheckSyscallSource, + id32Bit: Sys32Undefined, + name: "check_syscall_source", + sets: []string{}, + params: []trace.ArgMeta{ + {Type: "int", Name: "syscall"}, + {Type: "void*", Name: "ip"}, + {Type: "char*", Name: "vma_type"}, + {Type: "void*", Name: "vma_start"}, + {Type: "unsigned long", Name: "vma_size"}, + {Type: "unsigned long", Name: "vma_flags"}, + }, + }, // // Begin of Signal Events (Control Plane) // diff --git a/pkg/events/parse_args.go b/pkg/events/parse_args.go index 61b803c0dc0c..782208beb7ed 100644 --- a/pkg/events/parse_args.go +++ b/pkg/events/parse_args.go @@ -220,6 +220,24 @@ func ParseArgs(event *trace.Event) error { parseFsNotifyObjType(objTypeArg, uint64(objType)) } } + 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" + } + } + } + } + if vmaFlagsArg := GetArg(event, "vma_flags"); vmaFlagsArg != nil { + if flags, isUint64 := vmaFlagsArg.Value.(uint64); isUint64 { + vmaFlagsArg.Type = "string" + vmaFlagsArg.Value = parsers.ParseVmFlags(flags).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..3c8cc7c102f3 --- /dev/null +++ b/tests/e2e-inst-signatures/e2e-check_syscall_source.go @@ -0,0 +1,96 @@ +package main + +import ( + "fmt" + + "github.com/aquasecurity/tracee/pkg/events" + "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 + foundStack bool + foundHeap bool + foundAnonVma bool +} + +func (sig *e2eCheckSyscallSource) Init(ctx detect.SignatureContext) error { + sig.cb = ctx.Callback + + 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"}, + }, 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 "check_syscall_source": + syscall, err := helpers.ArgVal[int32](eventObj.Args, "syscall") + if err != nil { + return err + } + vmaType, err := helpers.ArgVal[string](eventObj.Args, "vma_type") + if err != nil { + return err + } + + // check expected values from test for detection + + if syscall != int32(events.Exit) { + return nil + } + + if vmaType == "stack" { + sig.foundStack = true + } else if vmaType == "heap" { + sig.foundHeap = true + } else if vmaType == "anonymous" { + 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..8de6a51c1c24 --- /dev/null +++ b/tests/e2e-inst-signatures/scripts/sys_src_tester.c @@ -0,0 +1,91 @@ +// gcc -o sys_src_tester -z execstack sys_src_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: ./sys_src_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..5b05bc157c41 100755 --- a/tests/e2e-inst-test.sh +++ b/tests/e2e-inst-test.sh @@ -122,6 +122,12 @@ for TEST in $TESTS; do fi "${TESTS_DIR}"/ftrace_hook.sh ;; + CHECK_SYSCALL_SOURCE) + if cat /proc/kallsyms | grep -qP "trace.*vma_store"; then + info "skip check_syscall_source test on kernel $(uname -r) (VMAs stored in maple tree)" + continue + fi + ;; esac # Run tracee @@ -129,20 +135,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