From 8cfb0fc903961cd23788393de31ced15edf59659 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 Populate binary_properties.caps_raised with the ProcessElevatedPrivsReasons on why this binary execution raised capabilities/privileges. 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 Example output 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": [ "BINARY_EXEC_FILE_CAP" ] } }, Signed-off-by: Djalal Harouni --- bpf/lib/bpf_cred.h | 59 ++++++++++++++++++ bpf/lib/process.h | 6 +- bpf/process/bpf_execve_bprm_commit_creds.c | 69 ++++++++++++++++++++-- pkg/api/processapi/processapi.go | 6 ++ pkg/process/process.go | 6 ++ pkg/reader/caps/caps.go | 22 +++++++ 6 files changed, 162 insertions(+), 6 deletions(-) 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 +}