From 1d3a0b6923fbda13c0c93b34bf6291f5c23390ae Mon Sep 17 00:00:00 2001 From: Roberto Sassu Date: Tue, 31 Jan 2023 17:02:30 +0100 Subject: [PATCH 1/3] Introduce expect_pass_if() and expect_fail_if() Introduce these functions to let the developer specify which kernel patches are required for the tests to be successful (either pass or fail). If a test is not successful, print those patches in the test result summary. First, the developer should declare an array, named PATCHES, with the list of all kernel patches that are required by the tests. For example: PATCHES=( 'patch 1 title' ... 'patch N title' ) Second, the developer could replace the existing expect_pass() and expect_fail() respectively with expect_pass_if() and expect_fail_if(), and add the indexes in the PATCHES array as the first argument, enclosed with quotes. The other parameters of expect_pass() and expect_fail() remain the same. In the following example, the PATCHES array has been added to a new test script, tests/mmap_check.test: PATCHES=( 'ima: Align ima_file_mmap() parameters with mmap_file LSM hook' 'ima: Introduce MMAP_CHECK_REQPROT hook' ) Then, expect_pass() has been replaced with expect_pass_if(): expect_pass_if '0' check_mmap "MMAP_CHECK" "read_implies_exec" The resulting output when a test fails (if the required patch is not applied) is: Test: check_mmap (hook="MMAP_CHECK", test_mmap arg: "read_implies_exec") Result (expect found): not found Possibly missing patches: - ima: Align ima_file_mmap() parameters with mmap_file LSM hook Signed-off-by: Roberto Sassu Signed-off-by: Mimi Zohar --- tests/functions.sh | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) 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 ] From 6a658e23d63127a0e7bae610dd8f15ef5e5f0930 Mon Sep 17 00:00:00 2001 From: Roberto Sassu Date: Thu, 2 Mar 2023 18:38:31 +0100 Subject: [PATCH 2/3] Add ima_policy_check.awk and ima_policy_check.test Add ima_policy_check.awk to check for possible overlapping of a rule being added by a test with the existing IMA policy (policy replacement by IMA at the first policy load is not taken into account). ima_policy_check.awk expects as input the rule to be added, followed by the IMA policy. It 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. Values can be individually checked by the test executing the awk script, to determine what to do (abort loading, print a warning in case of overlap, avoid adding an existing rule). The bit mask allows the test to see multiple statements regarding the new rule. For example, if the test added anyway an overlapping rule, it could also see that the policy already contains it at the next test execution, and does not add it again. Since ima_policy_check.awk uses GNU extensions (such as the or() function, or the fourth argument of split()), add gawk as dependency for the CI. Finally add ima_policy_check.test, to ensure that the awk script behaves as expected. Signed-off-by: Roberto Sassu Signed-off-by: Mimi Zohar --- ci/alpine.sh | 3 +- ci/debian.sh | 3 +- ci/tumbleweed.sh | 3 +- tests/Makefile.am | 2 +- tests/ima_policy_check.awk | 211 +++++++++++++++++++++++++++++++ tests/ima_policy_check.test | 245 ++++++++++++++++++++++++++++++++++++ 6 files changed, 463 insertions(+), 4 deletions(-) create mode 100755 tests/ima_policy_check.awk create mode 100755 tests/ima_policy_check.test 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..9a7d8a1 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -2,7 +2,7 @@ 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 .PHONY: check_logs check_logs: 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 From 6917e384d3b3119b8a7562edb47b508e2941c63d Mon Sep 17 00:00:00 2001 From: Roberto Sassu Date: Thu, 2 Mar 2023 19:05:02 +0100 Subject: [PATCH 3/3] Add tests for MMAP_CHECK and MMAP_CHECK_REQPROT hooks Add tests to ensure that, after applying the kernel patch 'ima: Align ima_file_mmap() parameters with mmap_file LSM hook', the MMAP_CHECK hook checks the protections applied by the kernel and not those requested by the application. Also ensure that after applying 'ima: Introduce MMAP_CHECK_REQPROT hook', the MMAP_CHECK_REQPROT hook checks the protections requested by the application. Test both with the test_mmap application that by default requests the PROT_READ protection flag. Its syntax is: test_mmap where mode can be: - exec: adds the PROT_EXEC protection flag to mmap() - read_implies_exec: calls the personality() system call with READ_IMPLIES_EXEC as the first argument before mmap() - mprotect: adds the PROT_EXEC protection flag to a memory area in addition to PROT_READ - exec_on_writable: calls mmap() with PROT_EXEC on a file which has a writable mapping Check the different combinations of hooks/modes and ensure that a measurement entry is found in the IMA measurement list only when it is expected. No measurement entry should be found when only the PROT_READ protection flag is requested or the matching policy rule has the MMAP_CHECK_REQPROT hook and the personality() system call was called with READ_IMPLIES_EXEC. mprotect() with PROT_EXEC on an existing memory area protected with PROT_READ should be denied (with an appraisal rule), regardless of the MMAP hook specified in the policy. The same applies for mmap() with PROT_EXEC on a file with a writable mapping. Signed-off-by: Roberto Sassu Signed-off-by: Mimi Zohar --- tests/Makefile.am | 5 +- tests/mmap_check.test | 407 ++++++++++++++++++++++++++++++++++++++++++ tests/test_mmap.c | 128 +++++++++++++ 3 files changed, 539 insertions(+), 1 deletion(-) create mode 100755 tests/mmap_check.test create mode 100644 tests/test_mmap.c diff --git a/tests/Makefile.am b/tests/Makefile.am index 9a7d8a1..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 ima_policy_check.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/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; +}