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