Процессы, которых вчера не было: как сверить систему с нормой
Вы смотрите на вывод ps aux и понимаете, что не можете с уверенностью сказать, какие из полусотни строк там законные, а какие — нет. То же самое с ss -tulpn: часть портов вы открывали сами, часть открыл какой-то пакет при установке, а часть — кто знает кто. Проблема не в том, что вы плохо знаете сервер. Проблема в том, что у вас нет эталона, с которым можно сравнить текущую картину. Ниже — рабочая методика: как один раз зафиксировать «норму» процессов, портов и автозагрузки, а потом дёшево и регулярно сверять с ней реальность.
Содержание
Почему разовая проверка не спасает
Аудит «на глаз» хорош ровно один раз — в момент, когда вы его провели. Через неделю вы уже не вспомните, был ли на порту 8081 процесс node в прошлый вторник или нет. А злоумышленник, который закрепился на сервере, обычно не спешит: тихий майнер, cron-задача, которая раз в час стучится на C2-сервер, или лишний пользователь с shell-доступом могут жить неделями незамеченными, потому что просто не с чем сравнить.
Разбор реальных инцидентов почти всегда упирается в один и тот же вопрос: «а это точно новое, или оно тут всегда было?» Без ответа на него приходится поднимать историю деплоев, спрашивать всех подряд в чате и терять часы там, где при наличии эталона хватило бы одной команды diff. Ровно так строился разбор в статье про reverse shell в cron у пользователя, которого никто не заводил — там половина времени ушла на восстановление картины «что тут должно быть», потому что готового эталона не было.
Снимок нормы (baseline) решает именно эту проблему: вы фиксируете состояние системы в спокойное время, когда уверены, что всё чисто, и дальше сверяетесь с фактами, а не с памятью.
Что входит в снимок нормы
Для практической цели — заметить чужой процесс — достаточно трёх срезов, снятых вместе, потому что новая активность почти всегда оставляет след хотя бы в одном из них:
- Запущенные процессы — что реально исполняется прямо сейчас.
- Слушающие порты — что принимает входящие соединения.
- Автозагрузка — что запустится само при рестарте: сервисы systemd и задания cron.
Процесс без записи в автозагрузке переживёт вас до первого ребута. Порт без процесса, который его слушает, — не порт. А автозагрузка без соответствующего процесса или порта — заготовка на будущее, которую стоит проверить в первую очередь.
Команды для снятия текущего состояния:
# процессы: pid, кто запустил, когда, полная командная строка
ps -eo pid,ppid,user,lstart,cmd --sort=cmd
# слушающие TCP/UDP-порты с привязкой к процессу
ss -tulnp
# запущенные сервисы systemd
systemctl list-units --type=service --state=running --no-legend
# что включено в автозагрузку
systemctl list-unit-files --type=service --state=enabled --no-legend
# пользовательские cron-задания (для каждого пользователя из /etc/passwd)
for u in $(cut -d: -f1 /etc/passwd); do
crontab -u "$u" -l 2>/dev/null && echo " ^ пользователь: $u"
done
# системные каталоги cron
ls -la /etc/cron.d /etc/cron.daily /etc/cron.hourly /etc/cron.weekly /etc/cron.monthly
Если хочется проверить открытые порты ещё и снаружи, а не только через ss изнутри — это отдельная и полезная привычка, подробно она разобрана в статье про открытые порты на сервере.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверКак снять первый эталон
Момент для снятия baseline — не «когда получится», а конкретно: сразу после чистой установки и базовой настройки безопасности, или сразу после аудита, когда вы прошлись по системе руками и уверены, что лишнего нет. Откладывать «пока не наведу порядок» бессмысленно — эталон и есть инструмент наведения порядка, просто он живой и обновляется вместе с сервером.
Простой скрипт, который снимает все три среза в отдельные файлы:
#!/bin/bash
# baseline-snapshot.sh — снять эталонный снимок нормы
set -euo pipefail
BASE_DIR=/var/lib/baseline
mkdir -p "$BASE_DIR"
# процессы — без PID, он всё равно каждый раз новый; берём пользователя и команду
ps -eo user,cmd --no-headers | sed 's/[0-9]\+/N/g' | sort -u > "$BASE_DIR/processes.txt"
# порты — тоже без изменчивых деталей вроде временных портов клиентских соединений
ss -tulnp | awk '{print $1, $5}' | sort -u > "$BASE_DIR/ports.txt"
systemctl list-units --type=service --state=running --no-legend \
| awk '{print $1}' | sort > "$BASE_DIR/services-running.txt"
systemctl list-unit-files --type=service --no-legend \
| awk '$2=="enabled"{print $1}' | sort > "$BASE_DIR/services-enabled.txt"
for u in $(cut -d: -f1 /etc/passwd); do
crontab -u "$u" -l 2>/dev/null | sed "s#^#[$u] #"
done | sort > "$BASE_DIR/cron-user.txt"
find /etc/cron.d /etc/cron.daily /etc/cron.hourly /etc/cron.weekly /etc/cron.monthly \
-type f 2>/dev/null | sort > "$BASE_DIR/cron-system.txt"
echo "Снимок сохранён в $BASE_DIR"
Важный нюанс — из процессов и портов стоит убрать всё, что меняется каждый запуск само по себе (PID, эфемерные клиентские порты), иначе сверка будет ложно находить «расхождения» там, где по сути ничего не изменилось. Отсюда sed 's/[0-9]\+/N/g' в примере — грубый, но рабочий способ нормализовать числа.
Файлы эталона стоит держать под git — так diff заменяется на git diff, а заодно появляется история: когда и что именно изменилось.
cd /var/lib/baseline
git init -q
git add -A
git commit -q -m "baseline: $(date +%Y-%m-%d)"
Автоматизация сверки
Сверка — это тот же самый скрипт снятия снимка, только результат не заменяет эталон, а сравнивается с ним и коммитится отдельно. Логика простая: снять текущее состояние во временный каталог, прогнать diff против эталона, и если есть расхождения — уведомить.
#!/bin/bash
# baseline-check.sh — сверить текущее состояние с эталоном
set -euo pipefail
BASE_DIR=/var/lib/baseline
CUR_DIR=$(mktemp -d)
DIFF_OUT=$(mktemp)
# те же команды, что и в baseline-snapshot.sh, но пишем в $CUR_DIR
ps -eo user,cmd --no-headers | sed 's/[0-9]\+/N/g' | sort -u > "$CUR_DIR/processes.txt"
ss -tulnp | awk '{print $1, $5}' | sort -u > "$CUR_DIR/ports.txt"
systemctl list-units --type=service --state=running --no-legend \
| awk '{print $1}' | sort > "$CUR_DIR/services-running.txt"
systemctl list-unit-files --type=service --no-legend \
| awk '$2=="enabled"{print $1}' | sort > "$CUR_DIR/services-enabled.txt"
for u in $(cut -d: -f1 /etc/passwd); do
crontab -u "$u" -l 2>/dev/null | sed "s#^#[$u] #"
done | sort > "$CUR_DIR/cron-user.txt"
find /etc/cron.d /etc/cron.daily /etc/cron.hourly /etc/cron.weekly /etc/cron.monthly \
-type f 2>/dev/null | sort > "$CUR_DIR/cron-system.txt"
FOUND_DIFF=0
for f in processes ports services-running services-enabled cron-user cron-system; do
if ! diff -u "$BASE_DIR/$f.txt" "$CUR_DIR/$f.txt" > "$DIFF_OUT.$f" 2>/dev/null; then
FOUND_DIFF=1
echo "=== Расхождение: $f ===" >> "$DIFF_OUT"
cat "$DIFF_OUT.$f" >> "$DIFF_OUT"
fi
done
if [ "$FOUND_DIFF" -eq 1 ]; then
# отправка уведомления — свой канал; здесь пример с curl в webhook
curl -s -X POST "$WEBHOOK_URL" --data-urlencode "text@$DIFF_OUT" >/dev/null || true
cat "$DIFF_OUT"
fi
rm -rf "$CUR_DIR" "$DIFF_OUT"*
Запускать это стоит не руками, а по расписанию — через cron или systemd-таймер:
# /etc/cron.d/baseline-check
*/30 * * * * root /usr/local/bin/baseline-check.sh >> /var/log/baseline-check.log 2>&1
Периодичность — вопрос вашего порога риска и цены ложных срабатываний. Раз в 30 минут — разумный компромисс для боевого сервера; для менее критичных достаточно раз в несколько часов. Слишком частая сверка на нагруженной машине добавляет заметный, хоть и небольшой, расход CPU на ps/ss/systemctl — на большинстве VPS это единицы процентов одного ядра, но если ресурсы и так впритык, есть смысл развести проверку по времени с другими фоновыми задачами.
Как читать расхождения: не всё новое — угроза
Это ключевой момент всей методики, и именно здесь чаще всего либо тонут в шуме, либо привыкают игнорировать алерты. Diff покажет вам любое изменение — и настоящую угрозу, и совершенно законный деплой. Задача не «реагировать на каждую строчку», а быстро отличить одно от другого.
Типичный шум, который не требует расследования, но требует обновления эталона:
- Новый процесс после планового деплоя или обновления пакетов.
- Новый systemd-сервис, который вы сами добавили (например, при установке нового бэкенда).
- Изменившийся
services-enabled.txtпослеapt upgrade, если апдейт менял состав пакетов. - Cron-задание, добавленное осознанно — например, ротацией логов или бэкапом.
Сигналы, которые требуют объяснения в первую очередь:
- Процесс, слушающий порт, о котором никто в команде не знает.
- Бинарник, который запускается из
/tmp,/dev/shmили другого каталога, куда обычно ничего не кладут на исполнение. - Cron-задание у пользователя, которого никто не заводил, или у системного пользователя, у которого cron в принципе не должен быть настроен.
- Новый systemd-сервис или enabled-юнит, которого не было ни в одном деплое или тикете.
- Процесс с именем, похожим на системный, но не совпадающим один в один (лишний пробел, похожая буква, неверный путь до бинарника).
Рабочий чек-лист триажа — три вопроса на каждую находку:
- Кто? Есть ли у изменения автор — деплой, тикет, конкретный человек, который может подтвердить, что это он.
- Когда? Совпадает ли время появления с известным событием (релиз, апдейт, ваши собственные действия), или оно случилось в 4 утра без всякой причины.
- Объяснимо ли за 2 минуты? Если на объяснение уходит больше времени и приходится гадать — это уже повод для полноценного разбора, а не «наверное, само собой».
Если после проверки оказывается, что расхождение реально не объясняется — дальше в ход идёт более глубокая диагностика: поиск скрытых процессов и их происхождения разобран в статье про скрытые процессы на сервере.
Куда встраивать сверку: рутина, а не разовая акция
Baseline работает только если он живой — то есть обновляется вместе с системой, а не лежит нетронутым полгода. Практическое правило: любое осознанное изменение автозагрузки, сервисов или открытых портов заканчивается перезапуском baseline-snapshot.sh и коммитом. Это несложно встроить в чек-лист деплоя как последний пункт — так эталон никогда не устаревает настолько, чтобы генерировать сплошной шум.
Ещё несколько практических моментов:
- Храните эталон отдельно от боевого раздела, если возможно — на отдельном разделе с ограниченными правами записи, а лучше вообще реплицируйте git-репозиторий на другую машину или в облачное хранилище. Если атакующий получил root, он может подменить и эталон, и результаты сверки — снимок, до которого не дотянуться с того же сервера, куда надёжнее.
- Держите историю, а не только последний коммит.
git log -p /var/lib/baselineза месяц — это готовая хронология всех изменений системы, которая пригодится не только для безопасности, но и для обычного разбора инцидентов вида «а когда у нас появился этот сервис». - Сверка — не замена мониторингу и логам, а его дополнение на другом уровне: мониторинг метрик покажет всплеск нагрузки, а сверка с эталоном подскажет, чем именно этот всплеск вызван на уровне процессов и портов. Для более глубокого расследования — кто именно и когда менял файлы или выполнял команды — пригодится настройка auditd, это следующий уровень детализации по сравнению с обычным diff.
- Не автоматизируйте реакцию сразу — на старте пусть скрипт только уведомляет. Автоматическое убийство «незнакомых» процессов или отключение «незнакомых» сервисов без ручной проверки почти гарантированно рано или поздно положит что-то легитимное.
Ограничения метода
Стоит быть честным: снимок нормы — это не защита от подмены самого механизма проверки. Если атакующий получил root и знает, что на сервере есть baseline, он в теории может подделать и снимок, и результаты diff. Метод не заменяет системы контроля целостности файлов на уровне ядра и не ловит атаки, которые не оставляют следа ни в процессах, ни в портах, ни в автозагрузке (например, разовое чтение секретов без запуска отдельного процесса).
То, что он реально даёт — это дешёвый и почти всегда достаточный первый рубеж: подавляющее большинство реальных инцидентов на обычных VPS всё-таки оставляет след в одном из трёх слоёв, а значит, простой diff их ловит. Для более высокого уровня защиты это дополняется, а не заменяется — журналом аудита ядра, ограничением исходящего трафика фаерволом и регулярным ручным аудитом.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Сколько версий эталона хранить?
Если он под git — практически неограниченно, репозиторий из текстовых файлов растёт медленно. Разумный минимум для истории — 90 дней, этого достаточно для разбора большинства инцидентов задним числом.
Что делать, если сервер уже давно в проде и «чистого» состояния для эталона нет?
Снимите baseline прямо сейчас, после того как пройдётесь по системе и уберёте всё, в чём не уверены — это не идеальный эталон, но заметно лучше, чем полное отсутствие точки отсчёта. Дальше он будет улучшаться с каждой ручной проверкой.
Нужно ли делать это на каждом сервере или достаточно одного «эталонного»?
На каждом отдельно — даже одинаковые по образу серверы со временем расходятся по составу процессов и cron-заданий, особенно если на них разный трафик или разная роль (например, один принимает внешний трафик, другой только внутренний).
Как быть с контейнерами и Docker?
Логика та же, но добавьте отдельный срез: docker ps --format '{{.Names}} {{.Image}}' для списка контейнеров и docker ps --format '{{.Ports}}' для проброшенных портов — они не всегда видны через обычный ss на хосте, если используется отдельная сетевая настройка.
Заменяет ли это готовые системы обнаружения вторжений?
Нет, это скорее их упрощённый бесплатный аналог для конкретной задачи — заметить новые процессы, порты и автозагрузку. Специализированные системы контроля целостности делают больше (хеши бинарников, мониторинг файловой системы на уровне ядра), но и настраивать их сложнее — начать со связки скриптов и git часто разумнее, чем откладывать защиту до момента, когда «руки дойдут» до более тяжёлого решения.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →