CoursesAdvanced Linux securityLive triage: contain, do not destroy

Live triage: contain, do not destroy

The first hour, done right.

Advanced14 min · lesson 15 of 17

A detection fires at 2 a.m. A process you have never seen is talking to an address in another country. Every instinct says do the fast thing: kill it, reboot, reimage, close the ticket. That reflex is the most expensive move in incident response, because it treats a crime scene like a dirty kitchen. You do not mop a crime scene. You photograph it, bag what is perishable, and only then decide what to touch. The first hour on a live compromised host is about keeping the scene intact while it is still warm. The order you work in decides whether you can ever answer the questions that actually matter: how did they get in, what did they touch, are they still here, and are they on your other machines too.

The rule fits on a sticky note and is hard to hold under pressure. Contain without destroying. Volatile evidence (the running processes, the open network connections, the contents of memory) vanishes on reboot and thins out every minute you wait. Preserve it first, contain the host second, investigate third. Get that order backwards and you evict the attacker from one box while burning the only record of what they did.

The first hour: order of operations
1Record
Start a session log, note the UTC time
2Snapshot live state
Processes, sockets, open files, modules
3Image memory
AVML to external media, then hash it
4Preserve logs
Copy the volatile journal off the box
5Isolate
Cut C2 upstream, keep the host running
6Hash and hand off
Manifest every artifact, begin analysis

Order of volatility: grab what evaporates first

Evidence has a shelf life. Some of it is carved in stone: a file written to disk is still there after a reboot. Some of it is a footprint in wet sand at the tide line: a network connection, a process that exists only in memory, gone in minutes and certainly gone once the power blinks. Order of volatility, first written down in RFC 3227 (Request for Comments 3227, an internet engineering guideline for collecting digital evidence), says collect the fastest-fading things first. In practice you grab the perishable state up front: the live process table and the open network connections, which a handful of commands capture in seconds, then the full contents of memory (RAM, the working memory the running system lives in), which takes minutes to copy off, then the disk, which can wait because it survives a reboot. On a host you suspect is rootkitted (running attacker code that lies to the tools you would normally trust), reach for a static toolkit you brought with you, write your output to external media, and check everything against logs the host cannot edit.

Start by writing down the time in a format nobody can argue about, then snapshot the live state that a reboot would erase.

~/secopslog — bash
$ # run as root from a static toolkit; write only to external media, never the suspect disk EVID=/mnt/collect/host42 ; mkdir -p "$EVID" date -u +%FT%TZ | tee "$EVID/00-start-utc.txt" ps -efww > "$EVID/ps.txt" # all processes, full args, untruncated ss -tanp > "$EVID/sockets.txt" # TCP sockets with owning PID lsof -nP > "$EVID/openfiles.txt" cat /proc/modules > "$EVID/modules.txt" # loaded kernel modules, raw cp /etc/ld.so.preload "$EVID/" 2>/dev/null # userland-rootkit hook point, if present who -a > "$EVID/who.txt" ls -lh "$EVID"
2026-07-17T02:14:53Z total 96K -rw-r--r-- 1 root root 21 Jul 17 02:14 00-start-utc.txt -rw-r--r-- 1 root root 2.3K Jul 17 02:14 modules.txt -rw-r--r-- 1 root root 61K Jul 17 02:14 openfiles.txt -rw-r--r-- 1 root root 14K Jul 17 02:14 ps.txt -rw-r--r-- 1 root root 3.1K Jul 17 02:14 sockets.txt -rw-r--r-- 1 root root 1.2K Jul 17 02:14 who.txt

Follow the connection back to its binary

A snapshot is only useful if you read it. The socket table is where a lot of intrusions give themselves away, because malware that phones home has to open a connection, and that connection has a process behind it. TCP (Transmission Control Protocol, the connection-oriented traffic most services use) sockets map straight to a PID (process ID, the number the kernel uses to track a running program).

~/secopslog — bash
$ # which sockets are talking to the outside, and who owns them? ss -tanp state established
Recv-Q Send-Q Local Address:Port Peer Address:Port Process 0 0 10.0.4.12:52344 185.220.101.44:443 users:(("kworker",pid=2041,fd=7)) 0 0 10.0.4.12:22 10.0.9.5:60122 users:(("sshd",pid=1980,fd=4))

One line here does two suspicious things at once. It is beaconing out to a random internet address on port 443, and it calls itself kworker. Real kworker entries are kernel worker threads. They have no program file on disk, so their /proc/<pid>/exe link points at nothing. This one points at a file that was deleted while the program kept running, sitting in /dev/shm (a temporary filesystem, tmpfs, that lives in RAM and is world-writable). Deleting your own binary while staying resident is a classic way to hide from anyone scanning the disk.

~/secopslog — bash
$ # a real kworker has no on-disk binary; this one does. follow it. ls -l /proc/2041/exe lsof -nP +L1 # files still open but with zero links = deleted-but-running
lrwxrwxrwx 1 root root 0 Jul 17 02:15 /proc/2041/exe -> '/dev/shm/.cache/kworker (deleted)' COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME kworker 2041 root txt REG 0,25 41128 0 918273 /dev/shm/.cache/kworker (deleted)

The bytes are gone from the directory listing but still mapped in memory, so you can copy the program straight out of /proc and hash it before the process ever dies.

~/secopslog — bash
$ # the bytes are still in memory; copy them out through /proc before the process exits cp /proc/2041/exe "$EVID/pid2041-kworker.elf" sha256sum "$EVID/pid2041-kworker.elf"
7b1e9c0f5a3d2e88a41b6c9f0d2e4a7c8b5f1e3d6a9c2b4e7f0a1d3c5b8e2f6a /mnt/collect/host42/pid2041-kworker.elf

Image memory before you touch the disk

Memory holds the things that never hit the disk: keys typed into a process, decrypted payloads, injected code, the real command line of a program that rewrote its own arguments to hide. It is also the first thing you lose. AVML (Acquire Volatile Memory for Linux, a single static binary) captures it without compiling a kernel module on the suspect host, which matters when you do not trust that host's compiler or headers. It writes LiME format (Linux Memory Extractor format), the standard input for later analysis in a tool like Volatility (an open-source memory-forensics framework).

~/secopslog — bash
$ # image RAM with one static binary, no kernel module to build on the victim ./avml "$EVID/mem.lime" sha256sum "$EVID/mem.lime" | tee "$EVID/mem.lime.sha256" ls -lh "$EVID/mem.lime"
2c9f0a7d1b6e34c8f5a2d9b0e7c4f1a8d3b6e29c0f5a1d7b4e8c2f6a9d0b3e51 /mnt/collect/host42/mem.lime -rw------- 1 root root 3.9G Jul 17 02:21 /mnt/collect/host42/mem.lime

The logs might be living in RAM too

Here is the detail that turns 'do not reboot' from a slogan into a hard rule. On most modern Linux systems, logging is handled by journald (the logging service that ships with systemd, the software that starts and supervises everything else on the machine). Where journald keeps its journal comes down to one setting.

/etc/systemd/journald.conf
[Journal]
Storage=auto
#Compress=yes
#SystemMaxUse=
#RuntimeMaxUse=
# Storage=auto -> persist to /var/log/journal IF that directory exists,
# otherwise keep the journal in /run/log/journal, which is tmpfs (RAM).

Storage=auto means: save logs to /var/log/journal if that directory exists, otherwise keep them in /run/log/journal, which is tmpfs, which is RAM. Plenty of cloud and container base images ship without /var/log/journal, so their entire log history is volatile. It reads fine right now with journalctl, and it evaporates the instant the machine reboots. Check where you actually stand, then serialize the journal to stable storage before anything can take it away.

~/secopslog — bash
$ # does this host keep logs across reboots, or only in RAM? ls -d /var/log/journal 2>/dev/null || echo "no persistent journal dir" journalctl --disk-usage
no persistent journal dir Archived and active journals take up 48.0M in the file system.
$ # serialize the whole journal to a stable file before /run can be wiped journalctl --no-pager -o export > "$EVID/journal.export" journalctl -k -o short-iso > "$EVID/kernel-ring.txt" # kernel messages, timestamped ls -lh "$EVID/journal.export"
-rw-r--r-- 1 root root 42M Jul 17 02:23 /mnt/collect/host42/journal.export
Reboot on reflex and the case is gone
'Just rebuild it' evicts the attacker from one host and destroys the answers you needed from that host: the memory image, the live process tree, the open sockets, and on a volatile-journald box, the logs themselves. You can rebuild a server any time. You get exactly one shot at its memory. Isolate and capture first, reimage last.

Isolate without pulling the plug

Containment is about stopping the spread while keeping the patient alive. Pulling the network cable works, but it can tip off malware that watches for isolation and wipes itself, and it cuts your own ability to pull evidence off the box. A cleaner move is to quarantine at the firewall: drop everything except the one host you collect from. On a modern system that firewall is nftables (the in-kernel packet filter framework, successor to iptables). Point the host at your forensic workstation and nothing else.

~/secopslog — bash
$ # quarantine atomically: one transaction commits the drop policy and the allow-rules together COLLECTOR=10.0.9.5 nft -f - <<EOF table inet quar { chain input { type filter hook input priority -400; policy drop; iif "lo" accept ip saddr $COLLECTOR accept } chain output { type filter hook output priority -400; policy drop; oif "lo" accept ip daddr $COLLECTOR accept } } EOF nft list table inet quar
table inet quar { chain input { type filter hook input priority -400; policy drop; iif "lo" accept ip saddr 10.0.9.5 accept } chain output { type filter hook output priority -400; policy drop; oif "lo" accept ip daddr 10.0.9.5 accept } }

The attacker's command-and-control channel (C2, the connection the malware uses to take orders) is now cut off, your collector still reaches the box over SSH (secure shell), and the machine stays up with its memory and process state intact. Loading the whole table in one shot with nft -f matters here: the drop policy and the collector-allow rules commit together, so there is never a split second where the box locks out the session you are working from. Priority -400 sits below every standard nftables hook priority, so this table runs ahead of any ordinary filter rule, and nothing else gets to accept the traffic you meant to drop.

Treat this host firewall as a fast first move, not the last word. A rule you set with nft can be pulled right back out by anyone who has root on the box, and on a compromised machine that may be the attacker. Cut it off somewhere root cannot reach: a quarantine VLAN (virtual local area network) on the switch, or a locked-down security group in the cloud. Authoritative isolation happens above the host, not on it.

Your own actions are evidence too

A chain of custody is the paper trail that lets someone else trust your evidence: who collected what, when, and that it has not changed since. Start recording before your first real command, so the record includes your own keystrokes and their output. Do not log in as the account that may be compromised, and lean on your static tools rather than the host's, because subverted binaries can hand you clean-looking lies.

~/secopslog — bash
$ # start a recorder before you touch anything else script --log-out "$EVID/session.log" --log-timing "$EVID/session.timing"
Script started, output log file is '/mnt/collect/host42/session.log'.

When collection is done, freeze a manifest. Hash every finished artifact with SHA-256 (a cryptographic checksum, so any later change is obvious) and store the list beside the evidence.

~/secopslog — bash
$ # hash every finished artifact; skip the session log, which is still recording find "$EVID" -type f ! -name manifest.sha256 ! -name 'session.*' -exec sha256sum {} + > "$EVID/manifest.sha256" wc -l "$EVID/manifest.sha256"
11 /mnt/collect/host42/manifest.sha256
Quick check
01A process calling itself kworker is beaconing to a foreign address on port 443. Its /proc/<pid>/exe points at a deleted file in /dev/shm, and the host has no /var/log/journal directory. What do you do first?
Incorrect — The moment it dies the /proc link dies with it, and the file was unlinked already, so /dev/shm has nothing left to copy. Some samples also wipe themselves on termination.
Correct — The link still resolves for as long as the program lives, so you get the bytes and a hash, then RAM, then containment placed where root on that box cannot reverse it.
Incorrect — With no /var/log/journal here, the journal is sitting in tmpfs. The reboot that gets you a clean host also erases the very history you planned to read.
Incorrect — Malware that watches its own link state can destroy itself the second isolation lands, and you have also cut the path your collector needs to pull evidence off the box.
02RFC 3227 (an internet engineering guideline for collecting digital evidence) sets an order of volatility for a live compromised host. Which sequence matches what this lesson does?
Correct — Those two cost you seconds and thin out while you type. The disk is still there tomorrow, so it goes to the back of the queue.
Incorrect — RAM does rank high, but the process and socket tables fade faster and take seconds to capture, so they go ahead of the multi-minute dump, and the disk still waits.
Incorrect — Nothing about a disk is urgent, since it survives a reboot untouched. Copying it first spends exactly the minutes your perishable evidence does not have.
Incorrect — Hashing proves an artifact has not changed since collection. It cannot bring back a connection that closed while you were busy copying something durable.
03You are quarantining a live host with nftables while still pulling evidence over SSH from your collector at 10.0.9.5. A colleague wants to run the drop-everything policy as one nft command and the collector-allow rule as a second command straight after. What is wrong with that?
Incorrect — Each command is its own transaction and commits the instant it returns. That first commit is precisely the window that cuts you off.
Incorrect — Adding a rule does not erase an earlier one. The hazard here is timing between two separate commits, not one command cancelling another.
Incorrect — Priority -400 puts this table ahead of standard filter rules, and both commands write into that same table, so priority is not what bites you.
Correct — One transaction means there is never a moment where the drop is live and 10.0.9.5 has not yet been permitted through.

Once the artifacts are copied, hashed, and off the box, and the host is cut off above the operating system, you have the one thing every later step depends on: a frozen picture of the machine exactly as the attacker left it. Now you can load the memory image into Volatility, diff the process tree against a known-good baseline, and trace the intrusion across the rest of your fleet without ever wondering what you wiped. Rebuild the server whenever you are ready. The scene is already saved.

Try this

Work through “Your own actions are evidence too” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: reboot on reflex and the case is gone. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related