Skip to content

Commit

Permalink
change hooked_syscalls event so users can specify syscalls to check. (#…
Browse files Browse the repository at this point in the history
…3136)

* change hooked_syscalls event so users can specify syscalls to check.
  • Loading branch information
AsafEitani committed Jun 26, 2023
1 parent 9d5eefe commit 637b65a
Show file tree
Hide file tree
Showing 12 changed files with 221 additions and 184 deletions.
69 changes: 69 additions & 0 deletions docs/docs/events/builtin/extra/hooked_syscalls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# hooked_syscalls

## Intro
`hooked_syscalls` is an event that checks the selected syscalls for any syscall hooking.

## Description
The purpose of the `hooked_syscalls` event is to monitor for system call hooking in the Linux kernel. It verifies the function pointer of the system call to ensure it lies between the etext and stext addresses. This helps identify instances of kernel code modifications, often used for malicious activities such as hiding processes, files, or network connections.

The `hooked_syscalls` event checks either user-specified syscalls or a default list of syscalls depending on the architecture of the system, with a different list for amd64 and arm64 respectively.

## Arguments
* `check_syscalls`:`[]string`[U] - the syscall checked for syscall hooking. Can be used to specify selected syscalls or use the default ones.
The default syscalls for amd64 are:

`read`
`write`
`open`
`close`
`ioctl`
`socket`
`sendto`
`recvfrom`
`sendmsg`
`recvmsg`
`execve`
`kill`
`getdents`
`ptrace`
`getdents64`
`openat`
`bpf`
`execveat`

The default syscalls for arm64 are:
`ioctl`
`openat`
`close`
`getdents64`
`read`
`write`
`ptrace`
`kill`
`socket`
`execveat`
`sendto`
`recvfrom`
`sendmsg`
`recvmsg`
`execve`
`bpf`
* `hooked_syscalls`:`[]trace.HookedSymbolData` [K] - The hooked syscalls that were found along with their owners. `Hidden` owner means that the pointed function owner is not a part of the kernel modules list.
## Hooks
### Various system calls
#### Type
Uprobe
#### Purpose
Detection of syscall hooking.

## Example Use Case
The `hooked_syscalls` event could be used as part of a broader system integrity monitoring solution. For example, a security engineer could use it to raise alerts or run further investigations if unexpected syscall hooking activities are detected. This could aid in the early detection and mitigation of malware or rootkit infections.
Example:

`tracee -f e=hooked_syscalls -f hooked_syscalls.args.check_syscalls=<syscall>,<syscall>,...`

## Issues
The `check_syscalls` argument is used as a parameter to specify the syscalls to be checked. This will change in the future to be an event parameter.

## Related Events

4 changes: 2 additions & 2 deletions pkg/bufferdecoder/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,8 @@ func (decoder *EbpfDecoder) DecodeIntArray(msg []int32, size uint32) error {

// DecodeUint64Array translate from the decoder buffer, starting from the decoder cursor, to msg, size * 8 bytes (in order to get int64).
func (decoder *EbpfDecoder) DecodeUint64Array(msg *[]uint64) error {
var arrLen uint8
err := decoder.DecodeUint8(&arrLen)
var arrLen uint16
err := decoder.DecodeUint16(&arrLen)
if err != nil {
return errfmt.Errorf("error reading ulong array number of elements: %v", err)
}
Expand Down
8 changes: 0 additions & 8 deletions pkg/ebpf/c/common/arch.h
Original file line number Diff line number Diff line change
Expand Up @@ -291,14 +291,6 @@ statfunc struct pt_regs *get_task_pt_regs(struct task_struct *task)
#define SYSCALL_PROCESS_MRELEASE 448
#endif

#if defined(bpf_target_x86)
#define NUMBER_OF_SYSCALLS_TO_CHECK 18
#elif defined(bpf_target_arm64)
#define NUMBER_OF_SYSCALLS_TO_CHECK 14
#else
#define NUMBER_OF_SYSCALLS_TO_CHECK 0
#endif

statfunc bool has_syscall_fd_arg(uint syscall_id)
{
// Only syscalls with one fd argument so far
Expand Down
30 changes: 21 additions & 9 deletions pkg/ebpf/c/common/buffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,25 +155,37 @@ add_u64_elements_to_buf(event_data_t *event, const u64 __user *ptr, int len, vol
return 1;
}

statfunc int save_u64_arr_to_buf(event_data_t *event, const u64 __user *ptr, int len, u8 index)
statfunc int save_u64_arr_to_buf(event_data_t *event, const u64 *ptr, int len, u8 index)
{
// Data saved to submit buf: [index][u8 count][u64 1][u64 2][u64 3]...
// Data saved to submit buf: [index][u16 count][u64 1][u64 2][u64 3]...
u16 restricted_len = (u16) len;
u32 total_size = sizeof(u64) * restricted_len;

if (event->buf_off > ARGS_BUF_SIZE - 1)
return 0;

// Save argument index
event->args[event->buf_off] = index;

// Save space for number of elements (1 byte)
event->buf_off += 1;
volatile u32 orig_off = event->buf_off;
if (event->buf_off > ARGS_BUF_SIZE - 1)
// Save number of elements
if (event->buf_off + sizeof(index) > ARGS_BUF_SIZE - sizeof(restricted_len))
return 0;
__builtin_memcpy(
&(event->args[event->buf_off + sizeof(index)]), &restricted_len, sizeof(restricted_len));

if ((event->buf_off + sizeof(index) + sizeof(restricted_len) >
ARGS_BUF_SIZE - MAX_BYTES_ARR_SIZE))
return 0;
event->args[event->buf_off] = 0;
event->buf_off += 1;

if (bpf_probe_read(&(event->args[event->buf_off + sizeof(index) + sizeof(restricted_len)]),
total_size & (MAX_BYTES_ARR_SIZE - 1),
(void *) ptr) != 0)
return 0;

event->context.argnum++;
event->buf_off += sizeof(index) + sizeof(restricted_len) + total_size;

return add_u64_elements_to_buf(event, ptr, len, orig_off);
return 1;
}

statfunc int
Expand Down
1 change: 0 additions & 1 deletion pkg/ebpf/c/maps.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ BPF_HASH(process_tree_map, u32, eq_t, 10240); // filter eve
BPF_LRU_HASH(proc_info_map, u32, proc_info_t, 10240); // holds data for every process
BPF_LRU_HASH(task_info_map, u32, task_info_t, 10240); // holds data for every task
BPF_HASH(ksymbols_map, ksym_name_t, u64, 1024); // holds the addresses of some kernel symbols
BPF_HASH(syscalls_to_check_map, int, u64, 256); // syscalls to discover
BPF_ARRAY(config_map, config_entry_t, 1); // various configurations
BPF_ARRAY(file_write_path_filter, path_filter_t, 3); // filter file write captures
BPF_ARRAY(file_read_path_filter, path_filter_t, 3); // filter file read captures
Expand Down
104 changes: 23 additions & 81 deletions pkg/ebpf/c/tracee.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -1417,36 +1417,24 @@ int BPF_KPROBE(trace_do_exit)
}

// uprobe_syscall_trigger submit to the buff the syscalls function handlers
// address from the syscall table. the syscalls are stored in map which is
// syscalls_to_check_map and the syscall-table address is stored in the
// kernel_symbols map.
// address from the syscall table. the syscalls are checked in user mode for hooks.

SEC("uprobe/trigger_syscall_event")
int uprobe_syscall_trigger(struct pt_regs *ctx)
{
u64 table_count = 0;
u64 caller_ctx_id = 0;

// clang-format off
//
// Golang calling convention is being changed from a stack based argument
// passing (plan9 like) to register based argument passing whenever
// possible. In arm64, this change happened from go1.17 to go1.18. Use a
// magic number argument to allow uprobe handler to recognize the calling
// convention in a simple way.
// Golang calling convention per architecture

#if defined(bpf_target_x86)
// go1.17, go1.18, go 1.19
caller_ctx_id = ctx->cx; // 2nd arg
caller_ctx_id = ctx->bx; // 1st arg
table_count = ctx->cx; // 2nd arg
#elif defined(bpf_target_arm64)
// go1.17
u64 magic_num = 0;
bpf_probe_read(&magic_num, 8, ((void *) ctx->sp) + 16); // 1st arg
bpf_probe_read(&caller_ctx_id, 8, ((void *) ctx->sp) + 24); // 2nd arg
if (magic_num != UPROBE_MAGIC_NUMBER) {
// go1.18, go 1.19
magic_num = ctx->user_regs.regs[1]; // 1st arg
caller_ctx_id = ctx->user_regs.regs[2]; // 2nd arg
}
caller_ctx_id = ctx->user_regs.regs[1]; // 1st arg
table_count = ctx->user_regs.regs[2]; // 2nd arg
#else
return 0;
#endif
Expand All @@ -1456,64 +1444,31 @@ int uprobe_syscall_trigger(struct pt_regs *ctx)
if (!init_program_data(&p, ctx))
return 0;

// Uprobes are not triggered by syscalls, so we need to override the false value.
p.event->context.syscall = NO_SYSCALL;
p.event->context.matched_policies = ULLONG_MAX;

// uprobe was triggered from other tracee instance
if (p.config->tracee_pid != p.task_info->context.pid &&
p.config->tracee_pid != p.task_info->context.host_pid)
return 0;

int key = 0;
// TODO: https://github.com/aquasecurity/tracee/issues/2055
if (bpf_map_lookup_elem(&syscalls_to_check_map, (void *) &key) == NULL)
return 0;
// Uprobes are not triggered by syscalls, so we need to override the false value.
p.event->context.syscall = NO_SYSCALL;
p.event->context.matched_policies = ULLONG_MAX;

char syscall_table_sym[15] = "sys_call_table";
u64 *syscall_table_addr = (u64 *) get_symbol_addr(syscall_table_sym);

if (unlikely(syscall_table_addr == 0))
return 0;

void *stext_addr = get_stext_addr();
if (unlikely(stext_addr == NULL))
// Get per-cpu string buffer
buf_t *string_p = get_buf(STRING_BUF_IDX);
if (string_p == NULL)
return 0;

void *etext_addr = get_etext_addr();
if (unlikely(etext_addr == NULL))
u64 table_size = table_count * sizeof(u64);
if ((table_size > MAX_PERCPU_BUFSIZE) || (table_size <= 0)) // verify no wrap occurred
return 0;

u64 idx;
u64 syscall_addr = 0;
u64 syscall_address[NUMBER_OF_SYSCALLS_TO_CHECK];

#pragma unroll
for (int i = 0; i < NUMBER_OF_SYSCALLS_TO_CHECK; i++) {
idx = i;

// syscalls_to_check_map format: [syscall#][syscall#][syscall#]
u64 *syscall_num_p = bpf_map_lookup_elem(&syscalls_to_check_map, (void *) &idx);
if (syscall_num_p == NULL) {
syscall_address[i] = 0;
continue;
}

bpf_core_read(&syscall_addr, sizeof(u64), &syscall_table_addr[*syscall_num_p]);
if (syscall_addr == 0) {
return 0;
}

// skip if in text segment range
if (syscall_addr >= (u64) stext_addr && syscall_addr < (u64) etext_addr) {
syscall_address[i] = 0;
continue;
}

syscall_address[i] = syscall_addr;
}

save_u64_arr_to_buf(p.event, (const u64 *) syscall_address, NUMBER_OF_SYSCALLS_TO_CHECK, 0);
save_u64_arr_to_buf(p.event, (const u64 *) syscall_table_addr, table_count, 0);
save_to_submit_buf(p.event, (void *) &caller_ctx_id, sizeof(uint64_t), 1);

return events_perf_submit(&p, PRINT_SYSCALL_TABLE, 0);
Expand All @@ -1524,32 +1479,19 @@ int uprobe_seq_ops_trigger(struct pt_regs *ctx)
{
u64 caller_ctx_id = 0;
u64 *address_array = NULL;
u64 struct_address;
u64 struct_address = 0;

// clang-format off
//
// Golang calling convention is being changed from a stack based argument
// passing (plan9 like) to register based argument passing whenever
// possible. In arm64, this change happened from go1.17 to go1.18. Use a
// magic number argument to allow uprobe handler to recognize the calling
// convention in a simple way.
// Golang calling convention per architecture

#if defined(bpf_target_x86)
// go1.17, go1.18, go 1.19
caller_ctx_id = ctx->cx; // 2nd arg
address_array = ((void *) ctx->sp + 8); // 3rd arg
caller_ctx_id = ctx->bx; // 1st arg
address_array = ((void *) ctx->sp + 8); // 2nd arg
#elif defined(bpf_target_arm64)
// go1.17
u64 magic_num = 0;
bpf_probe_read(&magic_num, 8, ((void *) ctx->sp) + 16); // 1st arg
bpf_probe_read(&caller_ctx_id, 8, ((void *) ctx->sp) + 24); // 2nd arg
address_array = ((void *) ctx->sp + 32); // 3rd arg
if (magic_num != UPROBE_MAGIC_NUMBER) {
// go1.18 and go1.19
magic_num = ctx->user_regs.regs[1]; // 1st arg
caller_ctx_id = ctx->user_regs.regs[2]; // 2nd arg
address_array = ((void *) ctx->sp + 8); // 3rd arg
}
caller_ctx_id = ctx->user_regs.regs[1]; // 1st arg
address_array = ((void *) ctx->sp + 8); // 2nd arg

#else
return 0;
#endif
Expand Down
5 changes: 4 additions & 1 deletion pkg/ebpf/events_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,10 @@ func (t *Tracee) processDoInitModule(event *trace.Event) error {
return errfmt.WrapError(err)
}
if okSyscalls {
t.triggerSyscallsIntegrityCheck(*event)
err = t.triggerSyscallsIntegrityCheck(*event)
if err != nil {
logger.Warnw("hooked_syscalls returned an error", "error", err)
}
}
if okSeqOps {
t.triggerSeqOpsIntegrityCheck(*event)
Expand Down

0 comments on commit 637b65a

Please sign in to comment.