[PATCH] fs/proc: add mask_secrets to prevent sensitive information leakage.

From: zhanglin
Date: Mon May 09 2022 - 02:05:43 EST


There are about 17000+ packages exists on centos. After investigation on
10000+ pacakges, About 200+ commands support passing plain(or encrypted)
passwords through command line arguments. Those sensitive information are
exposed through a global readable interface: /proc/$pid/cmdline.

To prevent the leakcage, adding mask_secrets procfs entry will hook the
get_mm_cmdline()'s output and mask sensitive fields in /proc/$pid/cmdline
using repeating 'Z's.

Signed-off-by: zhanglin <zhang.lin16@xxxxxxxxxx>
---
fs/proc/Kconfig | 20 ++
fs/proc/Makefile | 1 +
fs/proc/base.c | 10 +
fs/proc/mask_secrets.c | 593 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 624 insertions(+)
create mode 100644 fs/proc/mask_secrets.c

diff --git a/fs/proc/Kconfig b/fs/proc/Kconfig
index c93000105..3e5ce7162 100644
--- a/fs/proc/Kconfig
+++ b/fs/proc/Kconfig
@@ -107,3 +107,23 @@ config PROC_PID_ARCH_STATUS
config PROC_CPU_RESCTRL
def_bool n
depends on PROC_FS
+
+config PROC_MASK_SECRETS
+ bool "mask secret fields in process cmdline"
+ default n
+ help
+ mask secret fields in process cmdline to prevent sensitive information
+ leakage. Enable this feature, credentials including username, passwords
+ will be masked with repeating 'Z'. "ZZZZZZ..." but no real sensitive
+ information will appear in /proc/$pid/cmdline. for example: useradd -rp
+ ZZZZZZ will appear in /proc/$pid/cmdline instead iif you run 'echo 1 >
+ /proc/mask_secrets/enabled && echo "+/usr/sbin/useradd:-p:--password" >
+ /proc/mask_secrets/cmdtab'.
+
+ Say Y if you want to enable this feature.
+ Enable/Disable: echo 1/0 > /proc/mask_secrets/enabled.
+ Add masking rules: echo '+${command}:--${secret_opt1}:-${secret_opt2}:...
+ ' > /proc/mask_secrets/cmdtab.
+ Remove masking rules: echo '-${command}' > /proc/mask_secrets/cmdtab.
+ Commands must be well written in absolute path form.
+
diff --git a/fs/proc/Makefile b/fs/proc/Makefile
index bd08616ed..06521b7ff 100644
--- a/fs/proc/Makefile
+++ b/fs/proc/Makefile
@@ -34,3 +34,4 @@ proc-$(CONFIG_PROC_VMCORE) += vmcore.o
proc-$(CONFIG_PRINTK) += kmsg.o
proc-$(CONFIG_PROC_PAGE_MONITOR) += page.o
proc-$(CONFIG_BOOT_CONFIG) += bootconfig.o
+proc-$(CONFIG_PROC_MASK_SECRETS) += mask_secrets.o
diff --git a/fs/proc/base.c b/fs/proc/base.c
index d89526cfe..9fe0de79a 100644
--- a/fs/proc/base.c
+++ b/fs/proc/base.c
@@ -103,6 +103,10 @@

#include "../../lib/kstrtox.h"

+#ifdef CONFIG_PROC_MASK_SECRETS
+extern size_t mask_secrets(struct mm_struct *mm, char __user *buf, size_t count, loff_t pos);
+#endif
+
/* NOTE:
* Implementing inode permission operations in /proc is almost
* certainly an error. Permission checks need to happen during
@@ -312,6 +316,12 @@ static ssize_t get_mm_cmdline(struct mm_struct *mm, char __user *buf,
if (count > arg_end - pos)
count = arg_end - pos;

+#ifdef CONFIG_PROC_MASK_SECRETS
+ len = mask_secrets(mm, buf, count, pos);
+ if (len > 0)
+ return len;
+#endif
+
page = (char *)__get_free_page(GFP_KERNEL);
if (!page)
return -ENOMEM;
diff --git a/fs/proc/mask_secrets.c b/fs/proc/mask_secrets.c
new file mode 100644
index 000000000..035230d1e
--- /dev/null
+++ b/fs/proc/mask_secrets.c
@@ -0,0 +1,593 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * linux/fs/proc/mask_secrets.c
+ *
+ * Copyright (C) 2022, 2022 zhanglin
+ *
+ * /proc/mask_secrets directory handling functions
+ */
+
+#include <linux/ctype.h>
+#include <linux/fs.h>
+#include <linux/init.h>
+#include <linux/hashtable.h>
+#include <linux/mm.h>
+#include <linux/proc_fs.h>
+#include <linux/seq_file.h>
+#include <linux/slab.h>
+
+#define CMDLINE_HASHTABSIZE 1024
+#define cmdline_hash(x) ((x) % CMDLINE_HASHTABSIZE)
+
+static const char *SECRET_SEPARATOR = ":";
+static const int MASK_SECRETS_ENABLED = 1;
+static const int MASK_SECRETS_DISABLED;
+static DEFINE_SPINLOCK(mask_secrets_enabled_spinlock);
+static int __rcu *mask_secrets_enabled __read_mostly = (int *)&MASK_SECRETS_DISABLED;
+static DEFINE_SPINLOCK(cmdline_hashtab_spinlock);
+static struct hlist_head __rcu cmdline_hashtab[CMDLINE_HASHTABSIZE] __read_mostly = {
+ [0 ... (CMDLINE_HASHTABSIZE-1)] = HLIST_HEAD_INIT };
+static struct kmem_cache *cmdline_hashtab_item_cachep;
+
+struct cmdline_hashtab_item {
+ struct hlist_node hlist;
+ char *cmdline;
+ char *progname;
+ char *secrets;
+};
+
+static int is_mask_secrets_enabled(void)
+{
+ int ret = 0;
+
+ rcu_read_lock();
+ ret = *(rcu_dereference(mask_secrets_enabled));
+ rcu_read_unlock();
+ return ret;
+}
+
+size_t mask_secrets(struct mm_struct *mm, char __user *buf,
+ size_t count, loff_t pos)
+{
+ unsigned long arg_start = 0;
+ unsigned long arg_end = 0;
+ int mask_arg_len = 0;
+ size_t remote_vm_copied = 0;
+ struct file *file = 0;
+ struct inode *inode = 0;
+ char *kbuf = 0;
+ char *progname = 0;
+ int proghash = -1;
+ int prog_found = 0;
+ char *mask_arg_start = 0;
+ char *mask_arg_end = 0;
+ struct cmdline_hashtab_item *chi = 0;
+ char *psecret = 0;
+ size_t psecret_len = 0;
+ char *pmask = 0;
+ size_t pmask_len = 0;
+ size_t size;
+ size_t total_copied = 0;
+ int err = 0;
+
+ if (!is_mask_secrets_enabled()) {
+ err = -EPERM;
+ goto exit_err;
+ }
+
+ spin_lock(&mm->arg_lock);
+ arg_start = mm->arg_start;
+ arg_end = mm->arg_end;
+ spin_unlock(&mm->arg_lock);
+ if (arg_start >= arg_end) {
+ err = -ERANGE;
+ goto exit_err;
+ }
+ mask_arg_len = arg_end - arg_start + 1;
+
+ file = get_mm_exe_file(mm);
+ if (!file) {
+ err = -ENOENT;
+ goto exit_err;
+ }
+ inode = file_inode(file);
+ if (!inode) {
+ err = -ENOENT;
+ goto exit_err;
+ }
+ proghash = cmdline_hash(inode->i_ino);
+ kbuf = kzalloc(max(PATH_MAX, mask_arg_len), GFP_KERNEL);
+ if (!kbuf) {
+ err = -ENOMEM;
+ goto exit_err;
+ }
+ progname = d_path(&file->f_path, kbuf, PATH_MAX);
+ if (IS_ERR_OR_NULL(progname)) {
+ err = -ENOENT;
+ goto cleanup_kbuf;
+ }
+
+ rcu_read_lock();
+ prog_found = 0;
+ hash_for_each_possible_rcu(cmdline_hashtab, chi, hlist, proghash)
+ if (strcmp(chi->progname, progname) == 0) {
+ prog_found = 1;
+ break;
+ }
+
+ if (!prog_found) {
+ rcu_read_unlock();
+ goto cleanup_kbuf;
+ }
+
+ mask_arg_start = kbuf;
+ mask_arg_end = mask_arg_start + (arg_end - arg_start);
+ remote_vm_copied = access_remote_vm(mm, arg_start, mask_arg_start, mask_arg_len, FOLL_ANON);
+ if (remote_vm_copied <= 0) {
+ rcu_read_unlock();
+ err = -EIO;
+ goto cleanup_kbuf;
+ }
+ /*skip progname */
+ for (pmask = mask_arg_start; *pmask && (pmask <= mask_arg_end); pmask++)
+ ;
+
+ if (!chi->secrets) {
+ rcu_read_unlock();
+ /*mask everything, such as: xxxconnect host port username password.*/
+ for (pmask = pmask + 1; (pmask <= mask_arg_end); pmask++)
+ for (; (pmask <= mask_arg_end) && (*pmask); pmask++)
+ *pmask = 'Z';
+ goto copydata;
+ }
+
+ for (pmask = pmask + 1; pmask <= mask_arg_end; pmask++) {
+ psecret = chi->secrets;
+ while (*psecret) {
+ psecret_len = strlen(psecret);
+ if (psecret_len < 2) {
+ rcu_read_unlock();
+ err = -EINVAL;
+ goto cleanup_kbuf;
+ }
+
+ if (strcmp(pmask, psecret) == 0) {
+ pmask += psecret_len + 1;
+ goto mask_secret;
+ }
+
+ if (strncmp(pmask, psecret, psecret_len) == 0) {
+ /*handle case: --password=xxxx */
+ if ((psecret[0] == '-') && (psecret[1] == '-'))
+ if (pmask[psecret_len] == '=') {
+ pmask += psecret_len + 1;
+ goto mask_secret;
+ }
+
+ if (psecret[0] == '-') {
+ /*handle case: -password=xxxx or -p=xxxx*/
+ if (pmask[psecret_len] == '=') {
+ pmask += psecret_len + 1;
+ goto mask_secret;
+ }
+
+ /*handle case: -pxxxx*/
+ if (psecret_len == 2) {
+ pmask += psecret_len;
+ goto mask_secret;
+ }
+ }
+ }
+
+ if (psecret_len == 2) {
+ pmask_len = strlen(pmask);
+ /*handle case: -yp xxxx, such as: useradd -rp xxxx*/
+ if ((pmask_len > 2) && (*pmask == '-')
+ && (pmask[pmask_len - 1] == psecret[1])) {
+ pmask += pmask_len + 1;
+ goto mask_secret;
+ }
+ }
+
+ psecret += psecret_len + 1;
+ }
+
+ pmask += strlen(pmask);
+ continue;
+
+mask_secret:
+ for (; (pmask <= mask_arg_end) && (*pmask); pmask++)
+ *pmask = 'Z';
+ }
+ rcu_read_unlock();
+
+copydata:
+ size = arg_end - pos;
+ size = min_t(size_t, size, count);
+ if (copy_to_user(buf, mask_arg_start + pos - arg_start, size))
+ goto cleanup_kbuf;
+
+ total_copied = size;
+
+cleanup_kbuf:
+ kfree(kbuf);
+
+exit_err:
+ return total_copied;
+}
+
+static int show_mask_secrets_enabled(struct seq_file *m, void *v)
+{
+ rcu_read_lock();
+ seq_printf(m, "%d\n", *(rcu_dereference(mask_secrets_enabled)));
+ rcu_read_unlock();
+
+ return 0;
+}
+
+static int open_mask_secrets_enabled(struct inode *inode, struct file *file)
+{
+ return single_open(file, show_mask_secrets_enabled, NULL);
+}
+
+static ssize_t write_mask_secrets_enabled(struct file *file, const char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ int val, old_val;
+ int err = kstrtoint_from_user(buf, count, 0, &val);
+
+ if (err)
+ return err;
+
+ if ((val != 0) && (val != 1))
+ return -EINVAL;
+
+ rcu_read_lock();
+ old_val = *(rcu_dereference(mask_secrets_enabled));
+ rcu_read_unlock();
+
+ if (val == old_val)
+ return count;
+ spin_lock(&mask_secrets_enabled_spinlock);
+ rcu_assign_pointer(mask_secrets_enabled,
+ val ? &MASK_SECRETS_ENABLED : &MASK_SECRETS_DISABLED);
+ spin_unlock(&mask_secrets_enabled_spinlock);
+ synchronize_rcu();
+
+ return count;
+}
+
+static const struct proc_ops mask_secrets_enabled_proc_ops = {
+ .proc_open = open_mask_secrets_enabled,
+ .proc_read = seq_read,
+ .proc_write = write_mask_secrets_enabled,
+ .proc_lseek = seq_lseek,
+ .proc_release = single_release,
+};
+
+
+static int show_mask_secrets_cmdtab(struct seq_file *m, void *v)
+{
+ struct cmdline_hashtab_item *chi = 0;
+ char *secret;
+ int proghash = 0;
+ int err = 0;
+
+ if (!is_mask_secrets_enabled()) {
+ err = -EPERM;
+ return err;
+ }
+
+ rcu_read_lock();
+ hash_for_each_rcu(cmdline_hashtab, proghash, chi, hlist) {
+ seq_printf(m, "[%04d]: %s", proghash, chi->progname);
+ if (chi->secrets) {
+ secret = chi->secrets;
+ while (*secret) {
+ seq_printf(m, ":%s", secret);
+ secret += strlen(secret) + 1;
+ }
+ }
+ seq_puts(m, "\n");
+ }
+ rcu_read_unlock();
+
+ return err;
+}
+
+static int open_mask_secrets_cmdtab(struct inode *inode, struct file *file)
+{
+ return single_open(file, show_mask_secrets_cmdtab, NULL);
+}
+
+static size_t serialize_cmdtab(char *buf)
+{
+ struct cmdline_hashtab_item *chi = 0;
+ size_t secrets_prefix_len = strlen("[xxxx]: ");
+ size_t secrets_cmdtab_len = 0;
+ char *secret = 0;
+ size_t secret_len = 0;
+ int proghash = 0;
+
+ rcu_read_lock();
+ secrets_cmdtab_len = 0;
+ hash_for_each_rcu(cmdline_hashtab, proghash, chi, hlist) {
+ if (buf)
+ sprintf(buf + secrets_cmdtab_len, "[%04d]: %s", proghash, chi->progname);
+ secrets_cmdtab_len += secrets_prefix_len + strlen(chi->progname);
+ if (chi->secrets) {
+ secret = chi->secrets;
+ while (*secret) {
+ if (buf)
+ sprintf(buf + secrets_cmdtab_len, ":%s", secret);
+ secret_len = strlen(secret);
+ secret += secret_len + 1;
+ secrets_cmdtab_len += secret_len + 1;
+ }
+ }
+ if (buf)
+ buf[secrets_cmdtab_len++] = '\n';
+ }
+ rcu_read_unlock();
+
+ return secrets_cmdtab_len;
+}
+
+static ssize_t read_mask_secrets_cmdtab(struct file *file, char __user *buf,
+ size_t len, loff_t *offset)
+{
+ char *secrets_cmdtab = 0;
+ size_t secrets_cmdtab_len = 0;
+ ssize_t ret = 0;
+
+ secrets_cmdtab_len = serialize_cmdtab(0);
+ secrets_cmdtab = kzalloc(secrets_cmdtab_len, GFP_KERNEL);
+ if (!secrets_cmdtab)
+ return 0;
+ secrets_cmdtab_len = serialize_cmdtab(secrets_cmdtab);
+
+ ret = simple_read_from_buffer(buf, len, offset,
+ secrets_cmdtab, secrets_cmdtab_len);
+
+ kfree(secrets_cmdtab);
+
+ return ret;
+}
+
+static int cmdline_hashtab_add(char *cmdline)
+{
+ struct cmdline_hashtab_item *chi = 0;
+ char *progname = 0, *progname_start = cmdline + 1;
+ char *secrets_start = 0;
+ char *secret = 0;
+ int secret_len = 0;
+ int proghash = -1;
+ struct file *file;
+ struct inode *inode;
+ int err = 0;
+
+ progname = strsep(&progname_start, SECRET_SEPARATOR);
+ if (progname == NULL) {
+ err = -EINVAL;
+ goto exit_err;
+ }
+ if (progname[0] != '/') {
+ err = -EINVAL;
+ goto exit_err;
+ }
+ secrets_start = progname_start;
+ if (secrets_start) {
+ secret = secrets_start + strlen(secrets_start) - 1;
+ while ((!isspace(*secret)) && (secret >= secrets_start))
+ secret--;
+ if (isspace(*secret) && (secret >= secrets_start)) {
+ err = -EINVAL;
+ goto exit_err;
+ }
+
+ while ((secret = strsep(&secrets_start, SECRET_SEPARATOR)) != NULL) {
+ secret_len = strlen(secret);
+ if (secret_len < 2) {
+ err = -EINVAL;
+ goto exit_err;
+ }
+ if (secret[0] != '-') {
+ err = -EINVAL;
+ goto exit_err;
+ }
+ }
+ secrets_start = progname_start;
+ }
+
+ file = filp_open(progname, O_PATH, 0);
+ if (IS_ERR(file)) {
+ err = -ENOENT;
+ goto exit_err;
+ }
+ inode = file_inode(file);
+ if (!inode) {
+ filp_close(file, 0);
+ err = -ENOENT;
+ goto exit_err;
+ }
+ proghash = cmdline_hash(inode->i_ino);
+ filp_close(file, 0);
+
+ rcu_read_lock();
+ hash_for_each_possible_rcu(cmdline_hashtab, chi, hlist, proghash)
+ if (strcmp(chi->progname, progname) == 0) {
+ rcu_read_unlock();
+ err = -EEXIST;
+ goto exit_err;
+ }
+ rcu_read_unlock();
+
+ chi = kmem_cache_zalloc(cmdline_hashtab_item_cachep, GFP_KERNEL);
+ if (!chi) {
+ err = -ENOMEM;
+ goto exit_err;
+ }
+ INIT_HLIST_NODE(&chi->hlist);
+ chi->cmdline = cmdline;
+ chi->progname = progname;
+ chi->secrets = secrets_start;
+
+ spin_lock(&cmdline_hashtab_spinlock);
+ hash_add_rcu(cmdline_hashtab, &chi->hlist, proghash);
+ spin_unlock(&cmdline_hashtab_spinlock);
+ synchronize_rcu();
+
+exit_err:
+ return err;
+}
+
+
+static int cmdline_hashtab_remove(char *cmdline)
+{
+ char *progname = cmdline + 1;
+ struct file *file = 0;
+ struct inode *inode = 0;
+ int proghash = 0;
+ struct cmdline_hashtab_item *chi = 0;
+ int err = 0;
+
+ if (progname[0] != '/')
+ goto exit_noent;
+
+ file = filp_open(progname, O_PATH, 0);
+ if (IS_ERR(file))
+ goto exit_noent;
+ inode = file_inode(file);
+ if (!inode) {
+ filp_close(file, 0);
+ goto exit_noent;
+ }
+ proghash = cmdline_hash(inode->i_ino);
+ filp_close(file, 0);
+
+ rcu_read_lock();
+ hash_for_each_possible_rcu(cmdline_hashtab, chi, hlist, proghash)
+ if (strcmp(chi->progname, progname) == 0) {
+ rcu_read_unlock();
+ goto prog_found;
+ }
+ rcu_read_unlock();
+
+exit_noent:
+ return (err = -ENOENT);
+
+prog_found:
+ spin_lock(&cmdline_hashtab_spinlock);
+ hash_del_rcu(&chi->hlist);
+ spin_unlock(&cmdline_hashtab_spinlock);
+ synchronize_rcu();
+ kfree(chi->cmdline);
+ kmem_cache_free(cmdline_hashtab_item_cachep, chi);
+ kfree(cmdline);
+ return err;
+}
+
+static ssize_t write_mask_secrets_cmdtab(struct file *file, const char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ char *kbuf = 0;
+ char *op = 0;
+ int err = 0;
+
+ if (count < 3) {
+ err = -EINVAL;
+ goto exit_err;
+ }
+
+ if (!is_mask_secrets_enabled()) {
+ err = -EPERM;
+ goto exit_err;
+ }
+
+ kbuf = kzalloc(count + 1, GFP_KERNEL);
+ if (!kbuf) {
+ err = -ENOMEM;
+ goto exit_err;
+ }
+
+ if (copy_from_user(kbuf, buf, count)) {
+ err = -EFAULT;
+ goto cleanup_kbuf;
+ }
+ kbuf[count - 1] = '\0';
+ kbuf[count] = '\0';
+
+ op = kbuf;
+ if ((*op != '+') && (*op != '-')) {
+ err = -EINVAL;
+ goto cleanup_kbuf;
+ }
+
+ if (op[0] == '+')
+ err = cmdline_hashtab_add(kbuf);
+ else
+ err = cmdline_hashtab_remove(kbuf);
+
+ if (err)
+ goto cleanup_kbuf;
+
+ return count;
+
+cleanup_kbuf:
+ kfree(kbuf);
+
+exit_err:
+ return err;
+}
+
+static const struct proc_ops mask_secrets_cmdtab_proc_ops = {
+ .proc_open = open_mask_secrets_cmdtab,
+ .proc_lseek = seq_lseek,
+ .proc_read = read_mask_secrets_cmdtab,
+ .proc_write = write_mask_secrets_cmdtab,
+ .proc_release = single_release,
+};
+
+static int __init proc_mask_secrets_init(void)
+{
+ struct proc_dir_entry *pde_mask_secrets = NULL;
+ struct proc_dir_entry *pde_mask_secrets_enabled = NULL;
+ struct proc_dir_entry *pde_mask_secrets_cmdtab = NULL;
+
+ pde_mask_secrets = proc_mkdir("mask_secrets", NULL);
+ if (!pde_mask_secrets)
+ goto exit_nomem;
+
+ pde_mask_secrets_enabled = proc_create("enabled", 0644,
+ pde_mask_secrets, &mask_secrets_enabled_proc_ops);
+ if (!pde_mask_secrets_enabled)
+ goto cleanup_pde_mask_secrets;
+
+ pde_mask_secrets_cmdtab = proc_create("cmdtab", 0644,
+ pde_mask_secrets, &mask_secrets_cmdtab_proc_ops);
+ if (!pde_mask_secrets_cmdtab)
+ goto cleanup_pde_mask_secrets_enabled;
+
+ cmdline_hashtab_item_cachep = kmem_cache_create("cmdline_hashtab_item_cachep",
+ sizeof(struct cmdline_hashtab_item), 0, 0, NULL);
+ if (!cmdline_hashtab_item_cachep)
+ goto cleanup_pde_mask_secrets_cmdtab;
+
+ *((int *)&MASK_SECRETS_ENABLED) = 1;
+ *((int *)&MASK_SECRETS_DISABLED) = 0;
+
+ return 0;
+
+cleanup_pde_mask_secrets_cmdtab:
+ proc_remove(pde_mask_secrets_cmdtab);
+
+cleanup_pde_mask_secrets_enabled:
+ proc_remove(pde_mask_secrets_enabled);
+
+cleanup_pde_mask_secrets:
+ proc_remove(pde_mask_secrets);
+
+exit_nomem:
+ return -ENOMEM;
+}
+fs_initcall(proc_mask_secrets_init);
--
2.17.1