Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [thread-next>] [day] [month] [year] [list]
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.