An educational, open-source Linux rootkit demonstrating modern kernel-mode stealth on real 7.0 kernels — with detection documentation as a first-class deliverable.
All builds run in a Docker container — works on macOS and non-Linux hosts.
The container pulls the exact kernel headers for the chosen Ubuntu release
so the module is always compiled against the right KDIR.
$ git clone https://github.com/exec/rootkat $ cd rootkat $ ./scripts/build.sh
$ UBUNTU_VERSION=24.04 ./scripts/build.sh # Force container rebuild: $ BUILD_IMAGE_FORCE=1 ./scripts/build.sh
# Requires root + kernel with no module signing enforcement # insmod lkm/rootkat.ko # cat /proc/modules | grep rootkat # → nothing (self-hidden) # rmmod rootkat
Requires Docker + buildx. Colima users: brew install docker-buildx once.
The Makefile requires ROOTKAT_I_UNDERSTAND=1 to build — intentional friction that build.sh sets for you.
14 stealth techniques across three layers — LKM, CO-RE eBPF, and Rust.
Every technique ships with matching detection artifacts in docs/threat-model.md.
ftrace hooks on m_show and filldir64 remove rootkat from /proc/modules, lsmod, and /sys/module/ enumeration.
Magic signal (kill(0, 64)) rewrites all credential fields of the calling process to root using prepare_creds / commit_creds.
Adds a PID to a spinlock-protected hidden list. filldir64 skips matching entries so the process disappears from ps, /proc, and top.
Hooks tcp4/6_seq_show, udp4/6_seq_show, and inet_sk_diag_fill (NETLINK_SOCK_DIAG) — hides ports from both /proc/net/tcp and ss.
ftrace hook on vprintk_emit scans every log line before it reaches the ring buffer — any line containing "rootkat" is dropped before dmesg, /dev/kmsg, or journald see it.
CO-RE BPF program on lsm/file_open returns -ENOENT for registered paths — portable across kernel versions via BTF relocations.
Hooks both unix_seq_show and the module-private sk_diag_fill in unix_diag — sockets with a .rootkat path prefix are invisible to ss -lx and /proc/net/unix.
Hooks io_issue_sqe — an IORING_OP_NOP SQE with a magic user_data prefix triggers privesc / hide-pid / hide-port without a kill syscall.
Registers a NF_INET_PRE_ROUTING hook — a 16-byte magic UDP frame reaches the rootkit from any source with no open listener required.
rootkat_rust_canary.ko is a Rust LKM that exports rootkat_canary_tick() via EXPORT_SYMBOL_GPL. rootkat.ko calls it weak-linked — graceful degradation when the Rust LKM is absent.
All three channels call into the same magic_actions.c registry —
one implementation, three delivery paths. The teaching artifact: an auditd
ruleset watching kill catches the first but not the second or third.
# ── Channel 1: magic kill(2) signals ───────────────────────────────── $ kill -64 0 # SIGRTMAX — escalate calling process to root $ kill -63 0 # SIGRTMAX-1 — add calling PID to hidden list $ kill -62 4444 # SIGRTMAX-2 — hide local port 4444 from /proc/net/tcp # ── Channel 2: io_uring IORING_OP_NOP ──────────────────────────────── # SQE.user_data top 32 bits = 0x72_6b_61_74 ("rkat"), low byte = action # submitted via io_uring_enter(2) — invisible to kill auditing # ── Channel 3: inbound UDP magic frame ─────────────────────────────── # Send a 16-byte frame starting with "rootkat\0" to any port on the target $ printf 'rootkat\x00\x01\x00\x00\x00\x00\x00\x00\x00' | nc -uq1 target 1234
CI matrix: Ubuntu 26.04 / kernel 7.0 (13/13 tests pass) and Ubuntu 24.04 LTS /
kernel 6.x (12/13 — Rust canary skipped, no linux-lib-rust on 6.x).
A cross-distro survey on Debian 13 (6.12), Fedora 42 (6.14), and Ubuntu 24.04 (6.8)
is also green post--fno-optimize-sibling-calls.
| Feature | Mechanism | CI |
|---|---|---|
Self-hide from /proc/modules |
ftrace → m_show | ✓ both |
Self-hide from /sys/module/ |
ftrace → filldir64 | ✓ both |
| File hide | CO-RE eBPF lsm/file_open | ✓ both |
| Privesc to root | ftrace → __x64_sys_kill | ✓ both |
| Process hide | ftrace → filldir64 | ✓ both |
| TCP port hide (v4 + v6, /proc + ss) | tcp4/6_seq_show · inet_sk_diag_fill | ✓ both |
| UDP port hide (v4 + v6) | udp4/6_seq_show | ✓ both |
| AF_UNIX socket hide | unix_seq_show · unix_diag sk_diag_fill | ✓ both |
| BPF program self-hide | ftrace → __x64_sys_bpf (BPF_PROG_GET_NEXT_ID) | ✓ both |
| Audit log suppression | ftrace → audit_log_start | code-only |
| io_uring covert channel | ftrace → io_issue_sqe | ✓ both |
| Netfilter covert channel | nf_register_net_hook (NF_INET_PRE_ROUTING) | ✓ both |
| dmesg / printk self-hide | ftrace → vprintk_emit | ✓ both |
| Rust canary (cross-module) | EXPORT_SYMBOL_GPL stub + weak-link | ✓ 7.0 only |
Each layer does what the others structurally cannot. The stack is intentionally thin — every line exists to teach one thing.
ftrace IPMODIFY hooks with a kprobe-bootstrapped kallsyms_lookup_name (unexported since 5.7). Multi-candidate resolver handles symbol rename drift across kernel versions. Does the things eBPF cannot: kernel-text rewriting, self-hiding, syscall return-value patching.
LSM hooks loaded via libbpf with BTF-based CO-RE relocations — survives rebuilds across kernel versions without recompilation. Today: one program (lsm/file_open file hide). Intentionally narrow because most rootkit mechanics need text-rewriting.
rootkat_rust_canary.ko exports rootkat_canary_tick() and rootkat_canary_value() via EXPORT_SYMBOL_GPL C stubs. The C LKM calls them __attribute__((weak)) — graceful degradation on kernels without Rust support. Template for further C→Rust ports.
Each tests/qemu/test_*.sh boots a real kernel-7.0 cloud image with cloud-init, mounts the project via virtio-9p, and asserts behaviour inside the VM. CI exercises the full suite on both Ubuntu 26.04/7.0 and Ubuntu 24.04 LTS/6.x.
docs/threat-model.md ships alongside every stealth feature — not as an
afterthought. Every hook has a corresponding detection artifact documented: ftrace
entries in /sys/kernel/debug/tracing/enabled_functions, kernel audit
records, forensic filesystem artifacts, and behavioral anomalies.
A defender who reads the threat model can build a rootkat detector in an afternoon. That's the point.
# Sample detection artifacts # ftrace hooks are always observable: $ cat /sys/kernel/debug/tracing/enabled_functions m_show [rootkat] filldir64 [rootkat] __x64_sys_kill [rootkat] vprintk_emit [rootkat] ← even the printk hook can't hide itself here # Module still visible in kernel's internal structures: $ grep rootkat /proc/kallsyms # all symbols still exported $ ls /sys/kernel/debug/tracing/ # trace dir still present