[RFC] KPortReserve : kernel version of portreserve utility
[Posted August 14, 2013 by jake]
| From: |
| Tetsuo Handa <penguin-kernel-AT-I-love.SAKURA.ne.jp> |
| To: |
| twaugh-AT-redhat.com, amwang-AT-redhat.com, linux-security-module-AT-vger.kernel.org |
| Subject: |
| [RFC] KPortReserve : kernel version of portreserve utility |
| Date: |
| Sat, 3 Aug 2013 17:15:32 +0900 |
| Message-ID: |
| <201308031715.FCH73469.OVOHQStOFMLJFF@I-love.SAKURA.ne.jp> |
| Archive-link: |
| Article, Thread
|
Hello.
There is a blog post regarding how to reliably reserve local port numbers at
http://cyberelk.net/tim/2012/02/15/portreserve-systemd-so... . 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@I-love.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@I-love.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.
+
+ 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;
+ name = kpr_encode(pos);
+ break;
+ }
+ kfree(buf);
+ return name;
+}
+
+/**
+ * kpr_get_exe - Get kpr_realpath() of current process.
+ *
+ * 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@vger.kernel.org
More majordomo info at http://vger.kernel.org/majordomo-info.html
(
Log in to post comments)