|
|
Message-ID: <20130807143151.GA4560@cachalot>
Date: Wed, 7 Aug 2013 18:31:51 +0400
From: Vasily Kulikov <segoon@...nwall.com>
To: Tetsuo Handa <penguin-kernel@...ove.SAKURA.ne.jp>
Cc: twaugh@...hat.com, amwang@...hat.com,
linux-security-module@...r.kernel.org,
kernel-hardening@...ts.openwall.com
Subject: Re: [RFC] KPortReserve : kernel version of portreserve utility
(CC'ed kernel-hardening)
Hi Tetsuo,
See my comments inlined,
On Sat, Aug 03, 2013 at 17:15 +0900, Tetsuo Handa wrote:
> Hello.
>
> There is a blog post regarding how to reliably reserve local port numbers at
> http://cyberelk.net/tim/2012/02/15/portreserve-systemd-solution/ . Recently I
> heard a trouble in a RHEL5 system where portreserve utility is not available.
>
> I wrote this trivial LSM module as a really race-proof solution. But I'm
> expecting that this functionality becomes available in a way that all users can
> use regardless of their skill to use SELinux/SMACK/TOMOYO/AppArmor.
>
> (Question 1) Should this functionality implemented as LSM module?
> (Question 2) If yes, should this functionality implemented as a part of Yama?
>
> Regards.
> --------------------
> >From cbc76e3955e01dc6e590af860830b888ce7cbd0b Mon Sep 17 00:00:00 2001
> From: Tetsuo Handa <penguin-kernel@...ove.SAKURA.ne.jp>
> Date: Sat, 3 Aug 2013 16:58:05 +0900
> Subject: [PATCH] KPortReserve : kernel version of portreserve utility
>
> This module reserves local port like /proc/sys/net/ipv4/ip_local_reserved_ports
> does, but this module is designed for stopping bind() requests with non-zero
> local port numbers from unwanted programs.
>
> Signed-off-by: Tetsuo Handa <penguin-kernel@...ove.SAKURA.ne.jp>
> ---
> security/Kconfig | 6 +
> security/Makefile | 2 +
> security/kportreserve/Kconfig | 35 ++++
> security/kportreserve/Makefile | 1 +
> security/kportreserve/kpr.c | 443 ++++++++++++++++++++++++++++++++++++++++
> 5 files changed, 487 insertions(+), 0 deletions(-)
> create mode 100644 security/kportreserve/Kconfig
> create mode 100644 security/kportreserve/Makefile
> create mode 100644 security/kportreserve/kpr.c
>
> diff --git a/security/Kconfig b/security/Kconfig
> index e9c6ac7..f4058ff 100644
> --- a/security/Kconfig
> +++ b/security/Kconfig
> @@ -122,6 +122,7 @@ source security/smack/Kconfig
> source security/tomoyo/Kconfig
> source security/apparmor/Kconfig
> source security/yama/Kconfig
> +source security/kportreserve/Kconfig
>
> source security/integrity/Kconfig
>
> @@ -132,6 +133,7 @@ choice
> default DEFAULT_SECURITY_TOMOYO if SECURITY_TOMOYO
> default DEFAULT_SECURITY_APPARMOR if SECURITY_APPARMOR
> default DEFAULT_SECURITY_YAMA if SECURITY_YAMA
> + default DEFAULT_SECURITY_KPR if SECURITY_KPR
> default DEFAULT_SECURITY_DAC
>
> help
> @@ -153,6 +155,9 @@ choice
> config DEFAULT_SECURITY_YAMA
> bool "Yama" if SECURITY_YAMA=y
>
> + config DEFAULT_SECURITY_KPR
> + bool "KPortReserve" if SECURITY_KPR=y
> +
> config DEFAULT_SECURITY_DAC
> bool "Unix Discretionary Access Controls"
>
> @@ -165,6 +170,7 @@ config DEFAULT_SECURITY
> default "tomoyo" if DEFAULT_SECURITY_TOMOYO
> default "apparmor" if DEFAULT_SECURITY_APPARMOR
> default "yama" if DEFAULT_SECURITY_YAMA
> + default "kpr" if DEFAULT_SECURITY_KPR
> default "" if DEFAULT_SECURITY_DAC
>
> endmenu
> diff --git a/security/Makefile b/security/Makefile
> index c26c81e..87f95cc 100644
> --- a/security/Makefile
> +++ b/security/Makefile
> @@ -8,6 +8,7 @@ subdir-$(CONFIG_SECURITY_SMACK) += smack
> subdir-$(CONFIG_SECURITY_TOMOYO) += tomoyo
> subdir-$(CONFIG_SECURITY_APPARMOR) += apparmor
> subdir-$(CONFIG_SECURITY_YAMA) += yama
> +subdir-$(CONFIG_SECURITY_KPR) += kportreserve
>
> # always enable default capabilities
> obj-y += commoncap.o
> @@ -23,6 +24,7 @@ obj-$(CONFIG_AUDIT) += lsm_audit.o
> obj-$(CONFIG_SECURITY_TOMOYO) += tomoyo/built-in.o
> obj-$(CONFIG_SECURITY_APPARMOR) += apparmor/built-in.o
> obj-$(CONFIG_SECURITY_YAMA) += yama/built-in.o
> +obj-$(CONFIG_SECURITY_KPR) += kportreserve/built-in.o
> obj-$(CONFIG_CGROUP_DEVICE) += device_cgroup.o
>
> # Object integrity file lists
> diff --git a/security/kportreserve/Kconfig b/security/kportreserve/Kconfig
> new file mode 100644
> index 0000000..73ad5bc
> --- /dev/null
> +++ b/security/kportreserve/Kconfig
> @@ -0,0 +1,35 @@
> +config SECURITY_KPR
> + bool "KPortReserve support"
> + depends on SECURITY
> + depends on PROC_FS
> + select SECURITY_NETWORK
> + default n
> + help
> + This selects local port reserving module which is similar to
> + /proc/sys/net/ipv4/ip_local_reserved_ports . But this module is
> + designed for stopping bind() requests with non-zero local port
> + numbers from unwanted programs using white list reservations.
> +
> + If you are unsure how to answer this question, answer N.
> +
> + Usage:
> +
> + Use "add $port $program" format to add reservation.
> + The $port is a single port number between 0 and 65535.
> + The $program is the content of /proc/self/exe in TOMOYO's pathname
> + representation rule (i.e. consists with only ASCII printable
> + characters, and seen from the current thread's namespace's root (e.g.
> + /var/chroot/bin/bash for /bin/bash running inside /var/chroot/
> + chrooted environment)). The <kernel> means kernel threads).
> + For example,
> +
> + # echo "add 10000 /bin/bash" > /proc/reserved_local_port
> + # echo "add 20000 <kernel>" > /proc/reserved_local_port
> +
> + allows bind() on port 10000 to /bin/bash and allows bind() on port
> + 20000 to kernel threads.
Wait, it restrict the port to a *program*, not a user/group/security
domain? It means *any* user may run this program and obtain the port.
Is it intentional behaviour? I guess it would be MUCH more useful to
allow some port to this specific user, NOT a program. For most daemons
we have separate user accounts (SELinux contexts in some distros,
whatever), so it makes sense to limit the port to a UID/GID (or LSM's
security context).
> +
> + Use "del $port $program" format to remove reservation.
> +
> + Note that only port numbers which have at least one reservation are
> + checked by this module.
> diff --git a/security/kportreserve/Makefile b/security/kportreserve/Makefile
> new file mode 100644
> index 0000000..6342521
> --- /dev/null
> +++ b/security/kportreserve/Makefile
> @@ -0,0 +1 @@
> +obj-$(CONFIG_SECURITY_KPR) := kpr.o
> diff --git a/security/kportreserve/kpr.c b/security/kportreserve/kpr.c
> new file mode 100644
> index 0000000..7b19d32
> --- /dev/null
> +++ b/security/kportreserve/kpr.c
> @@ -0,0 +1,443 @@
> +/*
> + * kpr.c - kernel version of portreserve.
> + */
> +#include <linux/security.h>
> +#include <linux/proc_fs.h>
> +#include <linux/vmalloc.h>
> +#include <linux/net.h>
> +#include <linux/inet.h>
> +#include <linux/uaccess.h>
> +#include <net/sock.h>
> +#include <net/ip.h>
> +#include <net/ipv6.h>
> +
> +/* Max length of a line. */
> +#define MAX_LINE_LEN 16384
> +
> +/* Port numbers with at least one whitelist element exists. */
> +static unsigned long reserved_port_map[65536 / BITS_PER_LONG];
> +
> +/* Whitelist element. */
> +struct reserved_port_entry {
> + struct list_head list;
> + const char *exe;
> + u16 port;
> +};
> +/* List of whitelist elements. */
> +static LIST_HEAD(reserved_port_list);
> +
> +/**
> + * kpr_encode - Encode binary string to ascii string.
> + *
> + * @str: String in binary format.
> + *
> + * Returns pointer to @str in ascii format on success, NULL otherwise.
> + *
> + * This function uses kzalloc(), so caller must kfree() if this function
> + * didn't return NULL.
> + */
> +static char *kpr_encode(const char *str)
> +{
> + int i;
> + int len = 0;
> + const char *p = str;
> + char *cp;
> + char *cp0;
> + const int str_len = strlen(str);
> + for (i = 0; i < str_len; i++) {
> + const unsigned char c = p[i];
> + if (c == '\\')
> + len += 2;
> + else if (c > ' ' && c < 127)
> + len++;
> + else
> + len += 4;
> + }
> + len++;
> + cp = kzalloc(len, GFP_KERNEL);
> + if (!cp)
> + return NULL;
> + cp0 = cp;
> + p = str;
> + for (i = 0; i < str_len; i++) {
> + const unsigned char c = p[i];
> + if (c == '\\') {
> + *cp++ = '\\';
> + *cp++ = '\\';
> + } else if (c > ' ' && c < 127) {
> + *cp++ = c;
> + } else {
> + *cp++ = '\\';
> + *cp++ = (c >> 6) + '0';
> + *cp++ = ((c >> 3) & 7) + '0';
> + *cp++ = (c & 7) + '0';
> + }
> + }
> + return cp0;
> +}
> +
> +/**
> + * kpr_realpath - Returns realpath(3) of the given pathname but ignores chroot'ed root.
> + *
> + * @path: Pointer to "struct path".
> + *
> + * Returns the realpath of the given @path on success, NULL otherwise.
> + *
> + * This function uses kzalloc(), so caller must kfree() if this function
> + * didn't return NULL.
> + */
> +static char *kpr_realpath(struct path *path)
> +{
> + char *buf = NULL;
> + char *name = NULL;
> + unsigned int buf_len = PAGE_SIZE / 2;
> + while (1) {
> + char *pos;
> + buf_len <<= 1;
> + kfree(buf);
> + buf = kmalloc(buf_len, GFP_KERNEL);
> + if (!buf)
> + break;
> + pos = d_absolute_path(path, buf, buf_len);
> + if (IS_ERR(pos))
> + continue;
d_absolute_path() may fail in case of not only ENOMEM, but also EINVAL.
In this case you'll OOM the kernel with too big kmalloc(). buf_len
should be limited with some (not too big) number.
> + name = kpr_encode(pos);
> + break;
> + }
> + kfree(buf);
> + return name;
> +}
> +
> +/**
> + * kpr_get_exe - Get kpr_realpath() of current process.
Probably it should be called kpr_realpath_current() or
current_kpr_realpath() then?
> + *
> + * Returns the kpr_realpath() of current process on success, NULL otherwise.
> + *
> + * This function uses kzalloc(), so the caller must kfree()
> + * if this function didn't return NULL.
> + */
> +static const char *kpr_get_exe(void)
> +{
> + struct mm_struct *mm = current->mm;
> + const char *cp = NULL;
> + if (current->flags & PF_KTHREAD)
> + return kstrdup("<kernel>", GFP_KERNEL);
> + if (mm) {
> + down_read(&mm->mmap_sem);
> + if (mm->exe_file)
> + cp = kpr_realpath(&mm->exe_file->f_path);
> + up_read(&mm->mmap_sem);
> + }
> + return cp;
> +}
> +
> +/**
> + * kpr_socket_bind - Check permission for bind().
> + *
> + * @sock: Pointer to "struct socket".
> + * @addr: Pointer to "struct sockaddr".
> + * @addr_len: Size of @addr.
> + *
> + * Returns 0 on success, negative value otherwise.
> + */
> +static int kpr_socket_bind(struct socket *sock, struct sockaddr *addr,
> + int addr_len)
> +{
> + const char *exe;
> + u16 port;
> + switch (sock->sk->sk_family) {
> + case PF_INET:
> + case PF_INET6:
> + break;
> + default:
> + return 0;
> + }
> + switch (sock->type) {
> + case SOCK_STREAM:
> + case SOCK_DGRAM:
> + break;
> + default:
> + return 0;
> + }
> + switch (addr->sa_family) {
> + case AF_INET:
> + if (addr_len < sizeof(struct sockaddr_in))
> + return 0;
> + port = ((struct sockaddr_in *) addr)->sin_port;
> + break;
> + case AF_INET6:
> + if (addr_len < SIN6_LEN_RFC2133)
> + return 0;
> + port = ((struct sockaddr_in6 *) addr)->sin6_port;
> + break;
> + default:
> + return 0;
> + }
> + port = ntohs(port);
> + if (!test_bit(port, reserved_port_map))
> + return 0;
> + exe = kpr_get_exe();
> + if (!exe) {
> + pr_warn("Unable to read /proc/self/exe . Rejecting bind(%u) request.\n",
> + port);
> + return -ENOMEM;
> + } else {
> + struct reserved_port_entry *ptr;
> + int ret = 0;
> + rcu_read_lock();
> + list_for_each_entry_rcu(ptr, &reserved_port_list, list) {
> + if (port != ptr->port)
> + continue;
> + if (strcmp(exe, ptr->exe)) {
> + ret = -EADDRINUSE;
> + continue;
> + }
> + ret = 0;
> + break;
> + }
> + rcu_read_unlock();
> + kfree(exe);
> + return ret;
> + }
> +}
> +
> +/**
> + * kpr_read - read() for /proc/reserved_local_port interface.
> + *
> + * @file: Pointer to "struct file".
> + * @buf: Pointer to buffer.
> + * @count: Size of @buf.
> + * @ppos: Offset of @file.
> + *
> + * Returns bytes read on success, negative value otherwise.
> + */
> +static ssize_t kpr_read(struct file *file, char __user *buf, size_t count,
> + loff_t *ppos)
> +{
> + ssize_t copied = 0;
> + int error = 0;
> + int record = 0;
> + loff_t offset = 0;
> + char *data = vmalloc(MAX_LINE_LEN);
> + if (!data)
> + return -ENOMEM;
> + while (1) {
> + struct reserved_port_entry *ptr;
> + int i = 0;
> + data[0] = '\0';
> + rcu_read_lock();
> + list_for_each_entry_rcu(ptr, &reserved_port_list, list) {
> + if (i++ < record)
> + continue;
> + snprintf(data, MAX_LINE_LEN - 1, "%u %s\n", ptr->port,
> + ptr->exe);
> + break;
> + }
> + rcu_read_unlock();
> + if (!data[0])
> + break;
> + for (i = 0; data[i]; i++) {
> + if (offset++ < *ppos)
> + continue;
> + if (put_user(data[i], buf)) {
> + error = -EFAULT;
> + break;
> + }
> + buf++;
> + copied++;
> + (*ppos)++;
> + }
> + record++;
> + }
> + vfree(data);
> + return copied ? copied : error;
> +}
> +
> +/**
> + * kpr_normalize_line - Format string.
> + *
> + * @buffer: The line to normalize.
> + *
> + * Returns nothing.
> + *
> + * Leading and trailing whitespaces are removed.
> + * Multiple whitespaces are packed into single space.
> + */
> +static void kpr_normalize_line(unsigned char *buffer)
> +{
> + unsigned char *sp = buffer;
> + unsigned char *dp = buffer;
> + bool first = true;
> + while (*sp && (*sp <= ' ' || *sp >= 127))
> + sp++;
> + while (*sp) {
> + if (!first)
> + *dp++ = ' ';
> + first = false;
> + while (*sp > ' ' && *sp < 127)
> + *dp++ = *sp++;
> + while (*sp && (*sp <= ' ' || *sp >= 127))
> + sp++;
> + }
> + *dp = '\0';
> +}
> +
> +/**
> + * kpr_find_entry - Find an existing entry.
> + *
> + * @port: Port number.
> + * @exe: Pathname. NULL for any.
> + *
> + * Returns pointer to existing entry if found, NULL otherwise.
> + */
> +static struct reserved_port_entry *kpr_find_entry(const u16 port,
> + const char *exe)
> +{
> + struct reserved_port_entry *ptr;
> + bool found = false;
> + rcu_read_lock();
> + list_for_each_entry_rcu(ptr, &reserved_port_list, list) {
> + if (port != ptr->port)
> + continue;
> + if (exe && strcmp(exe, ptr->exe))
> + continue;
> + found = true;
> + break;
> + }
> + rcu_read_unlock();
> + return found ? ptr : NULL;
> +}
> +
> +/**
> + * kpr_update_entry - Update the list of whitelist elements.
> + *
> + * @data: Line of data to parse.
> + *
> + * Returns 0 on success, negative value otherwise.
> + *
> + * Caller holds a mutex to protect from concurrent updates.
> + */
> +static int kpr_update_entry(const char *data)
> +{
> + struct reserved_port_entry *ptr;
> + unsigned int port;
> + if (sscanf(data, "add %u", &port) == 1 && port < 65536) {
> + const char *cp = strchr(data + 4, ' ');
> + if (!cp++ || strchr(cp, ' '))
> + return -EINVAL;
> + if (kpr_find_entry(port, cp))
> + return 0;
> + ptr = kzalloc(sizeof(*ptr), GFP_KERNEL);
> + if (!ptr)
> + return -ENOMEM;
> + ptr->port = (u16) port;
> + ptr->exe = kstrdup(cp, GFP_KERNEL);
> + if (!ptr->exe) {
> + kfree(ptr);
> + return -ENOMEM;
> + }
> + list_add_tail_rcu(&ptr->list, &reserved_port_list);
> + set_bit(ptr->port, reserved_port_map);
> + } else if (sscanf(data, "del %u", &port) == 1 && port < 65536) {
> + const char *cp = strchr(data + 4, ' ');
> + if (!cp++ || strchr(cp, ' '))
> + return -EINVAL;
> + ptr = kpr_find_entry(port, cp);
> + if (!ptr)
> + return 0;
> + list_del_rcu(&ptr->list);
> + synchronize_rcu();
> + kfree(ptr->exe);
> + kfree(ptr);
> + if (!kpr_find_entry(port, NULL))
> + clear_bit(ptr->port, reserved_port_map);
> + } else {
> + return -EINVAL;
> + }
> + return 0;
> +}
> +
> +/**
> + * kpr_write - write() for /proc/reserved_local_port interface.
> + *
> + * @file: Pointer to "struct file".
> + * @buf: Domainname to transit to.
> + * @count: Size of @buf.
> + * @ppos: Unused.
> + *
> + * Returns bytes parsed on success, negative value otherwise.
> + */
> +static ssize_t kpr_write(struct file *file, const char __user *buf,
> + size_t count, loff_t *ppos)
> +{
> + char *data;
> + ssize_t copied = 0;
> + int error;
> + if (!count)
> + return 0;
> + if (count > MAX_LINE_LEN - 1)
> + count = MAX_LINE_LEN - 1;
> + data = vmalloc(count + 1);
> + if (!data)
> + return -ENOMEM;
> + if (copy_from_user(data, buf, count)) {
> + error = -EFAULT;
> + goto out;
> + }
> + data[count] = '\0';
> + while (1) {
> + static DEFINE_MUTEX(lock);
> + char *cp = strchr(data, '\n');
> + int len;
> + if (!cp) {
> + error = -EINVAL;
> + break;
> + }
> + *cp = '\0';
> + len = strlen(data) + 1;
> + kpr_normalize_line(data);
> + if (mutex_lock_interruptible(&lock)) {
> + error = -EINTR;
> + break;
> + }
> + error = kpr_update_entry(data);
> + mutex_unlock(&lock);
> + if (error < 0)
> + break;
> + copied += len;
> + memmove(data, data + len, strlen(data + len) + 1);
> + }
> +out:
> + vfree(data);
> + return copied ? copied : error;
> +}
> +
> +/* List of hooks. */
> +static struct security_operations kpr_ops = {
> + .name = "kpr",
> + .socket_bind = kpr_socket_bind,
> +};
> +
> +/* Operations for /proc/reserved_local_port interface. */
> +static const struct file_operations kpr_operations = {
> + .write = kpr_write,
> + .read = kpr_read,
> +};
> +
> +/**
> + * kpr_init - Initialize this module.
> + *
> + * Returns 0 on success, negative value otherwise.
> + */
> +static int __init kpr_init(void)
> +{
> + if (!security_module_enable(&kpr_ops))
> + return 0;
> + if (!proc_create("reserved_local_port", 0644, NULL, &kpr_operations) ||
> + register_security(&kpr_ops))
> + panic("Failure registering kportreserve");
> + pr_info("KPortReserve initialized\n");
> + return 0;
> +}
> +
> +late_initcall(kpr_init);
> --
> 1.7.1
> --
> To unsubscribe from this list: send the line "unsubscribe linux-security-module" in
> the body of a message to majordomo@...r.kernel.org
> More majordomo info at http://vger.kernel.org/majordomo-info.html
Thanks,
--
Vasily Kulikov
http://www.openwall.com - bringing security into open computing environments
Powered by blists - more mailing lists
Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.