|
|
Log in / Subscribe / Register

A free and open-source rootkit for Linux

By Daroc Alden
January 16, 2026

While there are several rootkits that target Linux, they have so far not fully embraced the open-source ethos typical of Linux software. Luckily, Matheus Alves has been working to remedy this lack by creating an open-source rootkit called Singularity for Linux systems. Users who feel their computers are too secure can install the Singularity kernel module in order to allow remote code execution, disable security features, and hide files and processes from normal administrative tools. Despite its many features, Singularity is not currently known to be in use in the wild — instead, it provides security researchers with a testbed to investigate new detection and evasion techniques.

Alves is quite emphatic about the research nature of Singularity, saying that its main purpose is to help drive security research forward by demonstrating what is currently possible. He calls for anyone using the software to "be a researcher, not a criminal", and to test it only on systems where they have explicit permission to test. If one did wish to use Singularity for nefarious purposes, however, the code is MIT licensed and freely available — using it in that way would only be a crime, not an instance of copyright infringement.

Getting its hooks into the kernel

The whole problem of how to obtain root permissions on a system and go about installing a kernel module is out of scope for Singularity; its focus is on how to maintain an undetected presence in the kernel once things have already been compromised. In order to do this, Singularity goes to a lot of trouble to present the illusion that the system hasn't been modified at all. It uses the kernel's existing Ftrace mechanism to hook into the functions that handle many system calls and change their responses to hide any sign of its presence.

Using Ftrace offers several advantages to the rootkit; most importantly, it means that the rootkit doesn't need to change the CPU trap-handling vector for system calls, which was one of the ways that some rootkits have been identified historically. It also avoids having to patch the kernel's functions directly — kernel functions already have hooks for Ftrace, so the rootkit doesn't need to perform its own ad-hoc modifications to the kernel's machine code, which might be detected. The Ftrace mechanism can be disabled at run time, of course — so Singularity helpfully enables it automatically and blocks any attempts to turn it off.

Singularity is concerned with hiding four classes of things: its own presence, the existence of attacker-controlled processes, network communication with those processes, and the files that those processes use. Hiding its own presence is actually fairly straightforward: when the kernel module is loaded, it resets the kernel's taint marker and removes itself from the list of active kernel modules. This also means that Singularity cannot be unloaded, since it doesn't appear in the normal interfaces that are used for unloading kernel modules. It also blocks the loading of subsequent kernel modules (although they will appear to load — they'll just silently fail). Consequently, Alves recommends experimenting with Singularity in a virtual machine.

Hiding processes

Hiding processes, on the other hand, is more complicated. The mechanism that Singularity uses starts by identifying and remembering which processes are supposed to be hidden. Singularity uses a single 32-entry array of process IDs to track attacker-controlled processes; this is because a more sophisticated data structure would introduce more opportunities for the rootkit to be caught, either by adding additional memory allocations that could be noticed, or by introducing delays whenever one of its hooked functions needs to check the list of hidden process IDs.

Singularity supports two ways to add processes to the list: by sending an unused signal, or by setting a special environment variable and launching a new process. To implement the former, it hooks the kill() system call to detect an unused signal (number 59, by default), quashes the signal, adds the target process to its internal list, and gives the process root permissions in the global namespace. This means that attacker-controlled processes can be added from inside containers, and automatically escape the container using their new root privileges. To handle the environment variable, the execve() system call is hooked in a similar way.

Once a process is in the list, attempts to send signal 0 (to check whether the process exists) are also intercepted, as are other system calls that could refer to the process, such as getpgid(), sched_getaffinity(), and others. The total number of processes on the system, as reported by sysinfo() is also decremented to keep things consistent. The process's files in /proc are hidden by Singularity's file-hiding code. That code is probably the trickiest part of the whole rootkit. The basic idea is to filter out hidden directory entries such that the filesystem appears to remain in a consistent state, but filesystem code is difficult to get right at the best of times.

Hiding files

When a program calls getdents(), the kernel fills the provided buffer with directory entries as normal. Then, Singularity's hook copies the buffer back from user memory, removes the hidden entries, puts the modified buffer back in user memory, and changes the return value of the system call to reflect the smaller number of directory entries returned. This slightly complicated process is because the kernel doesn't provide a good place for Singularity to inject a hook before the directory entries are written to user memory the first time. So, one potential way to identify the rootkit is to have another thread race with the attempt to read directory entries, trying to spot any that were removed.

Changing the number of returned directory entries alone would make the system appear to be in an inconsistent state, however. Directories in Linux filesystems are supposed to track the number of references to them; this includes the ".." references inside child directories. So, when hiding a directory, Singularity also needs to intercept calls to stat() in order to adjust the number of visible links to its parent directory.

Direct access to hidden directories, in the form of openat() and related system calls, is also made to fail. readlink() poses a special challenge because it resolves symbolic links without actually opening them; it has to be handled separately. In addition to the procfs files of hidden processes, Singularity also hides any directories matching a set of user-supplied patterns. By default, it hides things named "singularity", but the project's documentation suggests changing this in the build configuration, since otherwise detecting the rootkit becomes straightforward.

Despite this sophisticated file-hiding machinery, Singularity doesn't help against forensic examinations of a hard disk from another computer. If it isn't installed in the running kernel, it can't hide anything. Therefore, the documentation also recommends putting as many hidden files as possible onto temporary filesystems stored in RAM, so that they don't show up after the system is rebooted.

Another problem for the rootkit is files that contain traces of its presence, but that would raise eyebrows if they disappeared entirely. This includes things like the system log, but also files in procfs like kallsyms or enabled_functions that expose which kernel functions have had Ftrace probes attached. For those files, Singularity doesn't hide them at the filesystem level, but it does filter calls to read() to hide incriminating information.

Deciding which log lines are incriminating isn't a completely solved problem, though. Right now, Singularity relies on matching a set of known strings. This is another place where users will have to customize the build to avoid simple detection methods.

Hiding network activity

Even once an attacker's processes can hide themselves and their files, it is still usually desirable to communicate information back to a command-and-control server. Singularity will work to hide network connections using a specific TCP port (8081, by default), and hide packets sent to and from that port from packet captures. It supports both IPv4 and IPv6. Hiding the connections from tools like netstat uses the same filesystem-hiding code as before. Hiding things from packet captures requires hooking into the kernel's packet-receiving code.

On the other hand, this is another place where Singularity can't control the observations of uncompromised computers: if one is running a network tap on another computer, the packets to and from Singularity's hidden port will be totally visible.

The importance of compatibility

Singularity only supports x86 and x86_64, but it does support both 64-bit and 32-bit system call interfaces. This is important, because otherwise a 32-bit application running on top of a 64-bit kernel could potentially see different results, which would be suspicious. To avoid this, Singularity inserts all of the aforementioned Ftrace hooks twice, once on the 32-bit system call and once on the 64-bit system call. A generic wrapper function converts from the 32-bit calling convention to the 64-bit calling convention before forwarding to the actual implementation of the hook.

Singularity has been tested on a variety of 6.x kernels, including some versions shipped by Ubuntu, CentOS Stream, Debian, and Fedora. Since the tool primarily uses the Ftrace interface, it should be supported on most kernels — although since it interfaces with internal details of the kernel, there is always the chance that an update will break things.

The tool also comes bundled with a set of utility scripts for cleaning up evidence that it was installed in the first place. These include a script that mimics normal log-rotation behavior, except that it silently truncates the logs to hinder analysis; a script that securely shreds a source-code checkout in case the module was compiled locally; and a script that automatically configures the rootkit's module to be loaded on boot.

Overall, Singularity is remarkably sneaky. If someone didn't know what to look for, they would probably have trouble identifying that anything was amiss. The rootkit's biggest tell is probably the way that it prevents Ftrace from being disabled; if one writes "0" to /proc/sys/kernel/ftrace_enabled and the content of the file remains "1", that's a pretty clear sign that something is going on.

Readers interested in fixing that limitation are welcome to submit a pull request to the project; Alves is interested in receiving bug fixes, suggestions for new evasion techniques, and reports of working detection methods. The code itself is simple and modular, so it is relatively easy to adapt Singularity for one's own purposes. Perhaps having such a vivid demonstration of what is possible to do with a rootkit will inspire new, better detection or prevention methods.



to post comments

Stealth or anti-debug?

Posted Jan 16, 2026 18:29 UTC (Fri) by tux3 (subscriber, #101245) [Link] (4 responses)

This is nice. Unfortunately, I won't be able to daily drive this rootkit, as it doesn't seem compatible with DKMS!

Slightly more seriously, I'm a little surprised that it blocks modules and eBPF as an anti-detection feature. On one hand, some sort of antivirus might be able to find the suspicious hooks by loading a module or filter.
But it looks like the init_module hook just returns -ENOEXEC, that's bound to raise some alarms, too.

If the EDR calls home to the admin dashboard and says it failed to talk to its module, the user's device can fail some enterprise posture compliance thing and the machine won't be allowed to log in to the VPN or corporate SSO.
Or for desktop users, you will have a black screen after loading nvidia.ko... and actually you probably wouldn't suspect anything. Never mind, the stealth works in this case.

Stealth or anti-debug?

Posted Jan 16, 2026 23:15 UTC (Fri) by notriddle (subscriber, #130608) [Link]

They won't actually detect that the rootkit is present. So what? They're still going to reimage the machine, and then your payload won't be running any more.

Stealth or anti-debug?

Posted Jan 17, 2026 6:05 UTC (Sat) by wtarreau (subscriber, #51152) [Link]

I was thinking the same, some modules are loaded late after some manual operations (e.g. tun, loop etc) and seeing them fail when trying to mount an image or start a VPN would trigger deeper investigation trying to figure what's wrong with that machine.

Also, I was thinking that the code that deals with FS operation might have a tough work detecting accesses it needs to hide, and I suspect that such functions might be visible in "perf top" during heavy I/O. It's not to say that it would reveal it to the unsuspecting user, but those aware of these names might recognize the pattern.

In any case it's really nice to provide such a playground to demonstrate what can really happen and that intrusions are not science fiction.

Stealth or anti-debug?

Posted Jan 17, 2026 22:39 UTC (Sat) by matheuz (subscriber, #181907) [Link] (1 responses)

The hook in finit and init_module that returns -ENOEXEC is temporary. It exists only to block LKRG. However, a new feature will be committed to GitHub in the coming days or possibly within a week, which will bypass LKRG for privilege escalation.

Another point is that previously there was only a hook on finit and init_module to prevent other rootkit scanners that look for gaps in kernel memory from detecting it. In practice, they still fail to detect it. Even so, I will further improve module hiding using a technique that also avoids detection by LKM-based rootkit scanners.

The blocking of new modules is temporary, and this hook will be removed soon. The same applies to blocking certain eBPF operations. This is also temporary. Once I have more time to work on Singularity, eBPF operations that attempt to detect hidden processes or files will be bypassed as well.

That said, there will no longer be any behavioral changes related to these two modules.

Additionally, Singularity can bypass EDRs such as CrowdStrike Falcon, which is eBPF-based, Trend Micro EDR, which is LKM-based, Kaspersky, also LKM-based, Elastic Security (there is an article in the Singularity README explaining how to bypass it), and some other EDRs that I tested in my virtual machine.

LKRG

Posted Feb 5, 2026 6:03 UTC (Thu) by solardiz (guest, #35993) [Link]

LKRG co-maintainer here. Thank you for Singularity, it's helpful to have a reference open source kernel rootkit. We're tracking our stance on Singularity's bypass of LKRG, whether it matters (not yet fully relevant under our threat model), and what we're doing about it (already broke it in our git anyway), here: https://github.com/lkrg-org/lkrg/issues/455

ftrace_enabled

Posted Jan 16, 2026 21:22 UTC (Fri) by dud225 (subscriber, #114210) [Link] (4 responses)

if one writes "0" to /proc/sys/kernel/ftrace_enabled and the content of the file remains "1", that's a pretty clear sign that something is going on.
Naive suggestion : why not leveraging the same technique than for hidden files by catching read and write calls to that file and returning modified results?

ftrace_enabled

Posted Jan 16, 2026 22:52 UTC (Fri) by daroc (editor, #160859) [Link]

I don't see any reason why that wouldn't work; the project does accept pull requests, if you feel so inclined. You'd probably also need to intercept ftrace calls to make sure one can't add a new hook when ftrace is "disabled".

ftrace_enabled

Posted Jan 17, 2026 22:39 UTC (Sat) by matheuz (subscriber, #181907) [Link] (1 responses)

The claim that writing 0 to /proc/sys/kernel/ftrace_enabled while still reading 1 is a clear indicator of tampering is technically incorrect in this case. In Singularity, writes to /proc/sys/kernel/ftrace_enabled are explicitly intercepted, and the value provided by the user is stored and consistently reflected back on subsequent reads. As a result, there is no visible mismatch between what is written and what is read, and the file never unexpectedly returns 1 after writing 0.

This mechanism implements a fake disable of ftrace. From user space, ftrace appears to be properly disabled, since reading ftrace_enabled returns the expected value and no abnormal behavior is observed. Internally, however, ftrace remains fully operational. The internal state is determined by the intercepted write and tracked via internal flags, rather than relying on the real kernel ftrace toggle.

Additionally, when ftrace is in this fake-disabled state, access to tracing interfaces such as trace, trace_pipe, enabled_functions, and touched_functions is carefully controlled. Reads from trace return only static header information and no new events, while reads from trace_pipe block indefinitely without emitting trace data. This behavior closely matches that of a legitimately disabled ftrace subsystem and prevents the leakage of partial or suspicious output.

As a result, common detection techniques that rely on inconsistencies in ftrace_enabled, or on monitoring trace and trace_pipe for unexpected activity, are ineffective. The overall behavior remains coherent and indistinguishable from a normal ftrace disable operation, despite ftrace continuing to function internally.

ftrace_enabled

Posted Jan 23, 2026 4:14 UTC (Fri) by nevets (subscriber, #11875) [Link]

Disabling ftrace_enable only stops the function tracer. If you enable an event, trace and trace_pipe work normally. Thus if you enable both function tracer and events and disable ftrace_enable, you should only see events in trace_pipe. Anything else would be a sure sign that something is amiss.

ftrace_enabled

Posted Jan 18, 2026 4:35 UTC (Sun) by alison (subscriber, #63752) [Link]

A related question is, what happens if an investigator tries to use ftrace for debugging? Does regular ftrace still work? A lot of us would turn to ftrace early in any attempt to understand anomalies.

Another common test would be to check open ports on the host from a remote with nmap. That test would inevitably show that port 8081 is open.

Might dissidents also find Singularity valuable?

Posted Jan 18, 2026 4:49 UTC (Sun) by alison (subscriber, #63752) [Link]

Reading about Singularity's hiding on the filesystem reminds me of Benetech's Martus project:

https://martus.org/overview.html

In other words, might this sneaky rootkit be repurposed into a system which helps journalists and dissidents with life-or-death secrets to hide to conceal them on their system? Most of the needed pieces appear to be present, although a Martus-like system should also report the total storage capacity to be smaller than the actual amount. A security system for dissidents and journalists could reuse many of the components, but allow the user to deploy and control them.

Spectacularly bad choice of name

Posted Jan 20, 2026 2:38 UTC (Tue) by ScienceMan (subscriber, #122508) [Link] (4 responses)

The collision of naming with https://en.wikipedia.org/wiki/Singularity_(software) is unfortunate.

Spectacularly bad choice of name

Posted Jan 20, 2026 4:06 UTC (Tue) by edgewood (subscriber, #1123) [Link] (2 responses)

From that link, "In 2021 the community of Singularity open source project voted to rename itself to Apptainer."

So it seems like it gave up that name? In general I'm not sympathetic to projects that pick English words for the project name complaining when another project uses the same English word, but in this case the original project explicitly gave it up.

Spectacularly bad choice of name

Posted Jan 20, 2026 13:39 UTC (Tue) by leromarinvit (subscriber, #56850) [Link]

When I opened that Wikipedia link, I expected it to be about yet another piece of software named Singularity: https://en.wikipedia.org/wiki/Singularity_(operating_system)

Spectacularly bad choice of name

Posted Feb 6, 2026 0:18 UTC (Fri) by aphedges (subscriber, #171718) [Link]

More confusingly, Singularity has two forks: Apptainer (maintained under the Linux Foundation) and SingularityCE (maintained by a private company). I therefore wouldn't really say they "gave [the name] up".

Both are developed in parallel separately and have started to diverge, which is a bit annoying because there doesn't seem to be a consensus on which fork HPC clusters (where I've used these tools before) should follow.

Spectacularly bad choice of name

Posted Jan 20, 2026 17:14 UTC (Tue) by matheuz (subscriber, #181907) [Link]

@ScienceMan So many things to complain about and you're going to complain about the project's name? Lol, people like you are weird.

Hiding network

Posted Jan 20, 2026 12:58 UTC (Tue) by claudex (subscriber, #92510) [Link] (3 responses)

If I understand correctly, this will also hide legitimate network traffic and can be detected that way. If I run tcpdump -ni any port 8081 and run nc -v 3fff::1 8081, in parallel I won't see the traffic.

Hiding network

Posted Jan 20, 2026 17:15 UTC (Tue) by matheuz (subscriber, #181907) [Link] (2 responses)

Port 8081 is the default port, but you can choose whichever port you want; the project's README.md explicitly states that you can change the default port as well.

Hiding network

Posted Jan 20, 2026 18:08 UTC (Tue) by claudex (subscriber, #92510) [Link] (1 responses)

Yes of course, but my point was that it's easy to make a script to test all ports.

Hiding network

Posted Jan 21, 2026 21:18 UTC (Wed) by mathstuf (subscriber, #69389) [Link]

Couldn't the rootkit "notice" a port scan and "hop" around the "scanning front"?

Live kernel patching and root kits!

Posted Jan 23, 2026 3:46 UTC (Fri) by nevets (subscriber, #11875) [Link]

When I first added the fentry feature to ftrace, I said this would be great for live kernel patching and root kits!

But seriously, I have to try this out and see if I can figure out easy ways to detect it. The enabled_functions was one way to handle it. I wonder if we should add a sysrq trigger that also dumps it to console. Of course it could likely intercept that too.

This is something to think about.

Of course, I predicted this in 2007

Posted Jan 29, 2026 9:49 UTC (Thu) by davidgerard (guest, #100304) [Link]


Copyright © 2026, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds