Concrete indicators derived from a real incident — a userland rootkit that hid an XMRig miner behind a fake Proxmox system service. Every signal below is something you can check on a host today, with the command and the reason it works.

1. Unexpected systemd units

A unit whose name and description look like platform software but which is not owned by any distribution package. In the case study the unit was PVE-1, described as "Proxmox VE node health monitor" — plausible until you look at where it lives and what it runs.

# List every enabled unit, then diff against package-owned units.
systemctl list-unit-files --state=enabled

# A package-owned unit can be traced back:
dpkg -S /lib/systemd/system/<name>.service     # Debian/Ubuntu
rpm -qf /usr/lib/systemd/system/<name>.service # RHEL family

# A unit with NO owning package is suspect.

Also check for units outside the standard directories:

find /etc/systemd /usr/lib/systemd /lib/systemd -name '*.service' -newer /etc/hostname

2. LD_PRELOAD outside distribution units

The rootkit scoped its preload per-unit rather than globally (the attacker's own comment explained that a global /etc/ld.so.preload breaks sshd). That makes unit-level LD_PRELOAD the tell.

grep -rn "LD_PRELOAD" /etc/systemd /usr/lib/systemd /lib/systemd 2>/dev/null

# Also check the login-path files the rootkit used:
grep -rn "LD_PRELOAD" /etc/environment /etc/profile /etc/profile.d/ 2>/dev/null

Any hit that is not systemd, snapd, or a known vendor is worth investigating.

3. Processes running from improbable paths

Mining binaries do not belong under /var/lib/systemd/ or /tmp or a hidden dot-directory. Read the real executable path from /proc, not from ps (which the rootkit filtered).

# exe path and cmdline straight from the kernel, bypassing ps:
for p in /proc/[0-9]*; do
  exe=$(readlink "$p/exe" 2>/dev/null)
  case "$exe" in
    */var/lib/*|*/tmp/*|*/.cache/*|*/.hide/*) echo "$p -> $exe";;
  esac
done

4. Mining-pool outbound connections

A hypervisor has no reason to talk to a mining pool. Watch for the common ports regardless of destination.

ss -tunap | grep -E ':(20004|3333|4444|5555|7777|9001|8080)\b'

In the case study the destination was :20004 (MoneroOcean), but the port was also hidden from ss/netstat by the preload library — so also check from a source that is not filtered:

# Raw conntrack / firewall view is outside the libc the rootkit hooks:
conntrack -L 2>/dev/null | grep -E ':20004|:3333'
nft list ruleset 2>/dev/null | grep -E '20004|3333'

5. Immutable or unexpected ld.so.preload

The file should not exist on a normal host. Its presence, and especially an lsattr that fails on it, is a strong signal.

test -e /etc/ld.so.preload && echo "PRESENT — investigate" && cat /etc/ld.so.preload
lsattr /etc/ld.so.preload    # "Permission denied" => immutable flag or hardened

6. Sustained CPU with no matching workload

Correlate host CPU against the sum of guest CPU. On a hypervisor the two should track. A gap means something is running on the host itself.

# Host-side busy (approx) vs. how much the VMs claim.
top -bn1 | head -5
qm list

7. Self-updating persistence

A unit with a timer that re-downloads its own binary is not normal platform behaviour. Look for daily timers referencing an updater script.

systemctl list-timers --all
# Inspect any timer's ExecStart for a downloader.
systemctl cat <name>-update.service

Randomized delays (e.g. RandomizedDelaySec=1800) on such a timer can be a deliberate anti-lockstep measure rather than an ops choice.

Mapping to Wazuh rules

These signals translate to Wazuh detections in the companion observability-security-stack repository:

SignalWazuh angle
New systemd unit not owned by a packageFile integrity monitoring on /etc/systemd, /usr/lib/systemd
LD_PRELOAD in unit or login filesFile integrity monitoring + rule on content match
Process from /var/lib/*, /tmp/*Auditd execve rules + custom decoder
Mining-pool portsCustom rule on firewall/outbound logs
ld.so.preload creationFile integrity monitoring (alert on any write)

Health warning

A rootkit that hooks libc will lie to the tools you run on the host. Before trusting any single command, cross-check with a source that is not the compromised libc: kernel level (/proc), firewall (nft, conntrack), or — best — an external collector you pull to. That external vantage point is why monitoring lives on a separate VPS in this deployment.