Microsoft подвергла своих пользователей немалому риску, когда выпустила Windows Defender вне песочницы. Меня это удивило. Песочница — одна из самых эффективных техник усиления безопасности. Почему Microsoft изолировала в песочнице другие высоковероятные цели атаки, вроде кода JIT в Microsoft Edge, но оставила Windows Defender без защиты?
В качестве PoC (proof-of-concept) я изолировал Windows Defender, а сейчас выкладываю свой код в открытый доступ как Flying Sandbox Monster. Основа Flying Sandbox Monster — это AppJailLauncher-rs, фреймворк на Rust для помещения ненадёжных приложений в AppContainers. Он также позволяет вынести I/O приложения за TCP-сервер, чтобы приложение в песочнице работало на полностью другой машине. Это дополнительный уровень изоляции.
В статье я опишу процесс и результаты создания этого инструмента, а также выскажу свои мысли о Rust на Windows.
Flying Sandbox Monster запустил Windows Defender в песочнице для сканирования бинарника WannaCry
План
Беспрепятственный доступ Windows Defender к ресурсам системы и приём любых вредоносных форматов файлов делают его идеальной мишенью для злоумышленников. Ключевой процесс программы MsMpEng работает как сервис с привилегиями SYSTEM. Сканирующий компонент MpEngine поддерживает разбор астрономического количества форматов файлов. У него в комплекте также эмуляторы систем для различных архитектур и интерпретаторы для разных языков. Всё это вместе, исполняемое на высочайшем уровне привилегий в Windows. Оп-па.
Всё это заставило меня подумать. Насколько трудно будет изолировать MpEngine, используя ту же технику, которую я использовал на соревнованиях по сэндбоксингу сообщества CTF два года назад?
Первый шаг к изолированию Windows Defender — возможность запустить AppContainers. Я бы хотел опять использовать AppJailLauncher, но здесь была проблема. Оригинальный AppJailLauncher был написан как демонстрационный пример. Если бы я тогда знал, то написал бы его на C++ Core, чтобы не мучиться с управлением памятью. За последние два года я пытался переписать его на С++, но неудачно (почему зависимости всегда такая головная боль?).
Но затем меня посетило вдохновение. Почему бы не переписать код запуска AppContainer на Rust?
Создание песочницы
Несколько месяцев спустя, после беглого изучения учебников по Rust и написания своего первого кода, у меня появилось три главные опоры для запуска AppContainers на Rust: это SimpleDacl
, Profile
и WinFFI
.
- SimpleDacl — это обобщённый класс, который берёт на себя добавление и удаление простых дискреционных записей контроля доступа ACE (access control entries) под Windows. Хотя
SimpleDacl
работает и с файлами, и с директориями, у него есть несколько недостатков. Во-первых, он полностью переписывает существующую ACL и преобразует унаследованные элементы ACE в «нормальные». Кроме того, он пренебрегает теми элементами ACE, которые не может парсить (например, всё, кромеAccessAllowedAce
иAccessDeniedAce
. Примечание: мы не поддерживаем обязательные и проверочные записи контроля доступа). - Profile реализует создание профилей и процессов AppContainer. Из профиля мы можем получить SID, который можно использовать для создания ACE для ресурсов, к которым AppContainer должен иметь доступ.
- WinFFI даёт нам функции и структуры, которые в winapi-rs реализованы не так хорошо, как в полезных служебных классах/функциях. Я приложил много усилий, чтобы обернуть каждый исходный
HANDLE
и указатель в объекты Rust для управления временем их работы.
Далее нужно было понять, как установить интерфейс со сканирующим компонентом Windows Defender. Пример реализации на С и инструкции для начала сканирования MsMpEng были в репозитории loadlibrary Тэвиса Орманди. Портирование структур и прототипов функций на Rust несложно автоматизировать, хотя я поначалу забыл о полях-массивах и указателях функций, из-за чего возникло много разных проблем; однако благодаря встроенной в Rust функциональности тестирования я быстро решил все свои ошибки портирования — и вскоре у меня был минимальный тестовый вариант, который сканировал тестовый файл EICAR.
Базовая архитектура Flying Sandbox Monster
Наш пример Flying Sandbox Monster состоит из враппера песочницы и Malware Protection Engine (MpEngine). У единственного исполняемого файла два режима: родительский и дочерний процессы. Режим определяется присутствием переменных окружения, которые содержат HANDLEs
для сканируемого файла, и коммуникацией между процессами. Родительский процесс устанавливает два этих значения HANDLEs
перед созданием дочернего процесса AppContainer. Теперь изолированный дочерний процесс загружает библиотеку антивирусного движка и сканирует входящий файл на предмет вирусов.
Этого недостаточно для полноценной работы PoC, ибо Malware Protection Engine не хотел инициализироваться внутри AppContainer. Сначала я думал, что это проблема контроля доступа. Но после тщательной сверки отличий в ProcMon (сравнивая отличия исполнения в AppContainer и не в AppContainer), я понял, что проблема может быть в определении версии Windows. Код Тэвиса всегда отчитывался как версия Windows XP. Мой код сообщал реальную версию хостовой системы: в моём случае это Windows 10. Проверка в WinDbg доказала, что проблема инициализации действительно в этом. Нужно было соврать MpEngine о хостовой версии Windows. В С/C++ я бы использовал перехват функций с помощью Detours. К сожалению, для Rust под Windows нет эквивалентной библиотеки для перехвата функций (несколько библиотек для перехвата функций оказались гораздо «тяжеловеснее», чем мне надо). Естественно, я реализовал простую библиотеку для перехвата функций на Rust (только для 32-битной Windows PE).
Представление AppJailLauncher-rs
Поскольку я уже реализовал ключевые компоненты AppJailLauncher на Rust, почему бы не закончить работу и не обернуть это всё в сервер Rust TCP? Я это сделал, и теперь рад представить вам «вторую версию» AppJailLauncher — AppJailLauncher-rs.
AppJailLauncher был TCP-сервером, который прослушивал определённый порт и запускал процесс AppContainer для каждого принятого соединения TCP. Я не хотел изобретать велосипед, но mio, компактная I/O библиотека для Rust, просто не подходила. Во-первых, её TcpClient не обеспечивал доступа к исходным HANDLEs
сокетам под Windows. Во-вторых, эти сокеты не наследовались дочерним процессом AppContainer. Из-за этого приходится представить ещё одну «опору» для поддержки appjaillauncher-rs: TcpServer.
TcpServer
отвечает за асинхронный TCP-сервер и клиентский сокет, совместимый с перенаправлением STDIN/STDOUT/STDERR
. Созданные вызовом socket
сокеты не могут перенаправлять стандартные потоки ввода-вывода. Для правильного стандартного перенаправления ввода-вывода нужны «нативные» сокеты (как те, которые создаются через WSASocket
). Чтобы разрешить перенаправление, TcpServer
создаёт эти «нативные» сокеты и не запрещает явно для них наследование.
Мой опыт работы с Rust
В целом, мой опыт работы с Rust оказался очень приятным, несмотря на некоторые небольшие шероховатости. Позвольте упомянуть некоторые функции этого языка программирования, которые я особенно приметил во время разработки AppJailLauncher.
Cargo. Управление зависимостями в C++ под Windows — реально утомительное и сложное дело, особенно со ссылками на сторонние библиотеки. Rust ловко решает эту проблему с помощью пакетного менеджера cargo. Там большой набор пакетов, которые решают многие типичные проблемы вроде разбора аргументов (clap-rs), Windows FFI (winapi-rs и др.) и обработки широких строк (widestring).
Встроенное тестирование. Юнит-тесты для приложений C++ требуют применения сторонней библиотеки и большой ручной работы. Вот почему их редко пишут для маленьких проектов, вроде первой версии AppJailLauncher. В Rust юнит-тестирование встроено в систему cargo, где существует вместе с основной функциональностью.
Система макросов. В Rust система макросов работает на уровне абстрактного синтаксического дерева, в отличие от простого движка замены в С/C++. Хотя здесь нужно немного обучиться, но макросы Rust полностью лишены раздражающих свойств макросов C/C++, вроде коллизий именований и области.
Отладка. Отладка Rust под Windows работает как надо. Rust генерирует WinDbg-совместимые символы отладки (файлы PDB), которые обеспечивают беспрепятственную отладку исходников.
Интерфейс внешних функций. Windows API написаны на C/С++ и подразумевается, что таким образом и надо к нему обращаться. Другие языки, как и Rust, должны использовать интерфейс внешних функций (FFI), чтобы обратиться к Windows API. Rust FFI для Windows (winapi-rs) по большей части готов. Там есть ключевые API, но не хватает некоторых не так часто используемых подсистем вроде API для изменения списка контроля доступа.
Аттрибуты. Установка аттрибутов довольно обременительна, поскольку они применяются только для следующей строчки.
Borrow Checker. Концепция собственности — то, как Rust достигает безопасности памяти. Попытки понять, как работает Borrow Checker, сопровождаются загадочными, уникальными ошибками и требуют часов чтения документации и учебников. В конце концов оно того стоит: когда у меня «щёлкнуло» и я усвоил концепцию, мои навыки программирования кардинально продвинулись.
Векторы. В C++ контейнер std::vector
может выставить свой поддерживающий буфер другому коду. Оригинальный вектор остаётся валидным, даже если поддерживающий буфер изменили. В случае Vec
в Rust это не так. Здесь Vec
требует создания нового объекта из «заготовок» старого Vec
.
Типы Option и Result. Нативные типы Option и Result должны были упростить проверку ошибок, но на самом деле сделали её как будто более подробной. Можно сделать вид, что ошибок не существует, и просто вызвать unwrap
, но это приведёт к сбою рантайма, когда неизбежно вылезет Error
(или None
).
Другие типы и слайсы. К принадлежащим типам (owned types) и сопутствующим слайсам (например, String/str
, PathBuf/Path
) нужно немного привыкнуть. Они идут парами с похожими именами, но ведут себя по-разному. В Rust принадлежащий тип представляет расширяемый, изменчивый объект (обычно строку). Слайс — это вид неизменяемого буфера символов (тоже обычно строка).
Будущее
Экосистема Rust для Windows ещё растёт. Можно ещё создавать новые библиотеки Rust, упрощающие разработку ПО для безопасности под Windows. Я сделал начальные версии нескольких библиотек Rust для сэндбоксинга в Windows, разбора PE и перехвата IAT. Надеюсь, они будут полезны возникающему сообществу Rust под Windows.
С помощью Rust и AppJailLauncher я изолировал в песочнице Windows Defender, флагманский антивирусный продукт Microsoft. Моё достижение одновременно и замечательное, и немного постыдное. Замечательно то, что надёжный механизм песочницы Windows доступен для стороннего софта. Постыдно то, что Microsoft сама не изолировала Defender. Microsoft купила то, что впоследствии станет Windows Defender, в 2004 году. В те времена такие баги и архитектурные просчёты были неприемлемы, но объяснимы. За прошедшие Microsoft создала отличную организацию по разработке систем безопасности, для обычного тестирования и фаззинга. Она изолировала в песочнице критические части Internet Explorer. Каким-то образом Windows Defender застрял в 2004 году. Вместо использования методов Project Zero и непрерывного указания на симптомы этого врождённого недостатка, давайте перенесём Windows Defender обратно в будущее.
Автор: m1rko