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