Вызов мастеру ключей. Инжектим шелл-код в память KeePass, обойдя антивирус

Содержание статьи

  • Предыстория
  • Потушить AV
  • Получить сессию C2
  • Перепаять инструмент
  • Классическая инъекция шелл-кода
  • Введение в D/Invoke
  • DynamicAPIInvoke без D/Invoke
  • DynamicAPIInvoke с помощью D/Invoke
  • Зачем системные вызовы?
  • GetSyscallStub с помощью D/Invoke
  • Модификация KeeThief
  • Подготовка
  • Апгрейд функции ReadProcessMemory
  • Время для теста!
  • Выводы

Не­дав­но на пен­тесте мне понадо­билось вытащить мас­тер‑пароль откры­той базы дан­ных 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…

Впро­чем, мы отвлек­лись. Какие были вари­анты?

 

Потушить AV

Са­мый прос­той (и глу­пый) спо­соб — вырубить к чер­тям «Кас­пер­ско­го» на пару секунд. «Это не ред­тим, поэто­му пра­во имею!» — подумал я. Так как при­виле­гии адми­нис­тра­тора домена есть, есть и дос­туп к сер­веру адми­нис­три­рова­ния KES. Сле­дова­тель­но, и к учет­ке KlScSvc (в этом слу­чае исполь­зовалась локаль­ная УЗ), кре­ды от которой хра­нят­ся сре­ди сек­ретов LSA в плей­нтек­сте.

По­рядок дей­ствий прос­той. Дам­паю LSA с помощью secretsdump.py.

Пот­рошим LSA

Гру­жу кон­соль адми­нис­три­рова­ния KES с офи­циаль­ного сай­та и логинюсь, ука­зав хос­тнейм KSC.

Кон­соль адми­нис­три­рова­ния KES

Сто­порю «Кас­пера» и делаю свои гряз­ные делиш­ки.

AdobeHelperAgent.exe, ну вы поняли, ага

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

Все козыри рас­кры­вать не буду — мне еще работать пен­тесте­ром, одна­ко этот метод тоже не пред­став­ляет боль­шого инте­реса в нашей ситу­ации. Для глад­кого получе­ния сес­сии «маяч­ка» нужен внеш­ний сер­вак, на который надо нак­рутить валид­ный сер­тификат для шиф­рования SSL-тра­фика, а заражать таким обра­зом машину с внут­ренне­го перимет­ра заказ­чика — сов­сем невеж­ливо.

 

Перепаять инструмент

Са­мый инте­рес­ный и в то же вре­мя тру­дозат­ратный спо­соб — перепи­сать логику инъ­екции шелл‑кода таким обра­зом, что­бы EDR не спа­лил в момент исполне­ния. Это то, ради чего мы сегод­ня соб­рались, но для начала нем­ного теории.

Примечание

Де­ло здесь имен­но в укло­нении от эвристи­чес­кого ана­лиза, так как, если спря­тать сиг­натуру мал­вари с помощью недетек­тиру­емо­го упа­ков­щика, дос­туп к памяти нам все рав­но будет зап­рещен из‑за фей­ла инъ­екции.

За­пуск крип­тован­ного KeeTheft.exe при активном EDR

 

Классическая инъекция шелл-кода

Ог­лянем­ся назад и рас­смот­рим клас­сичес­кую тех­нику внед­рения сто­рон­него кода в уда­лен­ный про­цесс. Для это­го наши пред­ки поль­зовались свя­щен­ным трио Win32 API:

  • VirtualAllocEx — выделить мес­то в вир­туаль­ной памяти уда­лен­ного про­цес­са под наш шелл‑код.
  • WriteProcessMemory — записать бай­ты шелл‑кода в выделен­ную область памяти.
  • CreateRemoteThread — запус­тить новый поток в уда­лен­ном про­цес­се, который стар­тует све­жеза­писан­ный шелл‑код.

Ис­полне­ние шелл‑кода с помощью 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

Итак, что же со всем этим делать?

 

Введение в 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 строк.

    Источник: xakep.ru

    Ответить

    Ваш адрес email не будет опубликован. Обязательные поля помечены *