diff --git a/contrib/tester-progs/.gitignore b/contrib/tester-progs/.gitignore index 66d1b48ce3a..551ca9dbf7c 100644 --- a/contrib/tester-progs/.gitignore +++ b/contrib/tester-progs/.gitignore @@ -20,3 +20,4 @@ killer-tester killer-tester-32 /getcpu drop-privileges +change-capabilities diff --git a/contrib/tester-progs/Makefile b/contrib/tester-progs/Makefile index 0103194e461..4b5ecbe33b9 100644 --- a/contrib/tester-progs/Makefile +++ b/contrib/tester-progs/Makefile @@ -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. @@ -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 diff --git a/contrib/tester-progs/change-capabilities.c b/contrib/tester-progs/change-capabilities.c new file mode 100644 index 00000000000..b5c0ed8966b --- /dev/null +++ b/contrib/tester-progs/change-capabilities.c @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +// Copyright Authors of Tetragon + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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; +} diff --git a/pkg/reader/caps/caps.go b/pkg/reader/caps/caps.go index 35330408d20..45c42a4b6a0 100644 --- a/pkg/reader/caps/caps.go +++ b/pkg/reader/caps/caps.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strconv" "strings" + "sync" "github.com/cilium/tetragon/api/v1/tetragon" "github.com/cilium/tetragon/pkg/api/processapi" @@ -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 @@ -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) diff --git a/pkg/reader/caps/caps_test.go b/pkg/reader/caps/caps_test.go index 1535d8ef1e6..4570d37e584 100644 --- a/pkg/reader/caps/caps_test.go +++ b/pkg/reader/caps/caps_test.go @@ -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)) +} diff --git a/pkg/sensors/tracing/kprobe_test.go b/pkg/sensors/tracing/kprobe_test.go index f3b6182d09c..f49a8399316 100644 --- a/pkg/sensors/tracing/kprobe_test.go +++ b/pkg/sensors/tracing/kprobe_test.go @@ -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) +}