Skip to content

Commit

Permalink
Add suspicious_syscall_source event (#3953)
Browse files Browse the repository at this point in the history
* feat(events): add suspicious_syscall_source event

* Create generic extra probe groups in tracee struct
Instead of saving the probe group for suspicious_syscall_source directly to the tracee struct, it is added to a generic map that holds extra probe groups which can be added dynamically.
  • Loading branch information
oshaked1 authored Dec 1, 2024
1 parent abd8563 commit 8d22a07
Show file tree
Hide file tree
Showing 18 changed files with 733 additions and 35 deletions.
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
SUSPICIOUS_SYSCALL_SOURCE
jobs:
#
# DOC VERIFICATION
Expand Down
50 changes: 50 additions & 0 deletions docs/docs/events/builtin/extra/suspicious_syscall_source.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# suspicious_syscall_source

## Intro

suspicious_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 suspicious_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.
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
138 changes: 138 additions & 0 deletions pkg/ebpf/c/common/memory.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@

#include <common/common.h>

enum vma_type
{
VMA_STACK,
VMA_HEAP,
VMA_ANON,
VMA_OTHER
};

// PROTOTYPES

statfunc struct mm_struct *get_mm_from_task(struct task_struct *);
Expand All @@ -13,6 +21,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 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);
statfunc enum vma_type get_vma_type(struct vm_area_struct *vma);

// FUNCTIONS

Expand Down Expand Up @@ -51,4 +65,128 @@ 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;
}

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 strncmp("[vdso]", mapping_name, 7) == 0;
}

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;
}

#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
78 changes: 78 additions & 0 deletions pkg/ebpf/c/tracee.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -5184,6 +5184,84 @@ int BPF_KPROBE(trace_chmod_common)
return events_perf_submit(&p, 0);
}

SEC("kprobe/suspicious_syscall_source")
int BPF_KPROBE(suspicious_syscall_source)
{
program_data_t p = {};
if (!init_program_data(&p, ctx, SUSPICIOUS_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
12 changes: 12 additions & 0 deletions pkg/ebpf/c/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ enum event_id_e
PROCESS_EXECUTE_FAILED,
SECURITY_PATH_NOTIFY,
SET_FS_PWD,
SUSPICIOUS_SYSCALL_SOURCE,
HIDDEN_KERNEL_MODULE_SEEKER,
MODULE_LOAD,
MODULE_FREE,
Expand Down Expand Up @@ -421,6 +422,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 {
Expand Down Expand Up @@ -565,4 +569,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
Loading

0 comments on commit 8d22a07

Please sign in to comment.