8000 feat(events): add stack_pivot event by oshaked1 · Pull Request #4403 · aquasecurity/tracee · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat(events): add stack_pivot event #4403

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

Merged
merged 3 commits into from
Jan 15, 2025
Merged
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 @@ -78,6 +78,7 @@ env:
SECURITY_PATH_NOTIFY
SET_FS_PWD
SUSPICIOUS_SYSCALL_SOURCE
STACK_PIVOT
jobs:
#
# DOC VERIFICATION
Expand Down
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
6 changes: 6 additions & 0 deletions pkg/ebpf/c/common/context.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ statfunc int init_program_data(program_data_t *, void *, u32);
statfunc int init_tailcall_program_data(program_data_t *, void *);
statfunc bool reset_event(event_data_t *, u32);
statfunc void reset_event_args_buf(event_data_t *);
statfunc bool thread_stack_tracked(task_info_t *);

// FUNCTIONS

Expand Down Expand Up @@ -262,4 +263,9 @@ statfunc bool reset_event(event_data_t *event, u32 event_id)
return true;
}

statfunc bool thread_stack_tracked(task_info_t *task_info)
{
return task_info->stack.start != 0 && task_info->stack.end != 0;
}

#endif
118 changes: 103 additions & 15 deletions pkg/ebpf/c/common/memory.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@

enum vma_type
{
VMA_STACK,
VMA_HEAP,
VMA_FILE_BACKED,
VMA_ANON,
VMA_OTHER
VMA_MAIN_STACK,
VMA_THREAD_STACK,
VMA_HEAP,
VMA_GOLANG_HEAP,
VMA_VDSO,
VMA_UNKNOWN,
};

// PROTOTYPES
Expand All @@ -22,11 +26,14 @@ 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_file_backed(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(task_info_t *task_info, 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);
statfunc enum vma_type get_vma_type(task_info_t *task_info, struct vm_area_struct *vma);

// FUNCTIONS

Expand Down Expand Up @@ -121,7 +128,12 @@ statfunc struct vm_area_struct *find_vma(void *ctx, struct task_struct *task, u6
return vma;
}

statfunc bool vma_is_stack(struct vm_area_struct *vma)
statfunc bool vma_is_file_backed(struct vm_area_struct *vma)
{
return BPF_CORE_READ(vma, vm_file) != NULL;
}

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 @@ -138,7 +150,7 @@ statfunc bool vma_is_stack(struct vm_area_struct *vma)
return false;
}

statfunc bool vma_is_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 All @@ -158,7 +170,45 @@ statfunc bool vma_is_heap(struct vm_area_struct *vma)

statfunc bool vma_is_anon(struct vm_area_struct *vma)
{
return BPF_CORE_READ(vma, vm_file) == NULL;
return !vma_is_file_backed(vma);
}

// The golang heap consists of arenas which are memory regions mapped using mmap.
// When allocating areans, golang supplies mmap with an address hint, which is an
// address that the kernel should place the mapping at.
// Hints are constant and vary between architectures, see `mallocinit()` in
// https://github.com/golang/go/blob/master/src/runtime/malloc.go
// From observation, when allocating arenas the MAP_FIXED flag is used which forces
// the kernel to use the specified address or fail the mapping, so it is safe to
// rely on the address pattern to determine if it belongs to a heap arena.
#define GOLANG_ARENA_HINT_MASK 0x80ff00000000UL
#if defined(bpf_target_x86)
#define GOLANG_ARENA_HINT (0xc0UL << 32)
#elif defined(bpf_target_arm64)
#define GOLANG_ARENA_HINT (0x40UL << 32)
#else
#error Unsupported architecture
#endif

statfunc bool vma_is_golang_heap(struct vm_area_struct *vma)
{
u64 vm_start = BPF_CORE_READ(vma, vm_start);

return (vm_start & GOLANG_ARENA_HINT_MASK) == GOLANG_ARENA_HINT;
}

statfunc bool vma_is_thread_stack(task_info_t *task_info, struct vm_area_struct *vma)
{
// Get the stack area for this task
address_range_t *stack = &task_info->stack;
if (stack->start == 0 && stack->end == 0)
// This thread's stack isn't tracked
return false;

// Check if the VMA is **contained** in the thread stack range.
// We don't check exact address range match because a change to the permissions
// of part of the stack VMA will split it into multiple VMAs.
return BPF_CORE_READ(vma, vm_start) >= stack->start && BPF_CORE_READ(vma, vm_end) <= stack->end;
}

statfunc bool vma_is_vdso(struct vm_area_struct *vma)
Expand All @@ -174,19 +224,57 @@ statfunc bool vma_is_vdso(struct vm_area_struct *vma)
return strncmp("[vdso]", mapping_name, 7) == 0;
}

statfunc enum vma_type get_vma_type(struct vm_area_struct *vma)
statfunc enum vma_type get_vma_type(task_info_t *task_info, struct vm_area_struct *vma)
{
if (vma_is_stack(vma))
return VMA_STACK;
// The check order is a balance between how expensive the check is and how likely it is to pass

if (vma_is_heap(vma))
if (vma_is_file_backed(vma))
return VMA_FILE_BACKED;

if (vma_is_main_stack(vma))
return VMA_MAIN_STACK;

if (vma_is_main_heap(vma))
return VMA_HEAP;

if (vma_is_anon(vma) && !vma_is_vdso(vma)) {
if (vma_is_anon(vma)) {
if (vma_is_golang_heap(vma))
return VMA_GOLANG_HEAP;

if (vma_is_thread_stack(task_info, vma))
return VMA_THREAD_STACK;

if (vma_is_vdso(vma))
return VMA_VDSO;

return VMA_ANON;
}

return VMA_OTHER;
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
Loading
Loading
0