MAATRIX / Блог / Майнер вернулся через два дня после чистки: где он прячет якорь

Майнер вернулся через два дня после чистки: где он прячет якорь

MAATRIX

В конце августа 2026 года у одной команды повторился сюжет, который встречается чаще, чем кажется: нашли процесс, жравший CPU, убили его, выдохнули — а через два дня нагрузка снова поползла вверх. Причина не в том, что «майнер живучий» в каком-то мистическом смысле, а в том, что удалённый процесс был лишь верхушкой: где-то на диске остался механизм, который его перезапускает. Ниже — системный обход всех типичных мест закрепления в Linux, которые нужно проверить, прежде чем считать сервер чистым.

Почему один kill процесса ничего не решает

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

Если вы убили только PID, то по сути прервали работу симптома, а не устранили причину. Через минуту, час или два дня один из оставшихся механизмов снова запустит тот же (или уже переименованный) бинарник. Причём часто закладок несколько сразу — cron перезапустит файл, а systemd-сервис на всякий случай поднимет копию, если cron окажется найден и вычищен первым. Расчёт атакующего простой: чем больше независимых точек закрепления, тем меньше вероятность, что администратор найдёт их все за один проход.

Отсюда практический вывод: обнаружение вредоносного процесса — это не финал расследования, а его начало. Дальше нужен методичный обход всех мест, где Linux позволяет что-то запустить автоматически. Сам процесс диагностики — поиск подозрительных PID, сетевых соединений и файлов — подробно разобран в статье майнер на сервере: как он попал и как его выкорчевать; здесь сосредоточимся именно на том, что остаётся после удаления файла.

Cron: самый частый и самый недооценённый якорь

Cron — первое, что проверяют, и первое, что чаще всего пропускают не до конца, потому что смотрят только свой crontab. Проверять нужно crontab каждого пользователя в системе, а не только root и не только того аккаунта, под которым вы работаете:

for u in $(cut -f1 -d: /etc/passwd); do
  echo "== $u =="
  crontab -u "$u" -l 2>/dev/null
done

Отдельно — системные каталоги, где задачи прописываются в текстовых файлах, а не через crontab:

cat /etc/crontab
ls -la /etc/cron.d/
ls -la /etc/cron.hourly/ /etc/cron.daily/ /etc/cron.weekly/ /etc/cron.monthly/
cat /etc/anacrontab

Ищите строки с @reboot — это прямой сигнал «поднять при загрузке» — а также задачи, запускающие что-то из /tmp, /dev/shm, /var/tmp или скрытых директорий вроде /root/.cache/.x. Обращайте внимание не только на подозрительные команды, но и на файлы с недавней датой изменения там, где давно ничего не менялось:

find /etc/cron.d /etc/cron.daily /etc/cron.hourly -type f -mtime -14 -ls

Реальный разбор похожего случая, только с обратным шеллом вместо майнера — reverse shell в cron у пользователя, которого никто не заводил: там задача была прописана не в crontab, а через контейнерный API, и её искали не в первую очередь.

Отдельно проверьте at — менее популярный, но рабочий планировщик разовых задач, который часто забывают:

atq
ls -la /var/spool/cron/atjobs/ 2>/dev/null

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

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

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

Systemd: unit-файлы, таймеры и пользовательские сервисы

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

systemctl list-units --type=service --all
systemctl list-unit-files --state=enabled
systemctl list-timers --all

Ищите юниты с общими, «сливающимися с системой» именами — network-helper.service, systemd-worker.service, cache-sync.service — и обязательно откройте файл каждого подозрительного юнита:

systemctl cat <имя>.service

Смотрите на директиву ExecStart — если она указывает на файл в /tmp, /var/tmp, /dev/shm или в домашнюю директорию с непонятным именем, это закладка. Полезно найти unit-файлы, изменённые недавно, а не только новые по имени:

find /etc/systemd/system /usr/lib/systemd/system /lib/systemd/system -mtime -14 -name "*.service" -o -name "*.timer"

Отдельная категория — таймеры (systemd.timer), функциональный аналог cron, но менее очевидный при беглом просмотре, потому что сам таймер и исполняемый юнит — это два разных файла, которые нужно сопоставить.

И третья категория, про которую часто забывают — пользовательские сервисы без root, которые переживают даже смену пароля учётной записи, если для пользователя включён linger:

loginctl show-user <user> | grep Linger
ls -la ~<user>/.config/systemd/user/
systemctl --user -M <user>@ list-units --type=service

Если Linger=yes стоит у пользователя, которому вы это не настраивали — это тоже стоит расценивать как подозрительное.

Автозагрузка на уровне шелла и init-скриптов

Здесь набор мест меньше по числу вариантов, но не менее важен, потому что срабатывает при каждом входе в систему или при каждой перезагрузке независимо от cron и systemd:

  • /etc/rc.local — классический механизм, который на системах с systemd работает только через отдельный rc-local.service, но если он присутствует и активен — это готовый способ запускать что угодно при старте. Проверьте содержимое файла и то, включён ли сервис: systemctl is-enabled rc-local.
  • /etc/init.d/ и его подключение через update-rc.d (Debian/Ubuntu) или chkconfig (RHEL-семейство) — устаревший, но всё ещё рабочий способ автозапуска скриптов.
  • **/etc/profile.d/*.sh** — скрипты отсюда выполняются при каждом логине любого пользователя через логин-шелл. Один лишний файл здесь — и код исполняется у всех, кто заходит на сервер.
  • ~/.bashrc, ~/.bash_profile, ~/.profile, /etc/bash.bashrc, /etc/profile — проверьте на предмет добавленных в конец строк, особенно с curl, wget, base64 -d, nohup, обращением к /dev/shm или закодированными командами.
tail -20 ~/.bashrc ~/.bash_profile ~/.profile 2>/dev/null
grep -rn "curl\|wget\|base64" /etc/profile.d/ 2>/dev/null

Ещё одно место, которое стоит проверить на серверах с графическим окружением или desktop-компонентами (редкость на VPS, но встречается) — /etc/xdg/autostart/ и ~/.config/autostart/.

SSH: запасной вход, который переживёт любую чистку процессов

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

Проверьте authorized_keys для каждого пользователя, у которого вообще может быть шелл:

for u in $(cut -f1 -d: /etc/passwd); do
  f=$(eval echo ~$u)/.ssh/authorized_keys
  [ -f "$f" ] && echo "== $u ($f) ==" && cat "$f"
done

Сверяйте каждый ключ с тем, что реально выдавали вашей команде — лишняя строка, даже одна, означает постоянный доступ в обход паролей и двухфакторной аутентификации. Обратите внимание и на дату изменения файла:

stat ~/.ssh/authorized_keys

Дальше — сам конфиг SSH-демона. Проверьте не только /etc/ssh/sshd_config, но и каталог /etc/ssh/sshd_config.d/, куда можно незаметно добавить дополнительный конфиг, который переопределит основной — например, другой AuthorizedKeysFile или разрешение PermitRootLogin:

cat /etc/ssh/sshd_config
ls -la /etc/ssh/sshd_config.d/
grep -i "AuthorizedKeysFile\|PermitRootLogin" /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*

И, наконец, новые пользователи и sudo-права. Атакующий мог создать себе отдельный аккаунт, а не только подложить ключ:

cat /etc/passwd | awk -F: '$3 >= 1000 || $3 == 0 {print}'
ls -la /etc/sudoers.d/
cat /etc/sudoers

Ищите пользователей с UID 0 (кроме root) — это полноценный root-доступ под другим именем — и любые файлы в /etc/sudoers.d/, которых вы не создавали. Реальная история о том, как забытый ключ уволенного сотрудника оставался рабочим входом восемь месяцев — хороший пример того, насколько долго такой якорь может оставаться незамеченным: SSH-ключ уволенного сотрудника работал восемь месяцев.

Менее очевидные места: планировщики приложений и низкоуровневые трюки

Кроме системных механизмов, стоит проверить закрепление на уровне ваших собственных приложений и рантаймов — их часто вообще не рассматривают как часть чистки, а зря:

  • Планировщики приложений — если у вас крутится Laravel (schedule:run через cron, но задачи описаны в коде приложения), Django с django-crontab, Celery beat, PM2 (pm2 startup + pm2 save создают systemd-юнит от имени PM2) — проверьте список задач именно в конфигурации приложения, а не только системный cron, потому что вредоносная задача могла быть добавлена туда же, где легитимные.
  • Docker-контейнеры с политикой перезапуска. Контейнер с restart: always или --restart unless-stopped поднимется заново даже после docker kill, если сам образ или volume содержит вредоносный код. Проверьте список всех контейнеров, включая остановленные: docker ps -a и docker inspect <id> | grep -A3 RestartPolicy.
  • /etc/ld.so.preload. Файл, который подключает указанную в нём библиотеку ко всем запускаемым процессам — классический способ руткита перехватывать системные вызовы. В норме этот файл пуст или отсутствует; если в нём что-то есть — это серьёзный сигнал компрометации на уровне глубже, чем процесс.
  • /etc/udev/rules.d/. Менее известный вектор: правило udev может запускать команду при определённых событиях устройств — способ пережить перезагрузку, не попадая ни в cron, ни в systemd-автозагрузку в привычном виде.
cat /etc/ld.so.preload
grep -rn "RUN" /etc/udev/rules.d/ 2>/dev/null

Чек-лист и когда чистки уже недостаточно

Сведём места закрепления в одну таблицу — используйте её как чек-лист при разборе инцидента:

МеханизмГде проверятьНа что смотреть
Cron пользователейcrontab -l для каждого юзера@reboot, пути в /tmp, /dev/shm
Системный cron/etc/cron.d/, /etc/cron.daily/ и т.д.новые/изменённые файлы
at-задачиatq, /var/spool/cron/atjobs/разовые задачи с странным временем
Systemd-сервисыsystemctl list-units --allобщие имена, чужой ExecStart
Systemd-таймерыsystemctl list-timers --allсвязка таймер + юнит
User-сервисы systemd~/.config/systemd/user/, loginctl lingerсервисы без root
rc.local / init.d/etc/rc.local, /etc/init.d/активные скрипты автозапуска
Shell-профили.bashrc, .profile, /etc/profile.d/добавленные команды в конце файла
SSH-ключи~/.ssh/authorized_keys всех юзеровлишние публичные ключи
SSH-конфиг/etc/ssh/sshd_config(.d/)переопределённые директивы
Пользователи и sudo/etc/passwd, /etc/sudoers.d/UID 0, новые аккаунты
Dockerdocker ps -a, restart policyавтоперезапуск заражённых контейнеров
Планировщики приложенийконфиги Celery/PM2/Laravel и т.п.задачи, не относящиеся к вашему коду
Низкий уровень/etc/ld.so.preload, /etc/udev/rules.d/нехарактерные записи

Если вы прошли весь список и не нашли ничего подозрительного, кроме исходного процесса — вероятно, закрепление было единственным, и точечная чистка оправдана. Но если нашли хотя бы два независимых механизма (например, cron-задачу и подменённый authorized_keys), это признак того, что атакующий действовал методично, и вы не можете быть уверены, что нашли действительно всё — руткит может подменять вывод ps и ls, скрывая часть файлов даже от внимательного администратора.

В такой ситуации единственный надёжный вариант — переустановка системы с нуля: заберите с сервера только проверенные данные (базы, файлы приложения, но не бинарники), поднимите чистую ОС, смените все пароли и ключи без исключений, настройте SSH-доступ заново новой парой ключей. Общий порядок действий при компрометации сервера, не только для случая с майнером, расписан пошагово в статье взломали сервер — пошаговый план.

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

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

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

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

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

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

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

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

Можно ли автоматизировать этот чек-лист скриптом?

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

Если я нашёл только cron-задачу и больше ничего — точно ли этого достаточно?

Не обязательно достаточно того, что вы *нашли* одну задачу — важно, что вы проверили *все* места из списка и ничего больше не обнаружили. Разница существенная: искали везде и нашли одно — это одна ситуация, нашли одно и на этом остановились — другая.

Руткит может скрыть себя от всех команд из этой статьи?

Продвинутый руткит на уровне ядра действительно может подменить вывод ps, ls, netstat так, что стандартные команды ничего не покажут. В этом случае помогает только загрузка с внешнего live-образа и анализ диска в оффлайне, когда скомпрометированное ядро не участвует в выводе данных.

Что делать, если сервер продакшн и его нельзя остановить для полного обхода?

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

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

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

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