Linux on-disk forensics #

Logs

Linux logging is messy because there are two parallel systems: traditional text logs under /var/log/ (syslog, auth.log, messages - appended by rsyslog/syslog-ng) and the newer systemd journal (binary files under /var/log/journal/, queried with journalctl). Many distros run both, but Debian 12+, Fedora and Arch are journal-only by default - check whether a syslog daemon is installed before looking for the text files. Timestamp semantics differ: text logs record only wall-clock (and often local time without a TZ suffix, the single biggest Linux timeline trap); the journal carries both a wall-clock and a monotonic timestamp, letting you re-order events even if someone turned the system clock back.

Path / sourceWhat it tells youHow it's generated / key factTimestampsParsers / tools
/var/log/auth.log (Debian/Ubuntu), /var/log/secure (RHEL/Fedora)SSH logins, sudo, su, PAMWritten by the syslog daemon (rsyslog / syslog-ng) from messages sshd, sudo, su, PAM, cron and logind send to the auth / authpriv facility. Only exists if a syslog daemon is installed - Debian 12+, Fedora and Arch are journal-only by default. Rotated by logrotate (commonly weekly, 4 kept).Legacy prefix MMM DD HH:MM:SS: local time, no year, no TZ (RHEL, older releases). Debian 12+ / Ubuntu 24.04 rsyslog writes RFC 3339 with an offset (2025-01-01T10:00:00.123456+00:00). Confirm the format and the host's TZ.grep/awk, UAC, log2timeline/plaso, Logstash/Elastic, Splunk UF
/var/log/syslog, /var/log/messagesKernel / daemon messagesSame daemon - everything not split into a dedicated file: kernel (via journald or imklog), daemons, services. syslog on Debian/Ubuntu, messages on RHEL.Same format caveats as auth.log. dmesg ring-buffer lines are seconds since boot - anchor them to the boot time (journalctl --list-boots, uptime -s).grep, plaso, Chainsaw, Zircolite
/var/log/wtmp (last)Login / logout / reboot historyBinary utmp records appended by login, sshd, the display manager and init on every login, logout, reboot and shutdown. Rotated monthly by logrotate on Debian/RHEL (1 kept). Newer systems are moving to wtmpdb (SQLite /var/lib/wtmpdb/wtmp.db) - check for it. Easily edited by root (utmpdump round-trip).ut_tv = Unix seconds + microseconds, UTC. tv_sec is 32-bit for compatibility even on 64-bit glibc, so Y2038 applies.last -Faiwx, utmpdump, wtmpdb last, UAC
/var/log/btmp (lastb)Failed loginsSame record format, appended by login / sshd on failed authentication. Root-only (600) - an attacker with root may clean it.Same as wtmp - Unix seconds + µs, UTC.lastb -Faiwx, utmpdump
/var/log/lastlog (lastlog)Last login per userSparse file indexed by UID; pam_lastlog / login overwrites the user's record (time, tty, host) on every login. Being replaced by lastlog2 (/var/lib/lastlog/lastlog2.db). Useless for history, easy to tamper.ll_time = Unix seconds UTC - most recent login only.lastlog, lastlog2, plaso
/run/utmp (who)Currently logged inCurrent sessions, written by login / sshd / logind at login and cleared at logout. On tmpfs, so empty after a reboot - collect it live.Unix seconds UTC - current state only.who, w, utmpdump
journalctl (/var/log/journal/<machine-id>/)Structured logs; filter by _SYSTEMD_UNIT=, _UID=, _BOOT_ID=; --since/--untilsystemd-journald collects service stdout/stderr, syslog(3) calls, kernel and audit messages, and adds trusted fields from the kernel (_PID, _UID, _COMM, _EXE, _SYSTEMD_UNIT, _BOOT_ID) a process can't fake. Persistent in /var/log/journal/ if that folder exists (or Storage=persistent), otherwise volatile in /run/log/journal/ and lost at reboot. Default cap 10% of the filesystem, max 4 GB - on a busy host that can be days, not weeks.Two timestamps per entry: __REALTIME_TIMESTAMP (µs since Unix epoch, UTC) and __MONOTONIC_TIMESTAMP (µs since boot, scoped to _BOOT_ID, unaffected by clock changes). Use -o json to see both.journalctl -o json, jq, UAC, plaso, systemd-analyze
audit.log (/var/log/audit/)auditd - syscall-level if rules are in placeauditd receives events from the kernel audit subsystem for the loaded rules (auditctl, /etc/audit/rules.d/) plus built-in events (logins, account changes, SELinux). Without rules there's little syscall detail. Defaults are tiny (max_log_file=8 MB, 5 files) - harden before an incident, not during.msg=audit(<seconds>.<ms>:<serial>) - Unix seconds with milliseconds, UTC, plus a per-boot serial. Records of one event share the timestamp and serial.ausearch, aureport, auditctl, Laurel (JSON transform), go-audit
faillock dir, PAM tallyLockout countspam_faillock writes a per-user file in /var/run/faillock/ (tmpfs on most systems, cleared at reboot) recording each failed attempt; pam_tally2 on older systems.Unix epoch time per failed attempt.faillock --user, pam_tally2 --user

User / shell artifacts

ArtifactWhat it provesHow it's generated / key factTimestampsParsers / tools
Shell history
~/.bash_history, ~/.zsh_history, ~/.ash_history
Commands a user typed in an interactive shell, in orderThe shell keeps history in memory and writes it to the file when the shell exits (bash appends on exit unless PROMPT_COMMAND='history -a' or shopt -s histappend changes that). A killed shell, unset HISTFILE, HISTSIZE=0, a link to /dev/null or a size-0 file all leave gaps. Non-interactive shells (cron, scripts, ssh host cmd, AI agents) don't write history at all.bash writes no per-command time by default - only order. With HISTTIMEFORMAT exported, each entry gets a #<unix-epoch> line. zsh writes : <epoch>:<elapsed>;<cmd> only when EXTENDED_HISTORY is set (off by default, though some frameworks such as oh-my-zsh enable it). Otherwise the file's mtime ≈ when the last shell exited.cat, UAC, osquery shell_history, Velociraptor Linux.Forensics.BashHistory
Other app histories and dotfiles
~/.viminfo, ~/.lesshst, ~/.python_history, ~/.mysql_history, ~/.psql_history, ~/.sqlite_history, ~/.wget-hsts
Files edited, queries run, interpreters used, hosts contactedEach program writes its own history when it exits: vim records recently edited files, marks, search and command history; the database clients and Python REPL keep readline history; wget records HSTS hosts it connected to. Same gaps as shell history if the program was killed or the variable was redirected.Mostly no embedded time - use the file's stat times (ext4 / XFS v5 also give a birth time). ~/.viminfo entries carry Unix-epoch times; ~/.wget-hsts stores created and max-age per host (Unix epoch).Text editor, stat, UAC
Credential and config files
~/.aws/credentials, ~/.docker/config.json, ~/.config/git/credentials, ~/.kube/config, ~/.ssh/known_hosts
Which cloud, registry and git credentials were on the host (and could have been taken); which hosts the user has connected toWritten by the respective CLIs on login or configuration (aws configure, docker login, the git credential store, kubectl / cloud CLIs). known_hosts gets a line the first time the user's ssh client accepts a host key, hashed if HashKnownHosts yes, but you can still test candidate hosts with ssh-keygen -F.No embedded times; stat times show when the file was last written. Pair with the provider's own logs for when the credential was used.stat, ssh-keygen -F <host> -f known_hosts, UAC
SSH authorized keys
~/.ssh/authorized_keys, /etc/ssh/sshd_config (AuthorizedKeysFile, AuthorizedKeysCommand)
Which keys can log in as this user - attacker-added keys are classic persistenceEdited by the user, by ssh-copy-id, by config management or cloud agents (e.g. cloud-init or the provider's guest agent writing keys from instance metadata), or by an attacker appending a line. sshd reads it on every key-based login, so a new line takes effect immediately.No embedded times - use stat mtime / ctime (and birth time on ext4 / XFS v5). auth.log / the journal record Accepted publickey for <user> ... <key fingerprint> with the login time, so you can see which key logged in when.ssh-keygen -lf authorized_keys (fingerprints), stat, osquery authorized_keys
Shell startup files
~/.bashrc, ~/.bash_profile, ~/.profile, ~/.zshrc, /etc/profile, /etc/bash.bashrc, /etc/profile.d/*
Commands that run every time a user (or every user) starts a shell, a common persistence and alias-hijack spotCreated from /etc/skel when the account is made, or installed by packages (/etc/*); afterwards edited by the user, admins, installers, or an attacker. Login shells read /etc/profile and ~/.bash_profile / ~/.profile; interactive non-login shells read ~/.bashrc.No embedded times. Compare the file's stat mtime with the package install time (dpkg -V / rpm -V also flag changed package files).dpkg -V, rpm -Va, stat, diff against /etc/skel

Filesystem timestamp reality check: Modern Linux defaults to relatime mount option - atime only updates if (a) atime < mtime/ctime, or (b) atime is >24h older than now. On noatime volumes (common for busy servers, flash) atime never updates. Always check cat /proc/mounts at collection; otherwise your "last access" findings are wrong. ext4 and XFSv5 give you crtime (birth) via stat - ext2/ext3 do not. Attackers using touch -r reference target clone the mtime+atime of reference onto target; the ctime still updates because the inode changed, which is how you catch it.

User-artifact collection: UAC (Unix-like Artifacts Collector, the KAPE of Linux) ships profiles that sweep every history/dotfile; osquery packs unix_shell_history, sudoers, authorized_keys; Velociraptor Linux artifacts; GRR; CatScale (WithSecure) bash script triage; linux_dfir_triage.

AI agent artifacts

AI coding agents and assistants run commands, read files and edit code as the user, so their transcripts are often the best execution log on the host - the prompt, every command, and the output the agent saw. The same files collect any secrets the agent read. Paths below are for Linux; ~ = /home/<user> (and /root).

Trap: agent commands run through a non-interactive shell (bash -c / sh -c), so they usually don't appear in ~/.bash_history / ~/.zsh_history. If EDR shows shells spawned by node, claude, codex, cursor or code, the agent transcript is where the command lines and their output are recorded.
AgentWhereWhat it provesFormat, retention, notes
Terminal agents
Claude Code
(Anthropic)
~/.claude/projects/<encoded-cwd>/<session-uuid>.jsonl - full transcripts
~/.claude/history.jsonl - every prompt typed, with project path
~/.claude/file-history/ - pre-edit copies of files it changed
~/.claude/shell-snapshots/
~/.claude/debug/
~/.claude.json - per-project MCP servers, trusted folders, account
~/.claude/settings.json - permission allow-lists, hooks; also <repo>/.claude/settings*.json
Prompts, model replies, every tool call (shell command lines, file reads and writes, web fetches) and the tool output, with working directory and git branch. File-history lets you diff what it changed.JSONL, one event per line, ISO-8601 UTC timestamp on each. <encoded-cwd> is the project path with separators and dots turned into -. Transcripts older than cleanupPeriodDays (default 30) are deleted at startup - collect early. Optional OpenTelemetry export (CLAUDE_CODE_ENABLE_TELEMETRY) can mean a central copy in the SIEM. Enterprise policy: /etc/claude-code/managed-settings.json.
Codex CLI
(OpenAI)
~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl - session transcripts
~/.codex/history.jsonl - prompt history
~/.codex/log/ - TUI / debug log
~/.codex/config.toml - approval mode, sandbox, MCP servers
Prompts, model output, shell commands and results, patches applied, token use.JSONL with timestamp / type / payload per line; the date folders bound session days. Newer builds add a SQLite index and may compress old rollouts (.jsonl.zst). approval_policy = "never", --yolo or --dangerously-bypass-approvals-and-sandbox = commands ran unreviewed.
Gemini CLI
(Google)
~/.gemini/tmp/<project-hash>/chats/ - auto-saved sessions
~/.gemini/tmp/<project-hash>/logs/ - plus checkpoints/ and /chat save tags
~/.gemini/history/<project-hash>/ - shadow git repo (checkpointing, off by default)
~/.gemini/settings.json - and GEMINI.md context files
Conversation and tool calls per project; the shadow repo holds file states before edits.JSON. The hash comes from the absolute project path - open every folder rather than computing it. --yolo / approvalMode = tool calls ran unprompted.
GitHub Copilot CLI~/.copilot/session-state/<session>/events.jsonl - plus workspace.yaml, plan.md, checkpoints/
~/.copilot/session-store.db - SQLite index (newer builds)
~/.copilot/logs/process-<ms-epoch>-<pid>.log
~/.copilot/config.json
Full session event history, plans and file checkpoints; one debug log per process.Older builds used history-session-state/. Log names carry a millisecond epoch and PID, a ready-made process start time to line up with EDR. COPILOT_HOME can relocate the folder.
Amazon Q Developer / Kiro CLI$XDG_RUNTIME_DIR/qlog/ or /tmp/qlog/ - logs
~/.aws/amazonq/, ~/.kiro/ - config
chat.log covers startup, SDK calls and networking; AWS warns it can hold file paths, code, command output and account IDs.Temp-dir logs don't survive reboots or temp cleanup. Full prompt logging is an admin setting that writes to S3 - ask whether it's on. --trust-all-tools = no approval prompts.
AiderIn each repo: .aider.chat.history.md, .aider.input.history
~/.aider.conf.yml
Full chat as Markdown plus input history; aider also commits its own edits to git.Lives in the project, not the home dir - sweep for it, and check git log for aider: commits.
IDE agents
GitHub Copilot Chat / agent mode
(VS Code)
~/.config/Code/User/workspaceStorage/<hash>/chatSessions/ - chat sessions
~/.config/Code/User/workspaceStorage/<hash>/chatEditingSessions/ - files it changed
~/.config/Code/User/globalStorage/github.copilot-chat/
~/.config/Code/logs/<YYYYMMDDTHHMMSS>/window<n>/exthost/GitHub.copilot-chat/
Per-workspace chats including agent-mode tool calls and terminal commands; editing sessions show what it changed.JSON (newer builds JSONL). Map <hash> to a project with the workspace.json next to it; renaming or moving a project orphans the old folder, which stays on disk. Log folder names are editor launch times (local time).
Cursor~/.config/Cursor/User/globalStorage/state.vscdb - table cursorDiskKV: composerData:<id> (session), bubbleId:<id>:<msg> (messages, tool calls, diffs)
~/.config/Cursor/User/workspaceStorage/<hash>/state.vscdb - per-workspace chat list (ItemTable)
~/.cursor/mcp.json - and <repo>/.cursor/ rules and project MCP
~/.config/Cursor/logs/
Agent conversations, the terminal commands it ran, the diffs it applied.SQLite with JSON values. Composer IDs are UUIDs, not time-ordered - sort on the timestamps inside the JSON. The schema changes between versions, so list keys first (SELECT key FROM cursorDiskKV). The DB can be gigabytes.
Windsurf
(Cascade)
~/.codeium/windsurf/ - mcp_config.json, memories/, Cascade conversation data
~/.config/Windsurf/User/
~/.config/Windsurf/logs/
Configured MCP servers, auto-generated memories about the user's projects, Cascade activity logs.Conversation files are a proprietary binary format (reported as encoded protobuf) - expect to work from logs, memories and file timestamps. Verify against the build.
Cline / Roo Code
(VS Code extensions)
~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/tasks/<task-id>/ - Cline
~/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/tasks/<task-id>/ - Roo; same extensions also appear under the Cursor / Windsurf profiles
api_conversation_history.json = exactly what went to and from the model; ui_messages.json = what the user saw, incl. command approvals; task_metadata.json = files touched.Task IDs are millisecond epochs - the folder name is the task start time. checkpoints/ is a shadow git repo of the workspace.
Continue~/.continue/sessions/ - chat sessions (JSON)
~/.continue/config.yaml
~/.continue/dev_data/
Chat sessions and (if on) local usage data.Config shows which model endpoints, including self-hosted ones, it talked to.
Desktop assistants and local models
Ollama
(local models)
journalctl -u ollama - server log (systemd install)
~/.ollama/history - ollama run prompts
/usr/share/ollama/.ollama/models/ - models (service account home)
API requests with client IP, model pulls (where models came from), interactive prompts.Listens on 11434; if bound to 0.0.0.0, the server log shows who else on the network used it.
LM Studio~/.lmstudio/conversations/ - saved chats (JSON)
~/.lmstudio/ - models, local server settings
Saved chats; local API server configuration.Like Ollama, an exposed local server is an abuse and data-exfil surface.
Server-side copies: enterprise plans keep their own record, often with longer retention than the endpoint - Claude Enterprise / Team audit logs and Compliance API, ChatGPT Enterprise Compliance API, GitHub org audit log (Copilot policy and seat events, not prompts), Google Workspace admin audit for Gemini, Purview for Microsoft 365 Copilot (CopilotInteraction audit records, prompt copies via eDiscovery), and S3 prompt logs for Amazon Q / Kiro. Ask for these in the first data request.

What to look for in the transcripts:

  • Commands it ran - timeline every shell tool call next to EDR process events.
  • Secrets it read - .env, cloud credential files, SSH keys, kube configs, browser profiles. What was read is in the transcript, so the transcript is now a copy of the secret.
  • Where data went - web fetches, curl, git push to unfamiliar remotes, new MCP servers pointing at external URLs.
  • Safety switches off - --dangerously-skip-permissions, --yolo, --trust-all-tools, broad allow rules, auto-approve. Check the settings file's mtime against the incident window.
  • Instructions the user didn't write - prompt injection arrives in tool output (a web page, issue, README or MCP response). Read the tool results just before an odd action.
  • Persistence - a planted CLAUDE.md, AGENTS.md, GEMINI.md, .cursor/rules, hook or MCP server re-runs the attacker's instructions every session.
  • Agents driven by malware - the August 2025 Nx "s1ngularity" npm compromise ran claude, gemini and q with permission checks off to hunt for secrets. The parent is a package script, not a terminal, and there's no matching user prompt. Record agent and extension versions too: a poisoned Amazon Q VS Code release (1.84.0, July 2025) shipped a wipe prompt.

Inventory (then copy; take SQLite files with their -wal / -shm files, or close the app first - recent rows often live only in the WAL. Check that your KAPE / Velociraptor / UAC targets actually cover these folders):

for h in /home/* /root; do
  for t in .claude .claude.json .codex .gemini .copilot .continue .cursor .codeium .kiro .aws/amazonq .ollama .lmstudio \
           .config/Code/User .config/Cursor/User .config/Windsurf/User; do
    [ -e "$h/$t" ] && ls -ld "$h/$t"
  done
done 2>/dev/null
ls -ld /etc/claude-code /usr/share/ollama/.ollama /tmp/qlog 2>/dev/null
find / -xdev -name .aider.chat.history.md 2>/dev/null
Timestamps: JSONL transcripts (Claude Code, Codex, Copilot CLI) carry an ISO-8601 UTC timestamp on every line. Cline task folders and Copilot CLI log names are millisecond Unix epochs. VS Code-family log folders are local-time YYYYMMDDTHHMMSS. SQLite stores (Cursor, state.vscdb) keep epoch-millisecond values inside JSON blobs - convert them and don't trust row order. Paths in this category change often: verify against the tool version on the host.

Persistence

Linux persistence lives in three big buckets: scheduled execution (cron, systemd timers, at), process-start hooks (shell rc files, PAM, LD_PRELOAD, dynamic linker tricks), and kernel-level (loadable modules, eBPF programs). Below is the ASEP catalog you should sweep on every engagement. "Where to find it" matters more than "what tool to run" - attackers often customize to evade the obvious tools.

Scheduled execution

  • cron - The classic Unix scheduler. System-wide tabs live at /etc/crontab and in the /etc/cron.{hourly,daily,weekly,monthly} drop-dirs; files added to /etc/cron.d/ are included. Per-user tabs live at /var/spool/cron/crontabs/<user> (Debian/Ubuntu) or /var/spool/cron/<user> (RHEL/Fedora). Format is 5-field schedule + user (system crontab only) + command. Attackers love it because any user can schedule in their own crontab without privs, and the entry just looks like a one-liner. Trap: crontab -l shows only the invoking user's tab - you must iterate every user. anacron (/etc/anacrontab) handles missed jobs on laptops; also check it.
  • at / batch jobs - One-shot scheduled commands via at; queued in /var/spool/cron/atjobs/. Rarely used operationally, so a single at job on a server is highly suspicious. List with atq.
  • systemd units (services) - The modern Linux init system. A "unit" is a text file (.service, .timer, .socket, .path, .mount, etc.) that systemd parses and acts on. Service units specify what to run (ExecStart=), under which user (User=), and when (WantedBy=multi-user.target = run on boot). Three directory tiers (in load order): /etc/systemd/system/ (admin), /run/systemd/system/ (runtime), /usr/lib/systemd/system/ (distro). Per-user units in ~/.config/systemd/user/ run only when that user is logged in / lingering. Attackers drop a .service file named like a legit daemon, enable it (systemctl enable creates a symlink in the target wants-dir), and reboot survives. Hunt: systemctl list-unit-files --state=enabled, diff against a known-good distro baseline; systemd-analyze security <unit> for sandboxing weaknesses.
  • systemd timers - The modern cron replacement. A .timer unit says "every N minutes" or "at specific times" and activates a matching .service. More flexible than cron (random delays, monotonic schedules, calendar expressions) and easier to miss. systemctl list-timers --all is mandatory.
  • systemd generators - Executables in /etc/systemd/system-generators/, /usr/lib/systemd/system-generators/, or /run/systemd/system-generators/ run very early during boot, before most units, and can dynamically generate unit files. Niche attacker persistence that fires before many defenses are up.
  • systemd .path units - Trigger a service when a filesystem path changes. Attacker-friendly: "run my payload every time /tmp/.X-flag is touched."
  • systemd user lingering - loginctl enable-linger <user> keeps a user's systemd instance running even when they're not logged in. Combined with a per-user service, that's reboot-surviving code execution as a non-privileged user.

Legacy init (still present on older/RHEL systems)

  • /etc/rc.local - A plain shell script run late in boot (if the rc-local.service stub is enabled). Lives on through modern systemd distros for backwards compatibility.
  • /etc/init.d/* + /etc/rc?.d/ - SysV init scripts and their symlinked runlevel directories. Mostly superseded, but still invoked on systemd distros via the systemd-sysv-generator. New files in /etc/init.d/ are a red flag on any system from the last 10 years.
  • /etc/inittab - The classic SysV init config. Legacy; still present on older distros / embedded.

Shell & login hooks (user-level, no privs)

  • Shell init files - ~/.bashrc, ~/.bash_profile, ~/.profile, ~/.zshrc, ~/.zprofile, /etc/profile, /etc/bash.bashrc, /etc/profile.d/*.sh. Any of these runs when a user starts an interactive shell (bashrc = non-login, bash_profile/profile = login). Attackers append a line that executes a payload, downloads a C2 stub, or exports an LD_PRELOAD. If a user SSHs in once a day, that's your schedule.
  • MOTD / update-motd.d - Scripts in /etc/update-motd.d/ execute at every interactive login (SSH / console) to generate the message of the day. Writable by root; a new script here runs as root every login. Audit /etc/motd too (static) and /etc/pam.d/sshd for the pam_motd module.
  • XDG autostart - Desktop-environment persistence: ~/.config/autostart/*.desktop and /etc/xdg/autostart/*.desktop. Only triggers when a GUI session starts, so applicable to workstations more than servers.
  • Default login shell swap - Changing a user's shell in /etc/passwd to a wrapper that runs the payload and then execs bash.

Dynamic linker & library-level hooks

  • /etc/ld.so.preload - Any shared object listed in this plain-text file is loaded into every dynamically-linked ELF process on the system before any other library. Classic rootkit hook (Jynx, Azazel, Symbiote all use it). The malicious .so hooks libc functions (readdir, open, stat) to hide its own files, processes, and connections. A non-empty /etc/ld.so.preload on a server is almost always malicious.
  • LD_PRELOAD environment variable - Same mechanism, scoped to a process and its children. Attackers set it in ~/.bashrc or /etc/environment so it applies to every interactive user session. SUID binaries ignore it (good) but anything else honors it.
  • LD_LIBRARY_PATH injection - Prepending an attacker-controlled directory to LD_LIBRARY_PATH lets malicious libs shadow real ones. Set via /etc/environment, user shell rc, or systemd unit's Environment=.
  • Shared library trojaning - Replace or patch a system-wide library (libc.so.6, libpam.so, libssl.so) with a backdoored version. Detect by signature check against distro packages: debsums -c (Debian/Ubuntu), rpm -Va (RHEL/Fedora), AIDE, Tripwire.

Kernel-level

  • Loadable Kernel Modules (LKMs) - .ko files in /lib/modules/$(uname -r)/; loaded via modprobe / insmod. Diamorphine, Reptile, LKM-Rootkit hook syscalls to hide processes, files, and network connections. Enumerate with lsmod, cat /proc/modules, inspect /proc/kallsyms for suspicious symbols. dmesg | grep -i taint - any taint flag means a non-distro module got loaded. /etc/modules-load.d/ and /etc/modprobe.d/ are config dirs that cause modules to auto-load at boot.
  • eBPF-based rootkits - BPFDoor, Pumakit, Boopkit, TripleCross, Symbiote (hybrid). Modern alternative to LKMs because eBPF programs are loadable at runtime by root, don't need a kernel-matching compile, and bypass many LKM-focused detections. Hunt via bpftool prog list, bpftool map list, bpftool link list; inspect /sys/fs/bpf/ for pinned programs; cat /sys/kernel/debug/tracing/kprobe_events for attachment points. Tracee and Falco can detect anomalous eBPF loads at runtime.
  • Kernel core_pattern - /proc/sys/kernel/core_pattern defines what to do with core dumps; setting it to a pipe (|/tmp/evil.sh %p) means your script runs as root every time any process crashes. Classic container-escape primitive too.

Authentication & access-granting primitives

  • SSH authorized_keys - The simplest, most durable, most-often-missed attacker persistence on Linux. Any public key appended to ~/.ssh/authorized_keys grants that user shell access. Attackers also drop into /root/.ssh/authorized_keys (if they have root) and sometimes /home/*/\.ssh/authorized_keys across every user. Sweep every home directory. Check for ssh keys that reference an "options=" prefix (command=, no-pty) hiding what they do.
  • sshd_config backdoors - /etc/ssh/sshd_config and the newer /etc/ssh/sshd_config.d/*. Attacker-interesting directives: AuthorizedKeysFile pointed at a world-readable path they control; ForceCommand that runs their shim before dropping to a shell; Match User blocks that relax rules for specific users; PermitRootLogin yes; PubkeyAuthentication yes + a planted key; Banner set to a payload path (rare but seen). Also check ~/.ssh/config, ~/.ssh/authorized_keys options like command=, environment=.
  • SSH host-key or client-config swap - Replacing /etc/ssh/ssh_host_* to accept attacker clients, or planting a ~/.ssh/config that proxies through an attacker host.
  • PAM (Pluggable Authentication Modules) - /etc/pam.d/* are text files describing the auth stack per service (sshd, login, sudo, passwd, …). Each line names a .so module. Attackers either (a) add a line like auth sufficient pam_backdoor.so that always returns success, or (b) replace a legit module (pam_unix.so) with a backdoored version that logs cleartext credentials to disk (classic Skidmap, SkidMAP, variants of Tsunami). pam-rhino, Pambd are reference malicious modules. Enumerate loaded module paths with ldd on each listed .so; checksum them against the distro package.
  • sudoers manipulation - /etc/sudoers and /etc/sudoers.d/*. An entry like attacker ALL=(ALL) NOPASSWD: ALL grants pw-less root. More subtle: Defaults !tty_tickets, Defaults env_keep += "LD_PRELOAD", allowing specific binaries that have GTFO privesc paths. Always check for sudoers includes with recent mtime.
  • SUID / SGID binaries - Files with the setuid or setgid bit run with the owner's privileges regardless of invoker. find / -perm -4000 -type f 2>/dev/null. Cross-reference every unusual SUID against GTFOBins - any match is a privesc gadget. Attackers also drop their own SUID-root shell copy somewhere obscure (/var/tmp/.cache/sh) as a durable local privesc path.
  • File capabilities - A finer-grained alternative to full SUID; capability bits attached via setcap. getcap -r / 2>/dev/null. Dangerous ones to see on unexpected binaries: cap_sys_admin, cap_sys_ptrace, cap_dac_override, cap_net_raw, cap_setuid. Example: python3 with cap_setuid+ep = instant root.

Package manager & config hooks

  • apt hooks - /etc/apt/apt.conf.d/*. Lines like DPkg::Pre-Install-Pkgs or APT::Update::Post-Invoke execute commands as root around every package install/update. Attacker-perfect because sysadmins update frequently.
  • dnf/yum plugins - /usr/lib/python*/site-packages/dnf-plugins/*.py; equivalent code-execution opportunity on RHEL-family.
  • Debian dpkg triggers / RPM scriptlets - Package maintainer-scripts (pre-install, post-install, pre-remove, post-remove) run as root. Backdoored packages are a supply-chain concern (XZ Utils CVE-2024-3094).
  • Udev rules - /etc/udev/rules.d/ can trigger scripts on hardware events. Example: a USB insert that execs a payload. Rare but seen in physical-access attacks.
  • NetworkManager / dispatcher scripts - /etc/NetworkManager/dispatcher.d/* run on network state changes (as root).
  • logrotate hooks - /etc/logrotate.d/* allows prerotate/postrotate shell blocks; runs as root daily.
Persistence sweep (Linux): systemctl list-unit-files --state=enabled, systemctl list-timers --all, systemd-analyze security, iterate crontab -l -u <u> per user, cat /etc/crontab + /etc/cron.d/* + /etc/cron.{hourly,daily,weekly,monthly}/*, cat /etc/ld.so.preload, bpftool prog list, enumerate ~/.ssh/authorized_keys for every home, diff /etc/pam.d/* + /etc/ssh/sshd_config* against distro defaults, rpm -Va / debsums -c for binary tampering, Lynis hardening audit, UAC for packaged collection, linPEAS (ref: shows what an attacker would find), pspy (snoops cron/exec via procfs), osquery packs: startup_items, crontab, systemd_units, authorized_keys, pam_tokens.

Rootkits & fileless

Modern Linux malware increasingly runs without touching disk - either as an eBPF program loaded into the kernel's virtual-machine subsystem (visible to bpftool but invisible to ls), or as a process backed by an in-memory file descriptor created with memfd_create, where /proc/<pid>/exe resolves to /memfd:... or shows (deleted). The classic LKM rootkits (Diamorphine, Reptile) are still around but now compete with eBPF-based alternatives (BPFDoor, Pumakit) that are harder to detect because eBPF is a legitimate observability feature of the kernel. Hunting here is a game of comparing enumerations: process from /proc walk vs. ps, loaded modules vs. /proc/modules vs. sysfs, and auditing which eBPF programs are loaded against a known-clean baseline.

  • eBPF-era rootkits: BPFDoor, Symbiote (LD_PRELOAD), Pumakit, boopkit, TripleCross. Hunt via bpftool prog list, cat /sys/kernel/debug/tracing/kprobe_events, unexpected tracepoints.
  • Fileless execution - memfd_create: look for /proc/<pid>/exe → /memfd:…, fexecve, process whose /proc/<pid>/exe is (deleted) but still mapped (/proc/<pid>/maps).
  • io_uring - bypasses many hooks; watch for unusual io_uring_setup in auditd.
  • Hidden processes - compare /proc enumeration to ps; LKM rootkits (Diamorphine) hide via syscall hook.
Rootkit / fileless hunting: bpftool (prog list, map list, link list), /sys/kernel/debug/tracing/ inspection, Tracee (Aqua, eBPF runtime security), Falco, Sysdig Inspect, unhide (compares ps/proc), Lynis, Linux Forensics (Hal Pomeranz scripts), Volatility 3 Linux (linux.psaux, linux.malfind, linux.check_syscall, linux.check_modules, linux.bash), osquery kernel_modules, process_open_files. chkrootkit/rkhunter are junior-tier signature scanners.

Containers

Containers are a namespace + cgroup construct, not a hypervisor boundary - they share the host kernel, so a container compromise can escalate to host compromise under the right conditions (privileged flag, host-pid, mounted docker.sock, kernel LPE). Forensics splits into three layers: the image (what shipped - scan with Trivy/Grype, verify provenance), the runtime (what actually executed - Falco/Tracee eBPF events, container stdout/stderr, docker events), and the orchestration plane (K8s audit log, admission events, ServiceAccount token usage). The containerd/overlay2 directory tree persists the writable layer of every container that ran, so you can often reconstruct a destroyed container's filesystem from /var/lib/docker/overlay2/ even after docker rm.

  • Docker - /var/lib/docker/ (containers, image, overlay2, network). docker inspect, docker events --since, daemon.json.
  • Containerd - /var/lib/containerd/, task shim sockets.
  • K8s - kubectl get events --all-namespaces, audit logs on API server, admission controller trail, serviceaccount token mount (/var/run/secrets/kubernetes.io/serviceaccount/token).
  • Escapes to know by CVE: runC CVE-2024-21626 (Leaky Vessels), cgroups v2 release_agent abuse (older), docker.sock mount, privileged + --pid=host, CAP_SYS_ADMIN + user namespace tricks.
  • Image provenance - skopeo inspect, SBOM, signature (cosign) when evaluating a suspect image.
Container forensics tooling: docker/crictl/ctr CLIs, kubectl (get events -A, logs, describe), stern for multi-pod logs, k9s for live triage, Trivy/Grype for image scan, docker-explorer (Google) for offline analysis of docker filesystems, Cadvisor, Falco/Tracee runtime, kube-forensics, mkit (Microsoft Kubernetes attack toolkit, ref), Falcon Cloud Workload Protection for runtime k8s.

Linux CVEs to check exposure against

  • CVE-2022-0847 Dirty Pipe - pipe splice r/w on read-only files
  • CVE-2023-0386 OverlayFS - fuse privesc
  • CVE-2023-4911 Looney Tunables - glibc GLIBC_TUNABLES local root
  • CVE-2024-1086 nf_tables UAF - kernel LPE
  • CVE-2024-3094 XZ Utils backdoor (Jia Tan) - sshd supply chain; versions 5.6.0/5.6.1

Linux collection tooling

  • UAC - Unix-like Artifacts Collector (Thiago Canozzo Lahr). Script-based triage, modular, outputs tar. The "KAPE of Linux"; Velociraptor's Linux artifacts and offline collector cover the same ground and scale across a fleet.
  • AVML (Microsoft) - kernel-agnostic memory capture
  • LiME - kernel-module memory capture (requires matching kernel-headers)
  • Volatility3 Linux profiles (via dwarf2json / ISF)
  • osquery live triage; Velociraptor Linux artifacts; GRR
  • Loki/Thor YARA scanning; Sysmon for Linux (ETW-like)
Order of operations: on a Linux host with a suspected eBPF rootkit, (1) capture memory with AVML before anything else, because the rootkit can interfere with later collection; (2) run UAC for triage; (3) save bpftool and tracefs output; (4) take a disk image if needed.