Майнер прячется от top: почему процесс невидим и как его достать
Вентиляторы у хостера явно не просто так гудят, load average держится под потолком сутками — а вы открываете top, htop, ps aux --sort=-%cpu и видите десяток процессов, каждый из которых ест по 1-2%. Арифметика не сходится: где-то есть процесс, который жрёт ресурсы, но не показывается ни в одном стандартном инструменте. Это не глюк мониторинга — часть вредоносного ПО, включая майнеры криптовалюты, умеет прятать себя от ps/top либо на уровне библиотек, либо прямо на уровне ядра. Разберём, как заподозрить такую подмену по цифрам, а не по ощущениям, какими способами вытащить скрытый процесс на свет и в какой момент обычная диагностика становится бессмысленной и пора грузиться с чистого образа.
Содержание
- Как заподозрить: расхождение между суммарной загрузкой CPU и суммой процессов
- LD_PRELOAD: как процесс исчезает из ps и top, оставаясь в памяти
- Подмена /proc на уровне ядра: когда врёт уже не библиотека
- Практика: сравниваем /proc напрямую с ps и добираемся до unhide
- Статистика планировщика ядра: то, что сложнее подделать
- Когда пора грузиться с live-образа для чистой диагностики
Как заподозрить: расхождение между суммарной загрузкой CPU и суммой процессов
Первый и самый надёжный сигнал — не «сервер тормозит», а конкретное числовое расхождение. Верхняя строка top (%Cpu(s): us, sy, ni, id, wa, hi, si, st) считается ядром независимо от того, что top потом покажет в списке процессов ниже. Если id (idle) низкий, а сумма %CPU по всем видимым строкам далека от 100 - id, разница — это то, что съедено процессом, которого вы не видите.
Проверить руками:
top -bn1 | awk '/^%Cpu/{print; next} NR>7 && $9 ~ /^[0-9.]+$/{sum+=$9} END{print "Сумма %CPU видимых процессов:", sum}'
vmstat 1 5
mpstat -P ALL 1 5
Смотрите на id за 5-10 секунд усреднения, а не в моменте — майнеры почти всегда держат нагрузку ровно, без всплесков. Если idle стабильно низкий, а сумма %CPU процессов на многоядерном сервере (поделите на число ядер: nproc) даёт условные 20-30%, а не 70-90% — гэп в 40-50 процентных пунктов объяснить нечем, кроме скрытого процесса.
Два ложных срабатывания, которые стоит исключить сразу:
- Steal time. На виртуальном сервере высокий
%stозначает, что гипервизор не отдаёт вам процессор, а не то, что у вас что-то спрятано — это отдельная история, разобранная в статье про steal time и соседей по железу. - iowait и легитимные фоновые задачи. Прежде чем подозревать руткит, стоит пройти базовый чек-лист поиска прожорливого процесса — иногда виновник просто не туда отсортирован или скрыт контейнером. Общий план разбора нагрузки — в статье «высокая нагрузка на процессор: как найти причину».
Если оба варианта исключены, а расхождение стабильно воспроизводится на нескольких снятиях подряд — переходите к следующим разделам.
LD_PRELOAD: как процесс исчезает из ps и top, оставаясь в памяти
Самый частый и самый простой в реализации способ спрятать процесс — не трогать ядро вообще, а подменить библиотечные вызовы, которыми ps, top и ls /proc читают список процессов. Динамический линковщик Linux перед запуском любого динамически слинкованного бинарника читает переменную LD_PRELOAD или файл /etc/ld.so.preload и подгружает указанные там .so-библиотеки с приоритетом выше системной libc. Вредоносная библиотека переопределяет функции вроде readdir()/readdir64()/opendir() (а иногда и чтение /proc/net/tcp для сокрытия сетевых соединений) так, чтобы запись про нужный PID или имя процесса просто выпадала из результата.
Ключевая деталь: подмена происходит не в ядре, а в пользовательском пространстве, на уровне конкретного вызова библиотеки. Ядро честно отдаёт директорию /proc целиком — врёт именно библиотека, через которую ps/top/ls её читают.
Проверка:
cat /etc/ld.so.preload
ls -la /etc/ld.so.preload
find / -xdev -name '*.so' -newer /etc/passwd -mtime -30 2>/dev/null
grep -r LD_PRELOAD /etc/environment /etc/profile.d/ /etc/systemd/system/*.service 2>/dev/null
Пустой /etc/ld.so.preload не гарантирует чистоту — LD_PRELOAD можно выставить точечно, для конкретного демона, через Environment=LD_PRELOAD=... в systemd unit.
Слабое место этой техники для атакующего — она работает только против динамически слинкованных бинарников. Статически собранный инструмент (или busybox) не обращается к ld.so и не видит LD_PRELOAD вообще, потому что делает системные вызовы напрямую:
file /bin/busybox
ldd /bin/busybox # "not a dynamic executable" — хороший знак
busybox ps aux
Если busybox ps aux показывает лишний процесс — LD_PRELOAD-подмена подтверждена, и это относительно легко лечится: удаляете точку внедрения (/etc/ld.so.preload, юнит systemd, сам .so-файл), потом убиваете процесс и точку его персистентности.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПодмена /proc на уровне ядра: когда врёт уже не библиотека
Если статический busybox тоже не видит подозрительный процесс — дело серьёзнее LD_PRELOAD. Это означает, что подмена происходит внутри самого ядра: загружаемый модуль (LKM-руткит) перехватывает системный вызов getdents/getdents64 или коллбэк iterate_shared виртуальной файловой системы /proc, и тогда фильтрация касается абсолютно любого процесса, читающего директорию — динамического, статического, только что скомпилированного вами на месте.
Более новый вариант той же идеи — использование eBPF-программ, прицепленных к нужным syscall'ам или трейспоинтам, без загрузки классического .ko-модуля. Это усложняет обнаружение через lsmod, потому что формально никакого «нового модуля» может и не быть.
Косвенные признаки:
lsmod | wc -l
ls /sys/module | wc -l
diff <(lsmod | awk 'NR>1{print $1}' | sort) <(ls /sys/module | sort)
dmesg | tail -100 | grep -iE 'module|taint'
cat /proc/sys/kernel/tainted
Некоторые руткиты удаляют себя из связного списка, который читает lsmod, но забывают почистить /sys/module/<имя>/ — расхождение между этими источниками проверяйте в первую очередь. Ненулевой /proc/sys/kernel/tainted тоже повод присмотреться, хотя его «пачкают» и легитимные сторонние модули (проприетарные драйверы, ZFS), так что это не приговор сам по себе.
Если в системе есть bpftool, стоит посмотреть на активные программы:
bpftool prog list
bpftool prog show | grep -iE 'getdents|proc'
Учтите честно: легитимные security-агенты и системы трассировки тоже вешают eBPF-программы на похожие точки — сам факт наличия программы не доказательство компрометации, важен контекст.
Практика: сравниваем /proc напрямую с ps и добираемся до unhide
Прямое сравнение списка PID из /proc со списком из ps — рабочий метод, но с оговоркой: если ls/ps оба динамически слинкованы и оба подпадают под один и тот же LD_PRELOAD-хук, они солгут одинаково, и diff ничего не покажет. Поэтому одну сторону сравнения нужно брать из инструмента, который не проходит через подменённую libc:
busybox ls /proc | grep -E '^[0-9]+$' | sort -n > /tmp/proc_pids.txt
ps -eo pid --no-headers | sort -n > /tmp/ps_pids.txt
comm -23 /tmp/proc_pids.txt /tmp/ps_pids.txt
Результат этой команды даёт диагностическую развилку:
- Если
busyboxвидит PID, которых нет вps— подтверждена LD_PRELOAD-подмена (раздел выше), лечится относительно просто. - Если
busyboxтоже не видит ничего лишнего, а расхождение по CPU из первого раздела всё равно есть — сокрытие происходит на уровне ядра, и листинг директории/procв принципе не поможет, потому что лжёт сам источник данных, а не инструмент чтения.
Для второго случая нужны методы, которые не зависят от перечисления директории. Здесь пригождается unhide — набор утилит, специально написанный для такой задачи:
apt install unhide -y # Debian/Ubuntu
unhide proc # сверяет /proc с ps/ls/lsof несколькими способами
unhide sys # опрашивает PID через syscall'ы (getpgid, sched_getaffinity и т.п.), а не через листинг
unhide brute # комбинирует оба подхода по всему диапазону PID
Режим sys полезнее всего против ядерных руткитов: вместо чтения директории он перебирает PID и дёргает системные вызовы, которые ничего не «листают», а просто спрашивают ядро про конкретный номер процесса. Чтобы спрятать процесс и от такой проверки, пришлось бы перехватывать десяток syscall'ов индивидуально вместо одного getdents — на порядок больше работы, поэтому многие реальные образцы этого не делают.
Если unhide недоступен, тот же принцип воспроизводится вручную через kill -0 — сигнал 0 ничего не отправляет, а только проверяет, существует ли процесс:
for pid in $(seq 1 32768); do
kill -0 "$pid" 2>/dev/null && [ ! -e "/proc/$pid" ] && echo "возможно скрыт: $pid"
done
Запускайте под root — иначе kill -0 не пройдёт проверку прав для чужих процессов, и результат будет ложноотрицательным. Метод медленный, но именно поэтому его сложно обмануть частичным хуком.
Более широкий чек-лист компрометации — не только скрытые процессы, но и сеть, cron, целостность бинарников — собран в статье «скрытые процессы на сервере: как найти»; используйте её как общий каркас, а этот материал — как углублённый разбор именно механизма невидимости.
Статистика планировщика ядра: то, что сложнее подделать
Даже если процесс не виден ни в одном листинге, он физически исполняется на процессоре — а значит, где-то в учёте планировщика ядра его время всё равно фигурирует, просто не в виде удобной строчки с именем.
/proc/schedstat даёт агрегированные по каждому CPU счётчики (время выполнения задач, время ожидания в очереди, число переключений контекста). Это монотонные счётчики, поэтому смотреть нужно дельту между двумя снятиями:
cat /proc/schedstat > /tmp/sched1
sleep 10
cat /proc/schedstat > /tmp/sched2
diff /tmp/sched1 /tmp/sched2
Резкий рост «времени выполнения» на ядре, которое по сумме видимых процессов должно почти простаивать — ещё одно подтверждение расхождения из первого раздела, уже привязанное к конкретному core, что помогает понять, сидит ли скрытый процесс на одном ядре (частый случай для майнеров без настроенного affinity) или мигрирует.
Если у вас уже включён BSD-style process accounting, у вас есть кое-что посерьёзнее снапшотов — постоянный журнал, который ядро пишет само при завершении каждого процесса, независимо от ps/top:
apt install acct -y
accton /var/log/account/pacct
lastcomm | head -50
sa -m
lastcomm/sa показывают историю запусков с именем команды, пользователем и CPU-временем — запись создаётся ядром в момент выхода процесса, а не читается через /proc, поэтому простая LD_PRELOAD-подмена на неё не влияет. Минус — он полезен только с момента включения, задним числом ничего не восстановить. Включайте сразу после первого подозрения и не выключайте — для периодически прячущегося процесса это часто самый прямой источник правды.
Когда пора грузиться с live-образа для чистой диагностики
Если дошли до этого раздела и ни busybox, ни unhide sys, ни диагностика планировщика не дают ясности, а lsmod/sys/module показывают расхождение — доверять любому инструменту внутри работающей системы больше нельзя. Вы не знаете, насколько глубоко перехвачены системные вызовы, а значит не можете быть уверены даже в результатах только что описанных проверок.
Важная оговорка: перезагрузка убивает сам процесс, который вы ловите. Если есть время разобраться подробно, перед ребутом зафиксируйте что успеете — вывод unhide brute, /proc/<pid>/status, /proc/<pid>/maps, попытку прочитать /proc/<pid>/exe, активные соединения через ss/tcpdump. Если время критично — быстрее и правильнее сразу изолировать сервер от сети через панель хостера или файрвол гипервизора, не дожидаясь идеальной криминалистики.
Дальше — рескью-режим:
# у большинства провайдеров VPS/выделенных серверов есть live/rescue-режим в панели
mount -o ro /dev/vda1 /mnt/target
Принципиально важно: диск монтируется read-only, а проверка идёт инструментами из доверенного live-окружения, а не бинарниками с заражённого диска — даже chroot-переход не спасает, если внутри вы всё равно исполняете скомпрометированный /bin/ps:
debsums --root /mnt/target -c
# или для RHEL-based
rpm --root /mnt/target -Va | sort
find /mnt/target/lib/modules -name '*.ko*' -newer /mnt/target/etc/hostname
cat /mnt/target/etc/ld.so.preload 2>/dev/null
Критерии, по которым пора переходить именно на этот шаг, а не продолжать копать живьём:
- расхождение CPU из первого раздела воспроизводится, но статический
busyboxиunhide sysничего не находят — сокрытие явно ядерное; lsmodи/sys/moduleрасходятся, либо видны незнакомые eBPF-программы без внятного объяснения;- журнал
acct/lastcommвыглядит подозрительно пустым в моменты, когда нагрузка была явно высокой — то есть под сомнением уже и сама подсистема учёта.
После подтверждения компрометации на уровне ядра лечить систему «на месте» обычно бессмысленно — слишком много неизвестных о том, что и как глубоко изменено. Практичный путь — поднять окружение заново из чистого образа, перенеся только проверенные данные, а не бинарники. Как построить этот процесс и не наступить на те же грабли — в статьях «майнер на сервере: как он попал и как его выкорчевать» и «incident response plan: что делать при взломе».
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Если ps и top ничего не показывают, а нагрузка есть — это точно руткит?
Не обязательно. Сначала исключите steal time (для VPS) и iowait, потом посчитайте расхождение между idle из /proc/stat и суммой %CPU видимых процессов за несколько снятий подряд. Если гэп стабильный и большой — переходите к проверке LD_PRELOAD и статическим инструментам.
Почему diff между /proc и выводом ps иногда ничего не находит, хотя процесс точно скрыт?
Потому что оба инструмента могли пройти через один и тот же перехваченный библиотечный вызов и солгать одинаково. Сравнение имеет смысл, только если один источник статически слинкован (busybox) или использует принципиально другой путь получения данных — syscall-перебор через unhide sys, а не листинг директории.
Как быстро понять, LD_PRELOAD это или подмена на уровне ядра?
Запустите busybox ps aux рядом с обычным ps aux. Если busybox видит лишний процесс — это LD_PRELOAD, лечится вычисткой /etc/ld.so.preload и точки персистентности. Если оба инструмента слепы одинаково — ищите дальше на уровне модулей ядра и eBPF.
Стоит ли сразу включать process accounting (acct), не дожидаясь подозрений?
Разумно для серверов с чувствительной нагрузкой — накладные расходы минимальны, а исторический журнал, независимый от ps/top, потом экономит время именно в такой ситуации. Но он бесполезен задним числом: если инцидент уже случился, а acct включаете только сейчас, прошлого не восстановить.
Можно ли обойтись без ребута и live-образа, если процесс явно ядерный?
Технически можно углубиться дальше (анализ памяти, форензик-инструменты), но это требует опыта и времени, которых обычно нет в разгар инцидента. Практичнее изолировать сервер от сети, снять снапшот диска для анализа не спеша и переустановить систему с нуля.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →