diff --git a/bpf/lib/bpf_cred.h b/bpf/lib/bpf_cred.h index f847521bc0c..db38844885c 100644 --- a/bpf/lib/bpf_cred.h +++ b/bpf/lib/bpf_cred.h @@ -69,4 +69,63 @@ struct msg_cred_minimal { __u32 pad; } __attribute__((packed)); +/* + * SECURE_NOROOT means UID 0 has no special capabilities or privileges. + */ +#ifndef SECURE_NOROOT +#define SECURE_NOROOT 0 +#endif + +static inline __attribute__((always_inline)) bool +__issecure_mask(__u32 secbit, __u32 securebits) +{ + return (1 << (secbit)) & securebits; +} + +#define __issecure(X, securebits) __issecure_mask(X, securebits) + +/* + * 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; +} + +/* + * We check if it user id is global root. Right now we do not + * support per user namespace translation, example checking if + * root in user namespace. + */ +static inline __attribute__((always_inline)) bool +__is_uid_global_root(__u32 uid) +{ + return uid == 0; +} + +/* + * Root can have no capabilities if the SECURE_NOROOT is set. + * We use this to reduce noise. + */ +static inline __attribute__((always_inline)) bool +__root_is_privileged(__u32 securebits) +{ + return !__issecure(SECURE_NOROOT, securebits); +} + #endif diff --git a/bpf/lib/process.h b/bpf/lib/process.h index d9a3d6ffbee..bf78e21775b 100644 --- a/bpf/lib/process.h +++ b/bpf/lib/process.h @@ -156,8 +156,10 @@ 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_FS_CAPS 0x04 /* This binary execution gained new capabilities through file capabilities execution */ +#define EXEC_FS_SETUID 0x08 /* This binary execution gained new capabilities through setuid 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..865cb5c8a3e 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 @@ -46,8 +49,8 @@ BPF_KPROBE(tg_kp_bprm_committing_creds, struct linux_binprm *bprm) struct execve_map_value *curr; struct execve_heap *heap; struct task_struct *task; - __u32 pid, euid, uid, egid, gid, sec = 0, zero = 0; - __u64 tid; + __u32 pid, ruid, euid, uid, egid, gid, sec = 0, zero = 0; + __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,70 @@ 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 file capabilities are set on the binary. + * (2) the setuid of binary is the _mapped_ root id in current or parent owning namespace. + * + * If (2) 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. + * + * To solve this we do: + * 1. Check if the setuid bit is set first, if not then the gained capabilities + * are from file capabilities execution. + * 2. If the setuid bit is set we check if it is a privileged root and if + * it's real uid is not global root and effective uid is root, if so then + * it gained capabilities through setuid execution. + * + * Note: there is the case of a uid being in a user namespace + * and it is mapped to uid 0 root inside that namespace that we do + * not detect now, since we do not do user ids translation into + * user namespaces. For such case we may not report if the binary + * gained privileges through setuid. To be fixed in the future. + */ + 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)) { + /* If the setuid bit is set then this is probably a setuid execution. */ + if (!uid_eq(euid, uid)) { + sec = BPF_CORE_READ(task, cred, securebits); + ruid = BPF_CORE_READ(bprm, cred, uid.val); + if (__root_is_privileged(sec) && !__is_uid_global_root(ruid) && + __is_uid_global_root(euid)) { + /* If root is privileged and we executed from a non root + * uid to a real root uid then set the EXEC_FS_SETUID to indiate + * that there was a privilege elevation through binary setuid. + */ + heap->info.secureexec |= EXEC_FS_SETUID; + } + } else { + /* This is an fs caps execution */ + heap->info.secureexec |= EXEC_FS_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..af33e967c70 100644 --- a/pkg/api/processapi/processapi.go +++ b/pkg/api/processapi/processapi.go @@ -34,6 +34,12 @@ const ( /* Execve extra flags */ ExecveSetuid = 0x01 ExecveSetgid = 0x02 + /* Execve flags received from BPF */ + ExecveFsCaps = 0x04 // This binary execution gained new capabilities through file capabilities execution + ExecveFsSetuid = 0x08 // This binary execution gained new capabilities through setuid execution + /* Exported flags to user events */ + BinaryRaisedFsCaps = 0x01 // This binary execution gained new capabilities through file capabilities execution + BinaryRaisedFsSetuid = 0x02 // This binary execution gained new capabilities through setuid execution // flags of MsgCommon MSG_COMMON_FLAG_RETURN = 0x1 diff --git a/pkg/process/process.go b/pkg/process/process.go index db9e82a9bbf..e41484df211 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 != nil { + prop.CapsRaised = pi.apiBinaryProp.CapsRaised + update = true + } } // Take a copy of the process, add the necessary fields to the @@ -309,6 +313,8 @@ func initProcessInternalExec( apiBinaryProp.Setgid = &wrapperspb.UInt32Value{Value: creds.Egid} } + apiBinaryProp.CapsRaised = caps.GetElevatedPrivsReasons(process.SecureExec) + // Per thread tracking rules PID == TID // // Ensure that exported events have the TID set. For events generated by diff --git a/pkg/reader/caps/caps.go b/pkg/reader/caps/caps.go index da1cf5540d2..3e2d8966985 100644 --- a/pkg/reader/caps/caps.go +++ b/pkg/reader/caps/caps.go @@ -480,3 +480,25 @@ func GetSecureBitsTypes(secBit uint32) []tetragon.SecureBitsType { return bits } + +func GetElevatedPrivsReasons(reasons uint32) []tetragon.ProcessElevatedPrivsReasons { + if reasons == 0 { + return nil + } + + var bits []tetragon.ProcessElevatedPrivsReasons + + if reasons&uint32(processapi.ExecveFsCaps) != 0 { + bits = append(bits, tetragon.ProcessElevatedPrivsReasons_BINARY_EXEC_FILE_CAP) + } + + if reasons&uint32(processapi.ExecveFsSetuid) != 0 { + bits = append(bits, tetragon.ProcessElevatedPrivsReasons_BINARY_EXEC_FILE_SETUID) + } + + if len(bits) > 0 { + return bits + } + + return nil +}