v0.13

An educational, open-source Linux rootkit demonstrating modern kernel-mode stealth on real 7.0 kernels — with detection documentation as a first-class deliverable.

Linux 7.0+ C · eBPF · Rust ftrace IPMODIFY CO-RE BPF LSM MIT license

Build

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.

Ubuntu 26.04 / kernel 7.0 (default)
$ git clone https://github.com/exec/rootkat
$ cd rootkat
$ ./scripts/build.sh
Ubuntu 24.04 LTS / kernel 6.x
$ UBUNTU_VERSION=24.04 ./scripts/build.sh

# Force container rebuild:
$ BUILD_IMAGE_FORCE=1 ./scripts/build.sh
load and verify
# 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.

What it demonstrates

14 stealth techniques across three layers — LKM, CO-RE eBPF, and Rust. Every technique ships with matching detection artifacts in docs/threat-model.md.

👻

Module self-hide

ftrace hooks on m_show and filldir64 remove rootkat from /proc/modules, lsmod, and /sys/module/ enumeration.

⬆️

Privilege escalation

Magic signal (kill(0, 64)) rewrites all credential fields of the calling process to root using prepare_creds / commit_creds.

🕵️

Process hide

Adds a PID to a spinlock-protected hidden list. filldir64 skips matching entries so the process disappears from ps, /proc, and top.

🌐

Network port hide

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.

🔇

dmesg / printk self-hide

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.

📁

File hide via eBPF LSM

CO-RE BPF program on lsm/file_open returns -ENOENT for registered paths — portable across kernel versions via BTF relocations.

🔌

AF_UNIX socket hide

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.

🔄

io_uring covert channel

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.

📡

Netfilter covert channel

Registers a NF_INET_PRE_ROUTING hook — a 16-byte magic UDP frame reaches the rootkit from any source with no open listener required.

🦀

Rust cross-module canary

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.

Three ways to reach it

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

All techniques, CI-verified

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

Three-layer design

Each layer does what the others structurally cannot. The stack is intentionally thin — every line exists to teach one thing.

C Kernel module (LKM)

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.

eBPF CO-RE programs

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.

Rust Canary LKM

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.

bash QEMU test harness

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.

Built to be caught

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