Skip to content

Commit

Permalink
feature(events): process unique identifier murmur hashing
Browse files Browse the repository at this point in the history
By having a hash to represent each ever existent process, we can keep
track of the relationship between processes and its childs using these
identifiers instead of fragile PID/PPID relationship.

The eBPF murmur hashing function is added, despite not being currently
used, because it has been tested together with the userland portion, to
make sure the hashes are exactly the same (so both sides are talking the
same language).

> This code will be used by the process tree implementation in next
> commits.
  • Loading branch information
rafaeldtinoco committed Aug 4, 2023
1 parent 12101ff commit 6839ba7
Show file tree
Hide file tree
Showing 13 changed files with 274 additions and 82 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ require (
github.com/tinylib/msgp v1.1.8 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/twmb/murmur3 v1.1.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.opentelemetry.io/otel v1.15.1 // indirect
Expand Down Expand Up @@ -172,3 +173,5 @@ require (
)

replace github.com/kubernetes/cri-api => k8s.io/cri-api v0.23.5-rc.0

replace github.com/aquasecurity/tracee/types => ./types
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,8 @@ github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+Kd
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
Expand Down
40 changes: 20 additions & 20 deletions pkg/bufferdecoder/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,28 +56,28 @@ func (decoder *EbpfDecoder) DecodeContext(ctx *Context) error {

// task_context start
ctx.StartTime = binary.LittleEndian.Uint64(decoder.buffer[offset+8 : offset+16])
ctx.CgroupID = binary.LittleEndian.Uint64(decoder.buffer[offset+16 : offset+24])
ctx.Pid = binary.LittleEndian.Uint32(decoder.buffer[offset+24 : offset+28])
ctx.Tid = binary.LittleEndian.Uint32(decoder.buffer[offset+28 : offset+32])
ctx.Ppid = binary.LittleEndian.Uint32(decoder.buffer[offset+32 : offset+36])
ctx.HostPid = binary.LittleEndian.Uint32(decoder.buffer[offset+36 : offset+40])
ctx.HostTid = binary.LittleEndian.Uint32(decoder.buffer[offset+40 : offset+44])
ctx.HostPpid = binary.LittleEndian.Uint32(decoder.buffer[offset+44 : offset+48])
ctx.Uid = binary.LittleEndian.Uint32(decoder.buffer[offset+48 : offset+52])
ctx.MntID = binary.LittleEndian.Uint32(decoder.buffer[offset+52 : offset+56])
ctx.PidID = binary.LittleEndian.Uint32(decoder.buffer[offset+56 : offset+60])
_ = copy(ctx.Comm[:], decoder.buffer[offset+60:offset+76])
_ = copy(ctx.UtsName[:], decoder.buffer[offset+76:offset+92])
ctx.Flags = binary.LittleEndian.Uint32(decoder.buffer[offset+92 : offset+96])
ctx.ParentStartTime = binary.LittleEndian.Uint64(decoder.buffer[offset+16 : offset+24])
ctx.CgroupID = binary.LittleEndian.Uint64(decoder.buffer[offset+24 : offset+32])
ctx.Pid = binary.LittleEndian.Uint32(decoder.buffer[offset+32 : offset+36])
ctx.Tid = binary.LittleEndian.Uint32(decoder.buffer[offset+36 : offset+40])
ctx.Ppid = binary.LittleEndian.Uint32(decoder.buffer[offset+40 : offset+44])
ctx.HostPid = binary.LittleEndian.Uint32(decoder.buffer[offset+44 : offset+48])
ctx.HostTid = binary.LittleEndian.Uint32(decoder.buffer[offset+48 : offset+52])
ctx.HostPpid = binary.LittleEndian.Uint32(decoder.buffer[offset+52 : offset+56])
ctx.Uid = binary.LittleEndian.Uint32(decoder.buffer[offset+56 : offset+60])
ctx.MntID = binary.LittleEndian.Uint32(decoder.buffer[offset+60 : offset+64])
ctx.PidID = binary.LittleEndian.Uint32(decoder.buffer[offset+64 : offset+68])
_ = copy(ctx.Comm[:], decoder.buffer[offset+68:offset+84])
_ = copy(ctx.UtsName[:], decoder.buffer[offset+84:offset+100])
ctx.Flags = binary.LittleEndian.Uint32(decoder.buffer[offset+100 : offset+104])
// task_context end

ctx.EventID = events.ID(int32(binary.LittleEndian.Uint32(decoder.buffer[offset+96 : offset+100])))
ctx.Syscall = int32(binary.LittleEndian.Uint32(decoder.buffer[offset+100 : offset+104]))
ctx.MatchedPolicies = binary.LittleEndian.Uint64(decoder.buffer[offset+104 : offset+112])
ctx.Retval = int64(binary.LittleEndian.Uint64(decoder.buffer[offset+112 : offset+120]))
ctx.StackID = binary.LittleEndian.Uint32(decoder.buffer[offset+120 : offset+124])
ctx.ProcessorId = binary.LittleEndian.Uint16(decoder.buffer[offset+124 : offset+126])
// 2 byte padding
ctx.EventID = events.ID(int32(binary.LittleEndian.Uint32(decoder.buffer[offset+104 : offset+108])))
ctx.Syscall = int32(binary.LittleEndian.Uint32(decoder.buffer[offset+108 : offset+112]))
ctx.MatchedPolicies = binary.LittleEndian.Uint64(decoder.buffer[offset+112 : offset+120])
ctx.Retval = int64(binary.LittleEndian.Uint64(decoder.buffer[offset+120 : offset+128]))
ctx.StackID = binary.LittleEndian.Uint32(decoder.buffer[offset+128 : offset+132])
ctx.ProcessorId = binary.LittleEndian.Uint16(decoder.buffer[offset+132 : offset+134])
// event_context end

decoder.cursor += ctx.GetSizeBytes()
Expand Down
3 changes: 2 additions & 1 deletion pkg/bufferdecoder/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
type Context struct {
Ts uint64
StartTime uint64
ParentStartTime uint64
CgroupID uint64
Pid uint32
Tid uint32
Expand All @@ -48,7 +49,7 @@ type Context struct {
}

func (Context) GetSizeBytes() int {
return 128
return 136
}

type ChunkMeta struct {
Expand Down
5 changes: 5 additions & 0 deletions pkg/ebpf/c/common/buffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <vmlinux.h>

#include <common/hash.h>
#include <common/network.h>

// PROTOTYPES
Expand Down Expand Up @@ -340,6 +341,10 @@ statfunc int events_perf_submit(program_data_t *p, u32 id, long ret)
p->event->context.eventid = id;
p->event->context.retval = ret;

// KEEP THIS FOR DEBUGGING (until process tree is fully implemented)
// u32 hash = (u64) hash_u32_and_u64(p->event->context.task.pid, p->event->context.task.task_start_time);
// bpf_printk("hash: %u\n", hash);

// Get Stack trace
if (p->config->options & OPT_CAPTURE_STACK_TRACES) {
int stack_id = bpf_get_stackid(p->ctx, &stack_addresses, BPF_F_USER_STACK);
Expand Down
4 changes: 3 additions & 1 deletion pkg/ebpf/c/common/context.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef __COMMON_CONTEXT_H__
#define __COMMON_CONTEXT_H__

#include "common/hash.h"
#include <vmlinux.h>

#include <common/task.h>
Expand All @@ -22,7 +23,8 @@ init_context(void *ctx, event_context_t *context, struct task_struct *task, u32
{
long ret = 0;
u64 id = bpf_get_current_pid_tgid();
context->task.start_time = get_task_start_time(task);
context->task.task_start_time = get_task_start_time(task);
context->task.parent_start_time = get_parent_start_time(task);
context->task.host_tid = id;
context->task.host_pid = id >> 32;
context->task.host_ppid = get_task_ppid(task);
Expand Down
93 changes: 93 additions & 0 deletions pkg/ebpf/c/common/hash.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#ifndef __COMMON_HASH_H__

#define __COMMON_HASH_H__

#include "bpf/bpf_endian.h"
#include <vmlinux.h>

#include <maps.h>
#include <common/logging.h>
#include <common/common.h>

#define MURMUR_SEED ((u32) 0x18273645) // same as in userland

// PROTOTYPES

u32 murmur3i386(const void *, u32);
u32 hash_u32_and_u64(u32, u64);

// FUNCTIONS

// MurMurHash 3 x86 32-bit (https://en.wikipedia.org/wiki/MurmurHash): Small (u32), simple (for C
// and Go), high performant, optimized and collision resistant hashing function. This function is
// used to hash a task unique identifier (task pid + task_start_time). Userland uses this unique
// identifier to identify a task and construct the process tree.
u32 murmur3i386(const void *key, u32 len)
{
const u8 *data = (const u8 *) key;
const int nblocks = len / 4;

u32 h1 = MURMUR_SEED;
u32 c1 = 0xcc9e2d51;
u32 c2 = 0x1b873593;

// Body
const u32 *blocks = (const u32 *) (data + nblocks * 4);

for (int i = -nblocks; i; i++) {
u32 k1 = blocks[i];
k1 *= c1;
k1 = (k1 << 15) | (k1 >> 17);
k1 *= c2;

h1 ^= k1;
h1 = (h1 << 13) | (h1 >> 19);
h1 = h1 * 5 + 0xe6546b64;
}

// Tail
const u8 *tail = (const u8 *) (data + nblocks * 4);
u32 k1 = 0;

switch (len & 3) {
case 3:
k1 ^= tail[2] << 16;
case 2:
k1 ^= tail[1] << 8;
case 1:
k1 ^= tail[0];
k1 *= c1;
k1 = (k1 << 15) | (k1 >> 17);
k1 *= c2;
h1 ^= k1;
};

// Final
h1 ^= len;
h1 ^= h1 >> 16;
h1 *= 0x85ebca6b;
h1 ^= h1 >> 13;
h1 *= 0xc2b2ae35;
h1 ^= h1 >> 16;

return h1;
}

// Hash a u32 and a u64 into a u32. This function is used to hash a task unique identifier.
// Identical to Golang (userland) HashU32AndU64 function: same hash for same input.
u32 hash_u32_and_u64(u32 arg1, u64 arg2)
{
uint8_t buffer[sizeof(arg1) + sizeof(arg2)];

#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
arg1 = __builtin_bswap32(arg1); // network byte order is big endian, convert for ...
arg2 = __builtin_bswap64(arg2); // ... consistent hashing among different endianness.
#endif

__builtin_memcpy(buffer, &arg1, sizeof(arg1));
__builtin_memcpy(buffer + sizeof(arg1), &arg2, sizeof(arg2));

return murmur3i386(buffer, 4 + 8); // 4 + 8 = sizeof(u32) + sizeof(u64)
}

#endif
6 changes: 6 additions & 0 deletions pkg/ebpf/c/common/task.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ statfunc u64 get_task_start_time(struct task_struct *task)
return BPF_CORE_READ(task, start_time);
}

statfunc u64 get_parent_start_time(struct task_struct *task)
{
struct task_struct *parent = BPF_CORE_READ(task, real_parent);
return BPF_CORE_READ(parent, start_time);
}

statfunc u32 get_task_host_pid(struct task_struct *task)
{
return BPF_CORE_READ(task, pid);
Expand Down
8 changes: 5 additions & 3 deletions pkg/ebpf/c/tracee.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -504,14 +504,16 @@ int tracepoint__sched__sched_process_fork(struct bpf_raw_tracepoint_args *ctx)
struct task_struct *parent = (struct task_struct *) ctx->args[0];
struct task_struct *child = (struct task_struct *) ctx->args[1];

u64 start_time = get_task_start_time(child);
u64 parent_start_time = get_task_start_time(parent);
u64 task_start_time = get_task_start_time(child);

task_info_t task = {};
__builtin_memcpy(&task, p.task_info, sizeof(task_info_t));
task.recompute_scope = true;
task.context.tid = get_task_ns_pid(child);
task.context.host_tid = get_task_host_pid(child);
task.context.start_time = start_time;
task.context.task_start_time = task_start_time;
task.context.parent_start_time = parent_start_time;
ret = bpf_map_update_elem(&task_info_map, &task.context.host_tid, &task, BPF_ANY);
if (ret < 0)
tracee_log(ctx, BPF_LOG_LVL_DEBUG, BPF_LOG_ID_MAP_UPDATE_ELEM, ret);
Expand Down Expand Up @@ -575,7 +577,7 @@ int tracepoint__sched__sched_process_fork(struct bpf_raw_tracepoint_args *ctx)
save_to_submit_buf(&p.event->args_buf, (void *) &child_ns_pid, sizeof(int), 5);
save_to_submit_buf(&p.event->args_buf, (void *) &child_tgid, sizeof(int), 6);
save_to_submit_buf(&p.event->args_buf, (void *) &child_ns_tgid, sizeof(int), 7);
save_to_submit_buf(&p.event->args_buf, (void *) &start_time, sizeof(u64), 8);
save_to_submit_buf(&p.event->args_buf, (void *) &task_start_time, sizeof(u64), 8);

events_perf_submit(&p, SCHED_PROCESS_FORK, 0);
}
Expand Down
42 changes: 24 additions & 18 deletions pkg/ebpf/c/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,38 @@
#include <linux/limits.h>
#include <common/consts.h>

// NOTE: Both, the task_start_time and parent_start_time, are used to uniquely identify a task. The
// uniqueness of a task is a hash(pid, start_time) u32 result. We need both, the task and its
// parent, to be unique in order to construct a faithful process tree (relating tasks not only
// by their pid, but also by their start time).

typedef struct task_context {
u64 start_time; // thread's start time
u64 cgroup_id;
u32 pid; // PID as in the userspace term
u32 tid; // TID as in the userspace term
u32 ppid; // Parent PID as in the userspace term
u32 host_pid; // PID in host pid namespace
u32 host_tid; // TID in host pid namespace
u32 host_ppid; // Parent PID in host pid namespace
u32 uid;
u32 mnt_id;
u32 pid_id;
char comm[TASK_COMM_LEN];
char uts_name[TASK_COMM_LEN];
u32 flags;
u64 task_start_time; // task's start time
u64 parent_start_time; // task's parent start time
u64 cgroup_id; // control group ID
u32 pid; // PID as in the userspace term
u32 tid; // TID as in the userspace term
u32 ppid; // Parent PID as in the userspace term
u32 host_pid; // PID in host pid namespace
u32 host_tid; // TID in host pid namespace
u32 host_ppid; // Parent PID in host pid namespace
u32 uid; // task's effective UID
u32 mnt_id; // task's mount namespace ID
u32 pid_id; // task's pid namespace ID
char comm[TASK_COMM_LEN]; // task's comm
char uts_name[TASK_COMM_LEN]; // task's uts name
u32 flags; // task's status flags (see context_flags_e)
} task_context_t;

typedef struct event_context {
u64 ts; // Timestamp
u64 ts; // timestamp
task_context_t task;
u32 eventid;
s32 syscall; // The syscall which triggered the event
s32 syscall; // syscall that triggered the event
u64 matched_policies;
s64 retval;
s64 retval; // syscall return value
u32 stack_id;
u16 processor_id; // The ID of the processor which processed the event
u16 processor_id; // ID of the processor that processed the event
} event_context_t;

enum event_id_e
Expand Down
43 changes: 34 additions & 9 deletions pkg/ebpf/events_pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,16 +183,23 @@ func (t *Tracee) decodeEvents(outerCtx context.Context, sourceChan chan []byte)
}

// Currently, the timestamp received from the bpf code is of the monotonic clock.
// Todo: The monotonic clock doesn't take into account system sleep time.
// Starting from kernel 5.7, we can get the timestamp relative to the system boot time instead which is preferable.
//
// TODO: The monotonic clock doesn't take into account system sleep time.
// Starting from kernel 5.7, we can get the timestamp relative to the system boot time
// instead which is preferable.

timeStamp := uint64(0)
startTime := uint64(0)

if t.config.Output.RelativeTime {
// To get the monotonic time since tracee was started, we have to subtract the start time from the timestamp.
ctx.Ts -= t.startTime
ctx.StartTime -= t.startTime
// To get the monotonic time since tracee was started, we have to subtract the start
// time from the timestamp.
timeStamp = ctx.Ts - t.startTime
startTime = ctx.StartTime - t.startTime
} else {
// To get the current ("wall") time, we add the boot time into it.
ctx.Ts += t.bootTime
ctx.StartTime += t.bootTime
timeStamp = ctx.Ts + t.bootTime
startTime = ctx.StartTime + t.bootTime
}

containerInfo := t.containers.GetCgroupInfo(ctx.CgroupID).Container
Expand Down Expand Up @@ -221,8 +228,8 @@ func (t *Tracee) decodeEvents(outerCtx context.Context, sourceChan chan []byte)
// get an event pointer from the pool
evt := t.eventsPool.Get().(*trace.Event)
// populate all the fields of the event used in this stage, and reset the rest
evt.Timestamp = int(ctx.Ts)
evt.ThreadStartTime = int(ctx.StartTime)
evt.Timestamp = int(timeStamp)
evt.ThreadStartTime = int(startTime)
evt.ProcessorID = int(ctx.ProcessorId)
evt.ProcessID = int(ctx.Pid)
evt.ThreadID = int(ctx.Tid)
Expand Down Expand Up @@ -252,6 +259,24 @@ func (t *Tracee) decodeEvents(outerCtx context.Context, sourceChan chan []byte)
evt.Syscall = syscall
evt.Metadata = nil

// Create unique identifier for both, the task and its parent. This will allow the
// engine to correlate events from the same task and from the same parent task. The
// identifier will never be the same even if the task is reused (e.g. a process forked
// and the child process reused the same PID).
evt.ProcIdentifier = trace.ProcUniqueIdentifier{
TaskStartTime: ctx.StartTime,
ParentStartTime: ctx.ParentStartTime,
TaskHash: utils.HashU32AndU64(ctx.Pid, ctx.StartTime),
ParentHash: utils.HashU32AndU64(ctx.Ppid, ctx.ParentStartTime),
}

// DEBUG (keep this until process tree is fully implemented)
//
// fmt.Printf("task start time: %d\n", ctx.StartTime)
// fmt.Printf("parent start time: %d\n", ctx.ParentStartTime)
// fmt.Printf("task hash: %d\n", utils.HashU32AndU64(ctx.HostPid, ctx.StartTime))
// fmt.Printf("parent hash: %d\n", utils.HashU32AndU64(ctx.HostPpid, ctx.ParentStartTime))

// If there aren't any policies that need filtering in userland, tracee **may** skip
// this event, as long as there aren't any derivatives or signatures that depend on it.
// Some base events (derivative and signatures) might not have set related policy bit,
Expand Down
Loading

0 comments on commit 6839ba7

Please sign in to comment.