Бурим ядро Windows: Kernel Pool Overflow – от теории к практикеЯдро винды всегда было лакомым кусочком для хакера, особенно при наличии законченных методик его эксплуатирования, приводящих к повышению прав. Учитывая тот факт, что за последние несколько лет количество уязвимостей, связанных с переполнением динамической памяти ядра, резко возросло, я активно заинтересовался данным направлением и, к собственному удивлению, в конечном итоге накопал столько материала, что его хватит не на один 0day-баг. Актуальность проблемыТехнология Memory Management является одной из самых важных в работе ядра. Уязвимости этого механизма, пожалуй, также самые страшные и, в то же время, актуальные. Они и являются основным стимулом для создания всяких разных видов защиты, таких как safe unlinking. В этой статье будут детально рассмотрены некоторые аспекты, как теоретические, так и практические, по эксплуатации динамического переполнения памяти ядра. Для начала я покажу пальцем на самых ярких представителей уязвимостей этой касты.
Обзор распределения памяти ядромКак в любой уважающей себя операционной системе, Windows (а точнее говоря, ее ядро) предоставляет некоторые функции для выделения/освобождения памяти. Виртуальная память состоит из блоков, называемых страницами. В архитектуре Intel x86 размер страницы составляет 4096 байт. Однако большинство запросов на выделение памяти меньше объема страницы. Поэтому функции ядра, такие как ExAllocatePoolWithTag и ExFreePoolWithTag, резервируют неиспользуемую память для последующего ее выделения. Внутренние функции напрямую взаимодействуют с железом каждый раз, когда страница задействована. Все эти процедуры достаточно сложны и деликатны, вот почему они реализованы именно в ядре. Различия между Paged и NonPaged poolПамять ядра системы делится на два различных пула. Этот финт был придуман для выделения наиболее часто используемых блоков памяти. Система должна знать, какие страницы наиболее востребованы, а от каких можно временно отказаться (логично, правда?). Paged pool может быть сохранен в оперативной памяти или вытеснен в файловую систему (swap). NonPaged pool используется для важных задач, существует только в оперативной памяти и для каждого уровня IRQL. Файл pagefile.sys содержит paged-память. В недалеком прошлом он уже становился жертвой атаки, в ходе которой неподписанный код внедрялся в ядро Vista. Среди обсуждаемых решений было предложено отключить paged-память. Джоанна Рутковска рекламировала такое решение как более безопасное по сравнению с другими, хотя следствием этого стала небольшая потеря физической памяти. Microsoft отказывается от прямого доступа к диску, что подтверждает важность таких возможностей ядра Windows, как Paged- и NonPaged-пулы. Эта статья написана с упором на NonPaged pool, так как обработка Paged-Pool происходит совершенно иначе. NonPaged pool можно рассматривать как более ли менее типичную реализацию heap. Подробная информация о системных пулах доступна в Microsoft Windows Internals. Таблица NonPaged poolАлгоритм выделения должен быстро распределять наиболее часто используемые объемы. Поэтому существуют три разные таблицы, каждая из которых выделяет память определенного диапазона. Такую структуру я обнаружил в большинстве алгоритмов управления памятью. Считывание блоков памяти с устройств занимает некоторое время, поэтому в алгоритмах Windows происходит балансировка между скоростью ответа и оптимальным выделением памяти. Время ответа сокращается, если блоки памяти сохраняются для последующего выделения. С другой стороны, избыточное резервирование памяти может сказаться на производительности. Таблица представляет собой отдельный способ хранения блоков памяти. Мы рассмотрим каждую таблицу и ее местоположение. NonPaged lookaside — таблица, назначаемая каждому процессору и работающая с объемами памяти, менее или равными 256 байт. У каждого процессора есть контрольный реестр (PCR), хранящий служебные данные процессора — уровень IRQL, GDT, IDT. Расширение реестра называется контрольным регионом (PCRB) и содержит lookaside-таблицы. Lookaside-таблицы предоставляют наиболее быстрое считывание блоков памяти по сравнению с другими типами. Для такой оптимизации очень важно время задержки, а односвязный список (который реализован в Lookaside) тут намного эффективнее, чем двухсвязный. Функция ExInterlockedPopEntrySList используется для выбора записи из списка с использованием аппаратной инструкции «lock». PPNPagedLookasideList и есть вышеупомянутая Lookaside-таблица. Она содержит два Lookaside-списка: P и L. Поле «depth» структуры GENERAL_LOOKASIDE определяет, как много записей может находиться в списке ListHead. Система регулярно обновляет этот параметр, используя различные счетчики. Алгоритм обновления основан на номере процессора и не одинаков для P и L. В списке P поле «depth» обновляется чаще, чем в списке L, потому что P оптимизирован под очень маленькие блоки. Вторая таблица зависит от числа процессоров и того, как ими управляет система. Данный способ выделения памяти будет использоваться, если объем менее или равен 4080 байт, или если lookaside-поиск не дал результатов. Даже если целевая таблица меняется, у нее будет та же структура POOL_DESCRIPTOR. В случае единственного процессора используется переменная PoolVector для считывания указателя NonPagedPoolDescriptor. В случае многих процессоров, таблица ExpNonPagedPoolDescriptor содержит 16 слотов с описаниями пулов. PCRB каждого процессора указывает на структуру KNODE. Узел может быть связан с более чем одним процессором и содержит поле «color», используемое как список для ExpNonPagedPoolDescriptor. Ниже приведен рисунок, иллюстрирующий этот алгоритм. Ядро определяет глобальную переменную ExpNumberOfNonPagedPools, если данная таблица используется несколькими процессорами. Она должна содержать количество процессоров. Дамп WinDbg отображает структуру POOL_DESCRIPTOR. В очереди spinlock'ов реализована синхронизация; часть библиотеки HAL используется для предотвращения конфликтов в дескрипторе пула (pool descriptor). Эта процедура позволяет только одному процессору и одной нити получать одновременный доступ к записи из дескриптора пула. Библиотека HAL различается на разных архитектурах. Для дескриптора пула по умолчанию главный NonPaged spinlock заблокирован (LockQueueNonPagedPoolLock). А если он не заблокирован, то для него создается отдельная очередь spinlock. Третья, и последняя таблица используется процессорами для обработки памяти объемов свыше 4080 байт. mNonPagedPoolFreeListHead также используется, если закончилась память в остальных таблицах. Доступ к этой таблице происходит при обращении к главной очереди NonPaged spinlock'ом, также называемой LockQueueNonPagedPoolLock. В ходе освобождения меньшего по объему блока памяти ExFreePoolWithTag объединяет его с предыдущим и следующим свободными блоками. Так может быть создан блок размером в страницу и более. В этом случае блок добавляется в таблицу MmNonPagedPoolFreeListHead. Алгоритмы выделения и освобождения памятиРаспределение памяти ядром в разных версиях ОС почти не меняется, но этот алгоритм не менее сложен, чем heap пользовательских процессов. В этой части статьи я хочу проиллюстрировать основы поведения таблиц в ходе процедур выделения и освобождения памяти. Многие детали, такие как механизмы синхронизации, будут намеренно опущены. Эти алгоритмы помогут в объяснении метода и понимании основ распределения памяти в ядре. От синего экрана смерти до исполнения желанийПри переполнении динамической памяти обычно затираются метаданные других выделенных блоков, что в основном ведет к нескольким BugCheck'ам (или, попросту, BSOD'ам): BAD_POOL_HEADER. Вызывается в коде ExFreePoolWithTag, если PreviousSize следующего чанка не равен BlockSize текущего чанка.
DRIVER_CORRUPTED_EXPOOL. Вызывается в коде ExFreePoolWithTag, если при unlink'e произошло исключение Page Fault.
BAD_POOL_CALLER. Вызывается в коде ExFreePoolWithTag, если чанк, который пытаются освободить, уже является освобожденным. Рассмотрим подробнее заголовок (метаданные) чанка:
Значения PreviousSize, BlockSize вычисляются следующим образом:
Если значение PoolType равно нулю, то такой чанк является освобожденным, и после заголовка идет структура nt!_LIST_ENTRY.
ЭксплуатацияАлгоритм освобождения чанка работает таким образом, что если после освобождаемого чанка есть свободный, то происходит слияние, то есть из двух свободных чанков склеивается один. Это происходит путем нехитрой операции unlink'a. Удаляем запись entry из двусвязного списка:
Это ведет к перезаписи 4 байт по контролируемому адресу:
Практикуемся!Обладая достаточными знаниями, рассмотрим уязвимость в драйвере одного антивирусного продукта.
Как видно из кода, уязвимость связана с приведением целочисленных типов, которая ведет к тому, что размер для юникод-строки будет рассчитан неправильно. Все это приведет к переполнению, если передать драйверу буфер с юникод-строкой больше 0xffff байт. Нехитрый код для воспроизведения BSoD
Эксплуатация данной уязвимости не так проста, как может показаться на первый взгляд. Здесь имеют место некоторые ограничения, а именно — переполнение (запись за границы чанка) огромное (больше 0xffff), что потенциально ведет к синему экрану еще до исполнения ExFreePoolWithTag (и, следовательно, к замене указателей при слиянии):
При перезаписи памяти мы можем переписать данные, которые являются указателями каких-либо ядерных структур, что может привести к самым неожиданным последствиям (очередному BSoD). Для улучшения эффективности эксплуатации данной уязвимости воспользуемся следующим трюком: создадим N потоков, которые вызывают DeviceIoControl, с такими параметрами, чтобы с какой-то вероятностью N количество блоков определенной длины (0xff0 в данном примере) были выделены, затем освобождены — это дает нам шанс, что при переполнении мы не получим синий экран типа Page Fault (PAGE_FAULT_IN_NONPAGED_AREA). Предложенный пример кода с подробными комментариями ищи на нашем DVD. ВыводыНа прощание могу лишь сказать, что в интернете очень мало информации об эксплуатации Kernel Pool Overflow. Также огорчает, что в паблике нет рабочих эксплойтов, из-за чего создается некое заблуждение, что переполнение памяти в ядре очень сложно эксплуатировать, а если и возможно, то максимальный исход злодеяний — обычный BSoD. В этой статье авторы попытались показать на реальном примере, что, подключив смекалку, можно улучшить стабильность методов эксплуатации подобных уязвимостей. В дальнейших статьях мы поговорим о более сложных аспектах эксплуатации Kernel Pool Overflow, которые, конечно, существуют и ждут своего часа :). Stay tuned! Links
|