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
related to #2055 and fix some bugs the triggered events.
  • Loading branch information
AsafEitani committed May 29, 2023
1 parent b855a20 commit 611244a
Show file tree
Hide file tree
Showing 13 changed files with 225 additions and 178 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 amd64 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


## 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
1 change: 0 additions & 1 deletion pkg/bufferdecoder/eventsreader.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ func ReadArgFromBuff(id events.ID, ebpfMsgDecoder *EbpfDecoder, params []trace.A
var res interface{}
var argIdx uint8
var arg trace.Argument

err = ebpfMsgDecoder.DecodeUint8(&argIdx)
if err != nil {
return 0, arg, errfmt.Errorf("error reading arg index: %v", err)
Expand Down
29 changes: 20 additions & 9 deletions pkg/ebpf/c/common/buffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,25 +155,36 @@ 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]...
u16 zero = 0;
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 > ARGS_BUF_SIZE - sizeof(restricted_len))
return 0;
event->args[event->buf_off] = 0;
event->buf_off += 1;
__builtin_memcpy(&(event->args[event->buf_off]), &restricted_len, sizeof(restricted_len));
event->buf_off += sizeof(restricted_len);
event->context.argnum++;

return add_u64_elements_to_buf(event, ptr, len, orig_off);
if ((event->buf_off > ARGS_BUF_SIZE - MAX_BYTES_ARR_SIZE))
return 0;

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

return 1;
}

statfunc int
Expand Down
3 changes: 2 additions & 1 deletion pkg/ebpf/c/common/consts.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@

#define MAX_PERCPU_BUFSIZE (1 << 15) // set by the kernel as an upper bound
#define MAX_STRING_SIZE 4096 // same as PATH_MAX
#define MAX_BYTES_ARR_SIZE 4096 // max size of bytes array (arbitrarily chosen)
#define MAX_BYTES_ARR_SIZE 8192 // max size of bytes array (arbitrarily chosen)
#define MAX_STR_FILTER_SIZE 16 // bounded to size of the compared values (comm)
#define MAX_BIN_PATH_SIZE 256 // max binary path size
#define MAX_ARR
#define FILE_MAGIC_HDR_SIZE 32 // magic_write: bytes to save from a file's header
#define FILE_MAGIC_MASK 31 // magic_write: mask used for verifier boundaries
#define NET_SEQ_OPS_SIZE 4 // print_net_seq_ops: struct size - TODO: replace with uprobe argument
Expand Down
106 changes: 24 additions & 82 deletions pkg/ebpf/c/tracee.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -1454,103 +1454,58 @@ 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
// clang-format on
// clang-frmat on

program_data_t p = {};
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;
u32 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(u32), &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 @@ -1561,32 +1516,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->cx; // 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
3 changes: 3 additions & 0 deletions pkg/ebpf/capture.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ func (t *Tracee) processFileCaptures(ctx context.Context) {
t.handleError(err)
continue
}
if mprotectMeta.Ts == 0 && mprotectMeta.Pid == 0 {
print("GOTTEM")
}
// note: size of buffer will determine maximum extracted file size! (as writes from kernel are immediate)
if t.config.Output.RelativeTime {
// To get the monotonic time since tracee was started, we have to subtract the start time from the timestamp.
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 @@ -384,7 +384,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 611244a

Please sign in to comment.