Signal9Signal9 — home
online
all lessons

Containment: Stopping the Spread

Intermediate40 min8 steps
What you'll learn
  • Sequence the containment phase correctly: preserve volatile evidence before any destructive action
  • Use a firewall rule to cut an attacker's command-and-control channel without alerting them more than necessary
  • Kill a planted malicious process directly instead of treating it like a managed service
  • Decide between surgical containment and full host isolation based on the risk of spread
  • Pin files with chattr +i / +a during an incident, and sweep with lsattr for files an attacker pinned
  1. Where containment sits in incident response

    Frame containment as the phase between detecting a threat and removing it — and understand the core tension.

    You've detected an intruder. The process forensics lesson taught you how to find the malicious process, the port it opened, and the C2 server it's calling home to. Containment is what comes next — and it comes before eradication (the clean-up).

    The goal of containment is narrow and urgent: stop the bleeding. Cut the attacker's control and halt the spread to other hosts. But there's a trap built into the phase, because almost every containment action is destructive — killing a process, blocking a connection, or pulling a host offline all destroy the very volatile evidence you'd want later to understand what happened.

    Think of a ship taking on water. When a compartment floods, the crew closes the watertight doors to seal it off — they don't scuttle the whole vessel, and they don't wait until they've fully surveyed the damage. They seal the compartment while the inspection continues. Containment is closing watertight doors: isolate the damage so it can't spread, but keep the ship afloat and investigable.

  2. Preserve volatile evidence FIRST

    Snapshot the live state of the system before you touch anything, because the next steps will erase it.

    Before you cut a single connection, take a snapshot. Volatile evidence is the data that exists only while the system is in its current state: the running process table, the live socket list, the established connections, and the malware's binary still resident in memory. All of it is gone once you contain.

    This is the cross-reference to your forensics toolkit — but here the purpose is different. In forensics you read these tables to find the threat. In containment you capture them to freeze the scene before you destroy it. Save everything to a separate evidence directory.

    Example — not runnable on your lab box

    sudo mkdir -p /evidence && sudo ps auxf > /evidence/ps.txt && sudo ss -tunap > /evidence/sockets.txt
    ps auxf
    Full process tree (forest) for every user — what's running and who launched it.
    ss -tunap
    All TCP and UDP sockets, numeric, with the owning process — the live connection map.
    > /evidence/...
    Redirect to files on disk so the snapshot survives the actions you're about to take.
    Silent success. You now have the process tree and the complete socket table written to /evidence/ — a permanent record of exactly what was running and what was connected at the moment of containment. If you skip this and kill the process first, that record is unrecoverable.

    Now capture the one piece of evidence that disappears most completely: the malware binary itself. Recall that an attacker often deletes the file from disk ((deleted) in /proc/<pid>/exe) while the process keeps running from memory. The kernel still holds that binary open, so you can copy it straight out of /proc before you kill the process.

    Example — not runnable on your lab box

    sudo cp /proc/2207/exe /evidence/sample.bin
    /proc/2207/exe
    The kernel's symlink to the live executable for PID 2207 — readable even if the on-disk file was deleted.
    This recovers the actual malware for later analysis, even though the attacker may have unlinked it from disk. The moment you kill the process, the kernel releases that inode and the binary is gone for good. This copy must happen before the kill — there is no second chance.

    Check yourself

    An attacker's process shows /proc/<pid>/exe -> '/tmp/.x (deleted)'. Why must you copy that binary out before killing the process?

  3. Network isolation: cut C2 first

    Understand why blocking the attacker's command channel is the fastest, highest-leverage containment move.

    With evidence frozen, your first containment action targets the attacker's lifeline: their command-and-control (C2) channel. As long as the implant can reach its C2 server, the attacker can issue commands, pull more tools down, pivot to other hosts, and exfiltrate data. Cut that channel and the attacker goes blind — the running process is now a beached fish.

    Network isolation is the highest-leverage containment because it's fast (one firewall rule), reversible (you can lift it), and it severs control without you having to touch the malicious process yet. It buys time to investigate calmly.

    There's an art to how much you cut. The crude move is to yank the network cable — but that takes the host fully offline, alerts the attacker that they've been caught (their session just died hard), and severs your own remote access to investigate. The surgical move is to block only the C2 address at the firewall. To the attacker it looks like an ordinary network blip; you keep your shell and every other connection for analysis.

    From your captured lsof/ss snapshot you have the C2 address. Here we'll use the reserved documentation address 203.0.113.5 as the C2. On a modern system the firewall is nftables; the rule blocks all outbound traffic to that one host.

    Example — not runnable on your lab box

    sudo nft add rule inet filter output ip daddr 203.0.113.5 drop
    add rule inet filter output
    Append a rule to the OUTPUT chain of the inet filter table — outbound traffic leaving this host.
    ip daddr 203.0.113.5
    Match packets whose destination address is the C2 server.
    drop
    Silently discard the packet — no rejection message back to the implant, so the failure looks like a network glitch.
    Outbound packets to 203.0.113.5 are now dropped. The implant's beacons fail silently — drop (rather than reject) means no error is returned, so the attacker sees a dead/laggy connection rather than an obvious firewall block. The process is still running, but it can no longer reach home. The equivalent legacy command is sudo iptables -A OUTPUT -d 203.0.113.5 -j DROP.

    Check yourself

    Why is drop preferred over reject for the C2-blocking rule during containment?

  4. Kill the process — it isn't a service

    Stop the malicious process directly, and understand why `systemctl stop` is the wrong tool.

    C2 is severed. Now stop the malicious process on the host itself. Here learners reach for the wrong tool out of habit: systemctl stop.

    A planted process is not a managed service. systemctl only knows about units — services that systemd was told to manage, with a unit file under /usr/lib/systemd/system/ or /etc/systemd/system/. An attacker's process that someone launched from a shell, or that a web exploit spawned, has no unit. systemd has never heard of it. Running systemctl stop kworkerd against it does literally nothing — there's no unit by that name to stop.

    Think of systemctl as the building's official staff directory: it can page anyone the company hired. An intruder who climbed through a window isn't in the directory, so paging the directory never reaches them. You have to go to them directly. That direct tool is kill, which sends a signal straight to a process by PID — no unit required.

    sudo kill -9 2207
    -9
    Send SIGKILL — an uncatchable, unignorable signal the process cannot trap, clean up after, or refuse.
    2207
    The target PID, taken from your ps/ss snapshot.
    SIGKILL terminates PID 2207 immediately and unconditionally. We use -9 deliberately: a graceful signal like SIGTERM (the default) can be caught by the process, which might let malware run a clean-up or kill-switch handler. SIGKILL gives it no such chance — the kernel reaps it on the spot. Silent success means it's gone; confirm with ps -p 2207 returning nothing.

    Check yourself

    You run systemctl stop badproc on an attacker's process and it has no effect. What's the most likely reason?

  5. Contain the identity: lock the compromised account

    Lock a stolen account without deleting it, and know what a password lock does not stop.

    Blocking the C2 and killing the process stop the malware. They do nothing about the credentials. If the attacker came in with a stolen or guessed password, they can simply log in again, so when threat intel or the auth log says an account is being used by someone else, contain the account too.

    Lock it, don't delete it. usermod -L <user> (or the equivalent passwd -l <user>) puts a ! in front of the password hash in /etc/shadow. No password can ever hash to a value starting with !, so password authentication fails, but the account, its home directory, its files and its UID all stay exactly as they were. That matters: the home directory, shell history and owned files are evidence, and userdel would throw them away (and free the UID for reuse, so the next account created inherits ownership of anything left behind). Locking is also reversible: usermod -U restores the original hash once the password has been reset.

    sudo useradd -M demo-ir && echo "demo-ir:$(head -c 12 /dev/urandom | base64)" | sudo chpasswd
    sudo passwd -S demo-ir | cut -d' ' -f2
    sudo usermod -L demo-ir
    sudo passwd -S demo-ir | cut -d' ' -f2
    sudo getent shadow demo-ir | cut -d: -f2 | cut -c1
    sudo userdel demo-ir
    P
    L
    !
    usermod -L
    Lock the password: prefix the hash in /etc/shadow with '!'. usermod -U unlocks it.
    passwd -S
    Show password status; field 2 is P (usable), L (locked) or NP (no password).
    A throwaway account shows the whole lifecycle. passwd -S prints the account's password status in its second field: P (a usable password) before the lock, L (locked) after usermod -L. The raw shadow entry confirms how: its password field now starts with !, ahead of the untouched hash. The last line removes the demo account; on a real incident you leave the locked account in place for the investigation.

    Check yourself

    You locked a compromised account with usermod -L, yet the attacker logs in again over SSH an hour later. What did they most likely authenticate with?

  6. Surgical containment vs. host isolation

    Choose the right scope of containment based on how far the threat could spread.

    Everything so far was surgical: block one IP, kill one process, keep the host running and investigable. That's the right default — minimal collateral damage, maximum information retained. But sometimes surgery isn't enough, and you escalate to full host isolation.

    The deciding question is spread risk: how likely is it that the compromise has gone beyond what you can see, or is actively reaching other systems right now? Back to the ship: you seal one compartment when the leak is contained to it, but you seal off a whole deck when water is pouring between compartments faster than you can patch.

    Stay surgical when: you've identified the specific process and its single C2, there's no sign of lateral movement, and you need to keep watching the attacker or keep the host's services running.

    Escalate to full host isolation when: the host is actively attacking other machines, you suspect a rootkit or unknown additional implants you can't fully enumerate, the blast radius is unclear, or the host is too critical to risk leaving even partially connected. Isolation means cutting the host off from the network entirely while leaving it powered on (so volatile evidence and your forensic access via an out-of-band path survive).

    Example — not runnable on your lab box

    sudo nft add rule inet filter output ip daddr != 10.0.0.0/8 drop
    ip daddr != 10.0.0.0/8
    Match any destination that is NOT the trusted internal management range.
    drop
    Discard it — so the host can reach only your isolation/management network, nothing else.
    This is the escalated move: instead of blocking one C2, you drop all outbound traffic except to a trusted management subnet (here the private range 10.0.0.0/8). The host is now network-isolated but still alive — volatile evidence intact, reachable from your forensics jump box, but unable to talk to the attacker or pivot to other hosts. It's a software equivalent of unplugging the cable that still leaves you a way in.

    Check yourself

    A compromised host is actively scanning and connecting to other servers on your network. Surgical or full isolation?

  7. Pin files with chattr, and find the ones the attacker pinned

    Use the immutable and append-only attributes in containment, and sweep for them before eradication.

    Some files must not change while you work: authorized_keys (so a re-planted key can't land), /etc/passwd and /etc/shadow, and the logs that record what happened. File permissions can't guarantee that on a host where the attacker may hold root, because root bypasses permission bits. Inode attributes are a separate layer the kernel checks with no root override:

    • chattr +i FILE, immutable: no write, delete, rename, hard link or mode/owner change, for root too.
    • chattr +a FILE, append-only: new lines can be added at the end, and nothing already written can be overwritten, truncated or removed. That makes a log tamper-evident.

    Root is not powerless against them. Clearing a flag (chattr -i) needs the CAP_LINUX_IMMUTABLE capability, which a normal root shell holds, so the flag won't stop a determined intruder with a full root shell. It does stop scripts, careless commands and any process that dropped that capability. Treat it as a speed bump that also leaves a trace. Clearing it is a deliberate act.

    Example — not runnable on your lab box

    sudo chattr +i /root/.ssh/authorized_keys && lsattr /root/.ssh/authorized_keys
    ----i---------e------- /root/.ssh/authorized_keys
    +i
    set the immutable attribute (-i clears it)
    Shown, not run: on a lab box the platform itself manages root's authorized_keys, so don't pin it there. On a real host, after you've removed the attacker's key, this stops anything from writing a new one while the incident is open. lsattr confirms it: the i in the flags column is the attribute (the e is ext4's own extents flag, always there). ls -l would show nothing different. Write down every file you pin, because each one will refuse its legitimate owner too (key rotation, package updates) until you run chattr -i on it.

    Attackers use the same flag in the other direction. A dropped cron job, systemd unit or SSH key pinned with +i survives your cleanup: rm as root answers Operation not permitted, which looks like a permissions problem until you run lsattr. So sweep for attributes before eradication, while you are still recording the scene, not after a removal mysteriously fails:

    sudo lsattr -Ra /etc /root /home /var/spool /usr/local/bin 2>/dev/null | grep -E '^[-a-zA-Z]*[ia][-a-zA-Z]* '
    -Ra
    recurse, including dotfiles
    2>/dev/null
    hide errors from paths that don't exist on this host, or special files with no flags
    Prints nothing on a clean box, and that silence is the result you want. Each line it does print is a file carrying i or a, and each one needs an explanation: either your team pinned it on purpose, or it is attacker persistence protected against removal. -a matters, because without it lsattr -R skips dotfiles exactly as ls does. The grep keeps only lines whose flags column (letters and dashes, no /) contains i or a. To remove a pinned implant: record it (sha256sum, copy it to /evidence), then chattr -i and remove it.

    Check yourself

    Your eradication step runs rm -f /etc/cron.d/dbus-health as root, and the file is still there with an ordinary -rw-r--r-- mode. What do you run next?

  8. Try it in your live terminal

    Practice the inspection muscle on the real sandbox — read-only, so you build the reflexes safely.

    You have a real root shell. Walk the inspection half of the containment workflow — the part you'd do before any destructive action. Read each output and ask: what would I capture, and what would I contain?

    ps auxf | head -n 30
    Snapshot the process tree. On a real call you'd redirect this to /evidence/ps.txt. Look for anything without a sensible parent or running from an odd path.
    sudo ss -tunap | head -n 30
    The live socket map: what's listening and what's connected out. An established outbound link to an unfamiliar IP is your candidate C2 to block.
    sudo nft list ruleset
    Inspect the current firewall ruleset — this is read-only. This is where a containment drop rule would appear after you added it; reviewing it confirms a block is actually in place.

    The destructive commands you'd add on a live incident — and which you should not run here without having captured evidence first — are:

    • sudo cp /proc/<pid>/exe /evidence/sample.bin — recover the binary from memory
    • sudo nft add rule inet filter output ip daddr <C2-IP> drop — sever C2
    • sudo kill -9 <pid> — stop the malicious process

    Rehearse the order, not just the keystrokes: capture, cut C2, kill, then decide isolation scope.

Recap
  • Containment sits between detection and eradication: stop the spread, but don't destroy the evidence you'll need later.
  • Capture first, contain second. Volatile evidence — process list, sockets, the in-memory binary — is gone the instant you act, so snapshot it before any destructive step.
  • Cutting the C2 channel is the fastest, highest-leverage move: surgical, reversible, and quiet. Block the IP rather than yanking the cable, which alerts the attacker and severs your own access.
  • A planted process is not a managed service — systemctl stop does nothing to it. Use kill -9 <pid>, and remember persistence removal belongs to eradication.
  • Contain stolen credentials too: usermod -L (or passwd -l) locks the password but keeps the account as evidence. A lock doesn't stop an SSH key, so expire the account (usermod -e 1) when keys may be planted.
  • Stay surgical by default; escalate to full host isolation only when spread risk is high (lateral movement, unknown implants). Isolate on the network, don't power off.
  • chattr +i (immutable) and +a (append-only) hold even against root's casual writes; clearing them needs CAP_LINUX_IMMUTABLE. Pin what must not change, sweep lsattr -Ra for attacker-pinned persistence before eradication, and clear your own flags before recovery.
Containment action → effect → evidence impact
ActionEffectPreserves volatile evidence?
Snapshot ps / ss to /evidenceFreezes the live scene to diskYes — this IS the preservation step
cp /proc/<pid>/exeRecovers the in-memory binaryYes — only chance before the kill
nft ... ip daddr <C2> dropSevers the attacker's C2 channel, quietlyYes — host stays up and investigable
kill -9 <pid>Stops the malicious process nowNo — destroys that process's memory; capture first
usermod -L <user> (+ usermod -e 1)Stolen password (and, expired, keys) can no longer log inYes — the account and its files stay for forensics
Full host isolation (drop all but mgmt)Stops lateral spread, keeps host aliveYes — if isolated, not powered off
Power off the hostHard stopNo — wipes all volatile evidence; avoid
systemctl stop <name>Nothing, if it's not a real unitN/A — wrong tool for a planted process
chattr +i / +a on a fileBlocks rewrite/delete (+a: allows appends only)Yes — changes only a flag; +a makes a log tamper-evident