A simple Linux Loadable Kernel Module (LKM) rootkit targeting kernel 6.x, built as a security research and portfolio project. Demonstrates core rootkit techniques including syscall table hooking, module self-hiding, file and process concealment, and privilege escalation.
Disclaimer: This project is strictly for educational purposes and security research. Only run in an isolated virtual machine. Do not deploy on any system you do not own.
| Feature | Mechanism |
|---|---|
| Module self-hiding | Unlinks from kernel module list, removes /sys/module/ entry |
| File hiding | getdents64 hook strips entries prefixed rkveil_ |
| Process hiding | execve hook auto-hides processes by argv[0] prefix |
| Privilege escalation | kill hook triggers commit_creds on signal 64 |
On kernel >= 5.7, kallsyms_lookup_name() is no longer exported to modules. rk-veil resolves it at runtime by registering a kprobe on the symbol. The kernel fills kp.addr with the function address, which is then cast to a function pointer and used to locate sys_call_table directly.
On kernel >= 5.4, the syscall table page is marked read-only at the page table entry level, making the traditional CR0 WP-bit approach insufficient. rk-veil uses lookup_address() to find the PTE for the syscall table page and directly sets the _PAGE_RW bit before patching, restoring it afterward.
THIS_MODULE is unlinked from the kernel's doubly-linked module list, making it invisible to lsmod and /proc/modules. kobject_del() removes the /sys/module/ entry.
The getdents64 hook copies the dirent buffer into kernel space, strips any entries whose name starts with rkveil_ or whose numeric name matches a hidden PID, then copies the modified buffer back to userspace. Files and /proc/<pid> entries are both hidden this way.
The execve hook checks argv[0] before the new process image loads. If it starts with rkveil_, the PID is added to a hidden list after a successful exec. The exit_group hook removes the PID on process exit to prevent stale entries.
The kill hook intercepts signal 64 (an unused real-time signal) as a backdoor trigger. On receipt, prepare_creds() clones the calling process's credentials, all uid/gid fields are zeroed, and commit_creds() atomically swaps them in. The signal is consumed and never delivered.
- Linux kernel 6.x (tested on 6.2.0-25-generic)
linux-headers-$(uname -r)build-essential
makesudo insmod rk_veil.ko
sudo dmesg | grep rk-veil# Any file or directory prefixed with rkveil_ is hidden from ls, find, etc.
touch rkveil_secret.txt
ls | grep rkveil # returns nothing# Spawn any process with argv[0] prefixed rkveil_
(exec -a "rkveil_backdoor" sleep 60) &
ps aux | grep rkveil # returns nothingwhoami # vagrant (or any unprivileged user)
kill -64 1
whoami # root# Note: rmmod will fail after load since the module is hidden from the list.
# Reboot the VM to unload cleanly.
sudo rebootA full behavioural test suite is included:
sudo bash test.shTests cover module hiding, file hiding, process auto-hiding, multiple simultaneous hidden processes, privilege escalation, syscall table resolution, and normal signal passthrough.
Expected result: 18/18 passing.
| Kernel | Status |
|---|---|
| 6.2.x | Tested, working |
| 5.7 -- 6.1 | Should work (kprobe resolution supported, PTE bypass required) |
| < 5.7 | Not supported (kallsyms_lookup_name was exported, different approach needed) |
Vagrant + VirtualBox, bento/ubuntu-22.04 with kernel 6.2.0-25-generic.
A Vagrantfile is included in the repo. To get started:
vagrant up
vagrant sshAfter vagrant ssh, install the specific kernel version:
sudo apt update
sudo apt install -y linux-image-6.2.0-25-generic linux-headers-6.2.0-25-generic
sudo rebootVerify after reboot:
uname -r # should show 6.2.0-25-genericThis rootkit is detectable by:
| Detection Method | Description |
|---|---|
| Syscall table integrity checks | comparing live syscall table entries against known-good values |
| eBPF-based monitoring | tools like Falco or Tetragon can detect anomalous kernel function calls regardless of userspace hiding |
Direct /proc access |
hiding is listing-only; processes remain accessible by direct path if the PID is already known |
| KASLR bypass detection | the kprobe resolution technique leaves a brief kprobe registration window observable via /sys/kernel/debug/kprobes/ |
| Module list cross-referencing | comparing kobject entries in sysfs against the module list can reveal hidden modules |
- xcellerator's Linux Rootkits series
- Linux Kernel Module Programming Guide
- Caraxes — academic LKM rootkit