From 3c8251dcfcc45f2c0aff419c45ee228093015465 Mon Sep 17 00:00:00 2001 From: Rafael David Tinoco Date: Mon, 23 Oct 2023 01:01:56 -0300 Subject: [PATCH] tests(event): add hooked_syscall instrumentation test --- .github/workflows/pr.yaml | 1 + .../e2e-inst-signatures/e2e-hooked_syscall.go | 74 +++++++++++++++++ tests/e2e-inst-signatures/export.go | 1 + .../scripts/hijack/.gitignore | 6 ++ .../scripts/hijack/Makefile | 13 +++ .../scripts/hijack/hijack.c | 56 +++++++++++++ .../scripts/hijack/load.sh | 15 ++++ .../scripts/hijack/unload.sh | 8 ++ .../scripts/hooked_syscall.sh | 15 ++++ tests/e2e-inst-test.sh | 80 +++++++++++++------ 10 files changed, 245 insertions(+), 24 deletions(-) create mode 100644 tests/e2e-inst-signatures/e2e-hooked_syscall.go create mode 100644 tests/e2e-inst-signatures/scripts/hijack/.gitignore create mode 100644 tests/e2e-inst-signatures/scripts/hijack/Makefile create mode 100644 tests/e2e-inst-signatures/scripts/hijack/hijack.c create mode 100755 tests/e2e-inst-signatures/scripts/hijack/load.sh create mode 100755 tests/e2e-inst-signatures/scripts/hijack/unload.sh create mode 100755 tests/e2e-inst-signatures/scripts/hooked_syscall.sh diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 4149f24ad6a0..7d58ef0f1c6d 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -59,6 +59,7 @@ env: DNS HTTP INSTTESTS: > + HOOKED_SYSCALL VFS_WRITE FILE_MODIFICATION SECURITY_INODE_RENAME diff --git a/tests/e2e-inst-signatures/e2e-hooked_syscall.go b/tests/e2e-inst-signatures/e2e-hooked_syscall.go new file mode 100644 index 000000000000..2ab9392dba04 --- /dev/null +++ b/tests/e2e-inst-signatures/e2e-hooked_syscall.go @@ -0,0 +1,74 @@ +package main + +import ( + "fmt" + + "github.com/aquasecurity/tracee/signatures/helpers" + "github.com/aquasecurity/tracee/types/detect" + "github.com/aquasecurity/tracee/types/protocol" + "github.com/aquasecurity/tracee/types/trace" +) + +type e2eHookedSyscall struct { + cb detect.SignatureHandler +} + +func (sig *e2eHookedSyscall) Init(ctx detect.SignatureContext) error { + sig.cb = ctx.Callback + return nil +} + +func (sig *e2eHookedSyscall) GetMetadata() (detect.SignatureMetadata, error) { + return detect.SignatureMetadata{ + ID: "HOOKED_SYSCALL", + EventName: "HOOKED_SYSCALL", + Version: "0.1.0", + Name: "Hooked Syscall Test", + Description: "Instrumentation events E2E Tests: Hooked Syscall", + Tags: []string{"e2e", "instrumentation"}, + }, nil +} + +func (sig *e2eHookedSyscall) GetSelectedEvents() ([]detect.SignatureEventSelector, error) { + return []detect.SignatureEventSelector{ + {Source: "tracee", Name: "hooked_syscall"}, + }, nil +} + +func (sig *e2eHookedSyscall) OnEvent(event protocol.Event) error { + eventObj, ok := event.Payload.(trace.Event) + if !ok { + return fmt.Errorf("failed to cast event's payload") + } + + switch eventObj.EventName { + case "hooked_syscall": + syscall, err := helpers.GetTraceeStringArgumentByName(eventObj, "syscall") + if err != nil { + return err + } + owner, err := helpers.GetTraceeStringArgumentByName(eventObj, "owner") + if err != nil { + return err + } + + if syscall == "uname" && owner == "hijack" { + m, _ := sig.GetMetadata() + sig.cb( + detect.Finding{ + SigMetadata: m, + Event: event, + Data: map[string]interface{}{}, + }, + ) + } + } + + return nil +} + +func (sig *e2eHookedSyscall) OnSignal(s detect.Signal) error { + return nil +} + +func (sig *e2eHookedSyscall) Close() {} diff --git a/tests/e2e-inst-signatures/export.go b/tests/e2e-inst-signatures/export.go index 406634198b35..58646040eaed 100644 --- a/tests/e2e-inst-signatures/export.go +++ b/tests/e2e-inst-signatures/export.go @@ -10,4 +10,5 @@ var ExportedSignatures = []detect.Signature{ &e2eContainersDataSource{}, &e2eBpfAttach{}, &e2eProcessTreeDataSource{}, + &e2eHookedSyscall{}, } diff --git a/tests/e2e-inst-signatures/scripts/hijack/.gitignore b/tests/e2e-inst-signatures/scripts/hijack/.gitignore new file mode 100644 index 000000000000..9c648647971b --- /dev/null +++ b/tests/e2e-inst-signatures/scripts/hijack/.gitignore @@ -0,0 +1,6 @@ +* +!.gitignore +!Makefile +!hijack.c +!load.sh +!unload.sh diff --git a/tests/e2e-inst-signatures/scripts/hijack/Makefile b/tests/e2e-inst-signatures/scripts/hijack/Makefile new file mode 100644 index 000000000000..cb18fe93d67a --- /dev/null +++ b/tests/e2e-inst-signatures/scripts/hijack/Makefile @@ -0,0 +1,13 @@ +obj-m += hijack.o + +PWD := $(shell pwd) + +KBUILD_CFLAGS += -g -Wall +KERNELDIR ?= /lib/modules/$(shell uname -r)/build + +hijack.o: + $(MAKE) -C $(KERNELDIR) M=$(PWD) modules + +clean: + rm -f hijack.mod hijack.o hijack.mod.c hijack.mod.o hijack.ko + rm -f modules.order Module.symvers diff --git a/tests/e2e-inst-signatures/scripts/hijack/hijack.c b/tests/e2e-inst-signatures/scripts/hijack/hijack.c new file mode 100644 index 000000000000..15827e095e0c --- /dev/null +++ b/tests/e2e-inst-signatures/scripts/hijack/hijack.c @@ -0,0 +1,56 @@ +#include +#include +#include +#include + +// Example of a kernel module hijacking a system call. + +MODULE_LICENSE("GPL"); + +ulong table; +module_param(table, ulong, 0); + +asmlinkage u64 (*orig_uname)(struct old_utsname *); + +asmlinkage u64 hooked_uname(struct old_utsname *name) +{ + printk(KERN_INFO "uname() intercepted!\n"); + return orig_uname(name); +} + +#define RO 0 +#define RW 1 + +static int set_page(u64 addr, int flag) +{ + u32 level; + pte_t *pte = lookup_address(addr, &level); + + if (pte && pte_present(*pte)) + pte->pte = flag ? pte->pte | _PAGE_RW : pte->pte & ~_PAGE_RW; + + return 0; +} + +static int __init hijack_init(void) +{ + if (!table) + return -EINVAL; + + set_page(table, RW); + orig_uname = (void *) ((u64 **) table)[__NR_uname]; + ((u64 **) table)[__NR_uname] = (u64 *) hooked_uname; + set_page(table, RO); + + return 0; +} + +static void __exit hijack_exit(void) +{ + set_page(table, RW); + ((u64 **) table)[__NR_uname] = (u64 *) orig_uname; + set_page(table, RO); +} + +module_init(hijack_init); +module_exit(hijack_exit); diff --git a/tests/e2e-inst-signatures/scripts/hijack/load.sh b/tests/e2e-inst-signatures/scripts/hijack/load.sh new file mode 100755 index 000000000000..ba6f9956f382 --- /dev/null +++ b/tests/e2e-inst-signatures/scripts/hijack/load.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +if [[ $UID -ne 0 ]]; then + echo must be root + exit 1 +fi + +sudo lsmod | grep -q hijack && { + echo module already loaded + exit 0 +} + +address=$(cat /proc/kallsyms | grep -E " sys_call_table$" | cut -d' ' -f1) +arg="./hijack.ko table=0x$address" +modprobe $arg || insmod $arg diff --git a/tests/e2e-inst-signatures/scripts/hijack/unload.sh b/tests/e2e-inst-signatures/scripts/hijack/unload.sh new file mode 100755 index 000000000000..5d5767438fd5 --- /dev/null +++ b/tests/e2e-inst-signatures/scripts/hijack/unload.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +if [[ $UID -ne 0 ]]; then + echo must be root + exit 1 +fi + +rmmod hijack diff --git a/tests/e2e-inst-signatures/scripts/hooked_syscall.sh b/tests/e2e-inst-signatures/scripts/hooked_syscall.sh new file mode 100755 index 000000000000..76e67fbdbfc9 --- /dev/null +++ b/tests/e2e-inst-signatures/scripts/hooked_syscall.sh @@ -0,0 +1,15 @@ +#!/usr/bin/bash -e + +exit_err() { + echo -n "ERROR: " + echo "$@" + exit 1 +} + +# Build and load module +dir="tests/e2e-inst-signatures/scripts/hijack" +cd $dir || exit_err "could not cd to $dir" +make && ./load.sh || exit_err "could not load module" + +# Unload module after 30 seconds +nohup sleep 30 > /dev/null 2>&1 && ./unload.sh & diff --git a/tests/e2e-inst-test.sh b/tests/e2e-inst-test.sh index 081fc047018d..786cf9028276 100755 --- a/tests/e2e-inst-test.sh +++ b/tests/e2e-inst-test.sh @@ -4,12 +4,17 @@ # This test is executed by github workflows inside the action runners # +ARCH=$(uname -m) + TRACEE_STARTUP_TIMEOUT=30 TRACEE_SHUTDOWN_TIMEOUT=30 TRACEE_RUN_TIMEOUT=60 SCRIPT_TMP_DIR=/tmp TRACEE_TMP_DIR=/tmp/tracee +# Default test to run if no other is given +TESTS=${INSTTESTS:=VFS_WRITE} + info_exit() { echo -n "INFO: " echo "$@" @@ -35,6 +40,8 @@ if [[ ! -d ./signatures ]]; then error_exit "need to be in tracee root directory" fi +rm -rf ${TRACEE_TMP_DIR:?}/* || error_exit "could not delete $TRACEE_TMP_DIR" + KERNEL=$(uname -r) KERNEL_MAJ=$(echo "$KERNEL" | cut -d'.' -f1) @@ -44,19 +51,15 @@ fi SCRIPT_PATH="$(readlink -f "$0")" SCRIPT_DIR="$(dirname "$SCRIPT_PATH")" +TESTS_DIR="$SCRIPT_DIR/e2e-inst-signatures/scripts" SIG_DIR="$SCRIPT_DIR/../dist/e2e-inst-signatures" -# run CO-RE VFS_WRITE test only by default -TESTS=${INSTTESTS:=VFS_WRITE} - -# startup needs -rm -rf ${TRACEE_TMP_DIR:?}/* || error_exit "could not delete $TRACEE_TMP_DIR" git config --global --add safe.directory "*" info info "= ENVIRONMENT =================================================" info -info "KERNEL: $(uname -r)" +info "KERNEL: ${KERNEL}" info "CLANG: $(clang --version)" info "GO: $(go version)" info @@ -67,20 +70,41 @@ set -e make -j"$(nproc)" all make e2e-inst-signatures set +e + +# Check if tracee was built correctly + if [[ ! -x ./dist/tracee ]]; then error_exit "could not find tracee executable" fi -# if any test has failed anyerror="" -# run tests +# Run tests, one by one + for TEST in $TESTS; do info info "= TEST: $TEST ==============================================" info + # Some tests might need special setup (like running before tracee) + + case $TEST in + HOOKED_SYSCALL) + if [[ $ARCH == "aarch64" ]]; then + info "skip hooked_syscall test in aarch64" + continue + fi + if [[ ! -d /lib/modules/${KERNEL}/build ]]; then + info "skip hooked_syscall test, no kernel headers" + continue + fi + "${TESTS_DIR}"/hooked_syscall.sh + ;; + esac + + # Run tracee + rm -f $SCRIPT_TMP_DIR/build-$$ rm -f $SCRIPT_TMP_DIR/tracee-log-$$ @@ -97,7 +121,8 @@ for TEST in $TESTS; do --scope comm=echo,mv,ls,tracee,proctreetester \ --events "$TEST" & - # wait tracee-ebpf to be started (30 sec most) + # Wait tracee to start + times=0 timedout=0 while true; do @@ -116,7 +141,8 @@ for TEST in $TESTS; do fi done - # tracee-ebpf could not start for some reason, check stderr + # Tracee failed to start + if [[ $timedout -eq 1 ]]; then info info "$TEST: FAILED. ERRORS:" @@ -127,20 +153,30 @@ for TEST in $TESTS; do continue fi - # give some time for tracee to settle + # Allow tracee to start processing events + sleep 3 - # run test scripts - timeout --preserve-status $TRACEE_RUN_TIMEOUT \ - ./tests/e2e-inst-signatures/scripts/"${TEST,,}".sh + # Run tests + + case $TEST in + HOOKED_SYSCALL) + ;; + *) + timeout --preserve-status $TRACEE_RUN_TIMEOUT "${TESTS_DIR}"/"${TEST,,}".sh + ;; + esac + + # So events can finish processing - # so event can be processed and detected sleep 3 - ## cleanup at EXIT + # The cleanup happens at EXIT logfile=$SCRIPT_TMP_DIR/tracee-log-$$ + # Check if the test has failed or not + found=0 cat $SCRIPT_TMP_DIR/build-$$ | jq .eventName | grep -q "$TEST" && found=1 errors=$(cat $logfile | wc -l 2>/dev/null) @@ -165,24 +201,20 @@ for TEST in $TESTS; do rm -f $SCRIPT_TMP_DIR/build-$$ rm -f $SCRIPT_TMP_DIR/tracee-log-$$ - # make sure we exit to start it again + # Make sure we exit tracee to start it again pid_tracee=$(pidof tracee | cut -d' ' -f1) - kill -2 "$pid_tracee" - sleep $TRACEE_SHUTDOWN_TIMEOUT - - # make sure tracee is exited with SIGKILL kill -9 "$pid_tracee" >/dev/null 2>&1 - - # give a little break for OS noise to reduce sleep 3 - # cleanup leftovers + # Cleanup leftovers rm -rf $TRACEE_TMP_DIR done +# Print summary and exit with error if any test failed + info if [[ $anyerror != "" ]]; then info "ALL TESTS: FAILED: ${anyerror::-1}"