Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Недавно на пентесте мне понадобилось вытащить мастер‑пароль открытой базы данных KeePass из памяти процесса с помощью утилиты KeeThief из арсенала GhostPack. Все бы ничего, да вот EDR, следящий за системой, категорически не давал мне этого сделать — ведь под капотом KeeThief живет классическая процедура инъекции шелл‑кода в удаленный процесс, что не может остаться незамеченным в 2022 году.
В этой статье мы рассмотрим замечательный сторонний механизм D/Invoke для C#, позволяющий эффективно дергать Windows API в обход средств защиты, и перепишем KeeThief, чтобы его не ловил великий и ужасный «Касперский». Погнали!
В общем, пребываю я на внутряке, домен‑админ уже пойман и наказан, но вот осталась одна вредная база данных KeePass, которая, конечно же, не захотела сбрутиться с помощью hashcat и keepass2john.py. В KeePass — доступы к критически важным ресурсам инфры, определяющим исход внутряка, поэтому добраться до нее нужно. На рабочей станции, где пользак крутит интересующую нас базу, глядит в оба Kaspersky Endpoint Security (он же KES), который не дает расслабиться. Рассмотрим, какие есть варианты получить желанный мастер‑пароль, не прибегая к социнженерии.
Прежде всего скажу, что успех этого предприятия — в обязательном использовании крутой малвари KeeThief из коллекции GhostPack авторства небезызвестных @harmj0y и @tifkin_. Ядро программы — кастомный шелл‑код, который вызывает RtlDecryptMemory в отношении зашифрованной области виртуальной памяти KeePass.exe и выдергивает оттуда наш мастер‑пароль. Если есть шелл‑код, нужен и загрузчик, и с этим возникают трудности, когда на хосте присутствует EDR…
Впрочем, мы отвлеклись. Какие были варианты?
Самый простой (и глупый) способ — вырубить к чертям «Касперского» на пару секунд. «Это не редтим, поэтому право имею!» — подумал я. Так как привилегии администратора домена есть, есть и доступ к серверу администрирования KES. Следовательно, и к учетке KlScSvc
(в этом случае использовалась локальная УЗ), креды от которой хранятся среди секретов LSA в плейнтексте.
Порядок действий простой. Дампаю LSA с помощью secretsdump.py.
Потрошим LSA
Гружу консоль администрирования KES с официального сайта и логинюсь, указав хостнейм KSC.
Консоль администрирования KES
Стопорю «Каспера» и делаю свои грязные делишки.
AdobeHelperAgent.exe, ну вы поняли, ага
Profit! Мастер‑пароль у нас. После окончания проекта я опробовал другие способы решить эту задачу.
Многие C2-фреймворки умеют тащить за собой DLL рантайма кода C# (Common Language Runtime, CLR) и загружать ее отраженно по принципу RDI (Reflective DLL Injection) для запуска малвари из памяти. Теоретически это может повлиять на процесс отлова управляемого кода, исполняемого через такой трюк.
Полноценную сессию Meterpreter при активном антивирусе Касперского получить трудно из‑за обилия артефактов в сетевом трафике, поэтому его execute-assembly я даже пробовать не стал. А вот модуль execute-assembly Cobalt Strike принес свои результаты, если правильно получить сессию beacon (далее скриншоты будут с домашнего KIS, а не KES, но все техники работают и против последнего — проверено).
KeeTheft.exe с помощью execute-assembly CS
Все козыри раскрывать не буду — мне еще работать пентестером, однако этот метод тоже не представляет большого интереса в нашей ситуации. Для гладкого получения сессии «маячка» нужен внешний сервак, на который надо накрутить валидный сертификат для шифрования SSL-трафика, а заражать таким образом машину с внутреннего периметра заказчика — совсем невежливо.
Самый интересный и в то же время трудозатратный способ — переписать логику инъекции шелл‑кода таким образом, чтобы EDR не спалил в момент исполнения. Это то, ради чего мы сегодня собрались, но для начала немного теории.
Примечание
Дело здесь именно в уклонении от эвристического анализа, так как, если спрятать сигнатуру малвари с помощью недетектируемого упаковщика, доступ к памяти нам все равно будет запрещен из‑за фейла инъекции.
Запуск криптованного KeeTheft.exe при активном EDR
Оглянемся назад и рассмотрим классическую технику внедрения стороннего кода в удаленный процесс. Для этого наши предки пользовались священным трио Win32 API:
Исполнение шелл‑кода с помощью Thread Execution (изображение — elastic.co)
Напишем простой PoC на C#, демонстрирующий эту самую классическую инъекцию шелл‑кода.
using System;using System.Diagnostics;using System.Runtime.InteropServices;namespace SimpleInjector{ public class Program { [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] static extern IntPtr OpenProcess( uint processAccess, bool bInheritHandle, int processId); [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] static extern IntPtr VirtualAllocEx( IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect); [DllImport("kernel32.dll")] static extern bool WriteProcessMemory( IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, Int32 nSize, out IntPtr lpNumberOfBytesWritten); [DllImport("kernel32.dll")] static extern IntPtr CreateRemoteThread( IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId); public static void Main() { // msfvenom -p windows/x64/messagebox TITLE='MSF' TEXT='Hack the Planet!' EXITFUNC=thread -f csharp byte[] buf = new byte[] { }; // Получаем PID процесса explorer.exe int processId = Process.GetProcessesByName("explorer")[0].Id; // Получаем хендл процесса по его PID (0x001F0FFF = PROCESS_ALL_ACCESS) IntPtr hProcess = OpenProcess(0x001F0FFF, false, processId); // Выделяем область памяти 0x1000 байт (0x3000 = MEM_COMMIT | MEM_RESERVE, 0x40 = PAGE_EXECUTE_READWRITE) IntPtr allocAddr = VirtualAllocEx(hProcess, IntPtr.Zero, 0x1000, 0x3000, 0x40); // Записываем шелл-код в выделенную область _ = WriteProcessMemory(hProcess, allocAddr, buf, buf.Length, out _); // Запускаем поток _ = CreateRemoteThread(hProcess, IntPtr.Zero, 0, allocAddr, IntPtr.Zero, 0, IntPtr.Zero); } }}
Скомпилировав и запустив инжектор, с помощью Process Hacker можно наблюдать, как в процессе explorer.exe запустится новый поток, рисующий нам диалоговое окно MSF.
Классическая инъекция шелл‑кода
Если просто положить такой бинарь на диск с активным средством антивирусной защиты, реакция будет незамедлительной независимо от содержимого массива buf
, то есть нашего шелл‑кода. Все дело в комбинации потенциально опасных вызовов Win32 API, которые заведомо используются в большом количестве зловредов. Для демонстрации я перекомпилирую инжектор с пустым массивом buf
и залью результат на VirusTotal. Реакция ресурса говорит сама за себя.
VirusTotal намекает…
Как антивирусное ПО понимает, что перед ним инжектор, даже без динамического анализа? Все просто: пачка атрибутов DllImport
, занимающих половину нашего исходника, кричит об этом на всю деревню. Например, с помощью такого волшебного кода на PowerShell я могу посмотреть все импорты в бинаре .NET.
Примечание
Здесь используется сборка System.Reflection.Metadata
, доступная «из коробки» в PowerShell Core. Установка описана в документации Microsoft.
$assembly = "C:UserssnovvcrashsourcereposSimpleInjectorbinx64ReleaseSimpleInjector.exe"$stream = [System.IO.File]::OpenRead($assembly)$peReader = [System.Reflection.PortableExecutable.PEReader]::new($stream, [System.Reflection.PortableExecutable.PEStreamOptions]::LeaveOpen -bor [System.Reflection.PortableExecutable.PEStreamOptions]::PrefetchMetadata)$metadataReader = [System.Reflection.Metadata.PEReaderExtensions]::GetMetadataReader($peReader)$assemblyDefinition = $metadataReader.GetAssemblyDefinition()foreach($typeHandler in $metadataReader.TypeDefinitions) { $typeDef = $metadataReader.GetTypeDefinition($typeHandler) foreach($methodHandler in $typeDef.GetMethods()) { $methodDef = $metadataReader.GetMethodDefinition($methodHandler) $import = $methodDef.GetImport() if ($import.Module.IsNil) { continue } $dllImportFuncName = $metadataReader.GetString($import.Name) $dllImportParameters = $import.Attributes.ToString() $dllImportPath = $metadataReader.GetString($metadataReader.GetModuleReference($import.Module).Name) Write-Host "$dllImportPath, $dllImportParameters`n$dllImportFuncName`n" }}
Смотрим импорты в SimpleInjector.exe
info
Эти импорты представляют собой способ взаимодействия приложений .NET с неуправляемым кодом — таким, например, как функции библиотек user32.dll
, kernel32.dll
. Этот механизм называется P/Invoke (Platform Invocation Services), а сами сигнатуры импортируемых функций с набором аргументов и типом возвращаемого значения можно найти на сайте pinvoke.net.
При анализе этого добра в динамике, как ты понимаешь, дела обстоят еще проще: так как все EDR имеют привычку вешать хуки на userland-интерфейсы, вызовы подозрительных API сразу поднимут тревогу. Подробнее об этом можно почитать в ресерче @ShitSecure, а в лабораторных условиях хукинг нагляднее всего продемонстрировать с помощью API Monitor.
Хукаем kernel32.dll в SimpleInjector.exe
Итак, что же со всем этим делать?
В 2020 году исследователи @TheWover и @FuzzySecurity представили новый API для вызова неуправляемого кода из .NET — D/Invoke (Dynamic Invocation, по аналогии с P/Invoke). Этот способ основан на использовании мощного механизма делегатов в C# и изначально был доступен как часть фреймворка для разработки постэксплуатационных тулз SharpSploit, однако позже был вынесен в отдельный репозиторий и даже появился в виде сборки на NuGet.
С помощью делегатов разработчик может объявить ссылку на функцию, которую хочет вызвать, со всеми параметрами и типом возвращаемого значения, как и при использовании импорта с помощью атрибута DllImport
. Разница в том, что в отличие от импорта с помощью DllImport
, когда адрес импортируемых функций ищет исполняющая среда, при использовании делегатов мы должны самостоятельно локализовать интересующий нас неуправляемый код (динамически, в ходе выполнения программы) и ассоциировать его с объявленным указателем. Далее мы сможем обращаться к указателю как к искомой функции, без необходимости «кричать» о том, что мы вообще собирались ее использовать.
D/Invoke предоставляет не один подход для динамического импорта неуправляемого кода, в том числе:
ntdll.dll
, точно так же парсит ее структуру, чтобы в результате получить не что иное, как указатель на экспорт‑адрес системного вызова — последней черты перед переходом в мир мертвых kernel-mode (о системных вызовах поговорим чуть позже).
Чтобы было понятнее, разберем для начала простой пример, который делает нечто похожее на первый подход, но без использования D/Invoke.
Мне очень нравится пример из статьи xpn (второй листинг кода в разделе «A Quick History Lesson»), где он показывает, как можно использовать всю мощь делегатов вместе с ручным поиском экспорт‑адреса неуправляемой функции менее чем за 50 строк.
Источник: xakep.ru