В середине июня аналитик по информационной безопасности получил от коллеги свежий образец вредоносного файла. Утром антивирусное решение на основе машинного обучения зафиксировало подозрительное поведение, напоминающее работу инфостилера (программы для кражи данных). Сработал только поведенческий флаг, без указания на конкретное семейство вредоносного ПО. В среде не было развернута EDR-система. Перед исследователем стояла задача выяснить, что на самом деле представляет собой этот бинарный файл. Результатом стал полный цикл анализа, охвативший статическую и динамическую расшифровку, извлечение второго этапа, кластеризацию 55 образцов и картирование инфраструктуры злоумышленников.
Описание
Первичная триада не дала быстрого ответа. Хеш SHA-256 отсутствовал в VirusTotal, что указывало на свежесть образца или целенаправленную ротацию исполняемых файлов. Загрузка на VirusTotal показала: это PE32+ файл для Windows x64 размером около 3,3 мегабайта. Компилятор определился как Go - наличие метаданных рантайма Go выдавало язык разработки. Детектирование было слабым - лишь 12 из множества антивирусных движков присвоили метки "троян" или "вредоносный", без указания семейства. Локальный анализ с помощью Detect-It-Easy, PEStudio и BearPE подтвердил Go-структуру и аномально высокую энтропию в секциях .rdata (6,93) и overlay (7,58). Это говорило о сжатых или зашифрованных данных. Импортная таблица была почти пуста - лишь несколько API из kernel32.dll и Go-библиотек, причем классические функции выделения памяти вроде VirtualAlloc не присутствовали статически. Значимые строки (URL, домены, ключи реестра) отсутствовали.
Отчет указывает на то, что статический анализ занял значительную часть времени. В Ghidra исследователь обнаружил набор функций основного пакета main с именами, обфусцированными в стиле garble, а также характерные "мусорные" блоки, раздувающие граф потока управления. Ключом к расшифровке стала функция main.wbkyc, которая оказалась четырехстадийным шифром на основе байтовых операций и реверса буфера. Расшифровав с ее помощью строки, аналитик восстановил вызовы Windows API через динамическую загрузку. Главное открытие: загрузчик дешифрует и исполняет в памяти второй этап - полноценный PE-файл объемом около 1 МБ. Расшифровка полезной нагрузки потребовала уточнения смещения в дампе из-за чувствительности шифра к начальному адресу.
Извлеченный полезный модуль сразу показал два уровня обфускации: отсутствие таблицы импорта и всего семь видимых строк - заголовки секций. Анализ в Ghidra выявил хеш-функцию на основе FNV-1a с измененными константами, с помощью которой злоумышленник динамически разрешает имена API. Сканирование бинарного кода на наличие хеш-значений позволило восстановить 101 вызов - от кражи DPAPI-защищенных учетных данных (CryptUnprotectData) до функций захвата экрана (BitBlt) и эксфильтрации по WinHTTP. Строки злоумышленника были зашифрованы с помощью небольшой виртуальной машины - интерпретатора с 14 опкодами. Расшифровав ее, аналитик получил около 30 значимых строк, включая имена файлов, аргументы PowerShell и команды для запуска полезной нагрузки. Конфигурация C2 (командного центра) была зашифрована повторяющимся 16-байтовым XOR-ключом. После расшифровки вскрылся домен srv.turbo88ku[.]top, уникальные User-Agent с несуществующей версией Firefox 156.0 и резервные каналы через публичные страницы Telegram и Steam с маркером gr0u4.
Динамический анализ в изолированной среде подтвердил все выводы статики. Загрузчик декриптирует и запускает Vidar в памяти, после чего тот собирает учетные данные из браузеров Chrome, Edge и Firefox, файлы криптовалютных кошельков, документы с рабочего стола и данные программ FTP. Stealer упаковывает всё в ZIP, кодирует в Base64 и отправляет на C2 через HTTPS. Дополнительно происходит установка персистентности: загрузчик создает службу CadenceOptimizer, которая запускает DLL-библиотеку agt.dll под процессом svchost.exe с правами LocalSystem. Эта DLL выполняет роль бэкдора или дополнительного загрузчика. Подобный механизм выходит за рамки классического непостоянного Vidar.
Кластеризация на основе структурных и поведенческих признаков дала впечатляющие результаты. Прокрутив кластер через vhash VirusTotal и поиск по 256-байтовому S-блоку (уникальный отпечаток виртуальной машины строк), исследователь выявил 55 образцов, совпадающих по структуре и поведению. Все они контактировали с одними и теми же dead-drop страницами Telegram и Steam и имели C2-домены с единой тематикой - "glamis" и "turbo". Это подтвердило, что загрузчик на Go используется исключительно в данной кампании и не является универсальным криптером. Хотя статическая распаковка оказалась невозможной из-за смены ключа wbkyc, поведенческий анализ позволил объединить образцы в единый кластер.
Инфраструктурная разведка позволила заглянуть за Cloudflare-фронтенд. Один из резервных C2 не был спрятан за CDN, и его IP-адрес 65.21.96[.]134 через Shodan выдал BIND-сервер с именем server.taaktook.website. Дальнейший анализ выявил 4 сервера злоумышленника с одинаковым резолвером, а также самоподписанный TLS-сертификат с серийным номером b7d2d897adfb46cdbefa997d03bb4841. Поскольку одинаковый закрытый ключ был скопирован на машины с разными операционными системами, это стало надежным маркером оператора. Особый успех принесла превентивная методика: злоумышленник часто создает поддомены ggt. и puz. на новых доменах до их боевого использования. Поиск в VirusTotal по регулярному выражению выявил goturbo88[.]top, который на момент обнаружения не имел сообщающихся файлов, а на следующий день уже использовался в атаках. Так из одного образца была развернута панорама кампании: более 50 образцов, десятки C2 и инфраструктуры.
Индикаторы компрометации
IPv4
- 65.21.96.134
Domains
- srv.turbo88ku.top
Operator TLS cert serial
- b7d2d897adfb46cdbefa997d03bb4841
SHA256
- 725344564a8c9b0a033e9a5d41369fae5b8503d36e87002e7c064eface927e7b
- f332b8851b0137ab7fc02a7c64a8b82e62e37b61fc5abf588f25c9797b7ab005
YARA
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | import "pe" import "math" rule vidar_turb00_payload { meta: description = "Vidar turb00 campaign — unpacked stealer payload" author = "Gianluca Tiepolo" reference = "https://mrtiepolo.medium.com" date = "2026-06-15" hash1 = "725344564a8c9b0a033e9a5d41369fae5b8503d36e87002e7c064eface927e7b" strings: // custom FNV-1a constants $fnv_basis = { 85 8d ff 3a } $fnv_prime = { bb 06 00 01 } // string-decoder VM init constant 0x311b7f33 $vm_init = { 33 7f 1b 31 } // first 32 bytes of the 256-byte string-VM S-box, near-unique $sbox = { cc ea 32 c5 f3 62 05 8e 12 0b e3 ca 44 4c f9 bf 52 06 b3 fc b6 00 0f 7e 15 c3 6a 86 ac 8b 89 82 } // hash-resolved API dwords $h1 = { 4f fb 93 a4 } // CryptUnprotectData $h2 = { 14 36 82 45 } // WinHttpSendRequest $h3 = { a3 68 5a 56 } // CreateRemoteThread $h4 = { 66 59 ea d7 } // BitBlt condition: // the S-box alone is essentially unique; otherwise require the hash engine + APIs $sbox or (all of ($fnv_*) and $vm_init and 2 of ($h*)) } |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | rule vidar_turb00_go_loader { meta: description = "Vidar turb00 campaign — delivered Go crypter/loader" author = "Gianluca Tiepolo" reference = "https://mrtiepolo.medium.com" date = "2026-06-15" hash1 = "f332b8851b0137ab7fc02a7c64a8b82e62e37b61fc5abf588f25c9797b7ab005" strings: $gobuild = "Go build ID: \"" // one code sequence common to all sampled family loaders $code = { 2d 00 48 85 f6 75 0c 48 be ff ff ff ff ff ff ff 7f eb 40 48 83 fe 01 75 04 31 f6 eb 36 48 89 44 24 30 48 89 5c 24 38 48 89 4c 24 40 48 89 54 24 } condition: uint16(0) == 0x5a4d and pe.machine == pe.MACHINE_AMD64 and filesize > 3000KB and filesize < 3700KB and $gobuild and $code and // embedded encrypted payload: a large high-entropy region must be present math.entropy(0x150000, 0x80000) >= 7.2 } |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | import "pe" import "math" import "vt" rule vidar_turb00_loader_behaviour { meta: description = "Vidar turb00 campaign — delivered Go crypter/loader + actor-C2 behavior for LiveHunt" author = "Gianluca Tiepolo" reference = "https://mrtiepolo.medium.com" date = "2026-06-15" hash1 = "f332b8851b0137ab7fc02a7c64a8b82e62e37b61fc5abf588f25c9797b7ab005" strings: $gobuild = "Go build ID: \"" condition: // ---- static loader structure ---- uint16(0) == 0x5a4d and pe.machine == pe.MACHINE_AMD64 and filesize > 3000KB and filesize < 3700KB and $gobuild and math.entropy(0x150000, 0x80000) >= 7.2 and // ---- dynamic confirmation via VT sandbox behaviour ---- ( ( for any d in vt.behaviour.dns_lookups: ( d.hostname matches /glamis[a-z]*rent/i or d.hostname matches /turbo88ku/i ) ) or ( for any c in vt.behaviour.http_conversations: ( c.url matches /:\/\/(srv|ggt|ffe)\.[a-z0-9.]*(turbo88ku|glamis)/i ) ) or ( ( for any d in vt.behaviour.dns_lookups: ( d.hostname matches /telegram\.me/i ) ) and ( for any d in vt.behaviour.dns_lookups: ( d.hostname matches /steamcommunity\.com/i ) ) ) ) } |