SSH-ловушка, часть 2: что мы исправили по комментариям и на чём она ещё палилась
Первую статью про нашу 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, число ядер и модель процессора. Но мы прогнали список на ловушке и честно провалили три пункта из пяти.
Проверка | Что отвечала ловушка | Что теперь |
|---|---|---|
| всю таблицу целиком, ключи игнорировались | CSV с теми же цифрами, что в таблице, с единицами и без |
| список карт | без изменений |
| No such file or directory | nvidia0-3, nvidiactl, nvidia-uvm и остальные, как символьные устройства |
| cannot execute binary file: Exec format error | модули nvidia, nvidia_uvm, nvidia_drm, nvidia_modeset |
| версия 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 нет чужих заданий
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.