Пустили ботов по SSH на фейковый сервер с четырьмя H100: что они там делали

Как мы собрали SSH-ловушку на Cowrie, нарядили её ML-сервером с видеокартами, научили врать правдоподобно и что увидели за первые полсуток: 1 846 подключений, ботнет mdrfckr и разведчик, который 38 раз спрашивал про GPU.

Откройте журнал SSH на любом сервере с белым адресом, и там будет одно и то же: Failed password for root, Failed password for admin, Invalid user ubuntu, строка за строкой, с адресов со всего мира. Обычно на это смотрят как на шум, который fail2ban сам выметает. Мы тоже смотрели, пока однажды не задались вопросом: а что они собираются делать, если пароль подойдёт? Чтобы узнать, нужно их впустить. Не на настоящий сервер, конечно, а на поддельный.

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

Сначала хотелось с нейросетью

Первая идея была модная: поставить на 22 порт фейковый SSH, который отвечает на команды через языковую модель. Модель сама сочиняет вывод ls, cat и всего остального, атакующий думает, что он на живой машине. Такие проекты уже есть, ближе всего к этой идее Beelzebub: он слушает SSH и отдаёт команды модели.

Мы от этого отказались и взяли классику, Cowrie. Это ловушка на Python, которой больше десяти лет. Нейросети внутри нет: файловая система в ней заготовленная, команды эмулируются кодом или отдают заранее записанный вывод. Зато она предсказуема, ничего не стоит в работе и пишет абсолютно всё: каждую попытку входа, каждую введённую команду, каждый скачанный файл. Файлы сохраняются отдельно и никогда не запускаются, а сессию потом можно проиграть как видеозапись терминала.

Схема простая. Настоящий sshd остаётся на месте и пускает только с наших адресов. Всех остальных файрвол на 22 порту незаметно перенаправляет в Cowrie. Проброс портов в ловушке выключен, чтобы из неё нельзя было сделать прокси, скачивание файлов ограничено десятью мегабайтами. Для просмотра есть небольшая веб-панель, закрытая простой basic-аутентификацией: своей страницы входа у неё нет, посторонний видит голый ответ 401 без единого слова. Красивая форма логина на странице, куда никто не должен заходить, сама по себе подсказка.

Первый чужой бот пришёл через шесть минут после запуска.

Ловушка, которая не палится

Голая Cowrie из коробки узнаётся опытным ботом за пару команд, поэтому основную работу мы потратили на правдоподобие.

Вход как на настоящем сервере. По умолчанию Cowrie пускает почти с любым паролем. Бот, у которого подходит и root:123, и root:qwerty, и root:что-угодно, быстро понимает, где он. Мы написали свой класс авторизации. Новый адрес пускается не сразу, а после случайного числа неудачных попыток, от двух до пяти. Пара логин-пароль, с которой он вошёл, закрепляется за этим адресом, и дальше проходит только она. Всё как на сервере, где действительно есть один подобранный пароль. Суть класса, упрощённо:

fixed = creds.get(src_ip)
if fixed is not None:
    # за этим IP уже закреплена пара: пускаем только с ней
    return fixed[0] == login and fixed[1] == passwd

need = self.state["need"].setdefault(src_ip, randint(self.mintry, self.maxtry))
tries = self.state["tries"][src_ip] = self.state["tries"].get(src_ip, 0) + 1
if tries >= need:
    creds[src_ip] = [login, passwd]   # этот пароль теперь «настоящий»
    return True
return False

Класс включается в cowrie.cfg строками auth_class = StickyAuth и auth_class_parameters = 2,5, состояние лежит в JSON и переживает перезапуск.

Приманка. Если уж пускать, то так, чтобы было интересно. Мы собрали эталонный Debian 12 через mmdebstrap, сняли с него слепок файловой системы утилитой Cowrie createfs и превратили ловушку в сервер для обучения нейросетей: 128 ядер AMD EPYC 7763, терабайт памяти, четыре NVIDIA H100, NVMe на 3,5 ТБ и 28 ТБ под данные. Нагрузка 94, в top крутится обучение, в домашнем каталоге история команд, в /data/models лежат чекпоинты, а в /opt/ml/.env ключи от облаков. Ключи, разумеется, фейковые и помеченные. Для майнера такая машина выглядит как джекпот.

Вывод nvidia-smi в ловушке: четыре NVIDIA H100 80GB, загрузка около 90 процентов, процесс python3 train.py
Так ловушка отвечает на nvidia-smi. Ни одной видеокарты за этим нет.

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

Первый: grep в Cowrie понимал только ключи -q и -m, а остальные молча игнорировал. Боты массово делают lspci | grep -i nvidia, и наш сервер с четырьмя H100 отвечал пустой строкой, потому что без -i регистр не совпадал. Разведывательный скрипт одного бота так и докладывал своим хозяевам: «GPU:», а дальше ничего. Мы дописали в эмуляцию grep ключи -i, -v, -c, -w и -F, и после правки тот же скрипт увидел 128 ядер и четыре H100.

Второй: в Debian 12 каталог /bin это ссылка на /usr/bin, а готовые ответы Cowrie лежат по старым путям. Команда, у которой нет ни эмуляции, ни заготовленного вывода по новому пути, отвечала «cannot execute binary file: Exec format error». На живом Debian такого не бывает, это прямая улика. Пришлось разложить ответы df, dmesg, mount и других команд по правильным путям и дописать недостающие: lsblk, ip, ss, systemctl, docker, journalctl, top.

Третий, мелкий: команда hostname возвращала одно имя, а файл /etc/hostname другое, имя по умолчанию из поставки Cowrie. Человек такое заметит.

Честно скажем и про то, что осталось. Вывод nvidia-smi заготовлен заранее, поэтому время в его шапке никогда не меняется. На sort, screen, tmux и gzip ловушка пока не отвечает правдоподобно. Внимательный человек нас раскусит, но на ботов это пока не влияло.

Нейросеть всё-таки вернулась. Только не изображать сервер, а комментировать. Отдельная служба читает журнал Cowrie и присылает нам в Telegram каждый вход, каждую команду с коротким разбором от бесплатной модели и по закрытии сессии полную запись: что вводил бот и что ему отвечал наш сервер. Читать, как модель серьёзно объясняет, зачем боту понадобилось удалить папку .ssh, неожиданно увлекательно.

Цифры за первые двенадцать с половиной часов

Запись с 16:20 UTC 24 октября до 04:59 UTC 25 октября, наши собственные проверочные подключения из подсчёта исключены.

Что Сколько
Подключений 1 846
Разных адресов 126
Неудачных попыток входа 1 536
Успешных входов 175 с 70 адресов
Сессий, где вводились команды 102 с 65 адресов
Команд всего 1 524
Скачиваний файлов 219 (47 разных файлов)

Число успешных входов говорит о нашей настройке, а не о стойкости паролей: ловушка пускает любого, кто достаточно настойчив. Интереснее, что делали внутри.

Две трети подключений, 1 198 из 1 846, сделал один адрес. Его программа представлялась клиентом на Go и тупо шла по словарю: подключилась, пара логин-пароль, отключилась, и так почти тысячу двести раз. Вошла один раз, а дальше упиралась в закреплённую пару. Ничего не искала и ничего не знала про сервер.

Логин почти всегда root, 728 попыток из 1 711. Пароли из той же категории, что и десять лет назад:

Пароль Попыток
123456 83
345gs5662d34 58
3245gs5662d34 57
123 44
1234 39
admin 26
12345678 20

Две странные строки на втором и третьем месте не случайны, это подпись следующего героя.

Ботнет на 59 адресов

Почти вся работа после входа пришлась на одну сеть. Из 65 адресов, которые вводили команды, 59 выполнили один и тот же сценарий слово в слово. В SSH-ключе, который он оставляет, стоит комментарий mdrfckr, и исследователи связывают его с группой Outlaw, которая с 2018 года собирает из взломанных серверов сеть для майнинга.

Журнал ловушки Cowrie: после успешного входа с паролем из словаря тот же адрес пробует пары 345gs5662d34 и root 3245gs5662d34, потом снова входит со словарным паролем
Шесть минут журнала. Красным пары, которые ботнет пробует сразу после входа.

Почерк у него узнаваемый. Сначала подбирает пароль root по словарю. Как только вход удался, тот же адрес за несколько секунд пробует две свои пары: 345gs5662d34 с таким же паролем и root с паролем 3245gs5662d34. Затем снова входит с найденным паролем и начинает работать. Зачем ему эти две пары, мы можем только предполагать, похоже на проверку «а не наш ли это уже сервер». Строка 345gs5662d34 озадачила не только нас: в 2023 году про неё спрашивал автор дневника SANS, в его ловушке это был один из самых частых логинов.

Сценарий внутри занимает четыре-пять минут и от 18 до 27 команд. Если пересказать без самих команд:

Шаг Зачем
Удаляет папку .ssh и записывает туда свой ключ вход без пароля, даже если пароль сменят
Меняет пароль root выгнать конкурентов, подобравших тот же пароль, и самого владельца
Ищет и останавливает чужие процессы убрать чужие майнеры
Смотрит процессор, память, архитектуру понять, сколько тут можно намайнить
Ставит задание в cron и загружает архив dota закрепиться и запустить майнер

Пароль root в ловушке сменился 42 раза. Архив dota3 разбирала Elastic: в нём майнер, бэкдор с управлением через IRC и модуль перебора SSH, который ищет следующие жертвы. Круг замыкается: взломанный сервер сам начинает подбирать пароли к чужим серверам, а владелец узнаёт об этом из жалобы хостинга или по счёту за процессор.

Разведчик, которому понравились видеокарты

Оставшиеся шесть адресов вели себя иначе. Четверо спросили только версию системы командой uname и ушли: собирают список, где что стоит, чтобы прийти потом с подходящим инструментом. Двое прогнали длинные проверочные скрипты, и один из них вернулся 38 раз, каждый раз спрашивая про видеокарты. Наша наряженная машина его явно заинтересовала. Но майнер на неё за полсуток так никто и не поставил: то ли разведка и заражение у этой сети работают по разным расписаниям, то ли на что-то в ответах ловушки он всё-таки споткнулся. Это мы узнаем дальше.

Подписи клиентов тоже показательны: 1 305 подключений от программы на Go, 380 от библиотеки libssh 0.9.6, 32 от libssh 0.11.1. PuTTY, которым обычно пользуются люди, встретился три раза. В SSH ломятся автоматы, и отвечать им нужно автоматикой.

Как попробовать самому

Cowrie ставится из PyPI, актуальная версия 3.1.3. Подробная инструкция есть в документации проекта, коротко это выглядит так: отдельный пользователь без прав, виртуальное окружение, pip install cowrie, cowrie start. По умолчанию ловушка слушает 2222, а перенаправить на неё чужих с 22 порта можно правилом nftables, оставив свой адрес настоящему sshd:

define ssh_home = { 203.0.113.10 }

table ip nat {
  chain prerouting {
    type nat hook prerouting priority dstnat;
    tcp dport 22 ip saddr != $ssh_home redirect to :2222
  }
}

Адрес в примере условный, впишите свой. И не забудьте открыть 2222 только для перенаправленных пакетов (ct status dnat accept), иначе ловушку будет видно напрямую. Ставить её лучше на отдельную машину, где нет ничего ценного: вы сознательно приглашаете туда людей, которые пришли ломать.

Что из этого взять для своего сервера

Всё, что мы увидели, ломается об одну настройку: вход по паролю выключен. Боту из этой статьи нечего подбирать, если сервер пускает только по ключу.

Сначала проверьте, что вход по ключу у вас работает, и только потом выключайте пароли. В /etc/ssh/sshd_config или отдельном файле в /etc/ssh/sshd_config.d/:

PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password

sshd -t проверит синтаксис, systemctl reload ssh применит. Текущую сессию не закрывайте, пока не убедились из второго окна, что вход по ключу проходит.

Второе - fail2ban. При выключенных паролях он уже не про защиту, а про чистоту журналов: тот самый бот на 1 198 подключений отлетит после пятой попытки.

[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1h

Впишите в ignoreip свой адрес. Мы сами однажды забанили себя, когда слишком часто переподключались во время настройки ловушки, и не сразу поняли, почему молчит только SSH, а всё остальное на сервере отвечает.

Проверка

Если пароль на сервере был слабым и вы подозреваете, что к вам уже заходили, проверьте следы именно этого ботнета:

grep -r mdrfckr /root/.ssh /home/*/.ssh 2>/dev/null
crontab -l; ls -la /etc/cron.d
lastb | head -50

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

  • Вход по ключу проверен из второго окна
  • PasswordAuthentication no, PermitRootLogin prohibit-password
  • fail2ban включён, свой адрес в ignoreip
  • SSH открыт только для своих адресов, если это возможно
  • В authorized_keys нет чужих ключей, в cron нет чужих заданий

Ещё в блоге