Skip to content

Commit

Permalink
tetragon: detect if binary execution raised process capabilities
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
tixxdz committed Dec 8, 2023
1 parent 8631a92 commit 8cfb0fc
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 6 deletions.
59 changes: 59 additions & 0 deletions bpf/lib/bpf_cred.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 4 additions & 2 deletions bpf/lib/process.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
69 changes: 65 additions & 4 deletions bpf/process/bpf_execve_bprm_commit_creds.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions pkg/api/processapi/processapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions pkg/process/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions pkg/reader/caps/caps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit 8cfb0fc

Please sign in to comment.