diff --git a/ci/alpine.sh b/ci/alpine.sh index 6b17942..0ab648e 100755 --- a/ci/alpine.sh +++ b/ci/alpine.sh @@ -45,7 +45,8 @@ apk add \ util-linux \ wget \ which \ - xxd + xxd \ + gawk if [ ! "$TSS" ]; then apk add git diff --git a/ci/debian.sh b/ci/debian.sh index 431203a..7676191 100755 --- a/ci/debian.sh +++ b/ci/debian.sh @@ -53,7 +53,8 @@ $apt \ sudo \ util-linux \ wget \ - xsltproc + xsltproc \ + gawk $apt xxd || $apt vim-common $apt libengine-gost-openssl1.1$ARCH || true diff --git a/ci/tumbleweed.sh b/ci/tumbleweed.sh index 6f70b0f..c4bd75e 100755 --- a/ci/tumbleweed.sh +++ b/ci/tumbleweed.sh @@ -42,7 +42,8 @@ zypper --non-interactive install --force-resolution --no-recommends \ vim \ wget \ which \ - xsltproc + xsltproc \ + gawk zypper --non-interactive install --force-resolution --no-recommends \ gnutls openssl-engine-libp11 softhsm || true diff --git a/tests/Makefile.am b/tests/Makefile.am index a0463b7..03aa5b7 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -2,7 +2,10 @@ check_SCRIPTS = TESTS = $(check_SCRIPTS) check_SCRIPTS += ima_hash.test sign_verify.test boot_aggregate.test \ - fsverity.test portable_signatures.test + fsverity.test portable_signatures.test ima_policy_check.test \ + mmap_check.test + +check_PROGRAMS := test_mmap .PHONY: check_logs check_logs: diff --git a/tests/functions.sh b/tests/functions.sh index 2c4d205..ed06040 100755 --- a/tests/functions.sh +++ b/tests/functions.sh @@ -100,6 +100,25 @@ expect_pass() { return $ret } +expect_pass_if() { + local indexes="$1" + local ret idx + + shift + + expect_pass "$@" + ret=$? + + if [ $ret -ne 0 ] && [ $ret -ne 77 ] && [ -n "$PATCHES" ]; then + echo $YELLOW"Possibly missing patches:"$NORM + for idx in $indexes; do + echo $YELLOW" - ${PATCHES[$((idx))]}"$NORM + done + fi + + return $ret +} + # Eval negative test (one that should fail) and account its result expect_fail() { local ret @@ -137,6 +156,25 @@ expect_fail() { return $ret } +expect_fail_if() { + local indexes="$1" + local ret idx + + shift + + expect_fail "$@" + ret=$? + + if { [ $ret -eq 0 ] || [ $ret -eq 99 ]; } && [ -n "$PATCHES" ]; then + echo $YELLOW"Possibly missing patches:"$NORM + for idx in $indexes; do + echo $YELLOW" - ${PATCHES[$((idx))]}"$NORM + done + fi + + return $ret +} + # return true if current test is positive _test_expected_to_pass() { [ ! $TFAIL ] diff --git a/tests/ima_policy_check.awk b/tests/ima_policy_check.awk new file mode 100755 index 0000000..8d07245 --- /dev/null +++ b/tests/ima_policy_check.awk @@ -0,0 +1,211 @@ +#! /usr/bin/gawk -f +# SPDX-License-Identifier: GPL-2.0 +# +# Copyright (C) 2023 Roberto Sassu +# +# Check a new rule against the loaded IMA policy. +# +# Documentation/ABI/testing/ima_policy (Linux kernel) +# base: [[func=] [mask=] [fsmagic=] [fsuuid=] [fsname=] +# [uid=] [euid=] [gid=] [egid=] +# [fowner=] [fgroup=]] +# lsm: [[subj_user=] [subj_role=] [subj_type=] +# [obj_user=] [obj_role=] [obj_type=]] +# option: [digest_type=] [template=] [permit_directio] +# [appraise_type=] [appraise_flag=] +# [appraise_algos=] [keyrings=] +# +# Rules don't overlap if their actions are unrelated (cannot be matched without +# dont_) and there is no combination of appraise with another do action (e.g. +# measure, audit, hash). The second condition is due to the fact that appraise +# might still forbid other actions expected to be performed by a test that did +# not setup appraisal. Checking appraise for new rules is not sufficient, +# because that rule could be added anyway. By checking existing rules as well, +# a warning will be displayed when tests inserting rules with other do actions +# are reexecuted. +# +# Also, rules don't overlap if both include the same policy keyword(s) (in base +# or lsm, except func), at least one, with a different value. Different func +# values don't imply non-overlap, due to the fact that a test command might +# cause the execution of multiple hooks (e.g. FILE_CHECK in addition to +# MMAP_CHECK). Despite one test is willing to test a particular hook, this could +# have side effects on other tests (e.g. one test sets: appraise func=MMAP_CHECK +# and another: measure func=FILE_CHECK; the second test might see an unexpected +# measurement due to the first test being executed; or the second test cannot +# unexpectedly do mmap). +# +# Currently, the < > operators are not supported and overlapping is asserted +# even if intervals are disjoint. If supported, non-overlapping conditions could +# be found. With the ^ modifier, no disjoint intervals can be found. Overlapping +# is always reported. +# +# Rule equivalence is determined by checking each key/value pair, regardless of +# their order. However, the action must always be at the beginning of the rules. +# Rules with aliases are considered equivalent to those with their source (e.g. +# rules with PATH_CHECK and FILE_MMAP are considered as equivalent to rules with +# FILE_CHECK and MMAP_CHECK). +# +# Return a bit mask with the following values: +# - 1: invalid new rule; +# - 2: overlap of the new rule with an existing rule in the IMA policy; +# - 4: new rule exists in the IMA policy. + +BEGIN { + # Policy definitions. + actions_str="measure dont_measure appraise dont_appraise audit hash dont_hash" + split(actions_str, actions_array); + keywords_str="func mask fsmagic fsuuid fsname uid euid gid egid fowner fgroup subj_user subj_role subj_type obj_user obj_role obj_type"; + split(keywords_str, keywords_array); + options_str="digest_type template permit_directio appraise_type appraise_flag appraise_algos keyrings"; + split(options_str, options_array); + + # Key types. + key_type_unknown=0; + key_type_action=1; + key_type_keyword=2; + key_type_option=3; + + # Result values. + ret_invalid_rule=1; + ret_rule_overlap=2; + ret_same_rule_exists=4; + + for (action_idx in actions_array) + key_types[actions_array[action_idx]]=key_type_action; + for (keyword_idx in keywords_array) + key_types[keywords_array[keyword_idx]]=key_type_keyword; + for (option_idx in options_array) + key_types[options_array[option_idx]]=key_type_option; + + new_rule=1; + result=0; +} +{ + # Delete arrays from previous rule. + if (!new_rule) { + delete current_rule_array; + delete current_rule_operator_array; + } + + # Check empty rules. + if (!length($0)) { + if (new_rule) { + result=or(result, ret_invalid_rule); + exit; + } + next; + } + + for (i=1; i<=NF; i++) { + # Parse key/value pair. + split($i, key_value_array, /[=,>,<]/, separator_array); + key=key_value_array[1]; + value=key_value_array[2]; + + if (key == "func") { + # Normalize values of IMA hooks to what IMA will print. + if (value == "FILE_MMAP") + value="MMAP_CHECK"; + else if (value == "PATH_CHECK") + value="FILE_CHECK"; + } + + # Basic validity check (not necessary in general for the IMA policy, but useful to find typos in the tests). + if (key_types[key] == key_type_unknown || + (i == 1 && key_types[key] != key_type_action)) { + result=or(result, ret_invalid_rule); + exit; + } + + # Store key/value pair and operator into an array. + if (new_rule) { + new_rule_array[key]=value; + new_rule_operator_array[key]=separator_array[1]; + } else { + current_rule_array[key]=value; + current_rule_operator_array[key]=separator_array[1]; + } + + # Store original action and action without dont_. + if (i == 1) { + if (new_rule) { + new_rule_action=key; + new_rule_action_sub=key; + gsub(/dont_/, "", new_rule_action_sub); + } else { + current_rule_action=key; + current_rule_action_sub=key; + gsub(/dont_/, "", current_rule_action_sub); + } + } + } + + # Go to the next line, to compare the new rule with rules in the IMA policy. + if (new_rule) { + new_rule=0; + next; + } + + # No overlap by action (unrelated rules and no combination appraise - ), new rule safe to add to the IMA policy. + if (current_rule_action_sub != new_rule_action_sub && + (current_rule_action != "appraise" || new_rule_action ~ /^dont_/) && + (new_rule_action != "appraise" || current_rule_action ~ /^dont_/)) + next; + + same_rule=1; + overlap_rule=1; + + for (key in key_types) { + if (!(key in new_rule_array)) { + # Key in current rule but not in new rule. + if (key in current_rule_array) + same_rule=0; + # Key not in new rule and not in current rule. + continue; + } + + if (!(key in current_rule_array)) { + # Key in new rule but not in current rule. + if (key in new_rule_array) + same_rule=0; + # Key not in current rule and not in new rule. + continue; + } + + # Same value and operator. + if (new_rule_array[key] == current_rule_array[key] && + new_rule_operator_array[key] == current_rule_operator_array[key]) + continue; + + # Different value and/or operator. + same_rule=0; + + # Not a policy keyword, not useful to determine overlap. + if (key_types[key] != key_type_keyword) + continue; + + # > < operators are not supported, cannot determine overlap. + if (new_rule_operator_array[key] != "=" || current_rule_operator_array[key] != "=") + continue; + + # ^ modifier does not make disjoint sets, cannot determine overlap. + if (new_rule_array[key] ~ /^\^/ || current_rule_array[key] ~ /^\^/) + continue; + + # One test command can invoke multiple hooks, cannot determine overlap from func. + if (key == "func") + continue; + + # No overlap by policy keyword, new rule safe to add to the IMA policy. + overlap_rule=0; + next; + } + + if (same_rule) + result=or(result, ret_same_rule_exists); + else if (overlap_rule) + result=or(result, ret_rule_overlap); +} +END { + exit result; +} diff --git a/tests/ima_policy_check.test b/tests/ima_policy_check.test new file mode 100755 index 0000000..179b52e --- /dev/null +++ b/tests/ima_policy_check.test @@ -0,0 +1,245 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Copyright (C) 2023 Roberto Sassu +# +# Test for ima_policy_check.awk + +trap '_report_exit_and_cleanup' SIGINT SIGTERM EXIT + +cd "$(dirname "$0")" || exit 1 +. ./functions.sh + +export PATH=$PWD:$PATH + +check_result() { + local result + + echo -e "\nTest: $1" + echo "New rule: $2" + echo "IMA policy: $3" + + echo -n "Result (expect $4): " + + echo -e "$2\n$3" | ima_policy_check.awk + result=$? + + if [ "$result" -ne "$4" ]; then + echo "${RED}$result${NORM}" + return "$FAIL" + fi + + echo "${GREEN}$result${NORM}" + return "$OK" +} + +# ima_policy_check.awk returns a bit mask with the following values: +# - 1: invalid new rule; +# - 2: overlap of the new rule with an existing rule in the IMA policy; +# - 4: new rule exists in the IMA policy. + +# Basic checks. +desc="empty IMA policy" +rule="measure func=FILE_CHECK" +ima_policy="" +expect_pass check_result "$desc" "$rule" "$ima_policy" 0 + +desc="Empty new rule" +rule="" +ima_policy="" +expect_pass check_result "$desc" "$rule" "$ima_policy" 1 + +desc="Unknown policy keyword fun" +rule="measure fun=FILE_CHECK" +ima_policy="" +expect_pass check_result "$desc" "$rule" "$ima_policy" 1 + +desc="Missing action" +rule="func=FILE_CHECK" +ima_policy="" +expect_pass check_result "$desc" "$rule" "$ima_policy" 1 + +# Non-overlapping rules. +desc="Non-overlapping by action measure/dont_appraise, same func" +rule="measure func=FILE_CHECK" +ima_policy="dont_appraise func=FILE_CHECK" +expect_pass check_result "$desc" "$rule" "$ima_policy" 0 + +desc="Non-overlapping by action audit/dont_appraise, same func" +rule="audit func=FILE_CHECK" +ima_policy="dont_appraise func=FILE_CHECK" +expect_pass check_result "$desc" "$rule" "$ima_policy" 0 + +desc="Non-overlapping by action appraise/dont_measure, same func" +rule="appraise func=FILE_CHECK" +ima_policy="dont_measure func=FILE_CHECK" +expect_pass check_result "$desc" "$rule" "$ima_policy" 0 + +desc="Non-overlapping by action dont_measure/hash, same func" +rule="dont_measure func=FILE_CHECK" +ima_policy="hash func=FILE_CHECK" +expect_pass check_result "$desc" "$rule" "$ima_policy" 0 + +desc="Non-overlapping by uid, func is equal" +rule="measure func=FILE_CHECK uid=0" +ima_policy="measure uid=1 func=FILE_CHECK" +expect_pass check_result "$desc" "$rule" "$ima_policy" 0 + +desc="Non-overlapping by uid, func is equal, same policy options" +rule="measure func=FILE_CHECK uid=0 permit_directio" +ima_policy="measure uid=1 func=FILE_CHECK permit_directio" +expect_pass check_result "$desc" "$rule" "$ima_policy" 0 + +desc="Non-overlapping by mask, func and uid are equal, same policy options" +rule="measure func=FILE_CHECK uid=0 permit_directio mask=MAY_READ" +ima_policy="measure uid=0 mask=MAY_EXEC func=FILE_CHECK permit_directio" +expect_pass check_result "$desc" "$rule" "$ima_policy" 0 + +desc="Non-overlapping by mask, func and uid are equal, different policy options" +rule="measure func=FILE_CHECK uid=0 permit_directio mask=MAY_READ" +ima_policy="measure uid=0 mask=MAY_EXEC func=FILE_CHECK" +expect_pass check_result "$desc" "$rule" "$ima_policy" 0 + +# Overlapping and different rules. +desc="same actions, different keywords" +rule="appraise func=FILE_CHECK" +ima_policy="appraise uid=0" +expect_pass check_result "$desc" "$rule" "$ima_policy" 2 + +desc="unrelated actions with appraise and a do action, same func" +rule="appraise func=FILE_CHECK" +ima_policy="measure func=FILE_CHECK" +expect_pass check_result "$desc" "$rule" "$ima_policy" 2 + +desc="unrelated actions with appraise and a do action, different func" +rule="appraise func=FILE_CHECK" +ima_policy="measure func=MMAP_CHECK" +expect_pass check_result "$desc" "$rule" "$ima_policy" 2 + +desc="related actions, same func" +rule="measure func=FILE_CHECK" +ima_policy="dont_measure func=FILE_CHECK" +expect_pass check_result "$desc" "$rule" "$ima_policy" 2 + +desc="related actions, same func, different policy options" +rule="measure func=FILE_CHECK" +ima_policy="dont_measure func=FILE_CHECK permit_directio" +expect_pass check_result "$desc" "$rule" "$ima_policy" 2 + +desc="related actions, same func, different policy options" +rule="measure func=FILE_CHECK permit_directio" +ima_policy="dont_measure func=FILE_CHECK" +expect_pass check_result "$desc" "$rule" "$ima_policy" 2 + +desc="same actions, same func, same mask with different modifier (no disjoint sets with the ^ modifier)" +rule="measure func=FILE_CHECK mask=MAY_EXEC" +ima_policy="measure func=FILE_CHECK mask=^MAY_EXEC" +expect_pass check_result "$desc" "$rule" "$ima_policy" 2 + +desc="same actions, same func, different mask with same modifier (no disjoint sets with the ^ modifier)" +rule="measure func=FILE_CHECK mask=^MAY_READ" +ima_policy="measure func=FILE_CHECK mask=^MAY_EXEC" +expect_pass check_result "$desc" "$rule" "$ima_policy" 2 + +desc="same actions, same func, different policy options" +rule="measure func=FILE_CHECK" +ima_policy="measure func=FILE_CHECK permit_directio" +expect_pass check_result "$desc" "$rule" "$ima_policy" 2 + +desc="same actions, same func, different policy options" +rule="measure func=FILE_CHECK permit_directio" +ima_policy="measure func=FILE_CHECK" +expect_pass check_result "$desc" "$rule" "$ima_policy" 2 + +desc="same actions, MMAP_CHECK and MMAP_CHECK_REQPROT hooks" +rule="measure func=MMAP_CHECK" +ima_policy="measure func=MMAP_CHECK_REQPROT" +expect_pass check_result "$desc" "$rule" "$ima_policy" 2 + +desc="related actions, same func, same mask with same modifier" +rule="measure func=FILE_CHECK mask=^MAY_EXEC" +ima_policy="dont_measure func=FILE_CHECK mask=^MAY_EXEC" +expect_pass check_result "$desc" "$rule" "$ima_policy" 2 + +desc="same actions, same func, different uid with same operator (overlap because operators are not supported)" +rule="measure func=FILE_CHECK uid>0" +ima_policy="measure func=FILE_CHECK uid>1" +expect_pass check_result "$desc" "$rule" "$ima_policy" 2 + +desc="same actions, same func, same uid with different operator (overlap because operators are not supported)" +rule="measure func=FILE_CHECK uid>1" +ima_policy="measure func=FILE_CHECK uid<1" +expect_pass check_result "$desc" "$rule" "$ima_policy" 2 + +# Overlapping and same rules. +desc="same actions, same func" +rule="appraise func=FILE_CHECK" +ima_policy="appraise func=FILE_CHECK" +expect_pass check_result "$desc" "$rule" "$ima_policy" 4 + +desc="same actions, same func, same mask" +rule="appraise mask=MAY_READ func=FILE_CHECK" +ima_policy="appraise func=FILE_CHECK mask=MAY_READ" +expect_pass check_result "$desc" "$rule" "$ima_policy" 4 + +desc="same actions, same func, same mask, same policy options" +rule="appraise mask=MAY_READ func=FILE_CHECK permit_directio appraise_type=imasig" +ima_policy="appraise func=FILE_CHECK mask=MAY_READ permit_directio appraise_type=imasig" +expect_pass check_result "$desc" "$rule" "$ima_policy" 4 + +desc="same actions, same func" +rule="measure func=MMAP_CHECK_REQPROT" +ima_policy="measure func=MMAP_CHECK_REQPROT" +expect_pass check_result "$desc" "$rule" "$ima_policy" 4 + +desc="same actions, same func with alias (PATH_CHECK = FILE_CHECK)" +rule="measure func=FILE_CHECK" +ima_policy="measure func=PATH_CHECK" +expect_pass check_result "$desc" "$rule" "$ima_policy" 4 + +desc="same actions, same func with alias (PATH_CHECK = FILE_CHECK), same mask with same modifiers" +rule="measure mask=^MAY_READ func=FILE_CHECK" +ima_policy="measure func=PATH_CHECK mask=^MAY_READ" +expect_pass check_result "$desc" "$rule" "$ima_policy" 4 + +desc="same actions, same func with alias (PATH_CHECK = FILE_CHECK) and same mask with same modifiers, same uid with same operators" +rule="measure mask=^MAY_READ uid>0 func=FILE_CHECK" +ima_policy="measure func=PATH_CHECK mask=^MAY_READ uid>0" +expect_pass check_result "$desc" "$rule" "$ima_policy" 4 + +desc="same actions, same func with alias (PATH_CHECK = FILE_CHECK) and same mask with same modifiers, same uid with same operators" +rule="measure mask=^MAY_READ uid<1 func=FILE_CHECK" +ima_policy="measure func=PATH_CHECK mask=^MAY_READ uid<1" +expect_pass check_result "$desc" "$rule" "$ima_policy" 4 + +# Overlapping and two rules (one same, one different). +desc="first: same actions, same func, second: unrelated actions with appraise and a do action" +rule="appraise func=FILE_CHECK" +ima_policy="appraise func=FILE_CHECK\nmeasure func=FILE_CHECK" +expect_pass check_result "$desc" "$rule" "$ima_policy" 6 + +desc="first: unrelated actions with appraise and a do action, same func, second: same actions" +rule="appraise func=FILE_CHECK" +ima_policy="measure func=FILE_CHECK\nappraise func=FILE_CHECK" +expect_pass check_result "$desc" "$rule" "$ima_policy" 6 + +desc="first: same actions, same func, same mask, second: different policy options" +rule="appraise mask=MAY_READ func=FILE_CHECK" +ima_policy="appraise func=FILE_CHECK mask=MAY_READ\nappraise func=FILE_CHECK mask=MAY_READ permit_directio" +expect_pass check_result "$desc" "$rule" "$ima_policy" 6 + +desc="first: same actions, same func with alias (PATH_CHECK = FILE_CHECK), same mask, second: different policy options" +rule="appraise mask=MAY_READ func=FILE_CHECK" +ima_policy="appraise func=PATH_CHECK mask=MAY_READ\nappraise func=FILE_CHECK mask=MAY_READ permit_directio" +expect_pass check_result "$desc" "$rule" "$ima_policy" 6 + +# Non-overlapping and three rules. +desc="same actions, same func and mask, different uid" +rule="appraise mask=MAY_READ func=FILE_CHECK uid=0" +ima_policy="appraise mask=MAY_READ func=FILE_CHECK uid=1\nappraise mask=MAY_READ func=FILE_CHECK uid=2\nappraise mask=MAY_READ func=FILE_CHECK uid=3" +expect_pass check_result "$desc" "$rule" "$ima_policy" 0 + +desc="same actions, same func and mask, different uid, except one that is the same" +rule="appraise mask=MAY_READ func=FILE_CHECK uid=0" +ima_policy="appraise mask=MAY_READ func=FILE_CHECK uid=1\nappraise mask=MAY_READ func=FILE_CHECK uid=0\nappraise mask=MAY_READ func=FILE_CHECK uid=3" +expect_pass check_result "$desc" "$rule" "$ima_policy" 4 diff --git a/tests/mmap_check.test b/tests/mmap_check.test new file mode 100755 index 0000000..2dd3433 --- /dev/null +++ b/tests/mmap_check.test @@ -0,0 +1,407 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Copyright (C) 2022-2023 Roberto Sassu +# +# Check the behavior of MMAP_CHECK and MMAP_CHECK_REQPROT + +trap '_report_exit_and_cleanup _cleanup_env cleanup' SIGINT SIGTERM SIGSEGV EXIT + +PATCHES=( +'ima: Align ima_file_mmap() parameters with mmap_file LSM hook' +'ima: Introduce MMAP_CHECK_REQPROT hook' +) + +RET_INVALID_RULE=$((0x0001)) +RET_RULE_OVERLAP=$((0x0002)) +RET_SAME_RULE_EXISTS=$((0x0004)) + +EVM_INIT_HMAC=$((0x0001)) +EVM_INIT_X509=$((0x0002)) + +# Base VERBOSE on the environment variable, if set. +VERBOSE="${VERBOSE:-0}" + +# Errors defined in test_mmap +ERR_SETUP=1 +ERR_TEST=2 + +cd "$(dirname "$0")" || exit 1 +export PATH=$PWD/../src:$PWD:$PATH +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH +. ./functions.sh +_require evmctl + +cleanup() { + if [ "$g_loop_mounted" = "1" ]; then + popd > /dev/null || exit "$FAIL" + umount "$g_mountpoint" + fi + + if [ -n "$g_dev" ]; then + losetup -d "$g_dev" + fi + + if [ -n "$g_image" ]; then + rm -f "$g_image" + fi + + if [ -n "$g_mountpoint" ]; then + rm -Rf "$g_mountpoint" + fi + + if [ -n "$g_key_path_der" ]; then + rm -f "$g_key_path_der" + fi +} + +# Use the fsuuid= IMA policy keyword to select only files created/used by the +# tests below. Also use fowner= to differentiate between files created/used by +# individual tests. +IMA_UUID="28b23254-9467-44c0-b6ba-34b12e85a26e" +MEASURE_MMAP_CHECK_FOWNER=2000 +MEASURE_MMAP_CHECK_REQPROT_FOWNER=2001 +MEASURE_MMAP_CHECK_RULE="measure func=MMAP_CHECK fsmagic=0xef53 fsuuid=$IMA_UUID fowner=$MEASURE_MMAP_CHECK_FOWNER" +MEASURE_MMAP_CHECK_REQPROT_RULE="measure func=MMAP_CHECK_REQPROT fsmagic=0xef53 fsuuid=$IMA_UUID fowner=$MEASURE_MMAP_CHECK_REQPROT_FOWNER" +APPRAISE_MMAP_CHECK_FOWNER=2002 +APPRAISE_MMAP_CHECK_REQPROT_FOWNER=2003 +APPRAISE_MMAP_CHECK_RULE="appraise func=MMAP_CHECK fsmagic=0xef53 fsuuid=$IMA_UUID fowner=$APPRAISE_MMAP_CHECK_FOWNER" +APPRAISE_MMAP_CHECK_REQPROT_RULE="appraise func=MMAP_CHECK_REQPROT fsmagic=0xef53 fsuuid=$IMA_UUID fowner=$APPRAISE_MMAP_CHECK_REQPROT_FOWNER" + +check_load_ima_rule() { + local result new_policy color + + echo -e "$1\n$(cat /sys/kernel/security/ima/policy)" | ima_policy_check.awk + result=$? + + if [ $((result & RET_INVALID_RULE)) -eq $RET_INVALID_RULE ]; then + echo "${RED}Invalid rule${NORM}" + return "$HARDFAIL" + fi + + if [ $((result & RET_RULE_OVERLAP)) -eq $RET_RULE_OVERLAP ]; then + color=${YELLOW} + if [ -n "$TST_ENV" ]; then + color=${RED} + fi + + echo "${color}Possible interference with existing IMA policy rule${NORM}" + if [ -n "$TST_ENV" ]; then + return "$HARDFAIL" + fi + fi + + if [ $((result & RET_SAME_RULE_EXISTS)) -eq $RET_SAME_RULE_EXISTS ]; then + return "$OK" + fi + + new_policy=$(mktemp -p "$g_mountpoint") + echo "$1" > "$new_policy" + echo "$new_policy" > /sys/kernel/security/ima/policy + result=$? + rm -f "$new_policy" + + if [ "$result" -ne 0 ]; then + echo "${RED}Failed to set IMA policy${NORM}" + return "$HARDFAIL" + fi + + return "$OK" +} + +check_mmap() { + local hook="$1" + local arg="$2" + local test_file fowner rule result test_file_entry + + echo -e "\nTest: ${FUNCNAME[0]} (hook=\"$hook\", test_mmap arg: \"$arg\")" + + if ! test_file=$(mktemp -p "$PWD"); then + echo "${RED}Cannot create $test_file${NORM}" + return "$HARDFAIL" + fi + + if ! echo "test" > "$test_file"; then + echo "${RED}Cannot write $test_file${NORM}" + return "$FAIL" + fi + + fowner="$MEASURE_MMAP_CHECK_FOWNER" + rule="$MEASURE_MMAP_CHECK_RULE" + + if [ "$hook" = "MMAP_CHECK_REQPROT" ]; then + fowner="$MEASURE_MMAP_CHECK_REQPROT_FOWNER" + rule="$MEASURE_MMAP_CHECK_REQPROT_RULE" + fi + + if ! chown "$fowner" "$test_file"; then + echo "${RED}Cannot change owner of $test_file${NORM}" + return "$HARDFAIL" + fi + + check_load_ima_rule "$rule" + result=$? + if [ $result -ne "$OK" ]; then + return $result + fi + + test_mmap "$test_file" "$arg" + result=$? + + if [ $result -ne 0 ] && [ $result -ne "$ERR_TEST" ]; then + echo "${RED}Unexpected exit status $result from test_mmap${NORM}" + return "$HARDFAIL" + fi + + if [ "$TFAIL" != "yes" ]; then + echo -n "Result (expect found): " + else + echo -n "Result (expect not found): " + fi + + test_file_entry=$(awk '$5 == "'"$test_file"'"' < /sys/kernel/security/ima/ascii_runtime_measurements) + if [ -z "$test_file_entry" ]; then + if [ "$TFAIL" != "yes" ]; then + echo "${RED}not found${NORM}" + else + echo "${GREEN}not found${NORM}" + fi + return "$FAIL" + fi + + if [ "$TFAIL" != "yes" ]; then + echo "${GREEN}found${NORM}" + else + echo "${RED}found${NORM}" + fi + + if [ "$VERBOSE" -gt 0 ]; then + echo "$test_file_entry" + fi + + return "$OK" +} + +check_deny() { + local hook="$1" + local arg="$2" + local test_file fowner rule result + + echo -e "\nTest: ${FUNCNAME[0]} (hook=\"$hook\", test_mmap arg: \"$arg\")" + + if ! test_file=$(mktemp -p "$PWD"); then + echo "${RED}Cannot create $test_file${NORM}" + return "$HARDFAIL" + fi + + if ! echo "test" > "$test_file"; then + echo "${RED}Cannot write $test_file${NORM}" + return "$FAIL" + fi + + if ! evmctl ima_sign -a sha256 --key "$g_key_path" "$test_file" &> /dev/null; then + echo "${RED}Cannot sign $test_file${NORM}" + return "$HARDFAIL" + fi + + fowner="$APPRAISE_MMAP_CHECK_FOWNER" + rule="$APPRAISE_MMAP_CHECK_RULE" + + if [ "$hook" = "MMAP_CHECK_REQPROT" ]; then + fowner="$APPRAISE_MMAP_CHECK_REQPROT_FOWNER" + rule="$APPRAISE_MMAP_CHECK_REQPROT_RULE" + fi + + if ! chown "$fowner" "$test_file"; then + echo "${RED}Cannot change owner of $test_file${NORM}" + return "$HARDFAIL" + fi + + check_load_ima_rule "$rule" + result=$? + if [ $result -ne "$OK" ]; then + return $result + fi + + test_mmap "$test_file" exec + result=$? + + if [ $result -ne 0 ] && [ $result -ne "$ERR_TEST" ]; then + echo "${RED}Unexpected exit status $result from test_mmap${NORM}" + return "$HARDFAIL" + fi + + test_mmap "$test_file" "$arg" + result=$? + + if [ $result -ne 0 ] && [ $result -ne "$ERR_TEST" ]; then + echo "${RED}Unexpected exit status $result from test_mmap${NORM}" + return "$HARDFAIL" + fi + + if [ "$TFAIL" != "yes" ]; then + echo -n "Result (expect denied): " + else + echo -n "Result (expect allowed): " + fi + + if [ $result -eq 0 ]; then + if [ "$TFAIL" != "yes" ]; then + echo "${RED}allowed${NORM}" + else + echo "${GREEN}allowed${NORM}" + fi + return "$FAIL" + fi + + if [ "$TFAIL" != "yes" ]; then + echo "${GREEN}denied${NORM}" + else + echo "${RED}denied${NORM}" + fi + + return "$OK" +} + +# Run in the new environment if TST_ENV is set. +_run_env "$TST_KERNEL" "$PWD/$(basename "$0")" "TST_ENV=$TST_ENV TST_KERNEL=$TST_KERNEL PATH=$PATH LD_LIBRARY_PATH=$LD_LIBRARY_PATH VERBOSE=$VERBOSE TST_KEY_PATH=$TST_KEY_PATH" + +# Exit from the creator of the new environment. +_exit_env "$TST_KERNEL" + +# Mount filesystems in the new environment. +_init_env + +if [ "$(whoami)" != "root" ]; then + echo "${CYAN}This script must be executed as root${NORM}" + exit "$SKIP" +fi + +if [ ! -f /sys/kernel/security/ima/policy ]; then + echo "${CYAN}IMA policy file not found${NORM}" + exit "$SKIP" +fi + +if ! cat /sys/kernel/security/ima/policy &> /dev/null; then + echo "${CYAN}IMA policy file is not readable${NORM}" + exit "$SKIP" +fi + +if [ -n "$TST_KEY_PATH" ]; then + if [ "${TST_KEY_PATH:0:1}" != "/" ]; then + echo "${RED}Absolute path required for the signing key${NORM}" + exit "$FAIL" + fi + + if [ ! -f "$TST_KEY_PATH" ]; then + echo "${RED}Kernel signing key not found in $TST_KEY_PATH${NORM}" + exit "$FAIL" + fi + + g_key_path="$TST_KEY_PATH" +elif [ -f "$PWD/../signing_key.pem" ]; then + g_key_path="$PWD/../signing_key.pem" +elif [ -f "/lib/modules/$(uname -r)/source/certs/signing_key.pem" ]; then + g_key_path="/lib/modules/$(uname -r)/source/certs/signing_key.pem" +elif [ -f "/lib/modules/$(uname -r)/build/certs/signing_key.pem" ]; then + g_key_path="/lib/modules/$(uname -r)/build/certs/signing_key.pem" +else + echo "${CYAN}Kernel signing key not found${NORM}" + exit "$SKIP" +fi + +evm_value=$(cat /sys/kernel/security/evm) +if [ $((evm_value & EVM_INIT_X509)) -eq "$EVM_INIT_X509" ]; then + if [ $((evm_value & EVM_INIT_HMAC)) -ne "$EVM_INIT_HMAC" ]; then + echo "${CYAN}Incompatible EVM mode $evm_value${NORM}" + exit "$SKIP" + fi +fi + +g_key_path_der=$(mktemp) + +openssl x509 -in "$g_key_path" -out "$g_key_path_der" -outform der +if ! keyctl padd asymmetric pubkey %keyring:.ima < "$g_key_path_der" &> /dev/null; then + echo "${RED}Public key cannot be added to the IMA keyring${NORM}" + exit "$FAIL" +fi + +g_mountpoint=$(mktemp -d) +g_image=$(mktemp) + +if [ -z "$g_mountpoint" ]; then + echo "${RED}Mountpoint directory not created${NORM}" + exit "$FAIL" +fi + +if ! dd if=/dev/zero of="$g_image" bs=1M count=20 &> /dev/null; then + echo "${RED}Cannot create test image${NORM}" + exit "$FAIL" +fi + +g_dev=$(losetup -f "$g_image" --show) +if [ -z "$g_dev" ]; then + echo "${RED}Cannot create loop device${NORM}" + exit "$FAIL" +fi + +if ! mkfs.ext4 -U "$IMA_UUID" -b 4096 "$g_dev" &> /dev/null; then + echo "${RED}Cannot format $g_dev${NORM}" + exit "$FAIL" +fi + +if ! mount -o iversion "$g_dev" "$g_mountpoint"; then + echo "${RED}Cannot mount loop device${NORM}" + exit "$FAIL" +fi + +g_loop_mounted=1 +pushd "$g_mountpoint" > /dev/null || exit "$FAIL" + +# Ensure that IMA does not add a new measurement entry if an application calls +# mmap() with PROT_READ, and a policy rule contains the MMAP_CHECK hook. +# In this case, both the protections requested by the application and the final +# protections applied by the kernel contain only PROT_READ, so there is no +# match with the IMA rule, which expects PROT_EXEC to be set. +expect_fail check_mmap "MMAP_CHECK" "" + +# Ensure that IMA adds a new measurement entry if an application calls mmap() +# with PROT_READ | PROT_EXEC, and a policy rule contains the MMAP_CHECK hook. +expect_pass check_mmap "MMAP_CHECK" "exec" + +# Same as in the first test, but in this case the application calls the +# personality() system call with READ_IMPLIES_EXEC, which causes the kernel to +# add PROT_EXEC in the final protections passed to the MMAP_CHECK hook. +# +# Ensure that the bug introduced by 98de59bfe4b2 ("take calculation of final +# protections in security_mmap_file() into a helper") is fixed, by passing the +# final protections again to the MMAP_CHECK hook. Due to the bug, the hook +# received the protections requested by the application. Since those protections +# don't have PROT_EXEC, IMA was not creating a measurement entry. +expect_pass_if '0' check_mmap "MMAP_CHECK" "read_implies_exec" + +# Repeat the previous three tests, but with the new MMAP_CHECK_REQPROT hook, +# which behaves like the buggy MMAP_CHECK hook. In the third test, expect that +# no new measurement entry is created, since the MMAP_CHECK_REQPROT hook sees +# the protections requested by the application (PROT_READ). +expect_fail_if '1' check_mmap "MMAP_CHECK_REQPROT" "" +expect_pass_if '1' check_mmap "MMAP_CHECK_REQPROT" "exec" +expect_fail_if '1' check_mmap "MMAP_CHECK_REQPROT" "read_implies_exec" + +# Ensure that IMA refuses an mprotect() with PROT_EXEC on a memory area +# obtained with an mmap() with PROT_READ. This is due to the inability of IMA +# to measure/appraise the file for which mmap() was called (locking issue). +expect_pass check_deny "MMAP_CHECK" "mprotect" + +# Ensure that MMAP_CHECK_REQPROT has the same behavior of MMAP_CHECK for the +# previous test. +expect_pass_if '1' check_deny "MMAP_CHECK_REQPROT" "mprotect" + +# Ensure that there cannot be an mmap() with PROT_EXEC on a file with writable +# mappings, due to the inability of IMA to make a reliable measurement of that +# file. +expect_pass check_deny "MMAP_CHECK" "exec_on_writable" + +# Ensure that MMAP_CHECK_REQPROT has the same behavior of MMAP_CHECK for the +# previous test. +expect_pass_if '1' check_deny "MMAP_CHECK_REQPROT" "exec_on_writable" diff --git a/tests/test_mmap.c b/tests/test_mmap.c new file mode 100644 index 0000000..63e7597 --- /dev/null +++ b/tests/test_mmap.c @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2023 Huawei Technologies Duesseldorf GmbH + * + * Tool to test IMA MMAP_CHECK and MMAP_CHECK_REQPROT hooks. + */ +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * Convention: return 1 for errors that should not occur, as they are + * setup-related, return 2 for errors that might occur due to testing + * conditions. + */ +#define ERR_SETUP 1 +#define ERR_TEST 2 + +int main(int argc, char *argv[]) +{ + struct stat st; + void *ptr, *ptr_write = NULL; + int ret, fd, fd_write, prot = PROT_READ; + + if (!argv[1]) { + printf("Missing file parameter\n"); + return ERR_SETUP; + } + + if (argv[2] && !strcmp(argv[2], "read_implies_exec")) { + ret = personality(READ_IMPLIES_EXEC); + if (ret == -1) { + printf("Failed to set personality, err: %d (%s)\n", + -errno, strerror(errno)); + return ERR_SETUP; + } + } + + if (stat(argv[1], &st) == -1) { + printf("Failed to access %s, err: %d (%s)\n", argv[1], -errno, + strerror(errno)); + return ERR_SETUP; + } + + if (argv[2] && !strcmp(argv[2], "exec_on_writable")) { + fd_write = open(argv[1], O_RDWR); + if (fd_write == -1) { + printf("Failed to open %s in r/w, err: %d (%s)\n", + argv[1], -errno, strerror(errno)); + return ERR_SETUP; + } + + ptr_write = mmap(0, st.st_size, PROT_WRITE, MAP_SHARED, + fd_write, 0); + close(fd_write); + + if (ptr_write == MAP_FAILED) { + printf("Failed mmap() with PROT_WRITE on %s, err: %d (%s)\n", + argv[1], -errno, strerror(errno)); + return ERR_SETUP; + } + } + + fd = open(argv[1], O_RDONLY); + if (fd == -1) { + printf("Failed to open %s in ro, err: %d (%s)\n", argv[1], + -errno, strerror(errno)); + + if (ptr_write && munmap(ptr_write, st.st_size) == -1) + printf("Failed munmap() of writable mapping on %s, err: %d (%s)\n", + argv[1], -errno, strerror(errno)); + + return ERR_SETUP; + } + + if (argv[2] && !strncmp(argv[2], "exec", 4)) + prot |= PROT_EXEC; + + ptr = mmap(0, st.st_size, prot, MAP_PRIVATE, fd, 0); + + close(fd); + + if (ptr_write && munmap(ptr_write, st.st_size) == -1) { + printf("Failed munmap() of writable mapping on %s, err: %d (%s)\n", + argv[1], -errno, strerror(errno)); + return ERR_SETUP; + } + + if (ptr == MAP_FAILED) { + ret = ERR_SETUP; + if (argv[2] && !strcmp(argv[2], "exec_on_writable") && + errno == EACCES) + ret = ERR_TEST; + else + printf("Failed mmap() with PROT_READ%s on %s, err: %d (%s)\n", + (prot & PROT_EXEC) ? " | PROT_EXEC" : "", + argv[1], -errno, strerror(errno)); + + return ret; + } + + ret = 0; + + if (argv[2] && !strcmp(argv[2], "mprotect")) { + ret = mprotect(ptr, st.st_size, PROT_EXEC); + if (ret == -1) { + ret = ERR_SETUP; + if (errno == EPERM) + ret = ERR_TEST; + else + printf("Unexpected mprotect() error on %s, err: %d (%s)\n", + argv[1], -errno, strerror(errno)); + } + } + + if (munmap(ptr, st.st_size) == -1) { + printf("Failed munmap() of mapping on %s, err: %d (%s)\n", + argv[1], -errno, strerror(errno)); + return ERR_SETUP; + } + + return ret; +}