Skip to content

Commit

Permalink
tests: add security_capset test
Browse files Browse the repository at this point in the history
Signed-off-by: Djalal Harouni <[email protected]>
  • Loading branch information
tixxdz committed Feb 5, 2024
1 parent 272c639 commit 9bfc12c
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 1 deletion.
1 change: 1 addition & 0 deletions contrib/tester-progs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ killer-tester
killer-tester-32
/getcpu
drop-privileges
change-capabilities
6 changes: 5 additions & 1 deletion contrib/tester-progs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ PROGS = sigkill-tester \
killer-tester \
drop-privileges \
getcpu \
direct-write-tester
direct-write-tester \
change-capabilities

# For now killer-tester is compiled to 32-bit only on x86_64 as we want
# to test 32-bit binaries and system calls compatibility layer.
Expand All @@ -46,6 +47,9 @@ threads-exit: threads-exit.c
capabilities-tester: capabilities-tester.c
$(GCC) -Wall $< -o $@ -lcap

change-capabilities: change-capabilities.c
$(GCC) -Wall $< -o $@ -lcap

exit-tester: exit-tester.c
$(GCC) -Wall $< -o $@ -lpthread

Expand Down
107 changes: 107 additions & 0 deletions contrib/tester-progs/change-capabilities.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
// Copyright Authors of Tetragon

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/sched.h>
#include <linux/types.h>
#include <sched.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include <sys/capability.h>

#define errExit(msg) \
do { \
perror(msg); \
exit(EXIT_FAILURE); \
} while (0)

int check_cap(cap_value_t cap)
{
int ret;
cap_t caps;
cap_flag_value_t value = 0;

caps = cap_get_proc();
if (caps == NULL)
errExit("cap_get_proc");

ret = cap_get_flag(caps, cap, CAP_EFFECTIVE, &value);
if (ret)
errExit("cap_get_flag");

cap_free(caps);

return value;
}

void clear_cap(cap_value_t c)
{
cap_t cap;
cap_value_t cap_list[CAP_LAST_CAP+1];

cap = cap_get_proc();
if (cap == NULL)
errExit("cap_get_proc");

cap_list[0] = c;
if (cap_set_flag(cap, CAP_EFFECTIVE, 1, cap_list, CAP_CLEAR) == -1)
errExit("cap_set_flag");

if (cap_set_proc(cap) == -1)
errExit("cap_set_proc");

cap_free(cap);
}

void set_cap(cap_value_t c)
{
cap_t cap;
cap_value_t cap_list[CAP_LAST_CAP+1];

cap = cap_get_proc();
if (cap == NULL)
errExit("cap_get_proc");

cap_list[0] = c;
if (cap_set_flag(cap, CAP_EFFECTIVE, 1, cap_list, CAP_SET) == -1)
errExit("cap_set_flag");

if (cap_set_proc(cap) == -1)
errExit("cap_set_proc");

cap_free(cap);
}

int main(int argc, char *argv[])
{
pid_t pid = getpid();
int cap;

cap = check_cap(CAP_SYS_ADMIN);
if (cap) {
printf("(pid:%d) checking capability CAP_SYS_ADMIN: is set\n", pid);
printf("(pid:%d) clearing capability CAP_SYS_ADMIN and CAP_CHOWN\n", pid);
clear_cap(CAP_SYS_ADMIN);
clear_cap(CAP_CHOWN);
} else {
printf("checking capability CAP_SYS_ADMIN: not set\n");
return 0;
}

printf("(pid:%d) restoring capability CAP_SYS_ADMIN and CAP_CHOWN\n", pid);
set_cap(CAP_SYS_ADMIN);
set_cap(CAP_CHOWN);

fflush(stdout);
return 0;
}
50 changes: 50 additions & 0 deletions pkg/reader/caps/caps.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"

"github.com/cilium/tetragon/api/v1/tetragon"
"github.com/cilium/tetragon/pkg/api/processapi"
Expand All @@ -18,6 +19,33 @@ import (
"golang.org/x/sys/unix"
)

var (
// Set default last capability based on upstream unix go library
cap_last_cap = int32(unix.CAP_LAST_CAP)
lastCapOnce sync.Once
)

// GetLastCap() Returns unix.CAP_LAST_CAP unless the kernel
// defines another last cap which is the case for old kernels.
func GetLastCap() int32 {
lastCapOnce.Do(func() {
d, err := os.ReadFile(filepath.Join(option.Config.ProcFS, "/sys/kernel/cap_last_cap"))
if err != nil {
logger.GetLogger().WithError(err).Warnf("Could not read kernel cap_last_cap, using default '%d' as cap_last_cap", cap_last_cap)
}
val, err := strconv.ParseInt(strings.TrimRight(string(d), "\n"), 10, 32)
if err != nil {
logger.GetLogger().WithError(err).Warnf("Could not parse cap_last_cap, using default '%d' as cap_last_cap", cap_last_cap)
return
}
// just silence some CodeQL
if val >= 0 && val < unix.CAP_LAST_CAP {
cap_last_cap = int32(val)
}
})
return cap_last_cap
}

func isCapValid(capInt int32) bool {
if capInt >= 0 && capInt <= unix.CAP_LAST_CAP {
return true
Expand All @@ -26,6 +54,28 @@ func isCapValid(capInt int32) bool {
return false
}

// AreSubset() Checks if "a" is a subset of "set"
// Rerturns true if all "a" capabilities are also in "set", otherwise
// false.
func AreSubset(a uint64, set uint64) bool {
return (!((a & ^uint64(set)) != 0))
}

// capToMask() returns the mask of the corresponding u32
func capToMask(cap int32) uint32 {
return uint32(1 << ((cap) & 31))
}

// GetCapsFullSet() Returns up to date (go unix library) full set.
func GetCapsFullSet() uint64 {
// Get last u32 bits
caps := uint64(capToMask(GetLastCap()+1)-1) << 32
// Get first u32 bits
caps |= uint64(^uint32(0))

return caps
}

func GetCapability(capInt int32) (string, error) {
if !isCapValid(capInt) {
return "", fmt.Errorf("invalid capability value %d", capInt)
Expand Down
7 changes: 7 additions & 0 deletions pkg/reader/caps/caps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,10 @@ func TestGetCapability(t *testing.T) {
assert.Error(t, err)
assert.Empty(t, str)
}

func TestCapsAreSubset(t *testing.T) {
assert.Equal(t, true, AreSubset(0x000001ffffffffff, 0x000001ffffffffff))
assert.Equal(t, true, AreSubset(0x000001fffffffffe, 0x000001ffffffffff))
assert.Equal(t, false, AreSubset(0x000001ffffffffff, 0x000001fffffffffe))
assert.Equal(t, true, AreSubset(0x0, 0x0))
}
121 changes: 121 additions & 0 deletions pkg/sensors/tracing/kprobe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6254,3 +6254,124 @@ spec:
err = jsonchecker.JsonTestCheck(t, checker)
assert.NoError(t, err)
}

// Detect changing capabilities
func TestProcessSetCap(t *testing.T) {
var doneWG, readyWG sync.WaitGroup
defer doneWG.Wait()

ctx, cancel := context.WithTimeout(context.Background(), tus.Conf().CmdWaitTime)
defer cancel()

tracingPolicy := `
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: "privileges-raise"
annotations:
description: "Detects privileges change operations"
spec:
kprobes:
- call: "security_capset"
syscall: false
message: "Process changed its capabilities with capset system call"
args:
- index: 0
type: "nop"
- index: 1
type: "cred"
- index: 2
type: "cap_effective"
- index: 3
type: "cap_inheritable"
- index: 4
type: "cap_permitted"
selectors:
- matchArgs:
- index: 2
operator: "NotEqual"
values:
- "0"
- matchArgs:
- index: 4
operator: "NotEqual"
values:
- "0"
`

createCrdFile(t, tracingPolicy)

fullSet := caps.GetCapsFullSet()
firstChange := fullSet&0xffffffff00000000 | uint64(0xffdfffff) // Removes CAP_SYS_ADMIN
secondChange := fullSet&0xffffffff00000000 | uint64(0xffdffffe) // removes CAP_SYS_ADMIN and CAP_CHOWN

_, currentPermitted, currentEffective, _ := caps.GetPIDCaps(filepath.Join(option.Config.ProcFS, fmt.Sprint(os.Getpid()), "status"))

if currentPermitted == 0 || currentPermitted != currentEffective {
t.Skip("Skipping test since current Permitted or Effective capabilities are zero or do not match")
}

// Now we ensure at least that we have the full capabilities set active
if caps.AreSubset(fullSet, currentPermitted) == false ||
caps.AreSubset(fullSet, currentEffective) == false {
// full capabilities set is not set in current permitted
t.Skipf("Skipping test since current Permitted or Effective capabilities are not a full capabilities set %s - %s",
caps.GetCapabilitiesHex(currentPermitted), caps.GetCapabilitiesHex(currentEffective))
}

lastCap, _ := caps.GetCapability(caps.GetLastCap())
t.Logf("Test %s running with last capability:%d %s", t.Name(), caps.GetLastCap(), lastCap)
t.Logf("Test %s running with cap_permitted:%s - cap_effective:%s",
t.Name(), caps.GetCapabilitiesHex(currentPermitted), caps.GetCapabilitiesHex(currentEffective))

obs, err := observertesthelper.GetDefaultObserverWithFile(t, ctx, testConfigFile, tus.Conf().TetragonLib, observertesthelper.WithMyPid())
if err != nil {
t.Fatalf("GetDefaultObserverWithFile error: %s", err)
}
observertesthelper.LoopEvents(ctx, t, &doneWG, &readyWG, obs)
readyWG.Wait()

testSetCaps := testutils.RepoRootPath("contrib/tester-progs/change-capabilities")

t.Logf("Test %s Matching cap_permitted:%s - cap_inheritable:%s - cap_effective:%s",
t.Name(), caps.GetCapabilitiesHex(fullSet), fmt.Sprintf("%016x", 0), caps.GetCapabilitiesHex(firstChange))
kpCheckers1 := ec.NewProcessKprobeChecker("").
WithMessage(sm.Full("Process changed its capabilities with capset system call")).
WithFunctionName(sm.Full("security_capset")).
WithArgs(ec.NewKprobeArgumentListMatcher().
WithValues(
// effective caps
ec.NewKprobeArgumentChecker().WithCapEffectiveArg(sm.Full(caps.GetCapabilitiesHex(firstChange))),
// inheritable
ec.NewKprobeArgumentChecker().WithCapInheritableArg(sm.Full(fmt.Sprintf("%016x", 0))),
// permitted
ec.NewKprobeArgumentChecker().WithCapPermittedArg(sm.Full(caps.GetCapabilitiesHex(fullSet))),
))

t.Logf("Test %s Matching cap_permitted:%s - cap_inheritable:%s - cap_effective:%s",
t.Name(), caps.GetCapabilitiesHex(fullSet), fmt.Sprintf("%016x", 0), caps.GetCapabilitiesHex(secondChange))
kpCheckers2 := ec.NewProcessKprobeChecker("").
WithMessage(sm.Full("Process changed its capabilities with capset system call")).
WithFunctionName(sm.Full("security_capset")).
WithArgs(ec.NewKprobeArgumentListMatcher().
WithValues(
// effective caps
ec.NewKprobeArgumentChecker().WithCapEffectiveArg(sm.Full(caps.GetCapabilitiesHex(secondChange))),
// inheritable
ec.NewKprobeArgumentChecker().WithCapInheritableArg(sm.Full(fmt.Sprintf("%016x", 0))),
// permitted
ec.NewKprobeArgumentChecker().WithCapPermittedArg(sm.Full(caps.GetCapabilitiesHex(fullSet))),
))

testCmd := exec.CommandContext(ctx, testSetCaps)
if err := testCmd.Start(); err != nil {
t.Fatal(err)
}
if err := testCmd.Wait(); err != nil {
t.Fatalf("command failed with %s. Context error: %v", err, ctx.Err())
}

checker := ec.NewUnorderedEventChecker(kpCheckers1, kpCheckers2)
err = jsonchecker.JsonTestCheck(t, checker)
assert.NoError(t, err)
}

0 comments on commit 9bfc12c

Please sign in to comment.