Содержание статьи
- Предыстория
- Потушить AV
- Получить сессию C2
- Перепаять инструмент
- Классическая инъекция шелл-кода
- Введение в D/Invoke
- DynamicAPIInvoke без D/Invoke
- DynamicAPIInvoke с помощью D/Invoke
- Зачем системные вызовы?
- GetSyscallStub с помощью D/Invoke
- Модификация KeeThief
- Подготовка
- Апгрейд функции ReadProcessMemory
- Время для теста!
- Выводы
В этой статье мы рассмотрим замечательный сторонний механизм D/Invoke для C#, позволяющий эффективно дергать Windows API в обход средств защиты, и перепишем KeeThief, чтобы его не ловил великий и ужасный «Касперский». Погнали!
Предыстория
В общем, пребываю я на внутряке, домен‑админ уже пойман и наказан, но вот осталась одна вредная база данных KeePass, которая, конечно же, не захотела сбрутиться с помощью hashcat и keepass2john.py. В KeePass — доступы к критически важным ресурсам инфры, определяющим исход внутряка, поэтому добраться до нее нужно. На рабочей станции, где пользак крутит интересующую нас базу, глядит в оба Kaspersky Endpoint Security (он же KES), который не дает расслабиться. Рассмотрим, какие есть варианты получить желанный мастер‑пароль, не прибегая к социнженерии.
Прежде всего скажу, что успех этого предприятия — в обязательном использовании крутой малвари KeeThief из коллекции GhostPack авторства небезызвестных @harmj0y и @tifkin_. Ядро программы — кастомный шелл‑код, который вызывает RtlDecryptMemory в отношении зашифрованной области виртуальной памяти KeePass.exe и выдергивает оттуда наш мастер‑пароль. Если есть шелл‑код, нужен и загрузчик, и с этим возникают трудности, когда на хосте присутствует EDR...
Впрочем, мы отвлеклись. Какие были варианты?
Потушить AV
Самый простой (и глупый) способ — вырубить к чертям «Касперского» на пару секунд. «Это не редтим, поэтому право имею!» — подумал я. Так как привилегии администратора домена есть, есть и доступ к серверу администрирования KES. Следовательно, и к учетке KlScSvc
(в этом случае использовалась локальная УЗ), креды от которой хранятся среди секретов LSA в плейнтексте.
Порядок действий простой. Дампаю LSA с помощью secretsdump.py.
Гружу консоль администрирования KES с официального сайта и логинюсь, указав хостнейм KSC.
Стопорю «Каспера» и делаю свои грязные делишки.
Profit! Мастер‑пароль у нас. После окончания проекта я опробовал другие способы решить эту задачу.
Получить сессию C2
Многие C2-фреймворки умеют тащить за собой DLL рантайма кода C# (Common Language Runtime, CLR) и загружать ее отраженно по принципу RDI (Reflective DLL Injection) для запуска малвари из памяти. Теоретически это может повлиять на процесс отлова управляемого кода, исполняемого через такой трюк.
Полноценную сессию Meterpreter при активном антивирусе Касперского получить трудно из‑за обилия артефактов в сетевом трафике, поэтому его execute-assembly я даже пробовать не стал. А вот модуль execute-assembly Cobalt Strike принес свои результаты, если правильно получить сессию beacon (далее скриншоты будут с домашнего KIS, а не KES, но все техники работают и против последнего — проверено).
Все козыри раскрывать не буду — мне еще работать пентестером, однако этот метод тоже не представляет большого интереса в нашей ситуации. Для гладкого получения сессии «маячка» нужен внешний сервак, на который надо накрутить валидный сертификат для шифрования SSL-трафика, а заражать таким образом машину с внутреннего периметра заказчика — совсем невежливо.
Перепаять инструмент
Самый интересный и в то же время трудозатратный способ — переписать логику инъекции шелл‑кода таким образом, чтобы EDR не спалил в момент исполнения. Это то, ради чего мы сегодня собрались, но для начала немного теории.
Примечание
Дело здесь именно в уклонении от эвристического анализа, так как, если спрятать сигнатуру малвари с помощью недетектируемого упаковщика, доступ к памяти нам все равно будет запрещен из‑за фейла инъекции.
Классическая инъекция шелл-кода
Оглянемся назад и рассмотрим классическую технику внедрения стороннего кода в удаленный процесс. Для этого наши предки пользовались священным трио Win32 API:
- VirtualAllocEx — выделить место в виртуальной памяти удаленного процесса под наш шелл‑код.
- WriteProcessMemory — записать байты шелл‑кода в выделенную область памяти.
- CreateRemoteThread — запустить новый поток в удаленном процессе, который стартует свежезаписанный шелл‑код.
Напишем простой 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. Реакция ресурса говорит сама за себя.
Как антивирусное ПО понимает, что перед ним инжектор, даже без динамического анализа? Все просто: пачка атрибутов DllImport
, занимающих половину нашего исходника, кричит об этом на всю деревню. Например, с помощью такого волшебного кода на PowerShell я могу посмотреть все импорты в бинаре .NET.
Примечание
Здесь используется сборка System.
, доступная «из коробки» в PowerShell Core. Установка описана в документации Microsoft.
$assembly = "C:\Users\snovvcrash\source\repos\SimpleInjector\bin\x64\Release\SimpleInjector.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" }}
info
Эти импорты представляют собой способ взаимодействия приложений .NET с неуправляемым кодом — таким, например, как функции библиотек user32.
, kernel32.
. Этот механизм называется P/Invoke (Platform Invocation Services), а сами сигнатуры импортируемых функций с набором аргументов и типом возвращаемого значения можно найти на сайте pinvoke.net.
При анализе этого добра в динамике, как ты понимаешь, дела обстоят еще проще: так как все EDR имеют привычку вешать хуки на userland-интерфейсы, вызовы подозрительных API сразу поднимут тревогу. Подробнее об этом можно почитать в ресерче @ShitSecure, а в лабораторных условиях хукинг нагляднее всего продемонстрировать с помощью API Monitor.
Итак, что же со всем этим делать?
Введение в D/Invoke
В 2020 году исследователи @TheWover и @FuzzySecurity представили новый API для вызова неуправляемого кода из .NET — D/Invoke (Dynamic Invocation, по аналогии с P/Invoke). Этот способ основан на использовании мощного механизма делегатов в C# и изначально был доступен как часть фреймворка для разработки постэксплуатационных тулз SharpSploit, однако позже был вынесен в отдельный репозиторий и даже появился в виде сборки на NuGet.
С помощью делегатов разработчик может объявить ссылку на функцию, которую хочет вызвать, со всеми параметрами и типом возвращаемого значения, как и при использовании импорта с помощью атрибута DllImport
. Разница в том, что в отличие от импорта с помощью DllImport
, когда адрес импортируемых функций ищет исполняющая среда, при использовании делегатов мы должны самостоятельно локализовать интересующий нас неуправляемый код (динамически, в ходе выполнения программы) и ассоциировать его с объявленным указателем. Далее мы сможем обращаться к указателю как к искомой функции, без необходимости «кричать» о том, что мы вообще собирались ее использовать.
D/Invoke предоставляет не один подход для динамического импорта неуправляемого кода, в том числе:
- DynamicAPIInvoke — парсит структуру DLL (причем может как загружать ее с диска, так и обращаться к уже загруженному экземпляру в памяти текущего процесса), где размещена нужная функция, и вычисляет ее экспорт‑адрес.
-
GetSyscallStub — загружает в память библиотеку
ntdll.
, точно так же парсит ее структуру, чтобы в результате получить не что иное, как указатель на экспорт‑адрес системного вызова — последней черты перед переходом в мирdll мертвыхkernel-mode (о системных вызовах поговорим чуть позже).
Чтобы было понятнее, разберем для начала простой пример, который делает нечто похожее на первый подход, но без использования D/Invoke.
DynamicAPIInvoke без D/Invoke
Мне очень нравится пример из статьи xpn (второй листинг кода в разделе «A Quick History Lesson»), где он показывает, как можно использовать всю мощь делегатов вместе с ручным поиском экспорт‑адреса неуправляемой функции менее чем за 50 строк.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»