Разбор инцидента: от письма про таможенное оформление до 13 КБ шеллкода, который так и не удалось прочитать статически.
TL;DR. Пришло письмо с «документами для таможенного оформления» — внутри .vbs на 32 КБ. Под ним пять слоёв: VBS-декодер, PowerShell, отданный в cmd через %VAR%, XOR-загрузчик с кэшем в реестре и, наконец, PowerShell-инжектор, который поднимает в памяти шеллкод и 129 КБ зашифрованного модуля. Дизассемблер на последнем слое упирается в стену: оба бинарных блоба упакованы, ключ живёт в стабе. Домен C2 — взломанный сайт индийской строительной компании на DigitalOcean, финальный C2 — где-то дальше. Публичной атрибуции нет: индикаторы не встречаются ни в одном отчёте.
1. Письмо
Всё начинается с обычного делового письма. Получатели — в скрытой копии (undisclosed-recipients), что само по себе уже намекает на массовую рассылку, но в потоке из полусотни писем за день такое легко пропустить.
От: Илья Зелёный
<service@techrem.ru>Кому: undisclosed-recipients Тема: Message has been disinfected -Документы к авто на 19-20 октября Дата: 19.10.2026, 09:24Доброе утро, коллеги! На следующей неделе ожидаем прибытие авто на СВХ ILAR Во вложении документы для проведения таможенного оформления
С уважением, ТОО “ЮникомТехСервис” u.t.s-company@yandex.kz
Тема письма уже содержит приписку шлюза — «Message has been disinfected». То есть защита что-то заметила и что-то с письмом сделала, но объект до нас всё равно доехал.
Что не так с этим письмом, если читать его как охотник, а не как получатель:
- Тема составная. Префикс шлюза приклеен к теме без пробела:
disinfected -Документы. Сама тема — про документы, а не про заражение. - Домен отправителя мёртв.
techrem.ru— припаркован: NS указывают наns1/ns2.snparking.ru, MX — наvoid.blackhole.mx(то есть вся почта в этот домен уходит в чёрную дыру, ответить отправителю нельзя), SPF- и DMARC-записей нет вообще. Исторически домен принадлежал сети сервис-центров «ТехРем» — то есть выбран не случайно, а как «правдоподобный российский B2B-домен, с которого уже никто не ответит». - Подпись не сходится с отправителем. Пишет «Илья Зелёный» с
@techrem.ru, подписывается ТОО «ЮникомТехСервис» и даёт адрес наyandex.kz. Три разных источника идентичности в одном письме. - Приманка попадает в контекст. «Прибытие авто на СВХ ILAR», «таможенное оформление» — это язык реальной логистики, а не абстрактное «ваш счёт».
- Вложение — скрипт. Имя файла мимикрирует под регистрационный номер документа:
AIX6O23013-168-06-2026_10_19 UTS.vbs. Расширение.vbsпри этом видно, но рядом с «документами» оно читается как «какая-то служебная приписка».
Забегая вперёд: полезная нагрузка лежала на сервере с 04:25 МСК — примерно за пять часов до отправки письма.
2. Слой 1: VBS, в котором нет ни одной осмысленной строки
Файл — 32 660 байт, чистый ASCII, CRLF. Внутри — сотни присваиваний вида:
| |
Осмысленных строк ровно две — технические, по краям файла: Set fso = CreateObject("Scripting.FileSystemObject") в начале и Set Errand = CreateObject("WScript.Shell") на 129-й строке. Между ними — сотни присваиваний и вызовов, за которыми не видно ни одной команды. Всё держится на одной функции Predining() и одной глобальной переменной-аккумуляторе Geoteknik:
| |
Каждый вызов забирает каждый пятый символ, начиная с 17-й позиции, и дописывает к общей строке. Счётчик глобальный — поэтому куски склеиваются в правильном порядке только если вызывать их последовательно. Плюс Mundhulen(s) = Right(s, 1) — «перевод строки», который вставляется в нужных местах.
Внутри этой каши, если применить декодер, оказывается… обычный VBS:
| |
И финальная строка, ради которой всё написано:
| |
Обратите внимание на ^% и ^!. Это не часть полезной нагрузки — это экранирование для cmd. Внутри cmd каретка съедается, и PowerShell получает нормальный код. Если запустить строку как есть в PowerShell, получите ошибку «^!False is not recognized» — на этом легко потерять полдня, решив, что декодировал неправильно.
3. Слой 2: PowerShell, которого нет в командной строке
Конструкция echo <код> | powershell.exe - — это запуск PowerShell со stdin. В списке процессов вы увидите powershell.exe с аргументом - и пустым CommandLine: самого скрипта в командной строке нет, файла на диске тоже нет, а Script Block Logging при этом обычно всё фиксирует.
Скрипт собран из шести фрагментов (Voyagers), в сумме 6960 символов. Своя криптография, свой словарь — но всё то же самое:
| |
Для base64-блобов — XOR с ключом, для «инлайн-строк» — вычитание: тот же ключ, только вывернутый. Декодированный скрипт содержит:
- User-Agent:
5.0 (Windows NT 10.0; Win64; x64; rv:157.0) Gecko/20100101 Firefox/157.0— обратите внимание, без префиксаMozilla/. Такой строки не бывает у настоящего Firefox, зато она отлично ищется в прокси-логах. - URL:
http://thetamarindleafresidency.com/Lyksaligstes.asd— обычный HTTP, без TLS. - COM-объект:
Msxml2.ServerXMLHTTP.6.0— обход прокси-настроек браузера. - Кэш:
HKCU:\Idlenesses\Skandalens, значениеAdresselister.
Цикл загрузки незамысловат:
| |
Ответ сервера (base64-текст) складывается в реестр, читается обратно, декодируется и — Invoke-Expression. Ни одного файла на диске. Кэш в реестре нужен, чтобы пережить перезапуск: если процесс упал, следующий проход возьмёт payload оттуда, не обращаясь к C2.
4. Слой 3: 232 килобайта, которые отдал сервер
Скачиваем (одним GET через прокси, ничего не исполняя):
| |
232 592 символа base64 → 174 444 байта. В скрипте зашиты два числа: смещение 142216 и длина 32228. В сумме — ровно 174 444. Совпадение до байта: файл собран под этот скрипт, а не наоборот.
Разметка:
| Смещение | Размер | Что это |
|---|---|---|
| 0 – 13 310 | 13 310 | шеллкод-стаб |
| 13 310 – 142 216 | 128 906 | зашифрованный модуль |
| 142 216 – 174 444 | 32 228 | PowerShell-инжектор |
Первые 142 КБ выглядят как случайный шум — высокая энтропия, ни одной строки. Это и есть полезная нагрузка; всё, что до неё, скрипт даже не читает.
5. Слой 4: инжектор без Add-Type и без CreateThread
Третий скрипт — самый интересный. Он собран тем же генератором (те же датские слова в комментариях, тот же ключ), но при этом не содержит ни декодера, ни ключа: он переиспользует функции, которые уже висят в памяти процесса с предыдущего слоя. Мелочь, которая экономит атакующему байты и ломает наивный статический разбор.
Что он делает:
1. P/Invoke без Add-Type. Вместо компиляции C# на лету — Reflection.Emit:
| |
Импортируются VirtualAlloc и CallWindowProcA (user32), NtProtectVirtualMemory (ntdll), OpenProcessToken / CreateEnvironmentBlock / DestroyEnvironmentBlock. Add-Type в логах не появляется — а именно его обычно и ищут.
2. Два буфера. Первый — 13 310 байт, PAGE_EXECUTE_READWRITE. Второй — 68 472 832 байта (65 МБ), PAGE_READWRITE. Шестьдесят пять мегабайт под модуль на 129 КБ означает, что распакованный образ крупный — типично для упакованного .NET- или Go-импланта.
3. Копирование и запуск.
| |
Запуск через CallWindowProcA, а не через CreateThread — старый трюк: поток не создаётся, событие «создание потока» не генерируется, а EDR видит вызов user32 с указателем на приватную память. Стаб получает первым аргументом указатель на зашифрованный модуль, расшифровывает его и передаёт управление дальше.
4. Маскировка. ShowWindow для консоли, перезапись заголовка окна на случайную строку, перебор и перезапись блока окружения процесса (OpenProcessToken(-1, TOKEN_QUERY) → CreateEnvironmentBlock → SetEnvironmentVariable(..., 'Process') → DestroyEnvironmentBlock) — чтобы в окружении не осталось следов исходной командной строки.
5. Самое смешное. Скрипт снимает с себя cmd-экранирование:
| |
Те самые каретки из первой строки cmd.exe /v:on /c echo ^%...^% | ... — код знает, что его протащили через cmd, и приводит себя в порядок.
6. Где статика упирается в стену
Дальше — обрыв. Оба бинарных блоба упакованы:
- энтропия по окнам: 7,84–7,86 (стаб) и 7,28–7,77 (модуль);
- ни одной ASCII/UTF-16 строки;
- на входе стаба нет валидного пролога — линейный дизассемблер с нулевого смещения выдаёт мусор;
MZ/PEвнутри модуля по штатным смещениям нет.
Ключ и распаковщик лежат в самом стабе и работают только в памяти. Значит, финальный имплант и его C2 статически не достаются — и это правильно спроектировано: даже полный дамп c2-ответа не даёт семейства.
Хэши на память:
- файл:
e159dc2fd74322e66f96018325a3432c97369c85256e6201b76f3a70fb1f3aee - стаб:
3c5b9f6ba6055fc2f6fcd0da05e065432d69cb0a31aec52f6e9a3e007d5c4c17 - модуль:
6b7f7f8b906c0bff0d490749d1fc76439b849f1535809b8e6d0c00653f371e9d
7. Подпись, которая ничего не подписывает
В .vbs зашит Authenticode-блок с самоподписанным сертификатом:
| |
Датские слова, выдуманная организация, CA:TRUE у листового сертификата, выпуск за три недели до атаки. Доверенной цепочки нет — подпись не даёт ничего, кроме ещё одной сигнатуры генератора: тот же словарь, из которого берутся имена файлов на C2 (Lyksaligstes.asd, brikvvedes.ocx).
8. Инфраструктура: взломанный сайт, а не «сервер злодеев»
thetamarindleafresidency.com — не ботнет-нода, а скомпрометированный сайт жилого комплекса Tamarind Leaf Residency (сектор 50, Нойда, Индия). IP 142.93.209.119 — DigitalOcean, Бенгалуру; хостинг venus.hostitsmartserver.com, LiteSpeed. На обратной записи — около 499 доменов, почти все индийские малые бизнесы: типичный shared-хостинг, где сломали один аккаунт.
Публичные источники:
| Источник | Результат |
|---|---|
| urlscan.io | 2 скана домена, оба за 20.10.2026: /brikvvedes.ocx (00:32 UTC, отправитель из России) и главная страница сайта |
| Triage | 2 отчёта по этому payload: только static, score 1/10, семейство не определено |
| AlienVault OTX | 0 пульсов по домену и по обоим хэшам |
| URLhaus | нет в 30-дневном фиде |
| GreyNoise | IP не замечен в сканировании |
| Поиск | ни домена, ни имён файлов, ни XOR-ключа, ни subject сертификата — ни одного упоминания в отчётах |
Вывод: публичной атрибуции нет. Кампания либо новая, либо специально живёт ниже радаров. Хэши при этом пер-жертвенные: у второго образца из urlscan другой SHA-256, хотя сборка та же — детект по хэшам здесь бесполезен.
9. Таргет или массовая рассылка?
Признаков адресной операции нет:
undisclosed-recipients— рассылка по списку, а не письмо конкретному человеку;- в коде ноль проверок жертвы: ни языка, ни домена, ни имени пользователя, ни анти-VM, ни задержек;
- «проверки» в VBS (
GetDrive("Z:"),%PROCESSOR_ARCHITECTURE%) — пустышки, обе ветки ведут к одному и тому жеpowershell.exe; - инфраструктура одноразовая: взломанный shared-хостинг, домен отправителя — мёртвый парковочный;
- имена файлов на C2 генерируются из словаря и меняются от жертвы к жертве.
Что действительно выделяется — ориентация на русскоязычных получателей: приманка про таможню и СВХ, письмо на русском, две независимые отправки в urlscan за сутки. Это массовая кампания по региону, а не охота за конкретной компанией.
10. IOC
| |
MITRE ATT&CK: T1566.001, T1059.005, T1059.001, T1027, T1105, T1112, T1620, T1564.003.
11. Что делать
Блокировки. Домен C2 и IP — в прокси, DNS и почтовый шлюз. Помните: это останавливает только второй слой. Финальный C2 живёт в зашифрованном модуле и по этому домену не находится.
Реестр — главный артефакт. Ключ HKCU\Idlenesses\Skandalens со значением Adresselister — это кэш payload. Если он найден, значение можно разобрать без обращения к C2: base64 → ASCII → Substring(142216, 32228) даст текст инжектора, а сам файл — в карантин.
Процессы. Самая дешёвая и самая точная сигнатура:
| |
Сочетание /v:on и echo в командной строке cmd.exe, запущенного из wscript, в норме не встречается практически никогда.
Сеть. HTTP GET без TLS на *.asd/*.ocx с UA, начинающимся с 5.0 ( вместо Mozilla/5.0 (. Плюс обращения рабочих станций к 142.93.209.119:80.
Память. Если процесс ещё жив: приватная RWX-память рядом с 65-мегабайтным RW-буфером и последующий CallWindowProcA — характерная связка. Дамп процесса даст расшифрованный имплант; это единственный путь узнать семейство без песочницы.
Логирование. Script Block Logging (4104) и AMSI — при запуске через stdin содержимое скрипта обычно попадает в журнал, несмотря на пустой CommandLine.
Почта. По имени вложения и домену отправителя проверить, сколько получателей получили письмо и был ли запуск до обновления баз. И отдельно — почему письмо с префиксом «disinfected» всё-таки доставилось.
12. Выводы
.vbsв 2026 году работает. Ни макросов, ни LNK, ни ISO — просто скрипт, который запускается двойным кликом, и никто не удивляется.- Пять слоёв — это не паранойя, а экономика. Каждый слой стоит атакующему несколько строк генератора и ломает по одному классу детектов: сигнатуры, статический разбор,
Add-Type,CreateThread. - Реестр как кэш — недооценённый артефакт. Он превращает одноразовый скрипт в устойчивый и оставляет след, который переживает перезагрузку.
- Взломанный легитимный сайт дешевле и незаметнее, чем «своя» инфраструктура: репутация домена чистая, а живёт он ровно до того момента, как кто-то отправит его в urlscan.
- Публичная атрибуция — не показатель. Ноль упоминаний в OTX, URLhaus и поиске не значит «безобидно»; это значит «свежее и мало отслеживаемое».
- Детект по хэшам здесь не работает — payload собирается под каждую жертву. Работает поведение: цепочка процессов, ключ реестра, странный User-Agent, RWX-память и
CallWindowProcA.
Персональные данные получателей из переписки в статье не публикуются. Ссылки на C2 дефангованы.