From 5a34a8edfab4333a64f795df8148eaf87701e6fb Mon Sep 17 00:00:00 2001 From: Djalal Harouni Date: Thu, 23 Nov 2023 15:35:17 +0100 Subject: [PATCH] tetragon: detect if binary execution raised process capabilities Add binary_properties.caps_raised bool if set then the current execution gained new capabilities through binary execution. This happens when the executed binary has: 1. setuid to root bit set 2. file capabilities. To inspect which capabilities were raised, check the other permitted and effective capabilities fields of process_exec. The capabilities are re-calculated by the capability LSM during execve, so let's avoid trying to be smart and calculate or guess them... we just display the final results. To display these fields, Tetragon must be run with enable-process-cred The event: "process_exec": { "process": { "exec_id": "OjIwMTAxOTMxNzAzMjc4OjE0MDUwOA==", "pid": 140508, "uid": 1000, "cwd": "/home/tixxdz/work/station/code/src/github.com/tixxdz/tetragon", "binary": "/usr/bin/ping", "arguments": "ebpf.io", "flags": "execve clone", "start_time": "2023-11-23T14:32:53.227623175Z", "auid": 1000, "parent_exec_id": "OjI5NjYzNzAwMDAwMDA6NDQ4NTk=", "refcnt": 1, "cap": { "permitted": [ "CAP_NET_RAW" ], "effective": [ "CAP_NET_RAW" ] }, "tid": 140508, "process_credentials": { "uid": 1000, "gid": 1000, "euid": 1000, "egid": 1000, "suid": 1000, "sgid": 1000, "fsuid": 1000, "fsgid": 1000 }, "binary_properties": { "caps_raised": true } }, Signed-off-by: Djalal Harouni --- bpf/lib/bpf_cred.h | 23 +++++++++++ bpf/lib/process.h | 5 ++- bpf/process/bpf_execve_bprm_commit_creds.c | 45 ++++++++++++++++++++-- pkg/api/processapi/processapi.go | 5 ++- pkg/process/process.go | 7 ++++ 5 files changed, 78 insertions(+), 7 deletions(-) diff --git a/bpf/lib/bpf_cred.h b/bpf/lib/bpf_cred.h index f847521bc0c..05343e15ce2 100644 --- a/bpf/lib/bpf_cred.h +++ b/bpf/lib/bpf_cred.h @@ -69,4 +69,27 @@ struct msg_cred_minimal { __u32 pad; } __attribute__((packed)); +/* + * Check if "a" is a subset of "set". + * return true if all of the capabilities in "a" are also in "set" + * __cap_issubset(0100, 1111) will return true + * return false if any of the capabilities in "a" are not in "set" + * __cap_issubset(1111, 0100) will return false + */ +static inline __attribute__((always_inline)) bool +__cap_issubset(const __u64 a, const __u64 set) +{ + return !(a & ~set); +} + +#define __cap_gained(target, source) \ + !__cap_issubset(target, source) + +/* Right now we operate on global uids, we don't do user namespace translation. */ +static inline __attribute__((always_inline)) bool +uid_eq(__u32 left, __u32 right) +{ + return left == right; +} + #endif diff --git a/bpf/lib/process.h b/bpf/lib/process.h index d9a3d6ffbee..37d2350a577 100644 --- a/bpf/lib/process.h +++ b/bpf/lib/process.h @@ -156,8 +156,9 @@ struct msg_execve_key { }; // All fields aligned so no 'packed' attribute. /* Execution and cred related flags shared with userspace */ -#define EXEC_SETUID 0x01 /* This is a set-user-id execution */ -#define EXEC_SETGID 0x02 /* This is a set-group-id execution */ +#define EXEC_SETUID 0x01 /* This is a set-user-id execution */ +#define EXEC_SETGID 0x02 /* This is a set-group-id execution */ +#define EXEC_BIN_CAPS 0x04 /* This binary execution gained new capabilities through suid setuid or file capabilities execution */ /* This is the struct stored in bpf map to share info between * different execve hooks. diff --git a/bpf/process/bpf_execve_bprm_commit_creds.c b/bpf/process/bpf_execve_bprm_commit_creds.c index 0dba18c8adf..46a5d29f99f 100644 --- a/bpf/process/bpf_execve_bprm_commit_creds.c +++ b/bpf/process/bpf_execve_bprm_commit_creds.c @@ -19,6 +19,9 @@ char _license[] __attribute__((section("license"), used)) = "Dual BSD/GPL"; * current task part of the execve call. * For such case this hook must be when we are committing the new credentials * to the task being executed. + * For such the hook must run after: + * bprm_creds_from_file() + * |__cap_bprm_creds_from_file() capability LSM where the bprm is properly set. * * It reads the linux_bprm->per_clear flags that are the personality flags to clear * when we are executing a privilged program. Normally we should check the @@ -47,7 +50,7 @@ BPF_KPROBE(tg_kp_bprm_committing_creds, struct linux_binprm *bprm) struct execve_heap *heap; struct task_struct *task; __u32 pid, euid, uid, egid, gid, sec = 0, zero = 0; - __u64 tid; + __u64 tid, permitted, new_permitted, new_ambient = 0; sec = BPF_CORE_READ(bprm, per_clear); /* If no flags to clear then this is not a privileged execution */ @@ -76,12 +79,48 @@ BPF_KPROBE(tg_kp_bprm_committing_creds, struct linux_binprm *bprm) gid = BPF_CORE_READ(task, cred, gid.val); /* Is setuid? */ - if (euid != uid) + if (!uid_eq(euid, uid)) heap->info.secureexec |= EXEC_SETUID; /* Is setgid? */ - if (egid != gid) + if (!uid_eq(egid, gid)) heap->info.secureexec |= EXEC_SETGID; + /* Ensure that ambient capabilities are not set since they clash with: + * setuid/setgid on the binary. + * file capabilities on the binary. + * + * This is an extra guard. Since if the new ambient capabilities are set then + * there is no way the binary could provide extra capabilities, they cancel + * each other. + */ + BPF_CORE_READ_INTO(&new_ambient, bprm, cred, cap_ambient); + if (new_ambient) + return; + + /* Did we gain new capabilities through suid setuid or file capabilities execve? + * + * To determin if we gained new capabilities we compare the current permitted + * set with the new set. This can happen if: + * (1) the setuid of binary is the _mapped_ root id in current or parent owning namespace. + * (2) the file capabilities are set on the binary. + * + * If (1) is satisfied then it is setuid root binary execution in the user + * namespace that gained new capabilities. However to identify such case + * we have to check if the uid is _mapped_ as root id in the current + * user namespace or one of its parent. Currently this operation is not trivial. + * + * Hence we make it simple by just reporting that execution of this binary allowed + * to gain new capabilities which means: + * a root setuid binary or file capabilities execution provided new capabilities. + * + * Ultimately users are only interested to see if + * this binary when executed provided new capabilities. + */ + BPF_CORE_READ_INTO(&permitted, task, cred, cap_permitted); + BPF_CORE_READ_INTO(&new_permitted, bprm, cred, cap_permitted); + if (__cap_gained(new_permitted, permitted)) + heap->info.secureexec |= EXEC_BIN_CAPS; + /* Do we cache the entry? */ if (heap->info.secureexec != 0) execve_joined_info_map_set(tid, &heap->info); diff --git a/pkg/api/processapi/processapi.go b/pkg/api/processapi/processapi.go index d0e19d0b305..8e05ee6060d 100644 --- a/pkg/api/processapi/processapi.go +++ b/pkg/api/processapi/processapi.go @@ -32,8 +32,9 @@ const ( MsgUnixSize uint32 = 640 /* Execve extra flags */ - ExecveSetuid = 0x01 - ExecveSetgid = 0x02 + ExecveSetuid = 0x01 + ExecveSetgid = 0x02 + ExecveBinCaps = 0x04 // This binary execution gained new capabilities through suid setuid or file capabilities execution // flags of MsgCommon MSG_COMMON_FLAG_RETURN = 0x1 diff --git a/pkg/process/process.go b/pkg/process/process.go index db9e82a9bbf..f85ad788303 100644 --- a/pkg/process/process.go +++ b/pkg/process/process.go @@ -179,6 +179,10 @@ func (pi *ProcessInternal) UpdateExecOutsideCache(cred bool) (*tetragon.Process, prop.Setgid = pi.apiBinaryProp.Setgid update = true } + if pi.apiBinaryProp.CapsRaised { + prop.CapsRaised = true + update = true + } } // Take a copy of the process, add the necessary fields to the @@ -308,6 +312,9 @@ func initProcessInternalExec( if (process.SecureExec & tetragonAPI.ExecveSetgid) != 0 { apiBinaryProp.Setgid = &wrapperspb.UInt32Value{Value: creds.Egid} } + if (process.SecureExec & tetragonAPI.ExecveBinCaps) != 0 { + apiBinaryProp.CapsRaised = true + } // Per thread tracking rules PID == TID //