daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

linux-forensics.md (27757B)


      1 ---
      2 title: "Linux Forensics"
      3 description: "Linux DFIR from live response to dead-disk analysis: order of collection, the /var/log map, wtmp/btmp/lastlog, journalctl and auditd queries, shell histories, persistence hunting (cron, systemd, rc, ld.so.preload, SSH keys), package integrity, /proc tricks, timestomp detection, then SIFT Workstation with Sleuth Kit and plaso/log2timeline for the super-timeline."
      4 category: dfir
      5 subcategory: "Linux"
      6 tags: [dfir, forensics, linux, incident-response, logs, journalctl, auditd, persistence, systemd, cron, sleuth-kit, plaso, sift, timeline]
      7 tools: ["SIFT Workstation", "Sleuth Kit (mmls, fls, icat, istat, tsk_recover)", "plaso / log2timeline / psort", "journalctl", "ausearch / aureport", "last / lastb / utmpdump", "debsums / rpm -Va", "chkrootkit / rkhunter", "UAC (Unix-like Artifacts Collector)", "Velociraptor", "AVML / LiME"]
      8 difficulty: advanced
      9 updated: "2026-09-26"
     10 ---
     11 
     12 # Linux Forensics
     13 
     14 Linux boxes leave fewer "did it run" artefacts than Windows (no Prefetch, no Amcache, no ShimCache) and far more **text**: logs, histories, unit files, crontabs. The investigation therefore leans on **logs and timestamps** — `journalctl`, auditd, `wtmp`, the four `stat` times on every inode — and on knowing the **dozen places persistence hides**. This card is the live-response order, the `/var/log` map, the queries, the persistence hunt, and then the dead-disk workflow on **SIFT Workstation** with **Sleuth Kit** and **plaso** that produces the super-timeline.
     15 
     16 > [!warning] Every command you run on a live box changes it
     17 > Your shell writes `~/.bash_history`, `atime`s update on files you read (unless the filesystem is `relatime`/`noatime`), and your tools appear in `ps` and the journal. Run statically-linked binaries **from your own media**, redirect output to your own mount, note the exact time you started, and prefer capturing memory ([acquisition sheet](/sheets/dfir/disk-imaging)) before anything else.
     18 
     19 ## Live response, in order
     20 
     21 ```bash
     22 # 0. Your evidence mount and a log of what you run
     23 E=/mnt/usb/evidence/$(hostname)-$(date -u +%Y%m%dT%H%MZ); mkdir -p "$E"; exec > >(tee -a "$E/collector.log") 2>&1
     24 date -u; uptime; cat /etc/os-release; uname -a; hostnamectl 2>/dev/null
     25 
     26 # 1. Memory (see acquisition sheet) — AVML needs no kernel module
     27 /mnt/usb/bin/avml --compress "$E/mem.lime.compressed"
     28 
     29 # 2. Volatile: who / what / where
     30 w; last -Faiw | head -50; lastb -F | head -50
     31 ps -eo pid,ppid,user,lstart,etime,stat,%cpu,%mem,cmd --forest
     32 ss -tulpan; ss -xlp                      # sockets incl. unix
     33 lsof -nP +L1                             # deleted-but-open files (malware unlinking itself)
     34 lsof -nP -i                              # network by process
     35 ip a; ip r; ip neigh; cat /etc/resolv.conf /etc/hosts
     36 cat /proc/mounts; df -hT; lsblk -f
     37 lsmod; cat /proc/modules                 # compare later for hidden modules
     38 cat /proc/sys/kernel/tainted             # non-zero + no proprietary drivers = suspicious
     39 systemctl list-units --type=service --state=running --no-pager
     40 systemctl list-timers --all --no-pager
     41 env; cat /proc/1/environ | tr '\0' '\n'  # LD_PRELOAD in PID 1's env?
     42 
     43 # 3. Per-process detail for anything odd (deleted binary, weird cwd, no tty)
     44 for p in $(pgrep -f suspicious); do
     45   echo "== $p"; ls -l /proc/$p/exe /proc/$p/cwd; cat /proc/$p/cmdline | tr '\0' ' '; echo
     46   cat /proc/$p/maps | head; ls -l /proc/$p/fd
     47   cp /proc/$p/exe "$E/proc-$p.exe"       # recover a deleted binary from memory
     48 done
     49 
     50 # 4. Users and auth surface
     51 cat /etc/passwd /etc/group /etc/shadow /etc/sudoers; ls -la /etc/sudoers.d/
     52 for h in /root /home/*; do echo "== $h"; ls -la "$h/.ssh" 2>/dev/null; cat "$h/.ssh/authorized_keys" 2>/dev/null; done
     53 getent passwd | awk -F: '$3==0 || $7 !~ /(nologin|false)$/'   # UID 0 or interactive shells
     54 
     55 # 5. Copy the artefacts you will parse offline (timestamps preserved)
     56 tar --xattrs --acls -czpf "$E/logs.tgz" /var/log /etc /var/spool/cron /var/spool/at /root /home --exclude='*.iso' 2>/dev/null
     57 journalctl -o export > "$E/journal.export"          # lossless journal dump
     58 find / -xdev -newermt "$(date -d '7 days ago' +%F)" -type f 2>/dev/null > "$E/recent-files.txt"
     59 ```
     60 
     61 > [!tip] Automate it
     62 > **UAC** (`github.com/tclahr/uac`) is the Linux/macOS/*BSD answer to KAPE: `./uac -p ir_triage /mnt/usb/evidence` collects all of the above (and hundreds more artefacts) using its own statically-linked binaries, hashed, in minutes. **Velociraptor** does the same at fleet scale (`Linux.Triage.*` artifacts). **CyLR** is the older, smaller option. Use one of these on real cases; the manual list above is for understanding what they grab.
     63 
     64 ## The `/var/log` map
     65 
     66 Debian/Ubuntu on the left, RHEL/Fedora/SUSE where different.
     67 
     68 | File | Contains | Notes |
     69 |---|---|---|
     70 | `auth.log` / `secure` | sshd, sudo, su, PAM, login, useradd — **the** account log | `grep -E 'Accepted|Failed|Invalid|session opened|sudo:'` |
     71 | `syslog` / `messages` | everything else the daemon caught: cron, kernel, services | |
     72 | `kern.log`, `dmesg` | kernel ring buffer: USB plug-ins, module loads, OOM, segfaults | `dmesg -T` for human timestamps |
     73 | `wtmp` | login/logout/reboot records (binary) | `last -F -f wtmp` |
     74 | `btmp` | failed logins (binary) | `lastb -F -f btmp` — brute-force evidence |
     75 | `lastlog` | last login per user (binary, sparse) | `lastlog`; `lastlog -b 30` = not logged in for 30 days |
     76 | `utmp` (`/run/utmp` or `/var/run/utmp`) | **currently** logged-in sessions | `who`, `w`; `utmpdump /run/utmp` |
     77 | `journal/` | systemd journal (binary, structured) | `journalctl -D /path/to/journal` on an image |
     78 | `audit/audit.log` | auditd — syscalls, execve, file access per rule set | `ausearch`, `aureport` |
     79 | `cron.log` (or in syslog) | cron job executions `CMD (...)` | |
     80 | `dpkg.log`, `apt/history.log` / `yum.log`, `dnf.log` | package installs/removals with time | `grep -E ' install | remove ' dpkg.log` |
     81 | `apache2/`, `nginx/`, `httpd/` | web access/error logs — webshell uploads, LFI, scanner UAs | `access.log`: sort by IP, look at POSTs to odd paths |
     82 | `mysql/`, `postgresql/` | DB errors, slow queries, sometimes general log | |
     83 | `faillog`, `tallylog` | pam_tally/faillock counters | `faillock` |
     84 | `Xorg.*.log`, `lightdm/`, `gdm/`, `sddm.log` | graphical logins | |
     85 | `cloud-init*.log` | cloud first-boot: user-data script contents run as root | |
     86 | `installer/` (`anaconda/`) | OS install time | |
     87 | `~/.xsession-errors`, `~/.local/share/xorg/` | per-user X errors | |
     88 
     89 ```bash
     90 # Binary login records
     91 last -Faiw -f /mnt/img/var/log/wtmp          # -F full times, -a host in last column, -i IPs, -w full names
     92 last -Faiw -f wtmp reboot                     # boots
     93 lastb -F -f /mnt/img/var/log/btmp | awk '{print $3}' | sort | uniq -c | sort -rn | head    # top attacking IPs
     94 utmpdump /mnt/img/var/log/wtmp                # raw records incl. terminal and pid; also: utmpdump wtmp > wtmp.txt; edit; utmpdump -r < wtmp.txt
     95 lastlog -R /mnt/img                           # per-user last login from an image root
     96 # Rotated logs: read them all at once
     97 zgrep -h 'Accepted' /mnt/img/var/log/auth.log*  | sort -k1M -k2n
     98 ```
     99 
    100 ## `journalctl` — the systemd journal
    101 
    102 ```bash
    103 # Point at an image's journal dir (not the live one)
    104 J="journalctl -D /mnt/img/var/log/journal --no-pager -o short-iso --utc"
    105 
    106 $J --list-boots                                # boot IDs with first/last times
    107 $J -b -1                                       # previous boot only
    108 $J --since "2026-09-20" --until "2026-09-22 06:00"
    109 $J -u ssh -u sshd                              # a unit (or several)
    110 $J _COMM=sudo                                  # by process name
    111 $J _UID=1001                                   # by user
    112 $J -p warning..emerg                           # priority range (0 emerg … 7 debug)
    113 $J -k                                          # kernel messages (dmesg equivalent, all boots)
    114 $J _SYSTEMD_UNIT=cron.service                  # cron executions
    115 $J -g 'Accepted|Failed password|session opened|COMMAND='   # regex grep across everything
    116 $J -o verbose _PID=4242                        # every field of matching entries
    117 $J -o json | jq -r 'select(.MESSAGE|test("useradd|usermod|passwd")) | [.__REALTIME_TIMESTAMP,.MESSAGE]|@tsv'
    118 $J --disk-usage; $J --verify                   # is the journal intact? tampered files fail verification
    119 $J -o export > journal.export                  # lossless copy; re-import with systemd-journal-remote
    120 ```
    121 
    122 > [!info] Volatile vs persistent
    123 > If `/var/log/journal/` does not exist, the journal was **volatile** (`/run/log/journal`) and died at power-off — that is your gap. `Storage=` in `/etc/systemd/journald.conf` tells you which. Check `MaxRetentionSec`/`SystemMaxUse` for how far back you can expect to see.
    124 
    125 ## auditd — when it is on, it is the best log you have
    126 
    127 ```bash
    128 cat /etc/audit/auditd.conf /etc/audit/rules.d/*.rules           # what was being recorded
    129 auditctl -l                                                     # live rules
    130 
    131 A="ausearch -if /mnt/img/var/log/audit/audit.log -i"            # -i = interpret UIDs/syscalls; -if = file
    132 $A -m USER_LOGIN,USER_AUTH,USER_START                            # logins
    133 $A -m USER_CMD; $A -m EXECVE                                     # sudo commands / every execve (if rule -a always,exit -S execve)
    134 $A -ua 1001 -ts 09/20/2026 00:00:00 -te 09/22/2026 23:59:59     # by audit UID (survives su/sudo) in a window
    135 $A -k persistence                                               # by rule key
    136 $A -f /etc/passwd; $A -f /etc/shadow                            # who touched a file
    137 $A -sc connect; $A -sc ptrace                                   # by syscall
    138 $A -x /usr/bin/curl                                             # by executable
    139 $A --session 42                                                 # everything in one login session
    140 $A -m AVC                                                       # SELinux denials — malware tripping policy
    141 
    142 aureport -if audit.log --summary                                # counts by type
    143 aureport -if audit.log -au -i                                   # authentication report
    144 aureport -if audit.log -x -i --summary                          # executables by frequency
    145 aureport -if audit.log -f -i | grep -E '/tmp|/dev/shm|/var/tmp' # files touched in world-writable dirs
    146 ```
    147 
    148 `auid` (audit UID) is the login user even after `sudo -i`; `uid`/`euid` show the effective identity. `ses=` links every record of a session. Records with `key=` came from a named rule — many hardening baselines (CIS, Neo23x0's `auditd` rules) tag `T1xxx` MITRE keys straight into the log.
    149 
    150 ## Shell histories and user activity
    151 
    152 ```bash
    153 for h in /mnt/img/root /mnt/img/home/*; do
    154   for f in .bash_history .zsh_history .python_history .mysql_history .psql_history .lesshst .viminfo .wget-hsts .rediscli_history .sqlite_history .node_repl_history .local/share/fish/fish_history .local/share/nano/search_history .config/nvim/shada; do
    155     [ -s "$h/$f" ] && { echo "=== $h/$f ($(stat -c %y "$h/$f"))"; cat "$h/$f"; }
    156   done
    157 done 2>/dev/null
    158 
    159 # zsh extended history has epoch timestamps: ": 1758866400:0;whoami"
    160 awk -F'[:;]' '/^: [0-9]+/ {cmd=$0; sub(/^: [0-9]+:[0-9]+;/,"",cmd); print strftime("%F %T",$2), cmd}' .zsh_history
    161 # bash with HISTTIMEFORMAT set writes "#1758866400" lines above each command
    162 awk '/^#[0-9]+$/ {ts=strftime("%F %T",substr($0,2)); next} {print ts, $0}' .bash_history
    163 
    164 # Signs of history tampering
    165 ls -la ~/.bash_history                 # symlink to /dev/null? size 0 with old mtime?
    166 grep -E 'HISTSIZE=0|HISTFILE=|unset HIST|history -c|set \+o history' /mnt/img/etc/profile /mnt/img/etc/bash.bashrc /mnt/img/home/*/.bashrc /mnt/img/home/*/.profile 2>/dev/null
    167 # Other traces: .viminfo (files edited, searches), .lesshst, .recently-used.xbel (GTK file dialogs), .cache/, Trash
    168 cat /mnt/img/home/*/.local/share/recently-used.xbel | grep -o 'href="[^"]*"' | sort -u
    169 ls -la /mnt/img/home/*/.local/share/Trash/{files,info}/
    170 ```
    171 
    172 > [!tip] History is written at shell **exit**
    173 > A live attacker's session has no history file yet. Read it from memory instead: `cat /proc/<bash pid>/mem` regions via `gdb`/`pymem`, or Volatility's `linux.bash` plugin on the RAM image. `~/.bash_history` also has **no timestamps** unless `HISTTIMEFORMAT` was set — order is all you get.
    174 
    175 ## Persistence hunt
    176 
    177 Check every one of these; attackers use the boring ones.
    178 
    179 | Mechanism | Where to look |
    180 |---|---|
    181 | **cron** | `/etc/crontab`, `/etc/cron.d/*`, `/etc/cron.{hourly,daily,weekly,monthly}/*`, `/var/spool/cron/crontabs/*` (Debian) / `/var/spool/cron/*` (RHEL), `/etc/anacrontab`, `/var/spool/anacron/`; `at` jobs in `/var/spool/at/` or `/var/spool/cron/atjobs/` |
    182 | **systemd units** | `/etc/systemd/system/`, `/usr/lib/systemd/system/`, `/run/systemd/system/`, per-user `~/.config/systemd/user/`; look for `ExecStart=` pointing at `/tmp`, `/dev/shm`, `/var/tmp`, home dirs, base64 or `curl|sh`; **timers** (`*.timer`) and **path** units; `systemd-run` transient units; generators in `/etc/systemd/system-generators/` |
    183 | **init / rc** | `/etc/rc.local`, `/etc/rc*.d/`, `/etc/init.d/`, `/etc/inittab` (legacy), `/etc/init/*.conf` (upstart) |
    184 | **shell startup** | `/etc/profile`, `/etc/profile.d/*.sh`, `/etc/bash.bashrc`, `/etc/bashrc`, `/etc/zsh/*`, `~/.bashrc`, `~/.bash_profile`, `~/.profile`, `~/.zshrc`, `~/.bash_logout`, `/etc/environment` |
    185 | **ld.so.preload / LD_PRELOAD** | `/etc/ld.so.preload` (should not exist or be empty), `/etc/ld.so.conf.d/`, env of running processes; a rootkit here hides itself from `ls`/`ps` — verify with a static busybox |
    186 | **SSH** | `~/.ssh/authorized_keys` (all users incl. service accounts), `/etc/ssh/sshd_config` (`AuthorizedKeysFile`, `AuthorizedKeysCommand`, `PermitRootLogin`, extra `Include`), `/etc/ssh/sshd_config.d/`, host keys changed, `~/.ssh/rc` |
    187 | **PAM** | `/etc/pam.d/*` modified (`pam_exec.so`, unknown modules in `/lib/security/` or `/usr/lib/x86_64-linux-gnu/security/`), `/etc/security/` |
    188 | **users & groups** | new UID 0, users with shells that should not, `sudoers` / `sudoers.d` NOPASSWD, `wheel`/`sudo`/`docker`/`disk` group members, `/etc/passwd` vs `/etc/shadow` inconsistencies, `chage -l user` |
    189 | **SUID / capabilities** | `find / -xdev -perm -4000 -type f -newer /etc/os-release`, `getcap -r / 2>/dev/null` — compare against a clean install |
    190 | **kernel modules** | `/etc/modules`, `/etc/modules-load.d/`, `/etc/modprobe.d/`, modules in `/lib/modules/$(uname -r)/` not owned by a package, `/proc/modules` vs `lsmod` vs `/sys/module/` mismatch |
    191 | **udev** | `/etc/udev/rules.d/*.rules` with `RUN+=` |
    192 | **XDG autostart** | `/etc/xdg/autostart/*.desktop`, `~/.config/autostart/*.desktop` |
    193 | **package hooks** | `/etc/apt/apt.conf.d/` (`DPkg::Post-Invoke`), `/etc/yum/pluginconf.d/`, `/etc/dnf/plugins/` |
    194 | **web** | webshells in the docroot (recent `.php`/`.jsp`/`.aspx`, `eval(`, `base64_decode(`, `system(`), `.htaccess` rewrites, cron-driven `wget` |
    195 | **containers / orchestrators** | `docker ps -a`, `/var/lib/docker/containers/*/config.v2.json`, privileged containers, host mounts; k8s CronJobs |
    196 | **MOTD / issue** | `/etc/update-motd.d/*` runs as root at login |
    197 | **binary replacement** | `debsums -c` / `rpm -Va` below; `ls -la /usr/bin/ls /bin/ps /usr/bin/ss /usr/bin/netstat` sizes and mtimes vs siblings |
    198 
    199 ```bash
    200 R=/mnt/img
    201 # One-shot sweep of the file-based locations (adjust R for live: R=/)
    202 grep -rE 'ExecStart|ExecStartPre|ExecStop' $R/etc/systemd/system $R/usr/lib/systemd/system $R/home/*/.config/systemd 2>/dev/null | grep -Ev 'ExecStart=/(usr/)?(s?bin|lib)/' | sort -u
    203 cat $R/etc/crontab $R/etc/cron.d/* $R/var/spool/cron/crontabs/* $R/var/spool/cron/* 2>/dev/null | grep -Ev '^\s*(#|$)'
    204 cat $R/etc/ld.so.preload 2>/dev/null; ls -la $R/etc/ld.so.preload 2>/dev/null
    205 find $R/etc/systemd $R/etc/cron* $R/etc/profile.d $R/etc/init.d $R/etc/rc.local $R/etc/ssh $R/etc/pam.d $R/etc/sudoers.d $R/etc/udev/rules.d -type f -printf '%TY-%Tm-%Td %TH:%TM %p\n' 2>/dev/null | sort | tail -40
    206 find $R -xdev -type f \( -path '*/tmp/*' -o -path '*/dev/shm/*' -o -path '*/var/tmp/*' \) -perm -u+x -ls 2>/dev/null
    207 find $R -xdev -type f -name '.*' -perm -u+x -not -path '*/.git/*' -ls 2>/dev/null | head -50
    208 find $R -xdev -type f -perm -4000 -printf '%TY-%Tm-%Td %p\n' 2>/dev/null | sort | tail -20
    209 # Anything in a bin dir not owned by a package (Debian / RHEL)
    210 for f in $R/usr/bin/* $R/usr/sbin/* $R/bin/* $R/sbin/*; do dpkg -S "${f#$R}" >/dev/null 2>&1 || echo "unowned: $f"; done 2>/dev/null | head
    211 ```
    212 
    213 ## Package integrity
    214 
    215 ```bash
    216 # Debian/Ubuntu — compare installed files against package MD5s (from the image's own dpkg DB)
    217 debsums -c --root /mnt/img 2>/dev/null           # changed files only
    218 debsums -ac --root /mnt/img                       # include config files
    219 # RHEL/Fedora — S=size M=mode 5=md5 D=device L=link U=user G=group T=mtime P=caps
    220 rpm -Va --root /mnt/img 2>/dev/null | grep -Ev '^\.{9}\s+c '   # ignore untouched configs
    221 rpm -Va --root /mnt/img | awk '$1 ~ /5/'                       # content changed
    222 rpm -qf /mnt/img/usr/bin/ls; rpm -V coreutils
    223 # Arch
    224 pacman -Qkk --root /mnt/img 2>/dev/null | grep -v '0 altered'
    225 # Package DB itself tampered? Verify against the repo copy:
    226 apt-get download coreutils && dpkg-deb -x coreutils_*.deb /tmp/clean && diff <(sha256sum /tmp/clean/usr/bin/ls) <(sha256sum /mnt/img/usr/bin/ls)
    227 ```
    228 
    229 Rootkit scanners are a cheap second opinion, not a verdict: `chkrootkit -r /mnt/img`, `rkhunter --check --rootdir /mnt/img`. Better: `debsums`/`rpm -Va` plus comparing `ls`, `ps`, `netstat`, `ss`, `lsof`, `find` outputs against a **static busybox** on a live box (`busybox ps` vs `ps`).
    230 
    231 ## Hidden processes and `/proc`
    232 
    233 ```bash
    234 # PIDs in /proc that ps does not show (LD_PRELOAD rootkits hook readdir in libc, not the kernel)
    235 diff <(ls /proc | grep -E '^[0-9]+$' | sort -n) <(ps -eo pid= | tr -d ' ' | sort -n)
    236 # Brute-force /proc — bypasses readdir hooks
    237 for p in $(seq 1 65535); do [ -d /proc/$p ] && [ ! -e /proc/$p/status.x ] && echo -n "$p "; done 2>/dev/null; echo
    238 # Sockets with no owning process in ss (hidden by rootkit) vs /proc/net/tcp
    239 cat /proc/net/tcp /proc/net/tcp6 | awk 'NR>1{print $2, $4, $10}'    # local addr (hex), state (0A=LISTEN), inode
    240 # Process running from a deleted binary
    241 ls -l /proc/*/exe 2>/dev/null | grep '(deleted)'
    242 # Executable memory mappings not backed by a file (injected code, packers)
    243 grep -E 'rwx|r-xp 00000000 00:00 0' /proc/*/maps 2>/dev/null | head
    244 # Namespaces: container escapes / hidden via unshare
    245 ls -la /proc/*/ns/pid 2>/dev/null | awk '{print $NF}' | sort | uniq -c
    246 # Mount namespace tricks: bind-mount over /proc/<pid> to hide it
    247 cat /proc/mounts | grep -E ' /proc/[0-9]+'
    248 # Kernel modules: /proc/modules vs sysfs
    249 diff <(awk '{print $1}' /proc/modules | sort) <(ls /sys/module | sort) | grep '^>'
    250 ```
    251 
    252 ## Timestamps and timestomping
    253 
    254 Every inode has **atime, mtime, ctime**, and on ext4/xfs/btrfs also **crtime** (birth). `touch -d` and `utimensat` change atime/mtime freely; **ctime cannot be set from userspace** (it moves to *now* on any change), and **crtime** never changes. A file whose mtime is 2019 but ctime is last Tuesday was modified last Tuesday.
    255 
    256 ```bash
    257 stat file                                   # Access / Modify / Change / Birth
    258 stat -c '%n %w %x %y %z' /mnt/img/usr/bin/*  | awk '$3 > $4 || $4 > $5'    # mtime after ctime = impossible without tampering
    259 # crtime on ext4 when stat shows "-" for Birth (older coreutils):
    260 debugfs -R "stat /usr/bin/ls" /dev/loop0p2 | grep -E 'crtime|ctime|mtime|atime'
    261 # Files changed in a window, sorted by ctime (attacker cannot fake ctime)
    262 find /mnt/img -xdev -type f -newerct '2026-09-20' ! -newerct '2026-09-22' -printf '%CY-%Cm-%Cd %CH:%CM %p\n' 2>/dev/null | sort
    263 # Fractional-second timestamps: tools that set a whole-second time (touch -r, tar defaults) leave .000000000 — real writes almost never do
    264 find /mnt/img/usr/bin -type f -printf '%TS %p\n' | grep -E '^00\.000000000' | head
    265 # Mismatch between package mtime and actual
    266 dpkg-query -W -f='${Package}\n' | head -1 >/dev/null; ls -l --time-style=full-iso /mnt/img/usr/bin/ps /mnt/img/usr/bin/pgrep   # siblings from one package share mtimes
    267 ```
    268 
    269 > [!info] `relatime` and `noatime`
    270 > Most distros mount with `relatime`: atime updates only if it is older than mtime/ctime or >24 h old. So "atime = last read" is only sometimes true. Check `/etc/fstab` and `/proc/mounts` before concluding anything from access times.
    271 
    272 ## Dead-disk analysis on SIFT Workstation
    273 
    274 **SIFT** (SANS Investigative Forensic Toolkit) is the free Ubuntu-based analysis VM that FOR508 and most Linux DFIR modules run on: Sleuth Kit, plaso, Volatility, `ewf-tools`, `libvshadow`, RegRipper, Autopsy, `bulk_extractor`, YARA, `hindsight`, and the EZ CLI tools under .NET are preinstalled. Get it as an OVA from `sans.org/tools/sift-workstation` or bolt it onto any Ubuntu with `sift install` (Cast: `github.com/teamdfir/sift-cli`). Alternatives with the same toolset: **Tsurugi Linux**, **CAINE**, **REMnux** (malware focus), and Kali's `forensics` metapackage.
    275 
    276 ### Mount the image (read-only)
    277 
    278 ```bash
    279 mmls sdb.E01                                       # partition table with start sectors (TSK reads E01 natively)
    280 fsstat -o 2048 sdb.E01 | head -30                  # filesystem type, block size, last mount, label
    281 # Mount via ewfmount → loop → mount (see acquisition sheet for LVM / LUKS)
    282 sudo ewfmount sdb.E01 /mnt/ewf && sudo losetup -rfP --show /mnt/ewf/ewf1
    283 sudo mount -o ro,noexec,noload /dev/loop0p2 /mnt/img            # ext4: noload = don't replay journal
    284 sudo mount -o ro,noexec,norecovery /dev/loop0p3 /mnt/img/var    # xfs uses norecovery
    285 ```
    286 
    287 ### Sleuth Kit essentials
    288 
    289 | Layer | Tool | Use |
    290 |---|---|---|
    291 | media | `mmls`, `mmstat` | partitions and offsets |
    292 | filesystem | `fsstat` | fs metadata, journal info, block/inode counts |
    293 | filename | `fls`, `ffind` | list files (incl. **deleted**), find name for an inode |
    294 | metadata | `istat`, `ifind`, `icat`, `ils` | inode detail, inode for a block, dump file by inode, list inodes |
    295 | block | `blkls`, `blkcat`, `blkstat`, `blkcalc` | unallocated space, raw blocks |
    296 | journal | `jls`, `jcat` | ext3/4 journal entries (old versions of blocks!) |
    297 | recovery | `tsk_recover`, `tsk_loaddb`, `tsk_gettimes` | carve all deleted files, load into SQLite, bodyfile |
    298 
    299 ```bash
    300 O=2048                                           # partition offset in sectors from mmls
    301 fls -o $O -r -p sdb.E01 > fls-all.txt            # recursive, full paths; deleted entries are marked "* " and "(realloc)"
    302 fls -o $O -r -p -d sdb.E01                       # deleted only
    303 fls -o $O -r -p -m / sdb.E01 > body.txt          # bodyfile (mactime format) with mount point /
    304 istat -o $O sdb.E01 131074                       # timestamps, size, block list for inode 131074
    305 icat -o $O sdb.E01 131074 > recovered.bin        # dump a (deleted) file by inode
    306 ifind -o $O -n /etc/passwd sdb.E01               # inode for a path
    307 ffind -o $O sdb.E01 131074                       # path(s) for an inode
    308 tsk_recover -o $O -e sdb.E01 ./recovered/        # -e = everything incl. allocated; default = unallocated (deleted) only
    309 blkls -o $O sdb.E01 > unalloc.raw                # unallocated blocks → feed to bulk_extractor / foremost
    310 jls -o $O sdb.E01 | head; jcat -o $O sdb.E01 8 1234 | xxd | head    # journal block 1234 (old content)
    311 tsk_loaddb -o $O -d case.db sdb.E01              # SQLite of the whole fs → query with sqlite3 / Autopsy
    312 ```
    313 
    314 ### Timeline: `mactime` (fast) and plaso (complete)
    315 
    316 ```bash
    317 # 1. Filesystem-only timeline from the bodyfile — seconds
    318 mactime -b body.txt -d -z UTC 2026-09-15..2026-09-27 > fs-timeline.csv
    319 # columns: Date,Size,Type(m a c b),Mode,UID,GID,Meta,File Name
    320 
    321 # 2. Super-timeline with plaso: filesystem + logs + journal + histories + browser + apt/dpkg + cron + ... in one file
    322 log2timeline.py --storage-file case.plaso --parsers linux /mnt/img                 # against the mounted tree (preset "linux")
    323 log2timeline.py --storage-file case.plaso --partitions all sdb.E01                # or the image directly (TSK), all partitions
    324 log2timeline.py --storage-file case.plaso --parsers 'linux,!filestat' --hashers sha256 /mnt/img   # tune parsers, add hashes
    325 pinfo.py case.plaso                                                                # what got parsed, counts, errors
    326 
    327 # 3. Output: filter a window and sort
    328 psort.py -o l2tcsv -w timeline.csv case.plaso "date > '2026-09-20 00:00:00' AND date < '2026-09-23 00:00:00'"
    329 psort.py -o dynamic --fields datetime,timestamp_desc,source,source_long,message,parser,display_name -w tl.csv case.plaso
    330 psort.py -o l2tcsv case.plaso "message contains 'authorized_keys' OR message contains 'crontab'" | head
    331 psort.py --analysis tagging --tagging-file /usr/share/plaso/tag_linux.txt case.plaso   # tag logins, sudo, package installs ...
    332 psort.py -o opensearch ... / -o timesketch  → Timesketch for collaborative review
    333 ```
    334 
    335 Open `timeline.csv` in Timeline Explorer (Windows VM) or `visidata timeline.csv` on SIFT; filter around the first suspicious login and read outward. `timestamp_desc` tells you *which* timestamp (`Content Modification`, `Last Access`, `Metadata Modification`, `Creation`); `source` tells you the artefact (`FILE`, `LOG`, `WEBHIST`, `BASH`, `UTMP`, `JOURNAL`).
    336 
    337 > [!tip] Pick the tool for the question
    338 > `fls`/`mactime`: "what files changed between 02:00 and 03:00" — a minute of work. plaso: "everything that happened, from every source, in one sorted list" — an hour of parsing, but it is the artefact of record for the report.
    339 
    340 ### Carving, strings and unknowns
    341 
    342 Unallocated space (`blkls` output), swap, and unknown binaries go to [strings & file triage](/sheets/dfir/strings-file-triage): `bulk_extractor`, `foremost`/`scalpel`, `strings`, `binwalk`, YARA. Memory → [Volatility 3](/sheets/dfir/volatility) (`linux.pslist`, `linux.bash`, `linux.lsof`, `linux.malfind`, `linux.check_syscall`, `linux.check_modules` need a symbol table built with `dwarf2json` from the exact kernel's debug symbols).
    343 
    344 ## Quick answers
    345 
    346 | Question | Command |
    347 |---|---|
    348 | First and last time an IP logged in | `last -Faiw -f wtmp \| grep 203.0.113.9`; `zgrep 203.0.113.9 auth.log*` |
    349 | Which key was used for an SSH login | `auth.log`: `Accepted publickey for bob from ... ssh2: ED25519 SHA256:xxxx` → match fingerprint with `ssh-keygen -lf authorized_keys` |
    350 | What ran with sudo | `grep 'COMMAND=' auth.log*`; `ausearch -m USER_CMD -i`; `journalctl _COMM=sudo` |
    351 | Was a user added | `grep -E 'useradd|usermod|groupadd|passwd\[' auth.log*`; `journalctl _COMM=useradd`; compare `/etc/passwd-` (backup) with `/etc/passwd` |
    352 | What did cron run | `grep CRON syslog*`; `journalctl _SYSTEMD_UNIT=cron.service`; `journalctl -u '*.timer'` |
    353 | USB plugged in | `journalctl -k -g 'usb .*New USB device\|sd .* Attached SCSI'`; `kern.log` |
    354 | Files modified last 48 h, by ctime | `find / -xdev -newerct '48 hours ago' -type f ! -path '/proc/*' ! -path '/sys/*' -printf '%CY-%Cm-%Cd %CH:%CM %p\n' \| sort` |
    355 | Deleted but running | `ls -l /proc/*/exe \| grep deleted`; `lsof +L1` |
    356 | Reboots / uptime gaps | `last -Fx reboot shutdown`; `journalctl --list-boots` |
    357 | Package installed when | `grep ' install ' dpkg.log*`; `rpm -qa --last \| head` |
    358 
    359 ## Related
    360 
    361 - [Acquisition](/sheets/dfir/disk-imaging) — AVML/LiME memory, `dc3dd`/`ewfacquire`, mounting images
    362 - [Volatility 3](/sheets/dfir/volatility) — the memory side
    363 - [strings & file triage](/sheets/dfir/strings-file-triage) — unknown binaries, carving unallocated space
    364 - [Digital Forensics reference](/sheets/dfir/forensics) — the cross-platform overview
    365 - [Linux credential & flag hunting](/sheets/password-attacks/linux-credential-flag-hunting) — the same file locations from the other side of the table