Оглавление
- Введение
- Анализ утилиты
- Технический анализ исполняемого файла
- Сходство с уже существующими утилитами
- Выводы и советы по защите от подобных атак
Введение
С 2022 года геополитическая обстановка в мире резко изменилась, и злоумышленники начали наносить киберудары по российским компаниям с беспрецедентной частотой. Резко выросло число как целевых атак, так и массовых, включая DDoS. Одной из утилит для проведения DDoS является Gorgon Stress Tester, чья функциональность предоставляет множество методов воздействия. Начиная с версии 1.9.2 (первое упоминание 18 марта 2024 года), инструмент публикуется в открытом доступе в Telegram (в каналах, посвященных хактивизму против России). Таким образом авторы подобных каналов хотели сделать проведение DDoS более доступным.
Конечно, данная утилита не является единственной в своем роде, существуют и другие (adss, db1000n, Distress и другие) – о части из них уже рассказывали наши коллеги. Мы же решили написать именно о Gorgon Stress, так как ее все чаще используют проукраинские хактивисты. Специалистам в области кибербезопасности понимание таких инструментов помогает лучше понять механизмы их действия и разработать эффективные стратегии противодействия. А широкому кругу пользователей - осознать важность защиты своих данных и систем.
Поэтому, словно Персей, попробуем заглянуть в глаза ужасной Горгоне.
Анализ утилиты
Анализ .deb пакета
Gorgon распространяется в виде .deb пакета, который имеет следующее описание (как на изображении ниже) и в виде зависимостей имеет tor, xvfb, openjdk 11. Для анализа взята версия 1.9.7.9.
Утилита устанавливается в виде сервиса со следующим конфигом.
[Unit]
Description=gorgon-stress
After=syslog.target network.target
[Service]
User=root
Group=root
Type=simple
RemainAfterExit=yes
ExecStart=/opt/gorgon-stress/gorgon
ExecReload=/bin/kill -HUP $MAINPID
WorkingDirectory=/opt/gorgon-stress/
LimitNOFILE=999999
Restart=on-failure
RestartSec=10
KillMode=process
Restart=always
[Install]
WantedBy=multi-user.target
При установке основная директория gorgon stress располагается в /opt/gorgon-stress, далее приведен postinst скрипт.
#!/bin/bash
# postinst script for stress
# Reload systemd manager configuration
systemctl daemon-reload
# Reload tor configuration
#mv /etc/tor/torrc /etc/tor/old_torrc
#cp /opt/gorgon-stress/torrc /etc/tor/torrc
# Check if the original torrc file exists
if [ -f /etc/tor/old_torrc ]; then
# Move the current torrc file to old_torrc
mv /etc/tor/old_torrc /etc/tor/torrc
echo "Moved existing old_torrc to torrc."
fi
pkill tor
systemctl stop tor
systemctl start tor
systemctl enable tor
# Enable and start your service
CUSTOM_CONFIG="/opt/gorgon-stress/config.json"
DEFAULT_CONFIG="/opt/gorgon-stress/config.json.distr"
# Check if the custom configuration file exists
if [ ! -f "$CUSTOM_CONFIG" ]; then
# If the custom config doesn't exist, copy the default one
cp "$DEFAULT_CONFIG" "$CUSTOM_CONFIG"
fi
systemctl stop gorgon-stress
systemctl start gorgon-stress
systemctl enable gorgon-stress
В скрипте postinst можно заметить файлы config.json.distr, а также torrc и url.csv. Файл конфигурации по умолчанию выглядит следующим образом:
{
"listen": "0.0.0.0:777",
"login": "admin",
"password": "admin1234",
"server": true,
"worker": true,
"serverurl": "http://127.0.0.1:777",
"tickerinterval": 5,
"lunaproxyAppKey" : "",
"lunaproxyNeek" : "",
"torNumPortsPerInstance": 10,
"torNumInstances" : 10,
"IdentityRenewalInterval": 900000000000
}
Из анализа файла конфигурации можно предположить, что сервис Gorgon Stress инициализирует http-сервер на всех интерфейсах на порту 777 с учетными данными по умолчанию admin:admin1234. Строки lunaproxyAppkey и lunaproxyNeek намекают на возможность использования для атак LunaProxy. Строки torNumPortsPerInstance и torNumInstances инициализируют инстансы Tor. Поле IdentityRenewalInterval указывает время, через которое необходимо обновить tor identity.
В файле torrc (на рисунке ниже) указан диапазон портов (SocksPort, 9055-9195), которые Tor будет использовать для входящих SOCKS запросов. ControlPort устанавливает порт, на котором будет работать интерфейс управления Tor. Значение IPv6Exit, установленное в единицу, разрешает Tor использовать выходные соединения через IPv6.
В файле url.csv (рисунок ниже) содержатся URL-адреса списков прокси в формате ТИП_ПРОКСИ, URL. В данном списке перечислены только SOCKS5 прокси.
Технический анализ исполняемого файла
Общие сведения
Утилита Gorgon Stress представлена в виде исполняемого файла (ELF), написанного на golang. Данный файл не обфусцирован, все символы находятся в открытом виде, что облегчает анализ исходного кода. В момент запуска программы на 777 порту поднимается веб-сервер с Basic-авторизацией. Веб-интерфейс выглядит следующим образом:
Примечательно, что данное ПО, как оказалось, имеет механизм проверки лицензии. Сам файл лицензии не предоставляется в Telegram, нелицензированная версия, как заявляется, имеет ограниченную функциональность. Также утилита при запуске на основе UUID системы генерирует LicenseID.
Значение LicenseID получается в результате вычисления SHA512 от UUID системы.
Данное значение затем используется для валидации файла лицензии при его наличии.
В зависимости от валидности лицензионного ключа затем устанавливается либо сбрасывается значение глобальной bool-переменной licenseValid. Значение этой переменной меняется также и в функции licenseHandler, которая отвечает за обработку лицензионного ключа, введенного в форму в веб-интерфейсе программы.
Забавным фактом в лицензировании данного ПО является то, что несмотря на использование RSA для валидации лицензионного ключа, проверку можно обойти, поменяв всего один байт в программе. Отсутствие лицензии позволяет только атаковать цели, имеющие RU IP-адрес, а также ограничивает конфигурацию атаки. Значение GoMaxProcs ограничивает число тредов, выделяемых для горутинов. Все значения в лицензируемой версии берутся из введенных в веб-интерфейсе.
После проверки лицензии и первичной инициализации работы утилиты в цикле запускается главная функция, обрабатывающая введенные параметры атаки и выполняющая саму атаку в зависимости от ее типа, DialWorker (число воркеров изначально указывается в веб-интерфейсе или равно 10 при отсутствии лицензионного ключа).
Приложение также использует пакет embed, который позволяет встраивать вложенные файлы в исполняемый файл. Извлекая их (для этого можно использовать утилиту Gembe, подробнее про go embeds тут), получаем следующую организацию файлов приложения:
Файл public_key.pem содержит публичный ключ, используемый при проверке лицензии, ua.txt содержит список юзер агентов, которые случайно используются в запросах.
Типы атак
В документации к данной утилите версии 1.9.3 указано, что существует семь видов атак:
- Slowdos HTTP – Атака Slow Loris. Регулируется параметром Timeout;
- Flood HTTP – Обычный HTTP флуд;
- SMTP – Атака на SMTP сервер;
- SSH – Отправка безмерного числа запросов на SSH сервер;
- TCP – Тестирование сервера на обработку большого количества TCP соединений;
- DNS:TCP – Тестирование DNS сервера на обработку большого числа запросов через TCP;
- TRAF – Симуляция кастомного трафика для анализа поведения сервера.
В версии 1.9.7.9 (последней на момент публикации) вдобавок ко всему из веб-интерфейса доступны также SIP:TCP и HTTP2 атаки:
Среди функций, помимо уже перечисленных методов, есть и DNS:UDP. Похоже, авторы не успели добавить ее в веб-интерфейс или она еще в разработке (в функции DialWorker есть ветка, которая проверяет имя типа атаки на DNS:UDP, однако в веб-интерфейсе это не отражено). Тем не менее, атаку можно запустить через POST-запрос, но ничего не происходит.
Примечательно, что большинство методов имеют приставку doLoris, намекая тем самым на атаку SlowLoris, однако название не всегда соответствует функционалу. Например, сложно представить loris атаку для UDP в случае с функцией doLorisDNSudp. Рассмотрим каждый из методов подробнее.
Тип атаки TCP обрабатывает функция doTcpFlood. Ее название полностью описывает функциональность. На заданный хост посылается безмерное число TCP-запросов. Атаки типа TCP и TRAF довольно тривиальны, и их смысл заключается в отправке большого числа запросов на сервер.
HTTP Flood обрабатывается в функции doLorisL7. Здесь все немного интереснее. Данная функция позволяет отправлять как GET, так и POST-запросы. Последний отправляется в случае, если в интерфейсе заполнено поле PostData. В данной функции реализована случайная выборка юзер-агентов из файла ua.txt, выборка accept-language из предопределенного списка, генерация случайного IP для заголовка X-Forwarded For, а также возможность использования кастомных заголовков. Также из веб-интерфейса можно выбирать варианты заголовка Content-Type.
Отдельного внимания заслуживает функция parseURLTemplate. Она может обрабатывать следующие значения, введенные в поле URL.
Вычисление хэш функции:
- {BLAKE2B}
- {BLAKE2S}
- {SHA512}
- {SHA384}
- {SHA256}
- {SHA224}
- {MD5}
- {SHA1}
- {SHA3}
Вычисление случайных значений:
- {NUMBER} – случайное число
- {STRING} – Случайная ASCII строка
- {STRING_RU} – Случайная строка в кириллице
Шаблон строки имеет вид {TYPE;RANGE}, например {NUMBER;0-5}. В случае, если нужно сгенерировать случайную строку или число, данное значение выражает диапазон длины генерируемой строки или числа. При использовании для подстановки хэш-функции указанный RANGE на результат функции не влияет, так как вычисление хэша будет производиться от случайной последовательности байт размера 16. Для шаблонизатора RANGE является обязательным значением.
Кстати, при отсутствии разделителя «;», воркер улетает в бесконечный цикл. Данный шаблонизатор актуален не только для URL, но и для POST параметров, а также заголовков. Шаблонизатор актуален только для данного типа атаки.
Тип атаки Slowdos HTTP представляет собой классическую Slow Loris атаку (goloris). Происходит отправка сначала заголовков, а затем с выдержанным интервалом всех остальных данных. Кастомизация для данного типа атаки отсутствует, тип запроса всегда POST, Content-Type не меняется из веб-интерфейса, происходит генерация случайного user agent (в этом типе атаки уже используется пакет corpix/uarand), Accept-Language, а также заголовка X-Forwarded-For. Пример сгенерированного запроса приведен на рисунке ниже:
Тип атаки HTTP2 предназначен для веб-серверов, использующих соответствующий протокол. Обработка происходит в функции doLorisHTTP2. Сначала происходит отправка preface "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n”, затем создается экземпляр класса http2/framer, происходит отправка settings (RFC 9113), затем происходит отправка заголовков (:method – GET, генерируется случайный user agent с помощью библиотеки corpix/uarand) и формирование запроса. Данный тип атаки довольно тривиален за исключением необходимости реализации особенностей протокола. Для работы с фреймами используется класс net/http2/Framer.
SIP:TCP реализует атаку SIP Flood. В функции doSipFlood происходит генерация случайного контакта, выбирается один из доменов верхнего уровня com, net, org, info, biz, ru, su (пример контакта - RgTyRWBzAc@RgTyRWBzAc.biz, iCmTthMtgk@iCmTthMtgk.ru). Подобный маркер мог бы быть использован при разработке детектирующей логики. Примечательно, что имя пользователя в запросе всегда совпадает с доменом. Также генерируется случайный CallID. Пример пакета приведен на рисунке ниже.
Тип атаки SMTP обрабатывается в функции SMTPFlood. Сначала отправляется HELO-пакет. В качестве идентификатора домена в таком пакете выступает строка, введенная в поле Host Header веб-интерфейса программы. Затем случайным образом выбирается одна из команд (NOOP, EXPN, VRFY). В случае выбора команд EXPN или VRFY с помощью функции generateRandomContact генерируется случайный контакт. Это та же самая функция, что используется в типе атаки SIP TCP, т.е. пользователь и домен совпадают. Эти маркеры атаки могут использоваться для детектирования подозрительного трафика.
Тип атаки SSH Flood предполагает флуд пакетами клиента со значением SSH-2.0-Client. К примеру, для клиента OpenSSH данная строка имеет вид SSH-2.0-OpenSSH_for_Windows_8.6 или SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10 (т.е. строка вида SSH-2.0-OpenSSH_СИСТЕМА)
Данная атака также хорошо видна в логах sshd и имеет вид, представленный на рисунке ниже.
Атака типа DNS:TCP обрабатывается функцией doLorisDNStcp. Данная функция генерирует большое количество тяжеловесных DNSKEY/RRSIG-запросов. В качестве ключей и подписей используются случайные строки, а запрос формируется по имени хоста/FQDN с префиксом в виде случайной строки:
Сходство с уже существующими утилитами
Многие функции имеют приставку doLoris, поиск по строкам приводит на репозиторий goloris, созданный пользователем valyala (Aliaksandr Valialkin) и является Proof of Concept для Slow Loris атаки на golang. Сходство есть не только в нейминге функций, но и в самом коде (функция doLoris). Многие сходства (включая конфиг), говорят о том, что утилита Gorgon является прямой идейной наследницей goloris.
var (
contentLength = flag.Int("contentLength", 1000*1000, "The maximum length of fake POST body in bytes. Adjust to nginx's client_max_body_size")
dialWorkersCount = flag.Int("dialWorkersCount", 10, "The number of workers simultaneously busy with opening new TCP connections")
goMaxProcs = flag.Int("goMaxProcs", runtime.NumCPU(), "The maximum number of CPUs to use. Don't touch :)")
rampUpInterval = flag.Duration("rampUpInterval", time.Second, "Interval between new connections' acquisitions for a single dial worker (see dialWorkersCount)")
sleepInterval = flag.Duration("sleepInterval", 10*time.Second, "Sleep interval between subsequent packets sending. Adjust to nginx's client_body_timeout")
testDuration = flag.Duration("testDuration", time.Hour, "Test duration")
victimUrl = flag.String("victimUrl", "http://127.0.0.1/", "Victim's url. Http POST must be allowed in nginx config for this url")
hostHeader = flag.String("hostHeader", "", "Host header value in case it is different than the hostname in victimUrl")
)
...
func main() {
flag.Parse()
flag.VisitAll(func(f *flag.Flag) {
fmt.Printf("%s=%v\n", f.Name, f.Value)
})
runtime.GOMAXPROCS(*goMaxProcs)
victimUri, err := url.Parse(*victimUrl)
if err != nil {
log.Fatalf("Cannot parse victimUrl=[%s]: [%s]\n", victimUrl, err)
}
victimHostPort := victimUri.Host
if !strings.Contains(victimHostPort, ":") {
port := "80"
if victimUri.Scheme == "https" {
port = "443"
}
victimHostPort = net.JoinHostPort(victimHostPort, port)
}
host := victimUri.Host
if len(*hostHeader) > 0 {
host = *hostHeader
}
requestHeader := []byte(fmt.Sprintf("POST %s HTTP/1.1\nHost: %s\nContent-Type: application/x-www-form-urlencoded\nContent-Length: %d\n\n",
victimUri.RequestURI(), host, *contentLength))
dialWorkersLaunchInterval := *rampUpInterval / time.Duration(*dialWorkersCount)
activeConnectionsCh := make(chan int, *dialWorkersCount)
go activeConnectionsCounter(activeConnectionsCh)
for i := 0; i < *dialWorkersCount; i++ {
go dialWorker(activeConnectionsCh, victimHostPort, victimUri, requestHeader)
time.Sleep(dialWorkersLaunchInterval)
}
time.Sleep(*testDuration)
}
func dialWorker(activeConnectionsCh chan<- int, victimHostPort string, victimUri *url.URL, requestHeader []byte) {
isTls := (victimUri.Scheme == "https")
for {
time.Sleep(*rampUpInterval)
conn := dialVictim(victimHostPort, isTls)
if conn != nil {
go doLoris(conn, victimUri, activeConnectionsCh, requestHeader)
}
}
}
func activeConnectionsCounter(ch <-chan int) {
var connectionsCount int
for n := range ch {
connectionsCount += n
log.Printf("Holding %d connections\n", connectionsCount)
}
}
func dialVictim(hostPort string, isTls bool) io.ReadWriteCloser {
// TODO hint: add support for dialing the victim via a random proxy
// from the given pool.
conn, err := net.Dial("tcp", hostPort)
if err != nil {
log.Printf("Couldn't esablish connection to [%s]: [%s]\n", hostPort, err)
return nil
}
tcpConn := conn.(*net.TCPConn)
if err = tcpConn.SetReadBuffer(128); err != nil {
log.Fatalf("Cannot shrink TCP read buffer: [%s]\n", err)
}
if err = tcpConn.SetWriteBuffer(128); err != nil {
log.Fatalf("Cannot shrink TCP write buffer: [%s]\n", err)
}
if err = tcpConn.SetLinger(0); err != nil {
log.Fatalf("Cannot disable TCP lingering: [%s]\n", err)
}
if !isTls {
return tcpConn
}
tlsConn := tls.Client(conn, tlsConfig)
if err = tlsConn.Handshake(); err != nil {
conn.Close()
log.Printf("Couldn't establish tls connection to [%s]: [%s]\n", hostPort, err)
return nil
}
return tlsConn
}
func doLoris(conn io.ReadWriteCloser, victimUri *url.URL, activeConnectionsCh chan<- int, requestHeader []byte) {
defer conn.Close()
if _, err := conn.Write(requestHeader); err != nil {
log.Printf("Cannot write requestHeader=[%v]: [%s]\n", requestHeader, err)
return
}
activeConnectionsCh <- 1
defer func() { activeConnectionsCh <- -1 }()
readerStopCh := make(chan int, 1)
go nullReader(conn, readerStopCh)
for i := 0; i < *contentLength; i++ {
select {
case <-readerStopCh:
return
case <-time.After(*sleepInterval):
}
if _, err := conn.Write(sharedWriteBuf); err != nil {
log.Printf("Error when writing %d byte out of %d bytes: [%s]\n", i, *contentLength, err)
return
}
}
}
Выводы и советы по защите от подобных атак
Функциональность и простота использования Gorgon Stress Tester сделала ее доступной для широкой аудитории, включая хактивистов, а, значит, крайне опасной для потенциальных целей - организаций в России.
DoS или DDoS-атаки могут нанести серьезный удар по инфраструктуре компании: от нарушения работы отдельных сервисов до полной приостановки работы ключевых ресурсов. Это ведет к значительным финансовым и репутационным потерям. Рассмотренные в настоящей статье в рамках утилиты Gorgon Stress типы атак в большей степени относятся к Layer 7. Часть из них, а именно SIP, SMTP, SSH, имеют очень явные маркеры, а потому приведенная в нашем анализе информация может быть использована при разработке детектирующей логики.
Для борьбы с DDoS можно использовать следующие методы и инструменты:
- Балансировщики нагрузки. Входящий трафик таким образом распределяется между несколькими серверами, что может впоследствии смягчить последствия DDoS;
- Использование CDN. Они также предоставляют возможности распределения нагрузки, имеют функцию кэширования контента и зачастую предлагают встроенные механизмы защиты для фильтрации вредоносного трафика;
- Мониторинг, анализ и фильтрация трафика. В том числе использование межсетевого экрана уровня приложений (WAF);
- Изоляция критически важных сервисов. В том числе с использованием VPN для доступа к ним;
- Ограничение числа запросов (Rate Limiting).
Довольно трудно представить себе универсальное решение, которое обеспечивало бы полную защиту от DDoS-атак. Поэтому крайне важно иметь план действий на случай возникновения подобных инцидентов и эшелонированную защиту корпоративных онлайн-ресурсов.