Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add check_syscall_source event #3953

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ env:
DNS_DATA_SOURCE
WRITABLE_DATA_SOURCE
SET_FS_PWD
CHECK_SYSCALL_SOURCE
jobs:
#
# DOC VERIFICATION
Expand Down
50 changes: 50 additions & 0 deletions docs/docs/events/builtin/extra/check_syscall_source.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
9 changes: 9 additions & 0 deletions pkg/ebpf/bpf_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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{
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/ebpf/c/common/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ static __inline int has_prefix(char *prefix, char *str, int n)
}

// prefix is too long
return 0;
return 1;
}

#endif
Expand Down
3 changes: 2 additions & 1 deletion pkg/ebpf/c/common/kconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@

enum kconfig_key_e
{
ARCH_HAS_SYSCALL_WRAPPER = 1000U
ARCH_HAS_SYSCALL_WRAPPER = 1000U,
MMU = 1001U
};

// PROTOTYPES
Expand Down
125 changes: 125 additions & 0 deletions pkg/ebpf/c/common/memory.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <vmlinux.h>

#include <common/common.h>
#include <common/kconfig.h>

// PROTOTYPES

Expand All @@ -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

Expand Down Expand Up @@ -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
8 changes: 8 additions & 0 deletions pkg/ebpf/c/maps.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
101 changes: 101 additions & 0 deletions pkg/ebpf/c/tracee.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading