![](/uploads/posts/2022-08/kes-admin-console.png)
Содержание статьи
- Предыстория
- Потушить 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.
![Потрошим LSA Потрошим LSA](/uploads/posts/2022-08/kes-secretsdump-py.png)
Гружу консоль администрирования KES с официального сайта и логинюсь, указав хостнейм KSC.
![Консоль администрирования KES Консоль администрирования KES](/uploads/posts/2022-08/kes-admin-console.png)
Стопорю «Каспера» и делаю свои грязные делишки.
![AdobeHelperAgent.exe, ну вы поняли, ага AdobeHelperAgent.exe, ну вы поняли, ага](/uploads/posts/2022-08/kes-keethief.png)
Profit! Мастер‑пароль у нас. После окончания проекта я опробовал другие способы решить эту задачу.
Получить сессию C2
Многие 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 KeeTheft.exe с помощью execute-assembly CS](/uploads/posts/2022-08/cs-execute-assembly.png)
Все козыри раскрывать не буду — мне еще работать пентестером, однако этот метод тоже не представляет большого интереса в нашей ситуации. Для гладкого получения сессии «маячка» нужен внешний сервак, на который надо накрутить валидный сертификат для шифрования SSL-трафика, а заражать таким образом машину с внутреннего периметра заказчика — совсем невежливо.
Перепаять инструмент
Самый интересный и в то же время трудозатратный способ — переписать логику инъекции шелл‑кода таким образом, чтобы EDR не спалил в момент исполнения. Это то, ради чего мы сегодня собрались, но для начала немного теории.
Классическая инъекция шелл-кода
Оглянемся назад и рассмотрим классическую технику внедрения стороннего кода в удаленный процесс. Для этого наши предки пользовались священным трио Win32 API:
- VirtualAllocEx — выделить место в виртуальной памяти удаленного процесса под наш шелл‑код.
- WriteProcessMemory — записать байты шелл‑кода в выделенную область памяти.
- CreateRemoteThread — запустить новый поток в удаленном процессе, который стартует свежезаписанный шелл‑код.
![Исполнение шелл-кода с помощью Thread Execution (изображение — elastic.co) Исполнение шелл-кода с помощью Thread Execution (изображение — elastic.co)](/uploads/posts/2022-08/classic-shellcode-injection.png)
Напишем простой 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.
![Классическая инъекция шелл-кода Классическая инъекция шелл-кода](/uploads/posts/2022-08/simple-injector.png)
Если просто положить такой бинарь на диск с активным средством антивирусной защиты, реакция будет незамедлительной независимо от содержимого массива buf
, то есть нашего шелл‑кода. Все дело в комбинации потенциально опасных вызовов Win32 API, которые заведомо используются в большом количестве зловредов. Для демонстрации я перекомпилирую инжектор с пустым массивом buf
и залью результат на VirusTotal. Реакция ресурса говорит сама за себя.
![VirusTotal намекает... VirusTotal намекает...](/uploads/posts/2022-08/simple-injector-virustotal.png)
Как антивирусное ПО понимает, что перед ним инжектор, даже без динамического анализа? Все просто: пачка атрибутов DllImport
, занимающих половину нашего исходника, кричит об этом на всю деревню. Например, с помощью такого волшебного кода на PowerShell я могу посмотреть все импорты в бинаре .NET.
$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($typeHandlerin$metadataReader.TypeDefinitions){
$typeDef=$metadataReader.GetTypeDefinition($typeHandler)
foreach($methodHandlerin$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 Смотрим импорты в SimpleInjector.exe](/uploads/posts/2022-08/simple-injector-imports.png)
При анализе этого добра в динамике, как ты понимаешь, дела обстоят еще проще: так как все EDR имеют привычку вешать хуки на userland-интерфейсы, вызовы подозрительных API сразу поднимут тревогу. Подробнее об этом можно почитать в ресерче @ShitSecure, а в лабораторных условиях хукинг нагляднее всего продемонстрировать с помощью API Monitor.
![Хукаем kernel32.dll в SimpleInjector.exe Хукаем kernel32.dll в SimpleInjector.exe](/uploads/posts/2022-08/simple-injector-apimonitor.png)
Итак, что же со всем этим делать?
Введение в 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 строк.
Скачать:
Скриншоты:
Важно:
Все статьи и материал на сайте размещаются из свободных источников. Приносим свои глубочайшие извинения, если Ваша статья или материал была опубликована без Вашего на то согласия.
Напишите нам, и мы в срочном порядке примем меры.