Skip to content

Commit

Permalink
feat(events): add stack_pivot event
Browse files Browse the repository at this point in the history
This event detects usage of the stack pivot technique used during ROP exploits by checking the user's stack pointer at selected syscalls.
If the stack pointer does not point to the stack, an event is triggered.
  • Loading branch information
oshaked1 committed Dec 2, 2024
1 parent a8097bb commit a14e9a6
Show file tree
Hide file tree
Showing 14 changed files with 340 additions and 48 deletions.
50 changes: 50 additions & 0 deletions docs/docs/events/builtin/extra/stack_pivot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# stack_pivot

## Intro

stack_pivot - An event reporting a syscall that was invoked while the user's stack pointer doesn't point to the stack.

## Description

All native code executed makes use of the stack, a region of memory used for storage of function-local data, like function parameters, return address, and local variables.

A stack overflow vulnerability is a security vulnerability that allows an attacker to write data past the end of a stack allocated buffer, allowing him to overwrite other stack data. This kind of vulnerability could be exploited by overwriting the function return address to a location chosen by the attacker, causing the code at that location to run when the vulnerable function returns. An attacker can write multiple return addresses to the stack such that small code sequences, called gadgets, are executed in a chain dictated by the attacker. This exploitation method is called ROP (return oriented programming).

One potential limitation of such an exploit is the amount of data the attacker is able to write to the stack - in some cases, it may not be enough to write the full sequence of gadget addresses required to achieve the attacker's goal. To overcome this limitation, the attacker can use the stack pivot technique. This technique involves a gadget that writes an attacker controlled value to the stack pointer, effectively moving the stack to a new location that the attacker is able to write to (and thus achieving a longer ROP chain).

This event attempts to detect the usage of this technique by checking the stack pointer at the invocation of selected syscalls and detecting cases where it does not point to the original stack.

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 stack_pivot.args.syscall=open,openat`.

## Arguments

- `syscall`:`int`[K] - the syscall which was invoked while the stack pointer doesn't point to the orignal stack. 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.
- `sp`:`void *`[K] - the stack pointer at the time of syscall invocation
- `vma_type`:`char *`[K] - a string describing the type of the VMA which contains the address that the stack pointer points to
- `vma_start`:`void *`[K] - the start address of the VMA which contains the address that the stack pointer points to
- `vma_size`:`unsigned long`[K] - the size of the VMA which contains the address that the stack pointer points to
- `vma_flags`:`unsigned long`[K] - the flags of the VMA which contains the address that the stack pointer points to. 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 pointed to by the stack pointer.

## Example Use Case

Detect ROP exploits that use the stack pivot technique.

## Issues

The kernel manages the stack for the main thread of each process, but additional threads must create and manage their own stacks. The kernel has no notion of a thread stack, so in order to detect that an address belongs to a thread stack and avoid false positives, thread stacks are tracked by tracee by storing the memory region pointed to by the stack pointer at the time of a new thread's creation. This means that threads created before tracee started are not tracked, and we have no way to differentiate between a regular anonymous memory region and one allocated for the stack in such threads. To avoid false positives, anonymous memory regions are ignored for untracked threads, which may result in false negatives.


Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ To reduce noise in cases where code with significant syscall activity is being d

* `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_type`:`char *`[K] - a string describing the type of the VMA which contains the code that triggered the syscall
* `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.
Expand Down
51 changes: 41 additions & 10 deletions pkg/ebpf/c/common/memory.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
enum vma_type
{
VMA_FILE_BACKED,
VMA_STACK,
VMA_ANON,
VMA_MAIN_STACK,
VMA_THREAD_STACK,
VMA_HEAP,
VMA_GOLANG_HEAP,
VMA_THREAD_STACK,
VMA_VDSO,
VMA_ANON,
VMA_UNKNOWN,
};

Expand All @@ -27,11 +27,12 @@ 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_file_backed(struct vm_area_struct *vma);
statfunc bool vma_is_initial_stack(struct vm_area_struct *vma);
statfunc bool vma_is_initial_heap(struct vm_area_struct *vma);
statfunc bool vma_is_main_stack(struct vm_area_struct *vma);
statfunc bool vma_is_main_heap(struct vm_area_struct *vma);
statfunc bool vma_is_anon(struct vm_area_struct *vma);
statfunc bool vma_is_golang_heap(struct vm_area_struct *vma);
statfunc bool vma_is_thread_stack(struct task_struct *task, struct vm_area_struct *vma);
statfunc bool thread_stack_tracked(struct task_struct *task);
statfunc bool vma_is_vdso(struct vm_area_struct *vma);
statfunc enum vma_type get_vma_type(struct task_struct *task, struct vm_area_struct *vma);

Expand Down Expand Up @@ -133,7 +134,7 @@ statfunc bool vma_is_file_backed(struct vm_area_struct *vma)
return BPF_CORE_READ(vma, vm_file) != NULL;
}

statfunc bool vma_is_initial_stack(struct vm_area_struct *vma)
statfunc bool vma_is_main_stack(struct vm_area_struct *vma)
{
struct mm_struct *vm_mm = BPF_CORE_READ(vma, vm_mm);
if (vm_mm == NULL)
Expand All @@ -150,7 +151,7 @@ statfunc bool vma_is_initial_stack(struct vm_area_struct *vma)
return false;
}

statfunc bool vma_is_initial_heap(struct vm_area_struct *vma)
statfunc bool vma_is_main_heap(struct vm_area_struct *vma)
{
struct mm_struct *vm_mm = BPF_CORE_READ(vma, vm_mm);
if (vm_mm == NULL)
Expand Down Expand Up @@ -212,6 +213,12 @@ statfunc bool vma_is_thread_stack(struct task_struct *task, struct vm_area_struc
return BPF_CORE_READ(vma, vm_start) >= stack->start && BPF_CORE_READ(vma, vm_end) <= stack->end;
}

statfunc bool thread_stack_tracked(struct task_struct *task)
{
pid_t pid = BPF_CORE_READ(task, pid);
return (bpf_map_lookup_elem(&thread_stacks, &pid) != NULL);
}

statfunc bool vma_is_vdso(struct vm_area_struct *vma)
{
struct vm_special_mapping *special_mapping =
Expand All @@ -232,10 +239,10 @@ statfunc enum vma_type get_vma_type(struct task_struct *task, struct vm_area_str
if (vma_is_file_backed(vma))
return VMA_FILE_BACKED;

if (vma_is_initial_stack(vma))
return VMA_STACK;
if (vma_is_main_stack(vma))
return VMA_MAIN_STACK;

if (vma_is_initial_heap(vma))
if (vma_is_main_heap(vma))
return VMA_HEAP;

if (vma_is_anon(vma)) {
Expand All @@ -254,4 +261,28 @@ statfunc enum vma_type get_vma_type(struct task_struct *task, struct vm_area_str
return VMA_UNKNOWN;
}

statfunc const char *get_vma_type_str(enum vma_type vma_type)
{
switch (vma_type) {
case VMA_FILE_BACKED:
return "file backed";
case VMA_ANON:
return "anonymous";
case VMA_MAIN_STACK:
return "main stack";
case VMA_THREAD_STACK:
return "thread stack";
case VMA_HEAP:
return "heap";
case VMA_GOLANG_HEAP:
// Goroutine stacks are allocated on the golang heap
return "golang heap/stack";
case VMA_VDSO:
return "vdso";
case VMA_UNKNOWN:
default:
return "unknown";
}
}

#endif
93 changes: 63 additions & 30 deletions pkg/ebpf/c/tracee.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -5241,6 +5241,13 @@ struct {
__type(value, u32);
} suspicious_syscall_source_syscalls SEC(".maps");

struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_EVENT_ID);
__type(key, u32);
__type(value, u32);
} stack_pivot_syscalls SEC(".maps");

statfunc void check_suspicious_syscall_source(void *ctx, struct pt_regs *regs, u32 syscall)
{
program_data_t p = {};
Expand All @@ -5265,9 +5272,6 @@ statfunc void check_suspicious_syscall_source(void *ctx, struct pt_regs *regs, u
if (vma_is_file_backed(vma))
return;

// Get VMA type
enum vma_type vma_type = get_vma_type(task, vma);

// 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,
Expand All @@ -5281,40 +5285,66 @@ statfunc void check_suspicious_syscall_source(void *ctx, struct pt_regs *regs, u
// This key already exists, no need to submit the same syscall-vma-process combination again
return;

char *vma_type_str;
const char *vma_type_str = get_vma_type_str(get_vma_type(task, vma));
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);

switch (vma_type) {
case VMA_STACK:
vma_type_str = "main stack";
break;
case VMA_THREAD_STACK:
vma_type_str = "thread stack";
break;
case VMA_HEAP:
vma_type_str = "heap";
break;
case VMA_GOLANG_HEAP:
// Goroutine stacks are allocated on the golang heap
vma_type_str = "golang heap/stack";
break;
case VMA_ANON:
vma_type_str = "anonymous";
break;
case VMA_VDSO:
vma_type_str = "vdso";
break;
default:
vma_type_str = "unknown";
break;
}
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, (void *) 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);
}

statfunc void check_stack_pivot(void *ctx, struct pt_regs *regs, u32 syscall)
{
program_data_t p = {};

if (!init_program_data(&p, ctx, STACK_PIVOT))
return;

if (!evaluate_scope_filters(&p))
return;

// Get stack pointer
u64 sp = PT_REGS_SP_CORE(regs);

// Find VMA which contains the stack pointer
struct task_struct *task = (struct task_struct *) bpf_get_current_task();
if (unlikely(task == NULL))
return;
struct vm_area_struct *vma = find_vma(ctx, task, sp);
if (unlikely(vma == NULL))
return;

// Check if the stack pointer points to the stack region.
//
// Goroutine stacks are allocated on golang's heap, which means that an
// exploit performing a stack pivot on a go program will result in a false
// negative if the new stack location is on golang's heap.
//
// To identify thread stacks, they need to be tracked when new threads are
// created. This means that we cannot identify stacks of threads that were
// created before tracee started. To avoid false positives, we ignore events
// where the stack pointer's VMA might be a thread stack but it was not
// tracked for this thread. This may result in false negatives.
enum vma_type vma_type = get_vma_type(task, vma);
if (vma_type == VMA_MAIN_STACK || vma_type == VMA_GOLANG_HEAP || vma_type == VMA_THREAD_STACK ||
(vma_type == VMA_ANON && !thread_stack_tracked(task)))
return;

const char *vma_type_str = get_vma_type_str(vma_type);
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, &sp, sizeof(sp), 1);
save_str_to_buf(&p.event->args_buf, (void *) 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);
Expand All @@ -5336,6 +5366,9 @@ int BPF_KPROBE(syscall_checker)
if (bpf_map_lookup_elem(&suspicious_syscall_source_syscalls, &syscall) != NULL)
check_suspicious_syscall_source(ctx, regs, syscall);

if (bpf_map_lookup_elem(&stack_pivot_syscalls, &syscall) != NULL)
check_stack_pivot(ctx, regs, syscall);

return 0;
}

Expand Down
1 change: 1 addition & 0 deletions pkg/ebpf/c/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ enum event_id_e
SECURITY_PATH_NOTIFY,
SET_FS_PWD,
SUSPICIOUS_SYSCALL_SOURCE,
STACK_PIVOT,
HIDDEN_KERNEL_MODULE_SEEKER,
MODULE_LOAD,
MODULE_FREE,
Expand Down
5 changes: 5 additions & 0 deletions pkg/ebpf/event_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type eventParameterHandler func(t *Tracee, eventParams []map[string]filters.Filt

var eventParameterHandlers = map[events.ID]eventParameterHandler{
events.SuspiciousSyscallSource: prepareSuspiciousSyscallSource,
events.StackPivot: prepareStackPivot,
}

// handleEventParameters performs initialization actions according to event parameters,
Expand Down Expand Up @@ -145,3 +146,7 @@ func registerSyscallChecker(t *Tracee, eventParams []map[string]filters.Filter[*
func prepareSuspiciousSyscallSource(t *Tracee, eventParams []map[string]filters.Filter[*filters.StringFilter]) error {
return registerSyscallChecker(t, eventParams, "syscall", "suspicious_syscall_source_syscalls")
}

func prepareStackPivot(t *Tracee, eventParams []map[string]filters.Filter[*filters.StringFilter]) error {
return registerSyscallChecker(t, eventParams, "syscall", "stack_pivot_syscalls")
}
22 changes: 22 additions & 0 deletions pkg/events/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ const (
SecurityPathNotify
SetFsPwd
SuspiciousSyscallSource
StackPivot
HiddenKernelModuleSeeker
ModuleLoad
ModuleFree
Expand Down Expand Up @@ -13082,6 +13083,27 @@ var CoreEvents = map[ID]Definition{
{Type: "unsigned long", Name: "vma_flags"},
},
},
StackPivot: {
id: StackPivot,
id32Bit: Sys32Undefined,
name: "stack_pivot",
dependencies: Dependencies{
probes: []Probe{
{handle: probes.WakeUpNewTask, required: false}, // for thread stack tracking
{handle: probes.SchedProcessExec, required: false}, // for thread stack tracking
{handle: probes.SchedProcessExit, required: false}, // for thread stack tracking
},
},
sets: []string{},
fields: []trace.ArgMeta{
{Type: "int", Name: "syscall"},
{Type: "void*", Name: "sp"},
{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)
//
Expand Down
2 changes: 1 addition & 1 deletion pkg/events/parse_args.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ func ParseArgs(event *trace.Event) error {
parseFsNotifyObjType(objTypeArg, uint64(objType))
}
}
case SuspiciousSyscallSource:
case SuspiciousSyscallSource, StackPivot:
if syscallArg := GetArg(event, "syscall"); syscallArg != nil {
if id, isInt32 := syscallArg.Value.(int32); isInt32 {
if Core.IsDefined(ID(id)) {
Expand Down
3 changes: 2 additions & 1 deletion pkg/filters/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ func (af *DataFilter) Parse(filterName string, operatorAndValues string, eventsN
switch id {
case events.SysEnter,
events.SysExit,
events.SuspiciousSyscallSource:
events.SuspiciousSyscallSource,
events.StackPivot:
if dataName == "syscall" { // handle either syscall name or syscall id
_, err := strconv.Atoi(val)
if err != nil {
Expand Down
Loading

0 comments on commit a14e9a6

Please sign in to comment.