Linux Persistence: Rootkits

2025-04-15 DFIR persistence rootkit LKM linux persistence LKM rootkit LD_PRELOAD kprobe ftrace ld.so hooking

Linux Persistence: Rootkits

Rootkits are one of the deepest and most complex forms of persistence. This post will only scratch the surface, but will walk through the main categories, techniques, and practical detection advice.

Rootkits focus on stealth, persistence, or both. Some are built solely to hide activity (processes, files, network activity), while others provide persistent backdoors or easy privilege escalation. Many rootkits provide both–providing a full-fledged post-exploitation solution.

While there are many techniques used to implement them, most rootkits fall into a few main categories: binary replacement, LD_PRELOAD and dynamic linker abuse, Loadable Kernel Modules (LKMs), and debugging/tracing framework abuse (ftrace, kprobes, eBPF).

In practice, most rootkits are either built on or inspired by a handful of open-source projects–like Diamorphine, Reptile, Adore, vlany, JynxKit, t0rn rootkit, etc. Using open-source tooling offers attackers rapid development and plausible deniability.

Despite the wide variety of implementations, most rootkits share similar goals:

  • Hide files and directories belonging to the attacker.

  • Limit visibility of the attacker’s processes and network traffic from systems administrators and defenders.

  • Offer a backdoor back into the system, even if the administrator removes the initial entry vector.

  • Provide an easy way for the attacker to elevate privileges to the root user.

Most rootkits achieve this functionality by using the same, recycled methods and hooking predictable targets: file and process enumeration (getdents, readdir, …), system calls like open/read/write, and network-related functions and structures.

Some rootkits are easy–or even trivial–to detect or neutralize. The more advanced ones can be extremely difficult to identify and remove once they have been loaded, especially if defenders aren’t sure what to look for.

In this post, we will walk through the major types of Linux rootkits, how they’re typically implemented, and what defenders can do to detect or prevent them.

Types of Rootkits

Rootkits can be broadly categorized by the privilege level they operate at: Ring 3 (user mode), and Ring 0 (kernel mode).

The “ring” terminology comes from “protection ring” architecture which defines four privilege levels, or rings, ranging from Ring 0 (highest privilege) to Ring 3 (least privileged).

Rings 1 and 2 are defined in hardware but are rarely used in practice. They were originally intended for components like device drivers, system libraries, or hypervisors that required more privilege than Ring 3 provides, but less than the full privileges handled by Ring 0. In most modern systems, these use cases are typically handled in Ring 0 for simplicity, leaving a split between kernel and user space.

This distinction matters: rootkits operating in Ring 0 have direct access to kernel memory and execution, making them far more powerful–and often harder to detect–than their Ring 3 brethren.

See also: https://en.wikipedia.org/wiki/Protection_ring

Ring 3: User mode

Ring 3 rootkits operate in user space, without modifying the kernel. They achieve their goals by hijacking or subverting userland components–like system binaries, shared libraries, or running processes.

While generally less powerful than kernel-mode rootkits, userland rootkits tend to be easier to implement, more portable across systems, and are often more than enough to hide activity and maintain access.

Binary Replacement

Popular in the 1980’s through early 2000’s, binary replacement rootkits work by swapping out common system utilities–like ls, ps, and netstat–with trojanized versions that filter out the attacker’s activities. These modified binaries hide specific files, directories, processes, or network connections from users and administrators.

This technique is still occasionally seen in the wild today by serious actors, but has largely fallen out of favor due to its ease of detection. Modern systems provide built-in functionality to verify file integrity through package management tools or file hashing utilities, making tampered binaries relatively easy to spot.

Additionally, systems now receive frequent and often unattended updates–a largely manual task in the 80’s and 90’s. If an update replaces a binary that was previously swapped by the rootkit, it can decloak the attacker. As a result, more sophisticated techniques–such as LD_PRELOAD or LKM-based rootkits are now preferred.

Working source code for these kits is increasingly difficult to find, likely due to age and declining relevance. Notable historical examples of binary replacement rootkits include:

Despite their simplicity and ease of detection, binary replacement rootkits are straightforward to develop and can still be surprisingly effective under the right circumstances.

Process-level Hiding

Some attacker tools include logic to shut down, pause, or hide parts of their functionality when they detect that they may be under observation. Rather than modifying system binaries or hooking libraries, these rootkits rely on runtime behavioral tricks to reduce their visibility.

For example, an implant may spawn a thread that monitors for user logins or terminal activity, shutting itself down or going dormant when administrators are active.

shellgame

One of the best public examples of this type of rootkit is shellgame. Shellgame monitors directory access using inotify, and when it detects tools such as find traversing paths containing files it wishes to hide, it temporarily unlinks the files. Once the scanning process exits, the files are re-linked.

This allows the hidden files to evade indexing tools such as find or updatedb without needing to patch system calls or tamper with binaries.

ptrace

Another process-level technique is abusing ptrace to inject code into running processes. While not typically designed for stealth, this method can be extended to implement runtime hooks or in-process tampering.

One example is 3snake, which injects into sensitive binaries like sudo, su, or SSH-related tools in order to harvest credentials. Although 3snake is focused on credential theft, similar approaches can be used to create userland rootkits that hide processes, manipulate outputs, or evade specific tools without modifying anything on disk.

Shared Object Abuse

Instead of replacing entire binaries or modifying the kernel, rootkits may target the dynamic linking process itself. These rootkits typically inject malicious code by exploiting how shared libraries are loaded–either by replacing them directly, abusing the configuration of the dynamic linker, or hijacking the dynamic linker entirely.

These techniques are purely userland and can be highly effective.

Library Replacement

Attackers can replace legitimate shared libraries with malicious versions or create proxy libraries that wrap and forward legitimate functionality while performing additional tasks.

While this method is technically simple, it is rarely seen in practice. Like binary replacement rootkits, library replacement is easy to detect–either via package manager verification or checksums–and is generally considered inferior to stronger techniques like LD_PRELOAD or LKMs.

LD_PRELOAD

LD_PRELOAD-based rootkits are among the most common and practical userland rootkits. By specifying a custom shared object in the LD_PRELOAD environment variable, attackers can inject code into any dynamically-linked executable without modifying the underlying binaries themselves. This can be made persistent by configuring /etc/ld.so.preload to load the malicious binary into every new dynamically-linked process.

Some well-known examples include:

  • vlany
  • bdvl
  • BEURK
  • Azazel
  • JynxKit, JynxKit2
  • Umbreon

These rootkits typically hook system calls and libc functions, filtering the output to hide the attacker’s artifacts. They are highly portable and often continue to work even across system upgrades–so long as the affected tools remain dynamically-linked.

Dynamic Linker Hijacking

A more stealthy and invasive technique is to replace the dynamic linker itself. This approach is similar to LD_PRELOAD in outcome–code is injected into dynamically-linked executables, but does not rely on abusing features traditionally supplied by the dynamic linker. As a result, it avoids several common detection methods.

There are two main approaches to dynamic linker hijacking: modifying the PT_INTERP program header in ELF binaries to point to a malicious loader instead of the system’s default (e.g., /lib64/ld-linux-x86-64.so.2), or by replacing the existing dynamic linker altogether.

Tools like patchelf can be used to modify the PT_INTERP field. However, doing so changes the file’s checksum, making the tampering easily detectable.

Implementing a fully-functional malicious linker is also technically challenging. Writing a compliant ld.so clone that preserves normal program behavior while injecting malicious code is far more difficult than using LD_PRELOAD to implement simple function hooks. As a result, this technique is rarely seen in the wild.

Security researcher elfmaster has written extensively about these techniques and developed a tool named Shiva that demonstrates dynamic linker abuse:

Ring 0 (Kernel)

Ring 0–the kernel–is a powerful and popular place for rootkits to reside. Attackers with the ability to execute code at this level can fundamentally alter the behavior of the operating system itself. By introducing malicious code through Loadable Kernel Modules (LKMs) or hooking into debugging and tracing frameworks, they can hide their presence, intercept system calls, and maintain surreptitious access.

While Ring 0 rootkits are technically superior to their userland counterparts, they tend to be brittle. The Linux kernel changes frequently–both in its internal structures and exported symbols–which can easily break rootkits between versions. As a result, many kernel-level rootkits will only support specific kernel families (e.g., 2.6.x, 5.10.x, 6.x, …), and will fail to load or function properly outside of their expected environment.

Loadable Kernel Modules

The most common way to introduce malicious code into the kernel is with a Loadable Kernel Module (LKM). LKMs are intended to extend kernel functionality at runtime–often used for device drivers, filesystems, or protocol support–but they also provide a powerful interface for attackers to modify kernel behavior without rebooting or recompiling the kernel.

Once loaded, a malicious LKM can hook or patch critical kernel functions, intercept system calls, hide files and processes, or provide stealthy backdoor access. Since LKMs execute with full kernel privileges, their capabilities are only limited by the attacker’s imagination.

Some well-known open source LKM rootkits are:

  • Diamorphine

  • Reptile

  • Adore

  • suterusu

sys_call_table Patching

One classic LKM technique is syscall table patching. On Linux, sys_call_table is an in-memory structure that maps syscall numbers to their corresponding function addresses. An attacker can locate this table and overwrite selected entries (e.g., sys_read, sys_getdents, sys_execve) with malicious function pointers to tamper with system behavior.

This project of mine demonstrates a detection and protection mechanism for this concept. Upon loading, it copies sys_call_table, hooks finit_module() and compares the table’s contents after subsequent modules are loaded–alerting if any function pointers have changed and restoring the original handlers.

https://github.com/droberson/syscallslol

Hooking via Debugging and Tracing Frameworks

The Linux kernel provides several tracing and instrumentation mechanisms such as ftrace, kprobes, and eBPF. These frameworks are primarily intended for debugging, performance analysis, and visibility, but may also be abused by attackers to hook kernel functions.

While powerful, tracing-based hooks are generally considered to be inferior to traditional LKM rootkits. Their presence is easier to detect (e.g., via sysfs, procfs, or tracefs), and the frameworks themselves can often be disabled at runtime.

As a result, tracing frameworks are rarely used for persistent or stealthy rootkits in the wild. The main exception is eBPF, which is increasingly being explored for offensive use due to its flexibility and growing support.

ftrace

ftrace (function tracer) is a tracing framework built into the Linux Kernel that allows developers and administrators to trace function calls and other kernel activities for debugging and performance analysis.

Attackers can abuse ftrace by registering their own function hooks, allowing them to intercept or modify the behavior of selected kernel functions–without needing to patch memory directly or overwrite sys_call_table. This makes it a lower-friction alternative to traditional LKM rootkits.

However, ftrace-based rootkits are rare in the wild. Hooks set via ftrace are generally visible to system administrators, and ftrace itself can be disabled or flushed. That said, it remains an intriguing option for in-memory, ephemeral tooling, or as a component of a multi-layered stealth framework.

See also:

kprobe

kprobe is a Linux kernel debugging interface that allows developers to dynamically instrument kernel functions by placing probes at specific execution points. It enables observing, logging, and modifying kernel behavior without the need of recompiling the kernel or rebooting.

For attackers, kprobes offer another way to hook kernel functions and inject malicious logic. While not as widely used in production-grade rootkits, kprobe-based attacks have been demonstrated in several research and proof of concept projects.

See also:

eBPF

Extended Berkeley Packet Filter (eBPF) is a modern Linux Kernel technology that allows users to safely execute custom bytecode within the kernel. Originally designed for high-performance network filtering, it has since evolved into a general-purpose mechanism for observability, tracing, performance monitoring, and security tooling.

From a rootkit perspective, eBPF is especially attractive:

  • It allows in-kernel logic without the need for LKMs.

  • It works on many production systems without requiring kernel recompilation.

  • It can hook system calls.

  • It often flies under the radar of traditional rootkit detectors.

Due to its versatility, eBPF is increasingly being explored in offensive tooling.

See also:

Direct Memory Patching: /dev/mem, /dev/kmem, …

Some older rootkits and low-level tools exploit the ability to directly read and write kernel memory through /dev/mem or /dev/kmem. This allows the attacker to “hot patch” the running kernel–overwriting function pointers, data structures, or code regions in real time.

While extremely powerful, this technique is largely obsolete. Modern Linux kernels disable access to /dev/mem and /dev/kmem by default, or restrict it to specific ranges. It may still be viable on legacy systems, custom kernel builds, or embedded devices that expose these interfaces–intentionally or not.

Despite its age and the restrictions that have been imposed on these interfaces, this method remains conceptually important: it demonstrates that full Ring 0 control doesn’t always require LKMs or tracing frameworks.

See also:

Detecting Rootkits

By design, rootkits are built to evade detection and resist removal. Their entire purpose is to hide–whether that means concealing files, processes, or network activity, or even their own presence in memory. Despite their intentions and efforts to hide, detection isn’t hopeless. With the right knowledge, tools, and context, many rootkits can be decloaked, disrupted, or removed.

That said, detection is highly situational. Attempting to analyze or interfere with a rootkit can crash the system, trigger countermeasures, or leave the system in an unusable state–especially if you’re working with limited visibility from within a compromised host.

Things become especially difficult if the system was compromised before any logging or security tooling was in place. A successful rootkit will often blind syslog, audit trails, and userspace observation tools, leaving defenders in the dark. In many cases, especially when visibility is poor and trust in the system is lost, the safest and most reliable option is a full system rebuild.

Advanced attackers often layer their persistence mechanisms. Detecting and removing one implant doesn’t guarantee safety–others may be waiting dormant until the heat dies down.

The following sections walk through various detection strategies, ranging from quick checks to deeper forensic techniques. No single method is guaranteed to work, but you may get lucky or expose just enough of the rootkit to advance an investigation or mitigation effort.

dmesg, kern.log, and journalctl -k

These logs contain messages from the kernel’s logging ring buffer and are often the first place to look for signs of unusual activity. These logs are typically viewed with:

  • dmesg - Reads the current kernel ring buffer. This data is volatile and cleared when the system is rebooted, the ring buffer becomes full and begins wrapping, or is intentionally cleared.

  • journalctl -k - Shows kernel messages via the systemd journal.

  • /var/log/kern.log - Persistent kernel logs on non-systemd or rsyslog-based setups.

Be aware that attackers may clear or manipulate these logs to hide evidence. If possible, ensure kernel logs are shipped to a centralized logging facility off-host.

Things to look for:

  • Kernel taint flags (e.g., “kernel tainted” messages) indicating unsigned or out of tree modules being loaded.

  • Unexpected module load messages, especially from poorly-implemented rootkits that print debug output like “hooked sys_call_table[]” or “rootkit loaded successfully”.

  • Non-sequential timestamps or unexpected gaps.

Even small anomalies here can suggest deeper compromises.

/proc/sys/kernel/tainted

This procfs file indicates the taint status of the running kernel–a bitmask that indicates whether anything non-standard, unsupported, or potentially dangerous has occurred (e.g., loading unsigned modules, forcing operations, or encountering bugs).

Each bit in the integer value represents a specific reason for tainting. You can read the value with:

cat /proc/sys/kernel/tainted

This bitfield is documented here: https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html#tainted

A non-zero value doesn’t necessarily indicate that the system is compromised, but is often a red flag worth investigating–especially if you weren’t expecting any third-party or out-of-tree modules.

Comparing System.map With kallsyms

Comparing System.map to /proc/kallsyms can help detect certain types of kernel tampering such as syscall table hooks or function pointer redirection.

System.map is generated at kernel build time and contains a symbol-to-address map for the kernel. It tends to be located at:

  • /boot/System.map-VERSION for packaged kernels

  • /usr/lib/debug/boot/System.map-VERSION on Debian and Ubuntu systems with -dbg packages.

  • /usr/src/linux/System.map for custom-built kernels.

If your System.map file contains something like the following, you are looking at a placeholder file and must obtain the valid System.map using alternative methods (see your distribution’s documentation):

ffffffffffffffff B The real System.map is in the linux-image-<version>-dbg package

On Debian-like systems, installing the dbg package will supply System.map at /usr/lib/debug/boot/System.map-VERSION

Note: On systems with KASLR (Kernel Address Space Layout Randomization) enabled, the addresses in /proc/kallsyms will not match those found in System.map. You’ll need to calculate the KALSR offset and account for it.

To calculate the offset:

OFFSET = kallsysms_address - System.map_address

Once you’ve determined the offset, you may apply it to the System.map values prior to comparison with /proc/kallsyms. Mismatches after adjusting for KASLR may indicate tampering.

Boot into single-user mode

Some rootkits–especially those loaded through startup scripts, dynamically linked libraries, or runtime kernel modules may not be initialized if the system is booted into single-user mode.

To enter single-user mode, interrupt the boot process at the GRUB or LILO menu and edit the boot parameters appropriately. This varies depending on the boot loader used and the distribution, but common parameters are init=/bin/bash, single, or systemd.unit=rescue.target.

Due to the variability imposed by differences between distributions, refer to the official documentation for instructions on how to boot into single-user or rescue mode.

Single-user mode boots the system into a minimal environment, typically without networking, unnecessary drivers, or background services. It is intended for maintenance and recovery tasks and generally requires physical access or a hypervisor console.

Because many rootkits rely on features and tools that aren’t initialized in this mode, it can offer a safer environment for performing analysis and recovery. If the rootkit depends on services that don’t start in single-user mode, they will not activate–allowing you to inspect the system without interference from the rootkit.

USB/Live CD boot -> mount hard drive -> analysis

An alternative to single-user mode is booting the system from a trusted USB or Live CD/DVD image. This launches a clean environment from read-only media, allowing you to inspect the compromised system’s disks without interference from any rootkits or malicious processes.

Once booted, you can manually mount the affected system’s hard drive and perform analysis using your own known-good tools. Because the compromised OS isn’t running, this method can offer a more reliable and rootkit-free view of the filesystem.

However, this approach may not be viable in some cases. Complications such as RAID, LVM, or full disk encryption can make it difficult or impossible to mount disks without additional tooling, credentials, or recovery keys. Practicality will vary depending on system configuration.

Rootkit Default Settings

Most attackers don’t bother writing custom, bespoke rootkits from scratch–opting to deploy open-source or commodity rootkits with minimal (if any) modification. These defaults can become an advantage for defenders.

If you can identify the family of rootkit in use, you may be able to leverage its built-in control mechanisms to disable, unload, or decloak it. Many rootkits include command interfaces, backdoor triggers, or cleanup routines–especially during development or red team use.

For example, Reptile by default installs its control binary at /reptile/reptile_cmd. If the attacker left the default path unchanged, a defender could use that binary to disable or uncloak the rootkit.

Leveraging threat intelligence reports against data points surrounding the incident may reveal a particular threat actor or activity cluster, providing loose attribution to the attackers involved. Often, attackers will go through the trouble of modifying the rootkit once and use the same settings across multiple victims or operations.

Your mileage may vary, of course–but checking for default configurations or hard-coded strings can often provide quick wins.

Decloaking Network Listeners With bind() Brute Forcing

Many rootkits implement functionality to hide listening sockets from tools like netstat or ss, often by filtering entries in /proc/net/tcp. One way to detect hidden listeners is to attempt to bind to every TCP port and observing failures.

The process works like this:

  1. Write a program or script that attempts to bind() a socket to each port: 0-65535

  2. If bind() fails with EADDRINUSE, the port is in use.

  3. Cross-reference that port with /proc/net/tcp, netstat, ss, lsof or similar tools.

  4. If nothing shows up, you may have found a hidden listening socket.

This method is relatively simple and doesn’t rely on trusted system binaries. It works because the kernel won’t allow a second bind to an active port–even if it is being hidden in userspace.

This technique won’t detect connections bound to INADDR_ANY if the test client binds to specific interfaces, so it is worth testing against INADDR_ANY as well as specific interfaces.

Detecting Hidden Processes by Brute Forcing /proc/PID/

Similar to the bind() brute forcing method for sockets, defenders can detect hidden processes by iterating through the numeric entries in /proc and comparing them to the output of process enumeration tools like ps, top, or pgrep.

The idea is simple:

  1. Write a script or program that attempts to access /proc/PID directories for all possible PIDs. This is typically 1 through 4194304.

  2. For each PID found, attempt to open files like /proc/PID/cmdline, /proc/PID/exe, or /proc/PID/status.

  3. If the PID is accessible in /proc, but doesn’t appear in ps or similar tools, it may indicate a cloaked process.

  4. If access to a PID yields inconsistent or suspicious results–such as empty files, incomplete metadata, or strange errors–this may also be a sign of rootkit activity.

An alternative variation of this technique involves sending signals to every possible PID using kill(pid, 0). This won’t terminate the process–it checks whether the process exists and whether the caller has permission to signal it.

To implement this, replace step 2 from the process above with sending a signal instead of accessing files such as exe, status, or cmdline. If kill() is successful but the process doesn’t appear in a process list, this is suspicious.

Another alternative is to use ptrace()–if you are able to attach to a seemingly-non-existent process, you may have discovered a cloaked process.

Memory Forensics

A more advanced but powerful detection methodology involves capturing a memory dump of the system and analyzing it with memory forensics tools like Volatility. This allows the analyst to detect hidden processes, sockets, kernel modules, and other rootkit-related artifacts that may be invisible from within the running system.

While this approach requires more effort and preparation, it can reveal deeply hidden activity that userland tools and live analysis may miss entirely–especially against kernel-based rootkits.

See also:

Capturing memory from a live system is invasive and may trigger instability or rootkit countermeasures. Always use caution and test tooling in controlled environments before deploying on live systems.

Fighting Rootkits With Rootkits

One of the most potent and underused techniques for dealing with LKM-based rootkits is to fight them using the same tactics they rely on: loading your own trusted kernel modules to detect, decloak, or even forcibly remove them.

For example, many LKM-based rootkits implement cleanup logic to facilitate clean removal. This is especially common in red team tooling and development-stage rootkits, where being able to unload cleanly and without rebooting or tear down post-engagement infrastructure is operationally convenient.

Defenders can leverage cleanup or unload functionality by calling these functions directly from kernel space. I wrote about this technique in Defanging Linux LKM Rootkits With cleanup_module() with an accompanying PoC here: https://github.com/droberson/hammertime

Another defensive LKM, syscallslol, demonstrates detection and remediation of syscall table tampering. It compares the current sys_call_table against a known-good copy and restores any overwritten entries.

gokiller is another example. It hooks the mmap() syscall and watches for specific flags commonly used by Golang binaries. If matches, it copies the binary for forensic analysis and kills the associated process. At the time, Golang was relatively rare on production Linux systems but widely used by attackers in CTF and red team engagements–making it a practical behavioral heuristic.

Using LKMs defensively isn’t without risk–and may end up costing you your job–but in hostile environments where userland visibility is limited or compromised, fighting rootkits with rootkits can be very effective.

Bring Your Own Island: Static-Linked Binaries to Defeat LD_PRELOAD

On a compromised system, you can’t trust anything–not even common utilities like ls, ps, or netstat. Attackers may have installed rootkits, replaced binaries, or abused LD_PRELOAD to manipulate output. Any tools you rely on may be lying to you.

To get around this, bring your own trusted, statically-linked binaries. Static-linked binaries don’t rely on the system’s shared libraries, making them immune to LD_PRELOAD and binary replacement-based rootkits.

Building your own static-linked toolkits is a task in itself. Preferences and requirements can vary wildly, but BusyBox is a great starting point. Keeping a USB stick or ISO with statically-linked analysis tools is a wise technique for incident responders and systems administrators.

See also:

Filesystem Monitoring: Binaries and Configuration Files Targeted by Rootkits

Rootkits often modify a predicable set of binaries and configuration files to establish stealth or persistence. Monitoring these key files and directories with tools like file integrity monitoring, auditd, or an EDR can help detect rootkit installation or precursor activity.

Some common targets include:

  • /etc/ld.so.preload - LD_PRELOAD-based rootkits configure their malicious shared object here to ensure it is loaded into every dynamically-linked process. This file should almost always be empty or absent on healthy systems.

  • Kernel module directories (/lib/modules, modules.d``, etc.) - Malicious .ko` files or configuration changes may be dropped in these directories to ensure the rootkit is loaded at boot. Unexpected writes to these directories–especially from tools not tied to a package manager–should raise suspicion.

  • Common binaries (ps, ls, netstat, find, etc.) - Binary replacement rootkits target visibility tools. Hash these against known-good versions or check for anomalies in file size, timestamps, permissions, or unexpected dependencies.

Monitoring changes to these paths can provide early warning signs of compromise or highlight systems worth deeper inspection.

/sys, /proc, …

While many rootkits focus on hiding files and processes from user-facing tools like ls or ps, they often overlook deeper kernel-backed virtual filesystems like /proc and /sys.

These directories expose low-level kernel internals, and rootkits that fail to fully sanitize their presence can leave behind traces:

  • Strange entries in /sys/module

  • Undocumented or unexpected entries in /proc

  • Leftover tracepoints or probe registrations in /sys/kernel/debug

Since these filesystems are dynamically generated by the kernel, inconsistencies here often reflect live manipulation. A sharp eye or a well-written parser may spot rootkits that would otherwise go undetected by traditional tools.

Several of these virtual filesystem artifacts are explored in depth in The Art of Linux Kernel Rootkit.

Preventing Rootkits

Detection and removal of rootkits is notoriously difficult and risky. In many cases, by the time a rootkit is installed, it is already far too late–logs may be tampered with or erased, tools may be compromised, and your only viable recovery option may be a complete system rebuild.

Prevention is far more effective.

The measures below won’t stop all threats, but can significantly raise the bar for attackers. Hardening against rootkits is about reducing the surface area that they rely on: disabling module loading, removing unnecessary kernel features, restricting debugging interfaces, and placing restrictions on userland behavior.

Some of these strategies require compiling custom kernels, maintaining hardened configurations, and writing supporting code. They won’t be practical for every environment–but even partial adoption can go a long way.

As always, your mileage may vary.

Disable LKMs

Disabling Loadable Kernel Modules (LKMs) isn’t practical for many systems, but in hardened or highly-controlled environments, it can be a game-changer.

This involves building a custom kernel with only the necessary drivers, filesystems, and protocol support compiled in–omitting support for loadable modules entirely. Once LKMs are disabled, attackers can no longer simply insmod or modprobe a rootkit into memory.

The trade-off is operational complexity. An engineer must monitor for kernel-related patches, rebuilding and redeploying the kernel themselves when changes are needed. This is a maintenance burden–but for systems where security is paramount, it can be worthwhile.

Without LKM support, attackers would be forced to build custom rootkits directly into the kernel, install them, and reboot the system. This is far noisier and more difficult than slipping in a runtime-loaded module.

Enforcing LKM Signing

If disabling LKMs entirely isn’t feasible, enforcing module signing is a strong alternative. This ensures that only kernel modules signed with a trusted key can be loaded–blocking unsigned or tampered modules, including most commodity rootkits.

To use this functionality, the kernel must be built with module signature support (CONFIG_MODULE_SIG).

This won’t prevent all forms of rootkits, but it raises the bar significantly. Attackers would either need to compromise your signing infrastructure or escalate to full kernel replacement–both are much more difficult and detectable.

As with most hardening techniques, this adds operational friction: custom drivers or experimental modules will need to be signed, and package management workflows may need to be adjusted.

See also:

Patch the Dynamic Loader

The dynamic linker ld.so can be patched to remove or restrictthe LD_PRELOAD feature. While this is a powerful defensive measure, it is also a heavy lift–requiring you to compile and maintain your own version of the loader.

This approach is rarely practical for general-purpose systems, as the patched loader may be overwritten by system updates or break compatibility with legitimate software that relies on preloading shared objects.

That said, in hardened environments or controlled appliances, patching ld.so can be extremely effective. Defenders can:

  • Disable LD_PRELOAD entirely.

  • Restrict its usage to specific users.

  • Enforce allow-listed preload paths or file hashes for approved binaries.

This effectively eliminates an entire class of userland rootkits–forcing attackers to find more complex persistence methods.

Disable or Restrict Kernel Probes (kprobes, fprobes, …)

Both kprobes and fprobes are powerful tracing features in the Linux kernel that can be abused by attackers to hook and monitor kernel functions. While they serve legitimate debugging and observability purposes, they also provide mechanisms for rootkit deployment.

These probes can typically be enabled or disabled at runtime by the root user, and often leave visible traces in /proc, /sys. or tracefs. Their presence can serve as a red flag–especially if enabled on systems where no tracing is expected.

Attackers may attempt to hide or lock down probe infrastructure, but doing so can be noisy or brittle. If a defender tries to disable probes and receives unusual errors or behavior, that alone may indicate tampering.

For higher assurance, both kprobes and fprobes can be disabled at kernel compile time–completely removing the functionality from the system and eliminating this class of rootkit deployment.

See also:

Disabling ptrace With Yama

If debugging isn’t needed in production, it is wise to restrict ptrace using the Yama Linux Security Module. This eliminates an entire class of userland injection attacks–most of which rely on ptrace() to manipulate processes.

To enable basic restrictions:

echo 1 > /proc/sys/kernel/yama/ptrace_scope

This setting limits ptrace to parent-child relationships. Stricter modes (2 or 3) can enforce even tighter constraints.

Yama should be strongly considered for internet-facing systems, shared environments, or hardened infrastructure where debugging isn’t part of normal operations.

Conclusion

Rootkits represent one of the most dangerous and insidious forms of persistence on Linux systems. Once loaded, they can blind defenders, manipulate evidence, and bury deep into the system, leaving minimal traces.

Detection is possible but often difficult. Removal can be risky. Prevention is where defenders can provide the most leverage.

This post touched only on the surface of what’s possible with rootkits. New rootkits are always being developed, and older ones continue to evolve or be forked into even more powerful kits. Whether you’re building hardened infrastructure, responding to incidents, or researching malware, understanding how rootkits work is essential.


No notes link to this note