SSH-ловушка, часть 2: что мы исправили по комментариям и на чём она ещё палилась

Продолжение истории про Cowrie с четырьмя фейковыми H100. Проверили ловушку по списку из комментариев, нашли в ней отсутствие подстановки путей и обрыв сессий на пятой минуте, подключили автоматические жалобы и поймали червя на Rust.

Первую статью про нашу SSH-ловушку прочитали почти двадцать тысяч человек и оставили 25 комментариев. Часть из них оказалась полезнее, чем мы ожидали: один читатель прислал список проверок, на которых ловушка с видеокартами должна проколоться, другой назвал совет про fail2ban вредным, третий пошутил, что старую школу скоро перепишут на Rust. Шутка сбылась через сутки, но об этом в конце.

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

Проверка по списку из комментариев

Читатель tech_post_dev предположил, что разведчик не поставил майнер потому, что ловушка отвечает одной и той же таблицей nvidia-smi на любой вызов. Скрипты под майнинг спрашивают иначе, например nvidia-smi --query-gpu=name,memory.total --format=csv,noheader, и разбирают ответ построчно. Ещё он предложил заглянуть в /dev/nvidia*, lsmod | grep nvidia и /proc/driver/nvidia/version: на живом сервере с драйвером всё это непустое и согласовано между собой.

Сам разведчик, как выяснилось, ничего из этого не вызывал. Его интересовали только lspci, число ядер и модель процессора. Но мы прогнали список на ловушке и честно провалили три пункта из пяти.

Проверка Что отвечала ловушка Что теперь
nvidia-smi --query-gpu=... --format=csv всю таблицу целиком, ключи игнорировались CSV с теми же цифрами, что в таблице, с единицами и без
nvidia-smi -L список карт без изменений
ls /dev/nvidia* No such file or directory nvidia0-3, nvidiactl, nvidia-uvm и остальные, как символьные устройства
lsmod \| grep nvidia cannot execute binary file: Exec format error модули nvidia, nvidia_uvm, nvidia_drm, nvidia_modeset
/proc/driver/nvidia/version версия 550.90.07 без изменений, совпадает с шапкой nvidia-smi

Ответ Exec format error на lsmod особенно обидный. В первой статье мы рассказывали, что победили эту улику, разложив готовые ответы по путям Debian 12. Как видно, победили не везде.

Звёздочка, которая не раскрывалась

Проверка /dev/nvidia* вскрыла проблему крупнее видеокарт. Мы добавили устройства в файловую систему ловушки, а ls /dev/nvidia* всё равно отвечал, что ничего нет. При этом ls /dev/nvidia0 находил файл.

Оказалось, что в Cowrie 3.1.3 подстановки путей нет вообще. Об этом честно написано в комментарии к разбору команд: раскрытие *, ? и [...] оставлено на усмотрение отдельных команд. А команды его не делают. Получается, что любая строка со звёздочкой приходила в команду буквально. Ботнет из первой статьи в каждой сессии выполняет rm -rf /var/tmp/dota*, и у нас эта команда искала файл, который так и называется, со звёздочкой в имени.

Мы дописали подстановку в то место, где ещё видно, какие части слова стояли в кавычках, потому что в bash "*" в кавычках не раскрывается. Если совпадений нет, слово остаётся как было, тоже как в bash.

$ echo "/dev/nvidia*"
/dev/nvidia*
$ for f in /dev/nvidia[0-9]; do echo "card $f"; done
card /dev/nvidia0
card /dev/nvidia1
card /dev/nvidia2
card /dev/nvidia3

Заодно пришлось переделать ls. Когда мы впервые увидели вывод ls -l /dev/nvidia*, там было сразу несколько улик. Дата стояла в виде 2026-10-24 16:52, хотя GNU ls по умолчанию пишет Oct 24 16:52, а у файлов старше полугода вместо времени ставит год. У каталогов не было строки total. Устройства показывались как пустые обычные файлы, а не как crw-rw-rw- 1 root root 195, 0. Ещё одна мелочь, которую видит любой скрипт: без терминала GNU ls выводит по одному имени в строке, а Cowrie выстраивал колонки с пробелами в конце строк.

Боты, которые проверяют оболочку

После публикации пришли боты, которых в первые полсуток не было. Сразу после входа они отправляют пачку команд такого вида:

printf '\n%s|%s\n' 'check_..._small_math' "$((746*496+89))"
printf '\n%s|%s\n' 'check_..._small_pid' "$$"
set -- 'arg1' 'arg2'; printf '%s\n' "$#|$1|$2"

Каждая проверяет одну возможность настоящего sh: арифметику, номер процесса, позиционные параметры, функции, чтение /proc/self/stat встроенной командой read, подоболочку, которая не должна менять переменную снаружи, конвейер в цикл while read. Cowrie не умела почти ничего из этого. Арифметику $((...)) она возвращала буквально, $$ пустым, а read такой команды у неё не было вовсе. Ответ, где вместо 370105 написано $((746*496+89)), бот распознаёт мгновенно.

Чинили это вдвоём, и по дороге всплыла история, которой мы гордимся меньше всего. Утилита createfs, которой Cowrie снимает слепок файловой системы, потеряла все символьные ссылки, 3 598 штук. Она проверяет, что ссылка не выходит за пределы снимаемого дерева, а относительные пути этой проверки не проходили. Поэтому не было ни /usr/bin/which, ни /etc/os-release, ни /usr/bin/python3, а /bin/./uname отвечал «No such file or directory». В первой статье мы писали про «эталонный Debian 12». Эталоном он был до момента съёмки слепка.

Самый дотошный из этих ботов к утру прошёл все проверки и запустил разведку целиком: процессор, видеокарты, last и создание маленького скрипта, который должен напечатать xxxxxx. Всё сошлось, кроме одного. Скрипт искал карты и через lspci, и через busybox lspci, а наша эмуляция busybox, оставшаяся с баннером от Debian 7, отдавала полный вывод lspci. Каждая H100 приходила к нему дважды. В эталонном Debian 12 busybox не установлен, поэтому эмуляцию мы убрали, и теперь команда отвечает command not found, как на настоящем сервере. Похоже, именно это задвоение и смутило разведчика из первой статьи. Мы тогда сами написали в комментариях, что карты задвоены, но не проверили, откуда это берётся.

Обрыв на пятой минуте

Тут уже наша собственная ошибка. В комментариях Folist предложил автоматически отправлять жалобы на атакующих, и мы это сделали: адреса уходят в AbuseIPDB, скачанные ботами файлы в MalwareBazaar, базу образцов от abuse.ch. Через несколько часов на второй, тестовой ловушке заметили, что два отправленных архива из набора Outlaw битые. Они на четверть и на треть короче настоящего, и gzip их не распаковывает.

Причину мы нашли только сегодня. В журнале сессии с обрезанными загрузками заканчивались ровно через пять минут после начала: 02:33:11 вход, 02:38:12 обрыв, 05:02:33 вход, 05:07:35 обрыв. В Cowrie есть interactive_timeout, по умолчанию 300 секунд, и это не таймаут простоя, а жёсткий предел от входа: при активности он не сбрасывается. Outlaw заливает свой архив по SCP медленно, за четыре-пять минут, и половина заливок за ночь, 11 из 22, не успевала. Cowrie сохраняла обрывок, а наш отправщик нёс его в MalwareBazaar как образец.

Отправщик мы научили проверять целостность: битый gzip, ELF, у которого таблица секций лежит за концом файла, и файл ровно на пределе размера больше не отправляются. Предел сессии подняли до получаса. Простой без ввода по-прежнему режется через три минуты.

Если ставите Cowrie

Проверьте interactive_timeout в секции [honeypot]. По умолчанию он обрывает любую сессию через пять минут после входа, даже если внутри идёт загрузка. Если собираете образцы, поднимите его, иначе часть файлов будет обрывками, и вы не заметите этого, пока не попробуете их распаковать.

AbuseIPDB сначала не отправил ни одной жалобы. Порог мы поставили такой: адрес уходит после пяти попыток входа за два часа. Но ловушка пускает бота после двух-пяти попыток и дальше не мучает, так что до пятой многие просто не доходили. Потом пошло само: к вечеру второго дня ушла 51 жалоба, и у первых адресов AbuseIPDB уже показывал уверенность 100 процентов.

Про fail2ban и PerSourcePenalties

Sap_ru назвал совет поставить fail2ban вредным и предложил вместо него встроенную в OpenSSH защиту PerSourcePenalties. Он прав в главном: эта опция появилась в OpenSSH 9.8 и включена по умолчанию. sshd сам начисляет штраф адресу, который раз за разом не проходит аутентификацию или подключается и не доходит до входа, и пока штраф действует, соединения с этого адреса отклоняются. Повторным нарушителям штраф растёт.

Нюанс в версиях. В Debian 12 штатный OpenSSH 9.2, PerSourcePenalties в нём нет. В Debian 13 уже 10.0, и там защита работает из коробки (данные Debian). Проверить свою версию можно так:

sshd -V
sshd -T | grep -i persource

Если вторая команда что-то вывела, PerSourcePenalties у вас есть, и fail2ban для SSH становится скорее делом вкуса. Если нет, fail2ban по-прежнему полезен. А главная защита от всего, что мы видим в ловушке, не изменилась: вход по паролю выключен.

Пару ответов на остальное. Высокоинтерактивная ловушка на настоящей машине, о которой писали в комментариях, лучше подходит для разбора конкретного вредоноса, но там надо резать исходящий трафик, и бот это сразу видит. У Cowrie есть режим прокси, о котором напомнил KivApple: она пропускает сессию на настоящую машину и записывает всё, что проходит. Мы пока остались на эмуляции, потому что нам интересен сценарий бота, а не то, что останется на диске после него. И да, ограничивать вход по SSH своим адресом опасно ровно так, как написал MountainGoat: провайдер выдаёт другой адрес, и порт 22 молчит, а сайты на том же сервере работают. На этот случай у домашнего сервера есть монитор с клавиатурой, а у VPS консоль VNC в панели хостера.

Шутка про Rust

Под фразой про IRC-бэкдор в наборе Outlaw читатель poige написал: «Ничё, скоро на Rust перепишут». Ночью в ловушку зашёл бот, прочитал /bin/echo, чтобы узнать архитектуру по заголовку файла, и попробовал загрузить себе программу тремя способами подряд: через curl, через wget и через /dev/tcp, если ни того ни другого нет. Третьей попыткой он начал заливать бинарник прямо в оболочку командой head -c с точным размером. Cowrie этот поток не приняла, и у нас сохранилось три байта.

Файл мы забрали сами по той же ссылке и разобрали, не запуская. Это оказался P2Pinfect, червь на Rust, который описывала Cado Security, сейчас она часть Darktrace. Он распространяется через Redis и SSH и берёт цели прямо с захваченной машины: из .bash_history, .ssh/config и known_hosts. Упакован UPX, но три метки UPX! в файле заменены на 0000, поэтому стандартный upx -d отвечает, что файл не упакован. После возврата меток он распаковался штатно. Упакованная версия в MalwareBazaar уже была, распакованной не было, её мы туда и отправили.

Так что переписали, poige. Причём давно.

Цифры за полтора дня

Запись с 16:20 UTC 24 октября до 06:11 UTC 26 октября, без наших проверочных входов.

Что Сколько
Подключений 6 312
Разных адресов 308
Успешных входов 350 с 149 адресов
Сессий с командами 179 с 136 адресов
Из них сценарий Outlaw 122 сессии со 120 адресов
Команд 3 193
Разных скачанных файлов 146
Смен пароля внутри ловушки 84

Outlaw по-прежнему делает почти всю работу, и его сеть оказалась шире, чем казалось в первую ночь: 120 разных адресов за полтора дня. Пароли те же: 123456, 123, 1234 и две его фирменные пары с 345gs5662d34. Клиенты те же: 4 917 подключений от программ на Go, 812 от libssh 0.9.6, восемь от PuTTY.

  • sshd -T | grep -i persource: есть ли у вас PerSourcePenalties
  • Вход по паролю выключен, по ключу проверен из второго окна
  • Если держите Cowrie, поднят interactive_timeout
  • Образцы перед отправкой куда-либо проверены на целостность
  • В authorized_keys нет чужих ключей, в cron нет чужих заданий

Ещё в блоге