Various scripts, implementing detections for linux LKM rookits.
The scripts are tested against:
- singularity rootkit by MatheuZSecurity, only public one that goes through the effort to tamper with dmesg output.
- kovid by carloslack
- beautifullies by bahorn (me), a poc to test detection bypasses.
Build the LKMs with:
just build-goat
just build-nitara2
just build-singularity
just build-kovid
just build-beautifullies # custom module to test detection bypass approaches
These are found in detections/ and can be ran with just batch-tests or just batch-tests-lkm (to include ones that load an LKM)
Optionally run them like the following to save a load command:
just batch-tests singularity # or kovid, beautifullies, etc
just batch-tests-lkm singularity
This loads an unsigned module, and tries to see if you get the message about how loading one taints the kernel. If you don't get this message, its a good indicator about how one is currrently already loaded. This idea was briefly mentioned in my article in tmp.0ut #4.
sudo python3 unsigned_loaded.py
Should say if one is probably loaded or not. Remember to reboot if you have already ran it.
This tool diffs the output between two ways of reading the kernel message buffer, as both aren't hooked in some rootkits. This one doesn't need the setup to be ran.
sudo python3 diff_devkmsg_klogctl.py
You shouldn't get many differences shown on clean systems (though its possible because of my poor attempt at line normalization and maybe log levels).
If you get lines like:
- missing systemd-journal entries, as singularity removes lines containing "journal".
- lines about loading modules.
- the whole buffer being in only one, as kovid's run.sh clears the buffer.
those are pretty good detection signals.
For example, if you load singularity after already running detect.py, you'll
get output like:
$ sudo python3 diff_devkmsg_klogctl.py
539,540d538
< systemd[1]: Listening on systemd-journald-dev-log.socket - Journal Socket (/dev/log).
< systemd[1]: Listening on systemd-journald.socket - Journal Socket.
549d546
< systemd[1]: Starting systemd-journald.service - Journal Service...
552d548
< systemd-journald[277]: Collecting audit messages is disabled.
567d562
< systemd[1]: Started systemd-journald.service - Journal Service.
569,570d563
< systemd-journald[277]: Received client request to flush runtime journal.
< systemd-journald[277]: File /var/log/journal/5b8d8f5c116e4bb68ecb9d786884a225/system.journal corrupted or uncleanly shut down, renaming and replacing.
598d590
< systemd-journald[277]: File /var/log/journal/5b8d8f5c116e4bb68ecb9d786884a225/user-1000.journal corrupted or uncleanly shut down, renaming and replacing.
600d591
< CPU: 0 PID: 997 Comm: sshd Not tainted 6.8.0-85-generic #85-Ubuntu
664d654
< [ 277] 0 277 10543 1090 288 800 2 94208 0 -250 systemd-journal
697d686
< goat: module verification failed: signature and/or required key missing - tainting kernel
700d688
< singularity: loading out-of-tree module taints kernel.
Singularity hooks init_module() to always
return 0, even for unpriv'd users in older versions.
So you can attempt to load an invalid module from any user and see if the return
code is 0, and if so you got a detection.
In newer versions, singularity always returns ENOEXEC for everyone, even unprived users. We'd expect EPERM for them, so catching that.
python3 hooked_insmod.py
Multiple ftrace based tests, checking:
- can we disable ftrace
- is disabling being faked
- tries to see if enabled functions are being hidden
- any sus functions in touched_functions
sudo python3 ftrace_func.py
Detection for singularity, based on how it currently attempts to fake if ftrace
is enabled or not.
You can write random contents to ftrace_enabled and it'll echo it back out,
when the write should otherwise fail.
sudo python3 bad_ftrace_value.py
Uses nitara2 to see if it detects anything. Singularity currently bypasses upstream nitara2, but the submodule include a patch to work around it. Kovid also does, no fix yet.
sudo nitara2.sh
Detects kovid by looking at if the kernels taint value is reset every 5 seconds.
sudo python3 pcrtest.py
Injects a seek() call between reading each byte of a file and compares it to
the size reading the file normally.
If they don't match, this implies something is filtering the file.
sudo python3 seek_stutter.py /path/to/file
Looks for a mismatch between the number of allocations created by
create_trampoline (an ftrace internal function) and the number of ftrace hooks
showing up in touched_functions.
Might have false positives, but currently detects singularity and beautifullies.
kovid doesn't hook reading this file unless you use cat.
Output is only currently meaningful on systems that have not used ftrace
normally this boot (see issue #1).
sudo python3 count_trampolines.py
When you use ftrace normally, you can see what called each function. This exposes some rootkits.
This works by setting up function tracing for several commonly hooked functions, and sees if anything unexpected is calling them.
sudo python3 function_trace_parents.py`
This detection is bypassable by using the USE_FENTRY_OFFSET in the various
derrivatives of xcellerator's ftrace hooking framework, as the thunk will not be
triggered twice when calling the original function.
These are found in tools/ and might be handy in some cases.
Uses writev() as a work around to disable basic attempts at stoping ftrace
from being disabled.
Run this if you can't load a module, or want to use the systems with the hooks all gone for deploying further forensics tools.
sudo python3 disable_ftrace.py
(the current version of singularity patched this specific syscall, but other disable'ing techniques exist. @ me if you need one)
Read a file one byte at a time to bypass some hooks.
sudo python3 1bt.py /path/to/file
cat but with readv() instead. just to bypass incomplete hooks.
sudo python3 catv.py /path/to/file
(written using claude 4.5, as the models can currently generate sample syscall usage code pretty well. if this breaks, regen with another read like syscall)
These tools take a filtered snapshot of /sys and /proc to see what the
rootkit changed.
Might be a bit too agressive with skipping files.
just baseline singularity # or whatever the rootkit you want to check
This will output a diff showing whats changed. Not all differences are interesting.
(baseline script was written with claude 4.5, with a lot of work done on the filters)
MIT for detections, GPL for the goat kernel module (if that is even copyrightable)
Using claude to write some scripts, as I'm writing this on my phone while travelling, which the output is not copywritable as far as I'm aware (and I am not going to sue you over it). The readme and comments should indicate where this applies and doesn't include a substantial amount on my own work. Steal those at will, etc.