Мне в руки попался обычный файл с расширением .vbs. Ничего необычного. Открываю посмотреть, что там внутри, и сначала даже немного разочаровался. Какой-то VBScript, временные файлы, cmd.exe, PowerShell. Ну, думаю, очередной скрипт-загрузчик. Но потом заметил одну деталь. Скрипт читал сам себя. И вот тут стало уже интереснее.

Начинаем с VBS

В самом начале создаётся объект WScript.Shell и определяется %TEMP%:

Dim v16173 : Set v16173 = CreateObject("WScript.Shell")
Dim v39964 : v39964 = v16173.ExpandEnvironmentStrings("%TEMP%")

Дальше идут две небольшие функции, которые генерируют случайные имена файлов. Например, одна из них делает что-то вроде:

d48321.dat

а другая:

t73921

Ничего особенного. Но потом появляется вот это:

stmIn.LoadFromFile WScript.ScriptFullName
v73756 = stmIn.ReadText

Скрипт открывает собственный файл и читает его содержимое. Зачем? Через несколько строк ответ находится сам:

pos = InStrRev(v73756, "'DATA:")

То есть внутри VBS после специальной метки лежит ещё что-то. Вредоносный скрипт решил быть сам себе архивом.

DATA внутри самого себя

После DATA: содержимое очищается от переводов строк:

v71891 = Replace(Replace(v71891, vbCrLf, ""), vbLf, "")

А потом строка делится:

parts = Split(v71891, "[SPLIT]")

И вот тут уже начинается нормальная матрёшка. Внутри находятся три части:

part 0
part 1
part 2

Первые две сохраняются как .dat:

Dim v67468 : v67468 = F33274("dat")
Dim v65772 : v65772 = F33274("dat")

А третья часть оказывается PowerShell-скриптом. То есть исходный файл выглядит примерно так:

2.vbs
 |
 +-- VBScript
 |
 +-- DATA:
      |
      +-- данные №1
      +-- данные №2
      +-- PowerShell

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

v18705 = Replace(parts(2), "{N}", Chr(10))
v18705 = Replace(v18705, "__PF__", v67468)
v18705 = Replace(v18705, "__RF__", v65772)
v18705 = Replace(v18705, "__DP__", v13874)
v18705 = Replace(v18705, "__WD__", v29859)

После этого он записывается в .ps1. А дальше запускается PowerShell. Причём окно ему показывать никто не собирается:

-WindowStyle Hidden

И ещё перед запуском:

Set-ExecutionPolicy Bypass -Scope Process -Force

На этом месте уже понятно, что VBS был только первым этапом. Но я ещё не знал, что именно спрятано дальше.

PowerShell оказался интереснее

Открываю получившийся .ps1. И тут вместо привычного:

FromBase64String(...)

какой-нибудь длинной строки Base64 вижу собственную функцию:

function Read-StreamConfig {

Сначала она создаёт таблицу:

$lut = [int[]]::new(0x80)

А затем заполняет её буквами от A до Z и от a до z. Дальше начинается разбор строки блоками. Основной блок — 10 символов:

if ($r -ge 0xA) {
    $v = [long]$lut[[int]$sc[$i]]

    for ($q = 1; $q -lt 0xA; $q++) {
        $v = $v * 0x34 + [long]$lut[[int]$sc[$i+$q]]
    }
}

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

[byte[]]$c1 = Read-StreamConfig '__DLL__'

и:

[byte[]]$c2 = Read-StreamConfig '__PLD__'

Названия говорят сами за себя. Первый блок — DLL. Второй — payload. Осталось понять, что с ними делают дальше.

Просто декодировать оказалось мало

Для каждого блока есть свой ключ и IV.

Например:

[byte[]]$k1 = 0x5D, 0x12, 0xFC, 0x2C, ...
[byte[]]$v1 = 0xF0, 0x3B, 0x85, 0x7E, ...

И второй набор:

[byte[]]$k2 = 0xA7, 0x24, 0x03, 0xEB, ...
[byte[]]$v2 = 0xA5, 0xE0, 0x44, 0x06, ...

Самое интересное находится в _Expand. Сначала данные проходят через XOR с IV:

$s[$i] = $s[$i] -bxor $iv[$i -band 15]

Потом происходит вращение битов:

$r2 = ($k[($i + 7) -band 31] % 7) + 1

После чего байт ещё раз XOR'ится с ключом:

$s[$i] = [byte](
    (
        (($b -shr $r2) -bor ($b -shl (8 - $r2))) -band 0xFF
    ) -bxor $k[$i % $k.Length] -bxor $k[($i + 17) -band 31]
)

И только после этого начинается AES.

$aes = [Security.Cryptography.Aes]::Create()
$aes.Mode = [Security.Cryptography.CipherMode]::CBC
$aes.Padding = [Security.Cryptography.PaddingMode]::PKCS7
$aes.Key = $k
$aes.IV = $iv

То есть чтобы получить нормальные байты, нужно пройти несколько операций подряд. Сначала собственное кодирование. Потом XOR. Потом вращение битов. Потом ещё XOR. И только потом AES-CBC. В общем, обычный .vbs постепенно превращается в маленький криптографический конструктор.

Первый сюрприз — DLL не сохраняется

После всех этих преобразований получается $b1:

$b1 = _Expand $c1 $k1 $v1

И здесь я ожидал увидеть что-нибудь вроде:

[IO.File]::WriteAllBytes(...)

Но нет. Вместо этого:

$_asmH = [Reflection.Assembly]::Load([byte[]]$b1)

Получившиеся байты сразу загружаются как .NET Assembly. На диск DLL не сохраняется. Дальше PowerShell ищет тип:

$_ct = $_asmH.GetType('RSG_UnityApp.CSheader')

И вызывает метод:

DispatchContextualModulePipeline

Через Reflection:

$_ct.InvokeMember(
    'DispatchContextualModulePipeline',
    [Reflection.BindingFlags]'InvokeMethod,Public,Static',
    ...
)

Вот здесь я уже решил посмотреть, что именно этому методу передают. И тут появилась следующая интересная часть.

Причём здесь MSBuild

В коде есть:

$tp = "C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe"

То есть путь к системному MSBuild.exe. А вторым параметром передаётся второй расшифрованный блок:

$b2

Сам вызов:

@(
    $tp,
    [byte[]]$b2,
    [int]45000,
    [string]'',
    [bool]$false,
    $null
)

По сути, мы пришли к такой цепочке:

VBS
 ↓
сам себя читает
 ↓
достаёт DATA
 ↓
создаёт PowerShell
 ↓
достаёт два блока
 ↓
собственный декодер
 ↓
XOR + rotate
 ↓
AES-CBC
 ↓
.NET Assembly
 ↓
Reflection
 ↓
второй payload
 ↓
MSBuild.exe

И вот это уже совсем не похоже на безобидный script.vbs.

Но почему всё это вообще спрятано?

Если посмотреть на исходник, автор вполне мог сделать всё гораздо проще. Можно было положить DLL рядом с VBS. Можно было скачать payload обычным Invoke-WebRequest. Можно было использовать Base64. Но тогда аналитик открывает файл, видит пару строк и сразу понимает, куда копать. Здесь сделали немного иначе. Сначала нужно понять, что VBS читает сам себя. Потом найти DATA:. Потом разобраться с [SPLIT]. Потом достать PowerShell. Потом понять Read-StreamConfig. Потом восстановить байты. Потом разобраться с _Expand. И только после этого появляется .NET Assembly. Если анализировать такой файл «сверху вниз», можно потратить прилично времени просто на снятие слоёв.

Есть ещё один момент

В PowerShell есть таймер:

$_xt = New-Object Timers.Timer
$_xt.Interval = 0x927C0
$_xt.AutoReset = $false
$_xt.add_Elapsed({ [Environment]::Exit(0) })
$_xt.Start()

0x927C0 — это 600000 миллисекунд. То есть 10 минут. Если скрипт за это время не закончит работу, процесс завершится. Там же есть проверка режима PowerShell:

if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') {
    exit
}

Если доступен не FullLanguage, скрипт просто выходит. Это уже не просто упаковка данных. Автор явно учитывает условия, в которых код будет выполняться.

Вся цепочка целиком

После того как я разобрал первый слой, вся конструкция выглядит уже не так страшно:

                  2.vbs
                    |
                    v
             читает сам себя
                    |
                    v
                 DATA:
                    |
          +---------+---------+
          |         |         |
          v         v         v
       блок 1    блок 2    PowerShell
          |         |         |
          +---------+---------+
                    |
                    v
             Read-StreamConfig
                    |
                    v
             XOR / Rotate
                    |
                    v
                 AES-CBC
                    |
             +------+------+
             |             |
             v             v
        .NET Assembly    Payload
             |
             v
       Reflection.Load
             |
             v
 RSG_UnityApp.CSheader
             |
             v
DispatchContextualModulePipeline
             |
             v
          MSBuild.exe

И самое забавное во всей этой истории то, что начинается всё с обычного VBS.

Открываешь файл и видишь:

WScript.Shell
ADODB.Stream
TEMP
cmd.exe
PowerShell

Кажется, ну да, очередной скрипт. А потом начинаешь копать и выясняется, что под ним ещё один слой. Потом ещё один. Потом AES. Потом .NET. Потом Reflection.Assembly.Load. И где-то в конце уже появляется MSBuild.exe.

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

Сам по себе каждый приём не сказать чтобы новый. Скрытый PowerShell — не новость. Загрузка .NET-сборки через Reflection — тоже. AES в загрузчиках встречается постоянно. Использование системных компонентов Windows тоже давно никого не удивляет. Но когда всё это складывают в одну цепочку, получается уже интереснее. Причём автор не стал делать какой-то огромный километровый обфускатор. Кода относительно немного. Просто каждый следующий слой мешает сразу понять, что происходит. И именно поэтому такие файлы интересно разбирать. Не потому что там используется какой-то невероятный алгоритм. А потому что сначала кажется, что перед тобой маленький VBS. А потом этот маленький VBS начинает постепенно рассказывать, что он совсем не маленький.

Итог

В итоге обычный .vbs оказался загрузчиком с несколькими слоями. Первый слой читает собственное содержимое и достаёт из него две закодированные области и PowerShell. PowerShell декодирует данные, выполняет дополнительные преобразования, расшифровывает их через AES-CBC и получает .NET-сборку. Сборка загружается непосредственно в память через Reflection.Assembly.Load. После этого вызывается RSG_UnityApp.CSheader.DispatchContextualModulePipeline, которому передаётся второй payload и путь к MSBuild.exe. То есть расширение .vbs здесь довольно сильно обманывает ожидания. Снаружи — небольшой скрипт. Внутри — целая цепочка загрузки. И вот такие образцы я как раз люблю разбирать: сначала кажется, что сейчас будет очередной скучный VBS, а через полчаса уже сидишь и разбираешь AES, Reflection и MSBuild.exe.

Комментарии (1)


  1. AVX
    10.09.2026 10:19

    Стандартный подход в современном мире вредоносов.

    Я ожидал, что скрипт этот же файл отправит на выполнение в powershell и т.п. Но как понял - тут просто извлекаются данные и записываются в другие файлы, которые уже выполняются.

    Я делал файл .bat, который можно выполнять в винде, и он на определенном этапе сам себя же передает на выполнение в powershell. Если же тот же файл запустить в linux - отработает как bash скрипт (в котором конечно можно запустить ещë что угодно). Разделение на едва уловимых особенностях обработки спецсимволов и комментариев разными интерпретаторами. Файлик с безопасной нагрузкой, но можно начинить чем угодно, пока не хочу это куда-то раздавать.