kernel-6.6/0207-kiosk-Implement-kiosk-module.patch

749 lines
18 KiB
Diff
Raw Normal View History

From 311765c654b00a077f7225b0894509af0ffbc7c9 Mon Sep 17 00:00:00 2001
From: Oleg Solovyov <mcpain@altlinux.org>
Date: Thu, 28 Nov 2019 17:57:52 +0300
Subject: [PATCH] kiosk: Implement kiosk module
When activated, only trusted applications are permitted to execute
themselves.
List of trusted applications can be applied separately to all regular
users.
[ kernelbot: build fixed with kernel 5.8. ]
---
Documentation/admin-guide/LSM/AltHa.rst | 56 ++++
security/Kconfig | 1 +
security/Makefile | 1 +
security/kiosk/Kconfig | 9 +
security/kiosk/Makefile | 3 +
security/kiosk/kiosk-test.sh | 252 ++++++++++++++++++
security/kiosk/kiosk_lsm.c | 337 ++++++++++++++++++++++++
7 files changed, 659 insertions(+)
create mode 100644 security/kiosk/Kconfig
create mode 100644 security/kiosk/Makefile
create mode 100755 security/kiosk/kiosk-test.sh
create mode 100644 security/kiosk/kiosk_lsm.c
2022-03-17 10:23:10 +03:00
diff --git a/Documentation/admin-guide/LSM/AltHa.rst b/Documentation/admin-guide/LSM/AltHa.rst
index fc3ba1486194..beda40601c9e 100644
--- a/Documentation/admin-guide/LSM/AltHa.rst
2022-03-17 10:23:10 +03:00
+++ b/Documentation/admin-guide/LSM/AltHa.rst
@@ -6,6 +6,7 @@ AltHa is a Linux Security Module currently has three userspace hardening options
* ignore SUID and setcaps on binaries (with exceptions possible);
* prevent running selected script interpreters in interactive mode;
* disable open file unlinking in selected dirs.
2022-03-17 10:23:10 +03:00
+ * enable kiosk mode
It is selectable at build-time with ``CONFIG_SECURITY_ALTHA``, and should be
@@ -43,3 +44,58 @@ 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``.
2022-03-17 10:23:10 +03:00
+
+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/security/Kconfig b/security/Kconfig
index e98ca31a58fc..d8de2ee18cd7 100644
2022-03-17 10:23:10 +03:00
--- a/security/Kconfig
+++ b/security/Kconfig
@@ -195,6 +195,7 @@ source "security/safesetid/Kconfig"
2022-03-17 10:23:10 +03:00
source "security/lockdown/Kconfig"
source "security/landlock/Kconfig"
source "security/altha/Kconfig"
2022-03-17 10:23:10 +03:00
+source "security/kiosk/Kconfig"
source "security/integrity/Kconfig"
diff --git a/security/Makefile b/security/Makefile
index a0f04e1cd8cf..4fb7ea65d2c9 100644
2022-03-17 10:23:10 +03:00
--- a/security/Makefile
+++ b/security/Makefile
@@ -25,6 +25,7 @@ obj-$(CONFIG_CGROUPS) += device_cgroup.o
2022-03-17 10:23:10 +03:00
obj-$(CONFIG_BPF_LSM) += bpf/
obj-$(CONFIG_SECURITY_LANDLOCK) += landlock/
obj-$(CONFIG_SECURITY_ALTHA) += altha/
2022-03-17 10:23:10 +03:00
+obj-$(CONFIG_SECURITY_KIOSK) += kiosk/
# Object integrity file lists
obj-$(CONFIG_INTEGRITY) += integrity/
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 <mcpain@altlinux.org>
+ *
+ * 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 <linux/lsm_hooks.h>
+#include <linux/cred.h>
+#include <linux/binfmts.h>
+#include <linux/list.h>
+#include <linux/namei.h>
+#include <linux/printk.h>
+#include <linux/rwsem.h>
+#include <linux/tty.h>
+
+#include <net/genetlink.h>
+
+#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,
+};
--
2.40.1