persistence
The files Linux and Unix attackers use to keep access to a host (crontabs, at jobs, systemd units, init scripts, SSH authorized keys and sshd_config, rc.local and shell start-up files, /etc/ld.so.preload, sudoers, PAM, udev rules, XDG autostart entries and kernel modules), read into entries that say what runs, as whom and when, and what looks suspicious. Made for triage collections (UAC's [root]/…), works on any copy of a host's files. One dependency, its sibling sootmark-common (SHA-256).
[]
= "0.3"
let path = "[root]/etc/cron.d/sysupdate";
if let Some = detect
What you get
detect(path): the kind of file from its path on the host, relative (etc/crontab), absolute, or as a collection stores it (uac/[root]/etc/crontab).parse(kind, bytes, path): oneEntryper job, setting, key, command line, library or rule, with its line, the account it runs as or belongs to (user), what runs (command), when (schedule), and the rest indetail. Lines that can't be read go toproblems.- crontabs: users' (
var/spool/cron/crontabs/<user>, RHEL'svar/spool/cron/<user>, run as the account the file is named after) and the system's (etc/crontab,etc/cron.d/*, with their account field): five time fields or@reboot,@daily…, the command as written,SHELL=,PATH=,MAILTO=kept. cron doesn't continue lines: a trailing backslash stays in the command, as cron leaves it.etc/anacrontab: period, delay, job id, command. - systemd units (
.service,.timer,.path,.socket, and drop-ins<unit>.d/*.conf) in the system and user unit directories, homes'.config/systemd/userincluded: every setting with its section;Exec…=commands with their-@:+!prefixes removed and kept, run asUser=or the home's owner; timers'OnCalendar=,OnBootSec=… as the schedule. Backslash continuations,#and;comments. - authorized_keys (any
.ssh/authorized_keysorauthorized_keys2): options (command="…"as the entry's command,from="…",no-pty, …), key type, key, itsSHA256:fingerprint asssh-keygen -lprints it, comment; the account from the home. A key whose bytes name another type, or aren't base64, is reported. - rc.local (
etc/rc.local,etc/rc.d/rc.local, ESXi'setc/rc.local.d/local.sh) and shell start-up files (etc/profile,etc/profile.d/*.sh,etc/bash.bashrc,etc/bashrc, zsh's, and homes' andetc/skel's.bashrc,.profile,.bash_profile,.zshrc, …): each line that isn't blank or a comment, backslash continuations joined; the shell's grammar isn't read. - ld.so.preload: each library, read as glibc reads the file, its quirky comment handling included. Distributions ship none: any entry deserves a look.
- sudoers (
etc/sudoers,etc/sudoers.d/*): rules (users, hosts, run-as, tags such asNOPASSWD, commands), aliases kept as written,Defaultslines,@include/@includedir/#include/#includedir. - at jobs (
var/spool/cron/atjobs/*, RHEL'svar/spool/at/*, BSD'svar/at/jobs/*): the commands at's script ends with (after itscd … || {…}header, or in at 3.1's here-document), the account from its header, and the queue, job number and run time (UTC) from the file name. - init scripts (
etc/init.d/*,etc/rc.d/init.d/*): each command line, run by root, read asrc.localis. - PAM (
etc/pam.d/*,etc/pam.conf): each rule's service, type (-kept), control (bracketed lists included), module and arguments;pam_exec.so's program as the command;@includelines.#comments anywhere on a line, as Linux-PAM reads them. - sshd_config (
etc/ssh/sshd_config,etc/ssh/sshd_config.d/*): each setting,Keyword valueorKeyword=value, with theMatchcriteria it's under;ForceCommand,AuthorizedKeysCommandandSubsystemcommands as the entry's command. - udev rules (
etc/udev/rules.d/*.rules,usr/lib/udev/rules.d,lib/…,run/…): each rule'sKEY{attribute}op"value"pairs; what it runs (RUN,RUN{program},PROGRAM,IMPORT{program}, notRUN{builtin}) as the command, run by root when a matching device appears. - XDG autostart (
etc/xdg/autostart/*.desktop, a home's.config/autostart/*.desktop): theExec=command a desktop session starts at login, itsName=, and whetherHidden=trueorX-GNOME-Autostart-enabled=falseturns it off. - kernel modules: those loaded at boot (
etc/modules,modules-load.d/*.conf) and modprobe's directives (modprobe.d/*.conf),installandremovecommands as the entry's command.
- crontabs: users' (
Entry::summary(): a line saying what it does (@reboot as root: /dev/shm/.x/run).flags(entry): leads, not verdicts. Commands run from/tmp,/var/tmpor/dev/shm;curl/wgetpiped to a shell; base64 decoding;nc,ncat,socator bash's/dev/tcp;@rebootjobs; keys with a forced command; any ld.so.preload library; sudoNOPASSWD: ALLorDefaults !authenticate; PAM'spam_exec.so,auth sufficient pam_permit.soand modules given by a path outside/lib…/security;PermitRootLogin yes,PermitEmptyPasswords yes, and keys read from elsewhere than the homes'.ssh/authorized_keys(AuthorizedKeysFile,AuthorizedKeysCommand); modprobeinstall/removecommands other than/bin/trueor/bin/false.
Not read here: ssh_config, NetworkManager dispatcher scripts, motd scripts, git hooks.
How it's checked
- A Debian 13 system's default files: every one detected and read without a problem, values as Debian wrote them, entry counts, and not one flag raised. The permissively licensed ones (sudoers, OpenSSH's units) are vendored in
tests/fixtures/debian/(seeNOTICE); the rest are GPL-licensed, so CI copies them from a debian:trixie container (tests/debian/fetch.sh <folder>, thenSOOTMARK_PERSISTENCE_DEBIAN=<folder> cargo test --test debian). - Files written as an attacker might leave them (
tests/fixtures/synthetic/, documentation addresses only), each format's syntax covered: what runs, as whom, when, and what's flagged. systemd 257'ssystemd-analyze verifyaccepts every unit in both sets, visudo the sudoers files, Debian's cron (crontab -n) the synthetic crontabs, OpenSSH'ssshd -tthe synthetic sshd snippet, systemd'sudevadm verifythe synthetic udev rule; the synthetic at job is laid out as Debian 13's at 3.2.5 writes them. - Fingerprints as
ssh-keygen -lfprints them (OpenSSH 10.0 and 10.3), for Ed25519, RSA and ECDSA keys made for the tests. - ld.so.preload as glibc 2.41 reads it: the libraries it tried to load from the same files, and its comment loop transliterated and compared.
- Property tests: arbitrary bytes, text made of the formats' punctuation, and the fixtures damaged anywhere, read as every kind, give entries or problems, never a panic.
Licence
MIT or Apache-2.0, at your option. The Debian files under tests/fixtures/debian/ keep their packages' licences (ISC and OpenSSH's BSD-style).