security-misc/usr/lib/security-misc/permission-hardening
Patrick Schleizer f4b1df02ee
Remove suid / gid and execute permission for 'group' and 'others'.
Similar to: chmod og-ugx /path/to/filename

Removing execution permission is useful to make binaries such as 'su' fail closed rather
than fail open if suid was removed from these.

Do not remove read access since no security benefit and easier to manually undo for users.

chmod 744
2019-12-22 19:42:40 -05:00

416 lines
15 KiB
Bash
Executable file

#!/bin/bash
## Copyright (C) 2012 - 2019 ENCRYPTED SUPPORT LP <adrelanos@riseup.net>
## See the file COPYING for copying conditions.
## https://forums.whonix.org/t/disable-suid-binaries/7706
## https://forums.whonix.org/t/re-mount-home-and-other-with-noexec-and-nosuid-among-other-useful-mount-options-for-better-security/7707
## To view previous modes and how these were changed:
## meld /var/lib/permission-hardening/existing_mode/statoverride /var/lib/permission-hardening/new_mode/statoverride
## To undo:
## sudo /usr/lib/security-misc/permission-hardening-undo
#set -x
set -e
set -o pipefail
exit_code=0
mkdir -p /var/lib/permission-hardening/existing_mode
mkdir -p /var/lib/permission-hardening/new_mode
dpkg_admindir_parameter_existing_mode="--admindir /var/lib/permission-hardening/existing_mode"
dpkg_admindir_parameter_new_mode="--admindir /var/lib/permission-hardening/new_mode"
echo_wrapper_ignore() {
echo "run: $@"
"$@" 2>/dev/null || true
}
echo_wrapper_audit() {
echo "run: $@"
return_code=0
"$@" || \
{ \
return_code="$?" ; \
exit_code=203 ; \
echo "ERROR: above command failed with exit code '$return_code'! calling function name: '${FUNCNAME[1]}'" >&2 ; \
};
}
echo_wrapper_silent_audit() {
## TODO: remove echo
echo "run (debugging): $@"
return_code=0
"$@" || \
{ \
return_code="$?" ; \
exit_code=204 ; \
echo "ERROR: above command '$@' failed with exit code '$return_code'! calling function name: '${FUNCNAME[1]}'" >&2 ; \
};
}
add_nosuid_statoverride_entry() {
local fso_to_process
fso_to_process="$fso"
local should_be_counter
should_be_counter="$(find "$fso_to_process" -perm /u=s,g=s | wc -l)" || true
local counter_actual
counter_actual=0
local line
while read -r line; do
true "line: $line"
counter_actual="$(( counter_actual + 1 ))"
local arr file_name existing_mode existing_owner existing_group
arr=($line)
file_name="${arr[0]}"
existing_mode="${arr[1]}"
existing_owner="${arr[2]}"
existing_group="${arr[3]}"
if [ "$arr" = "" ]; then
echo "ERROR: arr is empty. line: '$line'" >&2
continue
fi
if [ "$file_name" = "" ]; then
echo "ERROR: file_name is empty. line: '$line'" >&2
continue
fi
if [ "$existing_mode" = "" ]; then
echo "ERROR: existing_mode is empty. line: '$line'" >&2
continue
fi
if [ "$existing_owner" = "" ]; then
echo "ERROR: existing_owner is empty. line: '$line'" >&2
continue
fi
if [ "$existing_group" = "" ]; then
echo "ERROR: existing_group is empty. line: '$line'" >&2
continue
fi
## -h file True if file is a symbolic Link.
## -u file True if file has its set-user-id bit set.
## -g file True if file has its set-group-id bit set.
if test -h "$file_name" ; then
## https://forums.whonix.org/t/kernel-hardening/7296/323
true "skip symlink: $file_name"
continue
fi
if test -d "$file_name" ; then
true "skip directory: $file_name"
continue
fi
local setuid setuid_output setsgid setsgid_output
setuid=""
setuid_output=""
if test -u "$file_name" ; then
setuid=true
setuid_output="set-user-id"
fi
setsgid=""
setsgid_output=""
if test -g "$file_name" ; then
setsgid=true
setsgid_output="set-group-id"
fi
local setuid_or_setsgid
setuid_or_setsgid=""
if [ "$setuid" = "true" ] || [ "$setsgid" = "true" ]; then
setuid_or_setsgid=true
fi
if [ "$setuid_or_setsgid" = "" ]; then
continue
fi
## Remove suid / gid and execute permission for 'group' and 'others'.
## Similar to: chmod og-ugx /path/to/filename
## Removing execution permission is useful to make binaries such as 'su' fail closed rather
## than fail open if suid was removed from these.
## Do not remove read access since no security benefit and easier to manually undo for users.
## Are there suid or sgid binaries which are still useful if suid / sgid has been removed from these?
new_mode="744"
local is_whitelisted
is_whitelisted=""
for white_list_entry in $whitelist ; do
if [ "$file_name" = "$white_list_entry" ]; then
is_whitelisted="true"
## Stop looping through the whitelist.
break
fi
done
local is_match_whitelisted
is_match_whitelisted=""
for matchwhite_list_entry in $matchwhitelist ; do
if echo "$file_name" | grep -q "$matchwhite_list_entry" ; then
is_match_whitelisted="true"
## Stop looping through the matchwhitelist.
break
fi
done
if [ "$is_whitelisted" = "true" ]; then
echo "INFO: SKIP whitelisted - $setuid_output $setsgid_output found - file_name: '$file_name' | existing_mode: '$existing_mode'"
continue
fi
if [ "$is_match_whitelisted" = "true" ]; then
echo "INFO: SKIP matchwhitelisted - $setuid_output $setsgid_output found - file_name: '$file_name' | existing_mode: '$existing_mode' | matchwhite_list_entry: '$matchwhite_list_entry'"
continue
fi
echo "INFO: $setuid_output $setsgid_output found - file_name: '$file_name' | existing_mode: '$existing_mode' | new_mode: '$new_mode'"
if dpkg-statoverride $dpkg_admindir_parameter_existing_mode --list "$file_name" >/dev/null ; then
true "OK Existing mode already saved previously. No need to save again."
else
## Save existing_mode in separate database.
## Not using --update as not intending to enforce existing_mode.
echo_wrapper_silent_audit dpkg-statoverride $dpkg_admindir_parameter_existing_mode --add "$existing_owner" "$existing_group" "$existing_mode" "$file_name"
fi
## No need to check "dpkg-statoverride --list" for existing entries.
## If existing_mode was correct already, we would not have reached this point.
## Since existing_mode is incorrect, remove from dpkg-statoverride and re-add.
## Remove from real database.
echo_wrapper_ignore dpkg-statoverride --remove "$file_name"
## Remove from separate database.
echo_wrapper_ignore dpkg-statoverride $dpkg_admindir_parameter_new_mode --remove "$file_name"
## Add to real database and use --update to make changes on disk.
echo_wrapper_audit dpkg-statoverride --add --update "$existing_owner" "$existing_group" "$new_mode" "$file_name"
## Not using --update as this is only for recording.
echo_wrapper_silent_audit dpkg-statoverride $dpkg_admindir_parameter_new_mode --add "$existing_owner" "$existing_group" "$new_mode" "$file_name"
## /lib will hit ARG_MAX if using bash 'shopt -s globstar' and '/lib/**'.
## Using 'find' with '-perm /u=s,g=s' is faster and avoids ARG_MAX.
## https://forums.whonix.org/t/disable-suid-binaries/7706/17
done < <( find "$fso_to_process" -perm /u=s,g=s -print0 | xargs -I{} -0 stat -c "%n %a %U %G" {} )
## Sanity test.
if [ ! "$should_be_counter" = "$counter_actual" ]; then
echo "INFO: fso_to_process: '$fso_to_process' | counter_actual : '$counter_actual'"
echo "INFO: fso_to_process: '$fso_to_process' | should_be_counter: '$should_be_counter'"
exit_code=202
echo "ERROR: counter does not check out." >&2
fi
}
set_file_perms() {
echo "INFO: START parsing config_file: '$config_file'"
local line
while read -r line; do
if [ "$line" = "" ]; then
continue
fi
if [[ "$line" =~ ^# ]]; then
continue
fi
if [[ "$line" =~ [0-9a-zA-Z/] ]]; then
true "OK line contains only white listed characters."
else
exit_code=200
echo "ERROR: cannot parse line with invalid character: $line" >&2
continue
fi
#global fso
local mode_from_config owner_from_config group_from_config capability_from_config
if ! read -r fso mode_from_config owner_from_config group_from_config capability_from_config <<< "$line" ; then
exit_code=201
echo "ERROR: cannot parse line: $line" >&2
continue
fi
local fso_without_trailing_slash
fso_without_trailing_slash="${fso%/}"
if [ "$mode_from_config" = "whitelist" ]; then
## TODO: test/add white spaces inside file name support
whitelist+="$fso_without_trailing_slash "
continue
fi
if [ "$mode_from_config" = "matchwhitelist" ]; then
## TODO: test/add white spaces inside file name support
matchwhitelist+="$fso "
continue
fi
if ! [ -e "$fso" ]; then
echo "INFO: fso: '$fso' - does not exist. This is likely normal."
continue
fi
## Use dpkg-statoverride so permissions are not reset during upgrades.
if [ "$mode_from_config" = "nosuid" ]; then
## If mode_from_config is "nosuid" the config does not set owner and
## group. Therefore do not enforce owner/group check.
add_nosuid_statoverride_entry
else
local string_length_of_mode_from_config
string_length_of_mode_from_config="${#mode_from_config}"
if [ "$string_length_of_mode_from_config" -gt "4" ]; then
echo "ERROR: Mode '$mode_from_config' is invalid!" >&2
continue
fi
if [ "$string_length_of_mode_from_config" -lt "3" ]; then
echo "ERROR: Mode '$mode_from_config' is invalid!" >&2
continue
fi
if ! getent passwd | grep -q "^${owner_from_config}:" ; then
echo "ERROR: owner_from_config '$owner_from_config' does not exist!" >&2
continue
fi
if ! getent group | grep -q "^${group_from_config}:" ; then
echo "ERROR: group_from_config '$group_from_config' does not exist!" >&2
continue
fi
local mode_for_grep
mode_for_grep="$mode_from_config"
first_character_of_mode_from_config="${mode_from_config::1}"
if [ "$first_character_of_mode_from_config" = "0" ]; then
## Remove leading '0'.
mode_for_grep="${mode_from_config:1}"
fi
local stat_output
stat_output=""
if ! stat_output="$(stat -c "%n %a %U %G" "$fso_without_trailing_slash")" ; then
echo "ERROR: failed to run 'stat' for fso_without_trailing_slash: '$fso_without_trailing_slash'!" >&2
continue
fi
local arr file_name existing_mode existing_owner existing_group
arr=($stat_output)
file_name="${arr[0]}"
existing_mode="${arr[1]}"
existing_owner="${arr[2]}"
existing_group="${arr[3]}"
if [ "$arr" = "" ]; then
echo "ERROR: arr is empty. stat_output: '$stat_output' | line: '$line'" >&2
continue
fi
if [ "$file_name" = "" ]; then
echo "ERROR: file_name is empty. stat_output: '$stat_output' | line: '$line'" >&2
continue
fi
if [ "$existing_mode" = "" ]; then
echo "ERROR: existing_mode is empty. stat_output: '$stat_output' | line: '$line'" >&2
continue
fi
if [ "$existing_owner" = "" ]; then
echo "ERROR: existing_owner is empty. stat_output: '$stat_output' | line: '$line'" >&2
continue
fi
if [ "$existing_group" = "" ]; then
echo "ERROR: $existing_group is empty. stat_output: '$stat_output' | line: '$line'" >&2
continue
fi
## Check there is an entry for the fso.
##
## example: dpkg-statoverride --list | grep /home
## output:
## root root 755 /home
##
## dpkg-statoverride does not show leading '0'.
if dpkg-statoverride --list "$fso_without_trailing_slash" >/dev/null ; then
true "There is an fso entry. Check if owner/group/mode match."
if dpkg-statoverride --list | grep -q "$owner_from_config $group_from_config $mode_for_grep $fso_without_trailing_slash" ; then
true "OK The owner/group/mode matches. No further action required."
else
true "The owner/group/mode do not match, therefore remove and re-add the entry to update it."
## fso_without_trailing_slash instead of fso to prevent
## "dpkg-statoverride: warning: stripping trailing /"
if dpkg-statoverride $dpkg_admindir_parameter_existing_mode --list "$fso_without_trailing_slash" >/dev/null ; then
true "OK Existing mode already saved previously. No need to save again."
else
## Save existing_mode in separate database.
## Not using --update as not intending to enforce existing_mode.
echo_wrapper_silent_audit dpkg-statoverride $dpkg_admindir_parameter_existing_mode --add "$existing_owner" "$existing_group" "$existing_mode" "$fso_without_trailing_slash"
fi
echo_wrapper_audit dpkg-statoverride $dpkg_admindir_parameter_new_mode --remove "$fso_without_trailing_slash"
## Remove from and add to real database.
echo_wrapper_audit dpkg-statoverride --remove "$fso_without_trailing_slash"
echo_wrapper_audit dpkg-statoverride --add --update "$owner_from_config" "$group_from_config" "$mode_from_config" "$fso_without_trailing_slash"
## Save in separate database.
## Not using --update as this is only for saving.
echo_wrapper_silent_audit dpkg-statoverride $dpkg_admindir_parameter_new_mode --add "$owner_from_config" "$group_from_config" "$mode_from_config" "$fso_without_trailing_slash"
fi
else
true "There is no fso entry. Therefore add one."
if dpkg-statoverride $dpkg_admindir_parameter_existing_mode --list "$fso_without_trailing_slash" >/dev/null ; then
true "OK Existing mode already saved previously. No need to save again."
else
## Save existing_mode in separate database.
## Not using --update as not intending to enforce existing_mode.
echo_wrapper_silent_audit dpkg-statoverride $dpkg_admindir_parameter_existing_mode --add "$existing_owner" "$existing_group" "$existing_mode" "$fso_without_trailing_slash"
fi
## Add to real database.
echo_wrapper_audit dpkg-statoverride --add --update "$owner_from_config" "$group_from_config" "$mode_from_config" "$fso_without_trailing_slash"
## Save in separate database.
## Not using --update as this is only for saving.
echo_wrapper_silent_audit dpkg-statoverride $dpkg_admindir_parameter_new_mode --add "$owner_from_config" "$group_from_config" "$mode_from_config" "$fso_without_trailing_slash"
fi
fi
if [ "$capability_from_config" = "" ]; then
continue
fi
if [ "$capability_from_config" = "none" ]; then
echo_wrapper_audit setcap -r "$fso"
else
if ! capsh --print | grep "Bounding set" | grep -q "$capability_from_config" ; then
echo "ERROR: capability_from_config '$capability_from_config' does not exist!" >&2
continue
fi
echo_wrapper_audit setcap "${capability_from_config}+ep" "$fso"
fi
done < "$config_file"
echo "INFO: END parsing config_file: '$config_file'"
}
parse_config_folder() {
shopt -s nullglob
for config_file in /etc/permission-hardening.d/*.conf /usr/local/etc/permission-hardening.d/*.conf; do
set_file_perms
done
}
parse_config_folder
if [ ! "$exit_code" = "0" ]; then
echo "ERROR: Will exit with non-zero exit code: '$exit_code'" >&2
fi
exit "$exit_code"