Documentation/admin-guide/LSM/AltHa.rst | 101 +++++++++ Documentation/admin-guide/LSM/index.rst | 1 + security/Kconfig | 4 +- security/Makefile | 2 + security/altha/Kconfig | 11 + security/altha/Makefile | 3 + security/altha/altha_lsm.c | 351 ++++++++++++++++++++++++++++++++ security/kiosk/Kconfig | 9 + security/kiosk/Makefile | 3 + security/kiosk/kiosk-test.sh | 252 +++++++++++++++++++++++ security/kiosk/kiosk_lsm.c | 337 ++++++++++++++++++++++++++++++ 11 files changed, 1073 insertions(+), 1 deletion(-) diff --git a/Documentation/admin-guide/LSM/AltHa.rst b/Documentation/admin-guide/LSM/AltHa.rst new file mode 100644 index 000000000000..be698709d3f0 --- /dev/null +++ b/Documentation/admin-guide/LSM/AltHa.rst @@ -0,0 +1,101 @@ +==== +AltHa +==== + +AltHa is a Linux Security Module currently has three userspace hardening options: + * ignore SUID on binaries (with exceptions possible); + * prevent running selected script interpreters in interactive mode; + * disable open file unlinking in selected dirs. + * enable kiosk mode + + +It is selectable at build-time with ``CONFIG_SECURITY_ALTHA``, and should be +enabled in runtime by command line option ``altha=1`` and configured +through sysctls in ``/proc/sys/kernel/altha``. + +NoSUID +============ +Modern Linux systems can be used with minimal (or even zero at least for OWL and ALT) usage of SUID programms, but in many cases in full-featured desktop or server systems there are plenty of them: uncounted and sometimes unnecessary. Privileged programms are always an attack surface, but mounting filesystems with ``nosuid`` flag doesn't provide enough granularity in SUID binaries management. This LSM module provides a single control point for all SUID binaries. When this submodule is enabled, SUID bits on all binaries except explicitly listed are system-wide ignored. + +Sysctl parameters and defaults: + +* ``kernel.altha.nosuid.enabled = 0``, set to 1 to enable +* ``kernel.altha.nosuid.exceptions =``, colon-separated list of enabled SUID binaries, for example: ``/bin/su:/usr/libexec/hasher-priv/hasher-priv`` + +RestrScript +============ +There is a one way to hardening: prevent users from executing ther own arbitrary code. Traditionally it can be done setting on user-writable filesystems ``noexec`` flag. But modern script languages such as Python also can be used to write exploits or even load arbitary machine code via ``dlopen`` and users can start scripts from ``noexec`` filesystem starting interpreter directly. +Restrscript LSM submodule provides a way to restrict some programms to be executed directly, but allows to execute them as shebang handler. + +Sysctl parameters and defaults: + +* ``kernel.altha.rstrscript.enabled = 0``, set to 1 to enable +* ``kernel.altha.rstrscript.interpreters =``, colon-separated list of restricted interpreters for example: ``/lib64/ld-linux-x86-64.so.2:/usr/bin/python:/usr/bin/python3:/usr/bin/perl:/usr/bin/tclsh``. Symlinks are supported in both ways: you can set symlink to interpreter as exception and interpreter and all symlinks on it will be restricted. + +Adding ld-linux into blocking list prevents running interpreters via ``ld-linux interpreter``. + +Note: in this configuration all scripts starting with ``#!/usr/bin/env python`` will be blocked. + +OLock +============ +Unlink disabling for open files needed for Russian certification, but this is a nasty feature leading to DOS. + +Sysctl parameters and defaults: + +* ``kernel.altha.olock.enabled = 0``, set to 1 to enable +* ``kernel.altha.olock.dirs =``, colon-separated list of dirs, for example: ``/var/lib/something:/tmp/something``. + +Kiosk +=========== +Disable execution for everything and everyone (including system users +and root, if required) except given whitelists. + +Kiosk interface uses generic netlink framework. +Interface name: ``altha`` + +Kiosk packet attributes:: + + static struct nla_policy kiosk_attrs_policy[KIOSK_MAX_ATTR] = { + [KIOSK_ACTION] = { + .type = NLA_S16, + }, + [KIOSK_DATA] = { + .type = NLA_STRING, + .maxlen = MAX_DATA /* 1024 */ + }, + }; + +Possible kiosk modes:: + + enum kiosk_mode { + KIOSK_PERMISSIVE = 0, /* kiosk is disabled */ + KIOSK_NONSYSTEM, /* kiosk is enabled for users with uid >= 500 */ + KIOSK_ALL, /* kiosk is enabled for all users */ + }; + +In ``KIOSK_ALL`` mode root will be restricted if running from tty +Otherwise application will be executed anyway, +enabling the system to boot without garbage in whitelists. + +Possible kiosk actions:: + + enum altha_kiosk_action { + KIOSK_SET_MODE = 0, /* set or get mode, see below */ + KIOSK_USERLIST_ADD, /* add app to user whitelist */ + KIOSK_USERLIST_DEL, /* remove app from user whitelist */ + KIOSK_SYSLIST_ADD, /* add app to system whitelist */ + KIOSK_SYSLIST_DEL, /* remove app from system whitelist */ + KIOSK_USER_LIST, /* retrieve user whitelist, see below */ + KIOSK_SYSTEM_LIST, /* retrieve system whitelist */ + }; + +``KIOSK_ACTION`` attribute is used. + +``SET_MODE`` action will send current mode if ``KIOSK_DATA`` is empty. + +When ``KIOSK_USER_LIST`` or ``KIOSK_SYSTEM_LIST`` action is requested, kernel sends +the first item from the list and waits for another request. +When end of list is reached, it sends an empty string and it will be safe +for client to request another list. + +``LD_*`` cheats will not be applied when kiosk is activated. diff --git a/Documentation/admin-guide/LSM/index.rst b/Documentation/admin-guide/LSM/index.rst index a6ba95fbaa9f..20b57e7adadd 100644 --- a/Documentation/admin-guide/LSM/index.rst +++ b/Documentation/admin-guide/LSM/index.rst @@ -47,3 +47,4 @@ subdirectories. tomoyo Yama SafeSetID + AltHa diff --git a/security/Kconfig b/security/Kconfig index 0ced7fd33e4d..42a72aa188dd 100644 --- a/security/Kconfig +++ b/security/Kconfig @@ -239,6 +239,8 @@ source "security/yama/Kconfig" source "security/safesetid/Kconfig" source "security/lockdown/Kconfig" source "security/landlock/Kconfig" +source "security/altha/Kconfig" +source "security/kiosk/Kconfig" source "security/integrity/Kconfig" @@ -282,7 +284,7 @@ config LSM default "landlock,lockdown,yama,loadpin,safesetid,integrity,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR default "landlock,lockdown,yama,loadpin,safesetid,integrity,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO default "landlock,lockdown,yama,loadpin,safesetid,integrity,bpf" if DEFAULT_SECURITY_DAC - default "landlock,lockdown,yama,loadpin,safesetid,integrity,selinux,smack,tomoyo,apparmor,bpf" + default "landlock,lockdown,yama,loadpin,safesetid,integrity,selinux,smack,tomoyo,apparmor,bpf,altha,kiosk" help A comma-separated list of LSMs, in initialization order. Any LSMs left off this list will be ignored. This can be diff --git a/security/Makefile b/security/Makefile index 18121f8f85cd..4fb7ea65d2c9 100644 --- a/security/Makefile +++ b/security/Makefile @@ -24,6 +24,8 @@ obj-$(CONFIG_SECURITY_LOCKDOWN_LSM) += lockdown/ obj-$(CONFIG_CGROUPS) += device_cgroup.o obj-$(CONFIG_BPF_LSM) += bpf/ obj-$(CONFIG_SECURITY_LANDLOCK) += landlock/ +obj-$(CONFIG_SECURITY_ALTHA) += altha/ +obj-$(CONFIG_SECURITY_KIOSK) += kiosk/ # Object integrity file lists obj-$(CONFIG_INTEGRITY) += integrity/ diff --git a/security/altha/Kconfig b/security/altha/Kconfig new file mode 100644 index 000000000000..4bafdef4e58e --- /dev/null +++ b/security/altha/Kconfig @@ -0,0 +1,11 @@ +config SECURITY_ALTHA + bool "AltHa security module" + depends on SECURITY + default n + help + Some hardening options: + * ignore SUID on binaries (with exceptions possible); + * prevent running selected script interprers in interactive move; + * WxorX for filesystems (with exceptions possible); + + If you are unsure how to answer this question, answer N. diff --git a/security/altha/Makefile b/security/altha/Makefile new file mode 100644 index 000000000000..56735b157567 --- /dev/null +++ b/security/altha/Makefile @@ -0,0 +1,3 @@ +obj-$(CONFIG_SECURITY_ALTHA) := altha.o + +altha-y := altha_lsm.o diff --git a/security/altha/altha_lsm.c b/security/altha/altha_lsm.c new file mode 100644 index 000000000000..c670ad7ed458 --- /dev/null +++ b/security/altha/altha_lsm.c @@ -0,0 +1,351 @@ +/* + * AltHa Linux Security Module + * + * Author: Anton Boyarshinov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, as + * published by the Free Software Foundation. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ALTHA_PARAMS_SIZE 4096 +char proc_nosuid_exceptions[ALTHA_PARAMS_SIZE]; +char proc_interpreters[ALTHA_PARAMS_SIZE]; +char proc_olock_dirs[ALTHA_PARAMS_SIZE]; + +/* Boot time disable flag */ +static bool altha_enabled = 0; + +/* sysctl flags */ +static int nosuid_enabled; +static int rstrscript_enabled; +static int olock_enabled; + +/* Boot parameter handing */ +module_param_named(enabled, altha_enabled, bool, S_IRUGO); + +static int __init altha_enabled_setup(char *str) +{ + unsigned long enabled; + int error = kstrtoul(str, 0, &enabled); + if (!error) + altha_enabled = enabled ? 1 : 0; + return 1; +} + +__setup("altha=", altha_enabled_setup); + +struct altha_list_struct { + struct path path; + char * spath; + char * spath_p; + struct list_head list; +}; + +/* Lists handling */ +DECLARE_RWSEM(nosuid_exceptions_sem); +DECLARE_RWSEM(interpreters_sem); +DECLARE_RWSEM(olock_dirs_sem); +LIST_HEAD(nosuid_exceptions_list); +LIST_HEAD(interpreters_list); +LIST_HEAD(olock_dirs_list); + +static int altha_list_handler(struct ctl_table *table, int write, + void __user * buffer, size_t * lenp, + loff_t * ppos) +{ + struct altha_list_struct *item, *tmp; + struct list_head *list_struct; + char *p, *fluid; + char *copy_buffer; + struct rw_semaphore *sem = table->extra2; + unsigned long error = proc_dostring(table, write, buffer, lenp, ppos); + down_write(sem); + if (error) + goto out; + + if (write && !error) { + copy_buffer = kmalloc(ALTHA_PARAMS_SIZE, GFP_KERNEL); + if (!copy_buffer) { + pr_err + ("AltHa: can't get memory for copy_buffer processing sysctl\n"); + error = -1; + goto out; + } + + list_struct = (struct list_head *)(table->extra1); + /*empty list and that fill with new info */ + list_for_each_entry_safe(item, tmp, list_struct, list) { + list_del(&item->list); + path_put(&item->path); + kfree(item->spath_p); + kfree(item); + } + + strlcpy(copy_buffer, table->data, ALTHA_PARAMS_SIZE); + + /* buffer can have a garbage after \n */ + p = strchrnul(copy_buffer, '\n'); + *p = 0; + + /* for strsep usage */ + fluid = copy_buffer; + + while ((p = strsep(&fluid, ":\n")) != NULL) { + if (strlen(p)) { + item = kmalloc(sizeof(*item), GFP_KERNEL); + if (item) + item->spath_p = kmalloc(PATH_MAX, GFP_KERNEL); + if (!item || !item->spath_p) { + pr_err + ("AltHa: can't get memory processing sysctl\n"); + kfree(copy_buffer); + error = -1; + goto out; + } + if (kern_path(p, LOOKUP_FOLLOW, &item->path)) { + pr_info + ("AltHa: error lookup '%s'\n", p); + kfree(item); + } else { + item->spath=d_path(&item->path,item->spath_p,PATH_MAX); + list_add_tail(&item->list, list_struct); + } + } + } + kfree(copy_buffer); + } +out: + up_write(sem); + return error; +} + +struct ctl_path nosuid_sysctl_path[] = { + {.procname = "kernel",}, + {.procname = "altha",}, + {.procname = "nosuid",}, + {} +}; + +static struct ctl_table nosuid_sysctl_table[] = { + { + .procname = "enabled", + .data = &nosuid_enabled, + .maxlen = sizeof(int), + .mode = 0644, + .proc_handler = proc_dointvec_minmax, + }, + { + .procname = "exceptions", + .data = proc_nosuid_exceptions, + .maxlen = ALTHA_PARAMS_SIZE, + .mode = 0644, + .proc_handler = altha_list_handler, + .extra1 = &nosuid_exceptions_list, + .extra2 = &nosuid_exceptions_sem, + }, + {} +}; + +struct ctl_path rstrscript_sysctl_path[] = { + {.procname = "kernel",}, + {.procname = "altha",}, + {.procname = "rstrscript",}, + {} +}; + +static struct ctl_table rstrscript_sysctl_table[] = { + { + .procname = "enabled", + .data = &rstrscript_enabled, + .maxlen = sizeof(int), + .mode = 0644, + .proc_handler = &proc_dointvec_minmax, + }, + { + .procname = "interpreters", + .data = proc_interpreters, + .maxlen = ALTHA_PARAMS_SIZE, + .mode = 0644, + .proc_handler = altha_list_handler, + .extra1 = &interpreters_list, + .extra2 = &interpreters_sem, + }, + {} +}; + +struct ctl_path olock_sysctl_path[] = { + {.procname = "kernel",}, + {.procname = "altha",}, + {.procname = "olock",}, + {} +}; + +static struct ctl_table olock_sysctl_table[] = { + { + .procname = "enabled", + .data = &olock_enabled, + .maxlen = sizeof(int), + .mode = 0644, + .proc_handler = &proc_dointvec_minmax, + }, + { + .procname = "dirs", + .data = proc_olock_dirs, + .maxlen = ALTHA_PARAMS_SIZE, + .mode = 0644, + .proc_handler = altha_list_handler, + .extra1 = &olock_dirs_list, + .extra2 = &olock_dirs_sem, + }, + {} +}; + +struct altha_readdir_callback { + struct dir_context ctx; + u64 inode; + int found; +}; + +int is_olock_dir(struct inode *inode) +{ + struct altha_list_struct *node; + down_read(&olock_dirs_sem); + list_for_each_entry(node, &olock_dirs_list, list) { + struct inode *exc_inode = node->path.dentry->d_inode; + if (exc_inode == inode) { + up_read(&olock_dirs_sem); + return 1; + } + } + up_read(&olock_dirs_sem); + return 0; +} + +/* Hooks */ +static int altha_bprm_creds_from_file(struct linux_binprm *bprm, struct file * fi) +{ + struct altha_list_struct *node; + /* when it's not a shebang issued script interpreter */ + if (rstrscript_enabled && bprm->executable == bprm->interpreter) { + char *path_p; + char *path_buffer; + + path_buffer = kmalloc(PATH_MAX, GFP_KERNEL); + if (!path_buffer) + return -ENOMEM; + + path_p = d_path(&bprm->file->f_path,path_buffer,PATH_MAX); + down_read(&interpreters_sem); + list_for_each_entry(node, &interpreters_list, list) { + if (strcmp(path_p, node->spath) == 0) { + uid_t cur_uid = from_kuid(bprm->cred->user_ns, + bprm->cred->uid); + pr_notice_ratelimited + ("AltHa/RestrScript: %s is blocked to run directly by %d\n", + bprm->filename, cur_uid); + up_read(&interpreters_sem); + kfree(path_buffer); + return -EPERM; + } + } + up_read(&interpreters_sem); + kfree(path_buffer); + } + if (unlikely(nosuid_enabled && + !uid_eq(bprm->cred->uid, bprm->cred->euid))) { + char *path_p; + char *path_buffer; + uid_t cur_uid; + + path_buffer = kmalloc(PATH_MAX, GFP_KERNEL); + if (!path_buffer) + return -ENOMEM; + + cur_uid = from_kuid(bprm->cred->user_ns, bprm->cred->uid); + path_p = d_path(&bprm->file->f_path,path_buffer,PATH_MAX); + down_read(&nosuid_exceptions_sem); + list_for_each_entry(node, &nosuid_exceptions_list, list) { + if (strcmp(path_p, node->spath) == 0) { + pr_notice_ratelimited + ("AltHa/NoSUID: %s permitted to setuid from %d\n", + bprm->filename, cur_uid); + up_read(&nosuid_exceptions_sem); + kfree(path_buffer); + return 0; + } + } + up_read(&nosuid_exceptions_sem); + pr_notice_ratelimited + ("AltHa/NoSUID: %s prevented to setuid from %d\n", + bprm->filename, cur_uid); + bprm->cred->euid = bprm->cred->uid; + kfree(path_buffer); + } + return 0; +} + +/* For OLock */ +static int altha_inode_unlink(struct inode *inode, struct dentry *dentry) +{ + if (olock_enabled && (atomic_read(&dentry->d_inode->i_writecount) +#ifdef CONFIG_IMA + || atomic_read(&dentry->d_inode->i_readcount) +#endif + )) { + if (is_olock_dir(inode)) + return -EPERM; + } + return 0; +} + +/* Initialization */ + +static struct security_hook_list altha_hooks[] = { + LSM_HOOK_INIT(bprm_creds_from_file, altha_bprm_creds_from_file), + LSM_HOOK_INIT(inode_unlink, altha_inode_unlink), +}; + +static int __init altha_init(void) +{ + if (altha_enabled) { + pr_info("AltHa enabled.\n"); + security_add_hooks(altha_hooks, ARRAY_SIZE(altha_hooks),"altha"); + + if (!register_sysctl_paths + (nosuid_sysctl_path, nosuid_sysctl_table)) + panic("AltHa: NoSUID sysctl registration failed.\n"); + + if (!register_sysctl_paths + (rstrscript_sysctl_path, rstrscript_sysctl_table)) + panic + ("AltHa: RestrScript sysctl registration failed.\n"); + + if (!register_sysctl_paths + (olock_sysctl_path, olock_sysctl_table)) + panic("AltHa: OLock sysctl registration failed.\n"); + } else + pr_info("AltHa disabled.\n"); + return 0; +} + +DEFINE_LSM(altha) = { + .name = "altha", + .init = altha_init, +}; + diff --git a/security/kiosk/Kconfig b/security/kiosk/Kconfig new file mode 100644 index 000000000000..c92214abf62f --- /dev/null +++ b/security/kiosk/Kconfig @@ -0,0 +1,9 @@ +config SECURITY_KIOSK + bool "kiosk module" + depends on SECURITY + default n + help + Implements "Kiosk mode", in which user can be restricted to run anything + not permitted by admin. + + If you are unsure how to answer this question, answer N. diff --git a/security/kiosk/Makefile b/security/kiosk/Makefile new file mode 100644 index 000000000000..d29aba92bb3e --- /dev/null +++ b/security/kiosk/Makefile @@ -0,0 +1,3 @@ +obj-$(CONFIG_SECURITY_KIOSK) := kiosk.o + +kiosk-y := kiosk_lsm.o diff --git a/security/kiosk/kiosk-test.sh b/security/kiosk/kiosk-test.sh new file mode 100755 index 000000000000..9ab8774183e6 --- /dev/null +++ b/security/kiosk/kiosk-test.sh @@ -0,0 +1,252 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# Kiosk test suite: just run the script +# + +try_run() { + echo "Test: $*" >&2 + if "$@"; then + echo "Success." >&2 + else + echo "Failed: exit code $?" >&2 + exit 1 + fi +} + +# File appending and removing + +clean () { + kiosk -m "0" + + for i in `kiosk --user-list` + do + kiosk -U "$i" + done +} + +check_empty() { + TMPFILE=$(mktemp) + try_run kiosk --user-list > $TMPFILE + + if [ -s "$TMPFILE" ] + then + echo "Failed: lists are not empty" >&2 + rm -f $TMPFILE + exit 1 + fi + + rm -f $TMPFILE +} + +kiosk_user_append() { + echo `readlink -f "$1"` >> $TMPFILE + try_run kiosk --user-list-append "$1" +} + +kiosk_user_remove() { + try_run kiosk --user-list-remove "$1" +} + +kiosk_user_list_check() { + TMPFILE=$1 + LISTFILE=$(mktemp) + try_run kiosk --user-list > $LISTFILE + + for i in `cat "$LISTFILE"` + do + try_run kiosk_user_remove "$i" + done + + if cmp --quiet $TMPFILE $LISTFILE; then + echo "Success: user-list match" >&2 + else + echo "Failed: user-list does not match" >&2 + diff -u $TMPFILE $LISTFILE + exit 1 + fi + + rm -f $TMPFILE $LISTFILE +} + +TMPFILE=$(mktemp) + +clean + +kiosk_user_append /bin/sh +kiosk_user_append /bin/bash +kiosk_user_append /bin/date +kiosk_user_append /bin/ls +kiosk_user_list_check "$TMPFILE" + +# Mode changing +kiosk_set_mode() { + echo "$1" > $TMPFILE + try_run kiosk --set-mode "$1" +} + +kiosk_check_mode() { + TMPFILE=$1 + MODEFILE=$(mktemp) + try_run kiosk --get-mode > $MODEFILE + + if cmp --quiet $TMPFILE $MODEFILE; then + echo "Success: mode match" >&2 + else + echo "Failed: mode does not match" >&2 + exit 1 + fi + + rm -rf $TMPFILE $MODEFILE +} + +check_empty + +TMPFILE=$(mktemp) + +kiosk_set_mode "1" +kiosk_check_mode "$TMPFILE" +kiosk_set_mode "0" +kiosk_check_mode "$TMPFILE" + +# Exec testing +try_exec() { + REACT=$1 + shift + + echo "Executing $@" >&2 + "$@" >/dev/null + if [ "x$REACT" = "xdeny" -a $? -ne 126 ] + then + echo "Error: application was executed while it should not be" >&2 + echo "React is $REACT, error code is $?" >&2 + exit 1 + fi + if [ "x$REACT" = "xperm" -a $? -eq 126 ] + then + echo "Error: application was not executed while it should be" >&2 + echo "React is $REACT, error code is $?" >&2 + exit 1 + fi +} + +try_exec_user() { + REACT=$1 + shift + + try_exec $1 su - -c \"$@\" test +} + +check_empty + +#necessary +raise_guard() { + kiosk_user_append /bin/bash + kiosk_user_append /usr/bin/id + kiosk_user_append /bin/egrep + kiosk_user_append /bin/grep + kiosk_user_append /bin/hostname + kiosk_user_append /usr/bin/natspec + kiosk_user_append /usr/share/console-scripts/vt_activate_unicode + kiosk_user_append /usr/share/console-scripts/vt_activate_user_map + kiosk_user_append /sbin/consoletype +} + +stop_guard() { + kiosk_user_remove /bin/bash + kiosk_user_remove /usr/bin/id + kiosk_user_remove /bin/egrep + kiosk_user_remove /bin/grep + kiosk_user_remove /bin/hostname + kiosk_user_remove /usr/bin/natspec + kiosk_user_remove /usr/share/console-scripts/vt_activate_unicode + kiosk_user_remove /usr/share/console-scripts/vt_activate_user_map + kiosk_user_remove /sbin/consoletype +} + +raise_guard +kiosk_user_append /bin/false + +kiosk_set_mode "0" +try_exec_user perm /bin/false +try_exec_user perm /bin/true +kiosk_set_mode "1" +try_exec_user perm /bin/false +try_exec_user deny /bin/true +kiosk_set_mode "0" + +kiosk_user_remove /bin/false +stop_guard + +# TODO: +# bogus append to list (non-exist file) +check_empty +kiosk_user_append /bin/tru + +# bogus append to list (no params) +kiosk_user_append "" +kiosk_user_append + +kiosk_user_append /bin/true +kiosk_user_append /bin/false + +# bogus remove from list (non-exist file) +kiosk_user_remove /bin/tru + +# bogus remove from list (file is not in list) +kiosk_user_remove /bin/date +kiosk_user_remove /bin/false +kiosk_user_remove /bin/true + +# bogus remove from list (list is empty) +check_empty +kiosk_user_remove /bin/false + +# bogus remove from list (no params) +kiosk_user_remove "" +kiosk_user_remove + +# bogus mode (no params) +kiosk_set_mode "" + +# bogus mode +kiosk_set_mode "3" + +FILE="/home/test/test" +cp /bin/true $FILE +kiosk_user_append $FILE +kiosk_set_mode "1" + +raise_guard + +# user executes his own script +chmod 555 $FILE +chown test $FILE +chgrp test $FILE +try_exec_user deny $FILE + +# user executes his script with his group +chown root $FILE +try_exec_user perm $FILE +chmod g+w $FILE +try_exec_user deny $FILE +chmod g-w $FILE + +# user executes script without permissions +chgrp root $FILE +try_exec_user perm $FILE +chmod o+w $FILE +try_exec_user deny $FILE +chmod o-w $FILE + +chmod 000 $FILE +setfacl -m "u:test:rwx" $FILE +try_exec_user deny $FILE +setfacl -m "u:test:r-x" $FILE +try_exec_user perm $FILE +getfacl $FILE + +stop_guard + +kiosk_set_mode "0" +kiosk_user_remove $FILE +rm -fv $FILE diff --git a/security/kiosk/kiosk_lsm.c b/security/kiosk/kiosk_lsm.c new file mode 100644 index 000000000000..cf7a7df65995 --- /dev/null +++ b/security/kiosk/kiosk_lsm.c @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Kiosk Linux Security Module + * + * Author: Oleg Solovyov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, as + * published by the Free Software Foundation. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define MAX_PATH 1024 + +struct kiosk_list_struct { + struct path path; + struct list_head list; +}; + +static struct kiosk_list_struct *list_iter; +static struct genl_family genl_kiosk_family; +static char pathbuf[MAX_PATH]; + +/* Lists handling */ +static DECLARE_RWSEM(user_sem); +static LIST_HEAD(user_list); + +enum kiosk_cmd { + KIOSK_UNSPEC = 0, + KIOSK_REQUEST, + KIOSK_REPLY, + KIOSK_CMD_LAST, +}; + +enum kiosk_mode { + KIOSK_PERMISSIVE = 0, + KIOSK_NONSYSTEM, + KIOSK_MODE_LAST, +}; + +static int kiosk_mode = KIOSK_PERMISSIVE; + +enum kiosk_action { + KIOSK_SET_MODE = 0, + KIOSK_USERLIST_ADD, + KIOSK_USERLIST_DEL, + KIOSK_USER_LIST, +}; + +enum kiosk_attrs { + KIOSK_NOATTR = 0, + KIOSK_ACTION, + KIOSK_DATA, + KIOSK_MAX_ATTR, +}; + +static struct nla_policy kiosk_policy[KIOSK_MAX_ATTR] = { + [KIOSK_ACTION] = { + .type = NLA_S16, + }, + [KIOSK_DATA] = { + .type = NLA_STRING, + .len = sizeof(pathbuf) - 1 + }, +}; + +static int kiosk_add_item(struct list_head *list, char *filename, + struct rw_semaphore *sem) +{ + struct kiosk_list_struct *item, *tmp; + int mode; + int rc; + + item = kmalloc(sizeof(*item), GFP_KERNEL); + if (!item) + return -ENOMEM; + + rc = kern_path(filename, LOOKUP_FOLLOW, &item->path); + if (rc) { + pr_err("Kiosk: error lookup '%s'\n", filename); + kfree(item); + return rc; + } + + mode = d_inode(item->path.dentry)->i_mode; + if (!S_ISREG(mode)) { + pr_err("Kiosk: given file is not a regular file, mode: %d\n", + mode); + path_put(&item->path); + kfree(item); + return -EINVAL; + } + + down_write(sem); + list_for_each_entry(tmp, list, list) { + if (item->path.dentry == tmp->path.dentry) { + up_write(sem); + path_put(&item->path); + kfree(item); + return 0; + } + } + list_add_tail(&item->list, list); + up_write(sem); + + return 0; +} + +static int kiosk_remove_item(struct list_head *list, char *filename, + struct rw_semaphore *sem) +{ + struct kiosk_list_struct *item, *tmp; + struct path user_path; + int rc; + + rc = kern_path(filename, LOOKUP_FOLLOW, &user_path); + if (rc) + return rc; + + down_write(sem); + list_for_each_entry_safe(item, tmp, list, list) { + if (item->path.dentry == user_path.dentry) { + if (item == list_iter) { + pr_err("Kiosk: list is being iterated, item removing is unsafe\n"); + up_write(sem); + path_put(&user_path); + return -EAGAIN; + } + list_del(&item->list); + path_put(&item->path); + kfree(item); + } + } + up_write(sem); + path_put(&user_path); + return 0; +} + +static int kiosk_nl_send_msg(struct sk_buff *skb, struct genl_info *info, + char *msg) +{ + int msg_size; + int res; + struct nlmsghdr *nlh; + struct sk_buff *skb_out; + + msg_size = strlen(msg) + 1; + /* we put string so add space for NUL-terminator */ + + skb_out = genlmsg_new(msg_size, GFP_KERNEL); + if (!skb_out) + return -ENOMEM; + + nlh = genlmsg_put_reply(skb_out, info, &genl_kiosk_family, 0, + KIOSK_REPLY); + if (!nlh) { + nlmsg_free(skb_out); + return -ENOMEM; + } + + res = nla_put_string(skb_out, KIOSK_DATA, msg); + if (res) { + nlmsg_free(skb_out); + return res; + } + + genlmsg_end(skb_out, nlh); + return genlmsg_reply(skb_out, info); +} + +static int kiosk_list_items(struct list_head *list, struct rw_semaphore *sem, + struct sk_buff *skb, struct genl_info *info) +{ + char *path; + + down_read(sem); + + if (!list_iter) { /* list iterating started */ + list_iter = list_first_entry_or_null(list, + struct kiosk_list_struct, + list); + } else if (list_iter == list_last_entry(list, + struct kiosk_list_struct, + list)) { + /* hit list end, cleaning temp variable */ + list_iter = NULL; + } else { /* iterating list */ + list_iter = list_next_entry(list_iter, list); + } + + if (list_iter) + path = d_path(&list_iter->path, pathbuf, sizeof(pathbuf)); + else + path = ""; + + up_read(sem); + return kiosk_nl_send_msg(skb, info, path); +} + +static int kiosk_genl_doit(struct sk_buff *skb, struct genl_info *info) +{ + int action; + + if (info->attrs[KIOSK_DATA]) + strlcpy(pathbuf, nla_data(info->attrs[KIOSK_DATA]), sizeof(pathbuf)); + else + pathbuf[0] = '\0'; + + action = info->attrs[KIOSK_ACTION] ? + nla_get_s16(info->attrs[KIOSK_ACTION]) : -1; + + switch (action) { + case KIOSK_SET_MODE: { + int new_mode; + int error; + char buf[4]; + + if (!strlen(pathbuf)) { + /* we want to retrieve current mode */ + snprintf(buf, sizeof(buf), "%d", kiosk_mode); + return kiosk_nl_send_msg(skb, info, buf); + } + + error = kstrtouint(pathbuf, 0, &new_mode); + + if (error || new_mode < 0 + || new_mode >= KIOSK_MODE_LAST) { + return -EINVAL; + } + kiosk_mode = new_mode; + return 0; + } + case KIOSK_USERLIST_ADD: + return kiosk_add_item(&user_list, pathbuf, &user_sem); + case KIOSK_USERLIST_DEL: + return kiosk_remove_item(&user_list, pathbuf, + &user_sem); + case KIOSK_USER_LIST: + return kiosk_list_items(&user_list, &user_sem, skb, + info); + default: + return -EINVAL; + } +} + +static const struct genl_ops genl_kiosk_ops[] = { + { + .doit = kiosk_genl_doit, + .flags = GENL_ADMIN_PERM, + }, +}; + +static struct genl_family genl_kiosk_family = { + .name = "kiosk", + .version = 1, + .netnsok = false, + .module = THIS_MODULE, + .ops = genl_kiosk_ops, + .n_ops = ARRAY_SIZE(genl_kiosk_ops), + .maxattr = KIOSK_MAX_ATTR, + .policy = kiosk_policy, +}; + +/* Hooks */ +static int kiosk_bprm_check_security(struct linux_binprm *bprm) +{ + uid_t cur_uid = __kuid_val(bprm->cred->uid); + struct kiosk_list_struct *node; + + if (kiosk_mode == KIOSK_PERMISSIVE) + return 0; + + if (cur_uid >= 500) { + bprm->secureexec = 1; + if (bprm->executable != bprm->interpreter) + return 0; + + if (cur_uid == __kuid_val(bprm->file->f_inode->i_uid) || + (bprm->file->f_inode->i_mode & 0022)) { + pr_notice_ratelimited("Kiosk: %s is writable for %d\n", + bprm->filename, cur_uid); + return -EPERM; + } + + down_read(&user_sem); + list_for_each_entry(node, &user_list, list) { + if (bprm->file->f_path.dentry == node->path.dentry) { + up_read(&user_sem); + return 0; + } + } + up_read(&user_sem); + } else { + return 0; + } + + pr_notice_ratelimited("Kiosk: %s prevented to exec from %d\n", + bprm->filename, cur_uid); + return -EPERM; +} + +static struct security_hook_list kiosk_hooks[] = { + LSM_HOOK_INIT(bprm_check_security, kiosk_bprm_check_security), +}; + +static int __init kiosk_init(void) +{ + int rc; + + rc = genl_register_family(&genl_kiosk_family); + + if (rc) { + pr_alert("Kiosk: Error registering family.\n"); + return rc; + } + + pr_info("Kiosk: Netlink family registered.\n"); + security_add_hooks(kiosk_hooks, ARRAY_SIZE(kiosk_hooks), "kiosk"); + + return 0; +} + +DEFINE_LSM(kiosk) = { + .name = "kiosk", + .init = kiosk_init, +};