Тема руткитов всегда актуальна. Тем более, если речь идет о новом типе руткитов - использующих технологию возвратноориентированного программирования. Вводную статью по этой теме мы представляли на твой суд в прошлом номере ][, сегодня же мы постараемся развить ее в более практическом направлении. Хотя, конечно, исходников не жди - все реальные наработки по этой теме сейчас находятся в глубоком привате.

На сегодняшний день использование драйверов для взаимодействия с «нутром» системы (особенно для систем безопасности Windows) — устоявшаяся практика. Многие программы используют их как окно для доступа в нулевое кольцо. Впрочем, тут стоит отметить один очевидный факт: кроме основных функций подобные драйверы оснащены также механизмами взаимодействия, предназначенными для обмена данными между драйвером и программными компонентами, работающими в пользовательском режиме. Заметь, код, работающий на высоком уровне привилегий, получает данные от кода, работающего на уровне привилегий более низком. Это значит, что на плечи разработчика ложится непростая задача – обеспечить проверку таких данных, потому что если пустить этот процесс на самотек, то в лучшем случае будут грабли со стабильным функционированием такого драйвера, в худшем – можно поставить под угрозу всю ОС.

 

Уязвимости в ядре? Их есть у меня!

Уязвимостей в ядре Windows не так много, но время от времени они появляются: иногда в виде призрачных намеков, иногда – в виде убедительных отверстий толщиной с главный калибр линкора «Миссури». Вспомним, например, 2008 год, когда впервые поя вилась информация об уязвимости MS08-025, эксплуатация которой позволяла выполнить произвольный код в режиме ядра и достичь благодаря этому локального повышения привилегий на операционных системах Windows XP и Windows Server 2003. Это далеко не первая уязвимость, которая была обнаружена в win32k.sys, и я абсолютно уверен, что и не последняя.

Такая ситуация сложилась в первую очередь из-за того, что изначально графическая подсистема работала в режиме пользователя (по Windows NT 4.0 включительно), но позже, чтобы сократить количество ресурсоемких операций по переключению потока в режим ядра, разработчики Windows решили перенести графическую подсистему в Ring-0. Однако, в силу достаточно большого объема кода и архитектурных особенностей, во время этого переноса не было уделено достаточно внимания вопросам безопасности, что и способствовало появлению в win32k.sys большого количества уязвимостей разной степени опасности.

Вообще подсистема win32k.sys — довольно дырявая штукенция. Например (спасибо Лозовскому за подгон инфы), недавно новая 0-day уязвимость была обнаружена в этой графической подсистеме винды. Атаке подвергся WinAPI RtlQueryRegistryValues, используемый для получения различных значений ключей реестра с помощью таблицы запросов и имеющий EntryContext в качестве буфера вывода. Для успешного обхода защиты злоумышленник должен создать поврежденный ключ реестра или управлять ключами, доступ к которым разрешен только обычным пользователям.

 

Детали общей мозаики

Как я говорил в начале этой статьи, для успешной реализации возвратно-ориентированного руткита нам все же понадобится r0-уязвимость, которая позволит подчинить себе поток выполнения программного кода на ядерном уровне. Точнее, нужен какой-нибудь драйвер, который содержит в себе баг с переполнением буфера, который позволит злоумышленнику овладеть стеком ядра. В основе руткита, созданного с помощью возвратно-ориентированного кодинга, будет IRP-пакет, а вернее — целый механизм, известный под общим названием «диспетчер ввода-вывода» и призванный взаимодействовать между ring3 и ring0.

Существует достаточно много хорошо и не очень документированных системных механизмов, которые могут быть использованы для организации взаимодействия кода пользовательского режима с драйверами режима ядра. Самыми функциональными и наиболее часто используемыми являются те механизмы, которые представляются диспетчером ввода-вывода. В конце концов, именно они и создавались разработчиками операционной системы для подобных задач. Давай вспомним, как обычно организуется работа с диспетчером ввода-вывода со стороны драйвера и приложения.

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

Структура, описывающая объект «драйвер», называется DRIVER_OBJECT, а эти функции – IRP-обработчиками (IRP — I/O Request Packet). Их адреса драйвер помещает в поле DRIVER_OBJECTx-MajorFunction, которое является массивом указателей на IRP-обработчики и имеет следующий прототип:

typedef
NTSTATUS
(*PDRIVER_DISPATCH) (
__in struct _DEVICE_OBJECT *DeviceObject,
__in struct _IRP *Irp
);

Параметр DeviceObject указывает на конкретное устройство (у одного драйвера их может быть много), а Irp – на структуру, содержащую различную информацию о запросе к устройству: контрольный код, буферы для входящих и исходящих данных, статус завершения обработки запроса и многое другое.

 

Эксплойт?

Мы постараемся заэксплойтить самую распространенную и часто встречающуюся уязвимость переполнения буфера. Это легко сделать путем перезаписи буфера в созданном драйвером «устройстве», тем самым заместив возвращаемое значение в стеке таким образом, чтобы оно указывало на последовательность ядерных инструкций «POP ESP; RET», а также следующее в стеке значение, чтобы оно указывало на точку входа в возвратно-ориентированную программу.

Переписав эти восемь байт, мы сможем сконфигурировать стек так, чтобы он указывал на начало нашей возвратноориентированной программы. Любой подобный небезопасный код позволяет легко реализовать нашу атаку: простая уязвимость в ядре или любом драйвере, загруженном в адресное пространство ядра, является достаточным условием, чтобы скомпрометировать всю систему и выполнить злонамеренный возвратно-ориентированный код. Правда, для реализации этих коварных планов нужно решить один существенный вопрос — куда запихнуть имидж нашей программы? Есть два пути.

Во-первых, эксплойт может перезаписать весь стек ядра нашим кодом, однако стек не резиновый, и в него может влезть (в случае Windows) всего три страницы, то есть 12 Кб. Во-вторых, эксплойт должен (по крайней мере, на начальном этапе выполнения) постараться продержать имидж самой программы в пользовательском режиме. Для решения этой проблемы идеально будет написать загрузчик нашего будущего руткита. Но об этом — чуть ниже.

 

Подводные камни

Их тоже полно. Одно из главных препятствий, возникающих на пути реализации возвратно-ориентированного руткита, — то, как Windows манипулирует своим ядерным стеком. Все существующие версии Windows используют в ядре так называемые уровни запросов прерываний (IRQL), являющиеся настоящей головной болью для системных разработчиков, пишущих драйвера. Если ты не знаешь, что такое IRQL, то совсем уж вкратце — это механизм приоритетов в ядре, весьма похожий на уровень приоритета потоков в юзермодных программах.

У каждого прерывания есть свой заранее определенный уровень. Когда возникает прерывание, то в первую очередь осуществляется сравнение с уровнем IRQL, который имеет текущий поток. В случае, если новое прерывание обладает более низким IRQL, исполнение программного кода от нового прерывания ставится в очередь до лучших времен — новое прерывание хода исполнения программы не может его заморозить, если оно ниже по уровню. Для грамотного читателя все это, конечно, не новость. Но самое интересное при этом происходит при попытке доступа к подкачиваемой памяти (то есть той, которую ядро периодически сбрасывает на жесткий диск), поскольку это имеет определенные последствия.

Главное, что следует уяснить — доступ к подкачиваемой памяти сильно ограничен на высоких IRQL'ах, и при реализации кода это приводит к проблемам (читай — BSOD'ам). Ведь всякий раз, когда происходят прерывания (и, следовательно, они должны быть обработаны ядром Windows), ядро, как правило, начинает оперировать со стеком. При этом обработчик прерывания выделяет память ниже текущего значения ESP, нежели обработчик стека. И хотя такое поведение ядра вполне приемлемо в общей ситуации, в нашем случае это может привести к нежелательным последствиям, поскольку значения стека, находящиеся ниже текущего ESP, могут серьезно подпортить выполнение кода руткита.

Решить эту проблему, как я говорил ранее, довольно просто — для этого нужен загрузчик. Можно сделать так: загрузчик должен выделить память в неподкачиваемом (nonpaged) пуле, который никогда не сбрасывается на жесткий диск, и скопировать туда тело руткита из пользовательского пространства перед тем как он будет запущен. При этом, заметь, имидж находится в пользовательском пространстве, что ограничивает способность механизмов защиты ядра запретить загрузку нашего руткита.

Когда общий ход выполнения программы захочет вернуться из некоей подпрограммы, это может привести к краху, поскольку есть вероятность того, что точка возврата перезаписана обработчиком прерывания. Чтобы избежать таких проблем, нужно также предусмотреть некую опцию динамического восстановления «поврежденного» участка кода, но уж это остается тебе в качестве домашнего задания.

Кроме того, если в стеке попадутся адреса из подкачиваемой памяти, не избежать проблем с IRQL, что неминуемо приведет к «синим экранам».

Это проблема решается использованием функции VirtualLock, которая позволит залочить в памяти определенное количество страниц процесса и не даст ядру скинуть эти страницы на диск. Однако, по неизвестным для меня причинам, это ноу-хау не всегда работает с областями памяти размером больше одной страницы.

 

Оффтоп

Большинство уязвимостей, возникающих из-за неправильной обработки данных, которые драйвер получает в IRP-запросе, довольно однотипны, и мы уже не раз о них писали. Знающие люди говорят, что некорректная обработка входных данных не является разовым явлением и, найдя одну уязвимость, можно с большой вероятностью найти и другую, проследив либо общий ход выполнения программы, либо другие участки программного кода, выполняющие аналогичную задачу. С такой задачей хорошо справляется утилитка IOCTL Fuzzer (code.google.com/p/ioctlfuzzer), действие которой заключается в генерации и отправке заведомо некорректных входных данных с расчетом на то, что код, который их обрабатывает, попросту не учитывает возможность присутствия подобных некорректностей.

На самом деле, таких уязвимостей переполнения буфера, которые можно заэксплойтить при помощи фич возвратноориентированного кодинга в сторонних драйверах, вагон и маленькая тележка.

 

Links

Два интересных блога:

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

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

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии