Рубрика:
Безопасность /
Механизмы защиты
|
Facebook
Мой мир
Вконтакте
Одноклассники
Google+
|
Артем Баранов
Технологии защиты ядра NT
За всю историю своей эволюции ядро NT постоянно развивалось. Ему прививались различные технологии защиты. О внутреннем устройстве этих технологий известно немного. А между тем они серьезно различаются как от версии к версии NT, так и на разных машинах. К тому же они далеко не совершенны, что и подтверждается растущим числом атак на ядро.
Ядро операционной системы – это тот программный модуль или набор модулей, который предоставляет минимум базовых возможностей операционной системы, без которых она работать не может. Также ядро ответственно за распределение ресурсов, что является одной из важнейших функций операционной системы. Поэтому если целостность ядра нарушается, нарушается работа всей системы. Соответственно ядру нужно предоставить какой-то уровень защиты, чтобы драйверы, содержащие в себе «жучки», не смогли бы повредить код или данные ядра операционной системы.
На Intel x386 NT может работать в пользовательском режиме и режиме ядра. Соответственно потоки самой ОС работают в привилегированном режиме, а потоки приложений – в пользовательском режиме. Таким образом, из четырех уровней защиты процессора NT пользуется только двумя. В x386 режим работы процессора характеризуется селектором в регистре CS, а точнее, его первыми двумя битами, которые и определяют CPL кода. Пользовательские приложения для перехода в нулевое кольцо используют либо инструкцию int 0x2e, либо оптимизированную sysenter. Сам код, который эти инструкции и вызывает, расположен на Native-уровне, т.е. между ядром и подсистемой Win32 (см. рис. 1).
Рисунок 1. Обработка системного сервиса NT
Так было сделано с расчетом на то, чтобы можно было подключать другие подсистемы, например POSIX. И вызов, специфичный для конкретной ОС (подсистемы), а точнее, его семантика, должны полностью совпадать с аналогичным вызовом в самой ОС (например, семантика fork должна быть такой, какой она регламентирована в самом стандарте POSIX для UNIX).
В момент запуска приложения подсистема Win32 формирует для контекста первичного потока необходимые селекторы (с RPL равным 3) и передает эту информацию ntdll.dll.
Структурно ядро разделяется на исполнительную систему Executive и ядро. Так было сделано специально для изоляции кода, работающего с конкретной аппаратно-зависимой конфигурацией. Этот код и вынесен в ядро.
Executive и ядро располагаются в одном файле – ntoskrnl.exe. Идентификация же функций ядра от функций Executive может осуществляться по префиксам функций, например, префикс названий функций ядра – Ke, а экспортируемых сервисов Executive – Nt. Нужно понимать, что в ядро вынесен код, работающий с конкретным процессором, но не с оборудованием.
Для изоляции самого ядра от специфики конкретного оборудования каждая версия NT для конкретной платформы располагает своим уровнем абстрагирования от оборудования (Hardware Abstraction Layer), который реализует специфичные функции по поддержке, например ввода/вывода для портов.
В современном мире ядро должно рассматривать окружающую среду как опасную и готовую к вторжениям. Загруженный драйвер может делать с ядром что угодно, в том числе модифицировать код, важные структуры данных.
С момента выхода Windows 2000 Microsoft решила адаптировать системы защиты ядра на новый уровень и попытаться защищать не только код NT, но и критические процессорно-зависимые данные.
Действительно, на момент выхода Windows 2000 достаточно было системы защиты, которая обнаруживала бы перезапись кода ядра или кода драйверов устройств в отличие от NT 4.0, в которой этого не было.
Но сегодня этого уже недостаточно. Растет процент вирусов, которые реализуют LKM-атаки на ядро (т.е. модифицируют системные таблицы, списки). Соответственно нужно выходить на новый уровень защиты.
Несмотря на все старания разработчиков ядра NT, на сегодняшний день оно является крайне незащищенным. В некоторых случаях (в системах с определенной конфигурацией) код и данные ядра защищены еще меньше, чем код обычного приложения.
Как следствие, именно сейчас все более вредоносные программы ориентируются на работу в режиме ядра. Как только код начинает работать в режиме ядра, он обладает такими же привилегиями (в том числе и аппаратными), как и само ядро. Это и является самым опасным.
Ниже рассматриваются механизмы, которые применяли разработчики ядра для того, чтобы хоть как-то обезопасить ядро от разрушения.
Нужно также понимать, что с выходами новых Service Pack ситуация все равно не изменится. Это обусловлено тем, что в NT изначально не было интегрированной системы защиты ядра.
Как и что нужно защищать
Защита структур данных ОС не может быть обеспечена только программно. Соответствующая поддержка должна быть и со стороны процессора. ОС должна защищать свои структуры данных и программный код ядра. Ядром NT является файл Ntoskrnl, который и содержит важнейшие для работы ядра структуры данных и его код. Собственно, такие структуры и должны быть защищены. В x386 с линейной моделью памяти NT это может быть сделано на уровне страниц. Однако нужно понимать, что не все структуры данных могут быть защищены, а лишь данные образа. Так, если система черпает память под структуры из резидентного пула, то он никак не может быть защищен, так как соответствующие страницы могут использоваться не только ядром, но и драйверами устройств.
Write-Protected System Code
Начиная с Windows 2000 код ядра может быть защищен от записи. Соответствующие страницы кода Ntoskrnl доступны только для чтения (бит Write в PTE обнулен). Но так происходит не на всех системах. Защита может быть активирована только в системе с ОЗУ меньше 128 Мб памяти, а для Windows XP ОЗУ меньше 256 Мб. В противном случае ядро для оптимизации TLB (буфер быстрого преобразования адресов, который содержит скэшированные проекции виртуальных страниц на физические, а также статус того, находятся ли они в физической памяти или нет), будет проецироваться 4 Мб-страницами. Точнее, для этого будет использоваться TLB, кэширующий проекции 4 Мб-страниц. В таком случае код и данные ядра окажутся на одном фрейме страницы и ей уже не может быть присвоен атрибут Read-Only. На уровне каталога страниц, это означает, что он адресует не таблицу страниц, описывающую данный 4 Мб-диапазон, а самую большую страницу. В итоге код режима ядра может свободно модифицировать не только данные ядра, но и его код!
Недостаток такого метода защиты в том, что в режиме ядра он может быть отключен. В управляющем регистре x386 (cr0) существует бит WP (Write Protection), который управляет всей защитой на уровне страниц. По умолчанию этот бит установлен ядром в единицу, но установка этого бита в ноль полностью отключает защиту на уровне страниц. Как следствие, процессор может писать данные даже на фреймы страниц, в PTE которых бит W равен 0. Код, отключающий Write-Protected System Code, применяется в руткитах режима ядра при модификации ntoskrnl. Также он был представлен в книге Hoglund, Butler «Rootkits. Subverting windows kernel».
Для наглядного представления техники Write-Protected System Code на рис. 2 изображены каталог и таблицы страниц Windows XP SP2 с выключенной защитой.
Как видно из рис. 2, каталог, который адресует Ntoskrnl, адресует 4 Мб-страницу, на которую Ntoskrnl вмещается целиком, поэтому невозможно отделить код от данных. На уровне больших страниц атрибут защиты адресуется самим PDE, так как он адресуется не к таблице, а к самому фрейму с данными.
Рисунок 2. Каталог и таблицы страниц в Windows XP SP2 в системе с ОЗУ 256 Мб
В результате погони за оптимизацией Microsoft понизила степень защищенности ядра. Как следствие, код ядра стал еще менее защищенным, чем код любого приложения.
Patch Guard
В Microsoft прекрасно понимали, что подобная незащищенность ядра пагубно сказывается на работе системы в целом. Нужен был механизм защиты, не зависящий от аппаратуры и увязанный в системных компонентах ядра. Зависимость от процессора также не самым лучшим образом сказывается на защите, так как код режима ядра ее может просто отключить, как это было с Write-Protected System Code. Но ввести программную защиту ядра значило нарушить совместимость с существующими драйверами, которые могли использовать на тот момент вполне «законную» модификацию ядра. При этом обычно подвергалась модификации таблица диспетчеризации системных сервисов (System Service Descriptor Table, SSDT) – KiServiceTable и таблица дескрипторов прерываний (IDT).
С выпуском 64-разрядных версий NT – Windows Server 2003 64-bit и Windows XP 64-bit ситуация изменилась. Microsoft «наложила запрет» на модификацию структур данных ядра, аргументируя это тем фактом, что код ядра для 64-разрядных версий перекомпилировать, а отчасти и переписывать все равно придется, поэтому разработчики могут внести в драйверы изменения и не опираться на модификацию ядра. Новая защита получила название Patch Guard. И представляет собой программную технологию защиты ядра от записи. Технология защищает следующие критические структуры:
- SSDT.
- Таблицу глобальных дескрипторов (GDT).
- IDT.
- Спроецированные образы ядра, включая ntoskrnl.exe, ndis.sys, hal.dll.
- MSR-регистры, предназначенные для активации диспетчера системных сервисов по sysenter.
Защита инициализируется при загрузке системы, причем чрезвычайно неявно, это является следствием того, что разработчики защиты старались максимально усложнить отладку системы защиты. В результате функции, отвечающие за инициализацию защиты, имеют названия, не выдающие их истинного назначения. Например, защита инициализируется обычной функцией деления со специальным значением, которое приводит к переполнению (переполнение генерируется, если при делении 64-разрядного операнда на 32-разрядный результат не является 32-разрядным, процессоры AMD64), в результате чего генерируется исключение, а обработчик исключения – KiDivideErrorFault – уже вызывает функцию инициализации защиты.
Patch Guard создает служебные структуры данных, в которых хранит контрольные суммы проверяемых компонентов, причем структуры создаются не для всего проверяемого объекта, а только для отдельной его части. Например, в случае с проверкой на валидность образов ядра создаются отдельные структуры для IAT, самих разделов и директории импорта.
Сердцем защиты является функция, создающая контрольные суммы структур, подлежащих верификации – PgCreateBlockChecksumSubContext. Эта функция вызывается как для создания таких контрольных сумм, как GDT, IDT, так и для IAT ntoskrnl.
Так как Windows XP поддерживает и многопроцессорные системы, то ядро способно хранить контрольные суммы IDT, GDT для каждого процессора отдельно. Для получения адресов IDT, GDT ядро использует функцию KeSetAffinityThread, привязывая таким образом поток к конкретному процессору. После того как адреса таблиц получены, ядро вызывает для инициализации защиты GDT функцию PgCreateGdtSubContext, а для IDT – PgCreateIdtSubContext, в которых и вызывается PgCreateBlockChecksumSubContext.
Аналогичная ситуация и с SSDT. Ее контрольную сумму создает та же PgCreateBlockChecksumSubContext. Кроме того, эта функция вызывается и для создания защиты таблицы дескрипторов – KeServiceDescriptorTable. В 64-разрядной системе эта таблица хранит не настоящие смещения функций в линейном адресном пространстве, а их смещения относительно самой KiServiceTable. Таким образом, чтобы получить адрес функции, на которую есть указатель в SSDT, нужно сложить адрес KiServiceTable со значением индекса в этой таблице.
Технологии интеграции защиты на примере Driver Verifier
Ядро NT поддерживает верификацию (проверку) драйверов на предмет ошибок работы с памятью, IRQL и пр. Такая технология получила название Driver Verifier. Для разработчиков драйверов она более известна не как технология, а как программа, позволяющая своевременно обнаруживать ошибки в драйверах. Между тем хотя пользовательская часть Driver Verifier и функционирует в пользовательском режиме, свои возможности она реализует в режиме ядра. Кроме того, в тесной интеграции с Driver Verifier работают: диспетчер памяти, диспетчер ввода-вывода, Win32k и HAL. Таким образом, проверка драйверов напрямую интегрирована в ядро. Пользовательская ее часть лишь записывает в реестр значения нужных параметров, которые считываются ядром при загрузке системы. Поэтому при изменении настроек верификации необходима перезагрузка.
Ядро хранит настройки Verifier в разделе реестра, который ответственен за хранение настроек диспетчера памяти – HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management. Поэтому первое, что делает Driver Verifier при вызове функции отображения существующих настроек, открывает этот раздел и сканирует ключевые параметры, среди которых: VerifyDriverLevel и VerifyDrivers. Первый параметр содержит битовую маску включенных проверок для драйверов, а второй содержит список драйверов для верификации. Проанализировав эти поля, программа выводит в окне статистику (см. рис. 6).
Сам же Driver Verifier рассредоточен по ядру и всегда начинается с префикса Verifier. Например, VerifierAllocate PoolWithTag. Активация Driver Verifier происходит следующим образом: на ранних этапах загрузки, когда диспетчер памяти считывает свои настройки из реестра, он также просматривает параметры Driver Verifier, если в списке есть проверяемые драйверы, то он сравнивает имена загружаемых драйверов с драйверами из этого списка: проходя по образу драйвера, заменяет ссылки на функции ядра на свои функции. Так, ExAllocatePool заменяется на VerifierAllocatePool, KeRaiseIrqlToDpcLevel на VerifierKeRaiseIrqlToDpcLevel, KeAcquireSpinLockAtDpcLevel на VerifierKeAcquireSpinLock AtDpcLevel. Перехватывая функции ядра, Driver Verifier способен проводить проверки на корректность действий драйверов. Например, для обнаружения одной из самых распространенных ошибок: buffer overrun/underrun, когда драйвер содержит в себе «жучок», вылетающий за границы буфера. Driver Verifier служит для обнаружения ошибок, которые допускают программисты при индексации буферов. Для этого Driver Verifier выделяет себе из пула регион, в который перенаправляет все запросы на выделение памяти. Для запуска Driver Verifier наберите в «Пуск -> Выполнить» команду verifier. При этом вы увидите окно, подобное рис. 3.
Рисунок 3. Главное окно Driver Verifier
При этом вам необходимо выбрать из списка задачу для Driver Verifier. Доступны следующие задачи:
- Create standart settings (создать стандартные настройки) – при этом Driver Verifier автоматически задаст часто используемые настройки для проверки, а вам необходимо выбрать только драйвер(ы) для проверки (см. рис. 4).
Рисунок 4. Окно выбора драйвера, подлежащего проверке
- Create custom settings (создать выборочные настройки) – при этом Driver Verifier позволяет вам задать необходимые настройки (см. рис. 5).
Рисунок 5. Окно выбора настроек для проверяемого драйвера
- Delete existing settings (удалить существующие настройки) – удаляет все настройки Driver Verifier и выходит из программы.
- Display existing settings (отобразить существующие настройки) – выводит окно с драйверами, подлежащими проверке, и настройки, применимые к ним (см. рис. 6).
Рисунок 6. Уже существующие настройки Driver Verifier
- Display information about the currently verified drivers (отобразить информацию о проверяемых драйверах) – выводит статистику по результатам мониторинга для конкретного драйвера (например, сколько байт выделено в пуле), а также отображает глобальные счетчики использования ресурсов.
Настройки Driver Verifier, используемые чаще всего:
Special Pool (особый пул)
Позволяет контролировать ошибки buffer overrun/underrun. При резервировании драйвером буфера в пуле управление передается Driver Verifier, который перенаправляет этот запрос на резервирование в выделенный регион (особый пул), далее размер буфера округляется до размера, кратного размеру страницы, и «прижимается» к верхним адресам страницы таким образом, чтобы последний байт буфера был последним байтом страницы. Это сделано для того, чтобы можно было обнаруживать ошибки buffer overrun, при которых драйвер пишет по неуправляемому указателю за границу выделенного буфера, т.е. по старшим адресам. Страницы, находящиеся выше буфера (т.е. по более старшим адресам), Driver Verifier делает недействительными, чтобы «жучок» драйвера сразу же привел к возникновению нарушения доступа. Таким образом, сразу же можно будет указать на драйвер, который сгенерировал ошибку. В противном случае драйвер перезаписал бы служебную структуру данных, а ее некорректность обнаружилась бы в контексте совершенно другого потока (см. рис. 7).
Рисунок 7. Таким образом Driver Verifier предотвращает ошибки переполнения
Force IRQL Checking (обязательная проверка IRQL)
Самая распространенная ошибка в коде режима ядра – это когда драйвер пытается перераспределить процессорное время на высоких IRQL (т.е. DPC/Dispatch или одном из DIRQL). Если такая ситуация происходит, то в очередь DPC ставится запрос к диспетчеру, чтобы тот переключил контекст. При этом так как DPC обрабатываются на том же уровне (в случае с текущим IRQL==DISPATCH_LEVEL) или, тем более, при DIRQL, то переключение контекста будет маскироваться до тех пор, пока DPC к диспетчеру не будет изъято из очереди. Суть ошибки заключается в том, что если драйвер инициирует перераспределение процессорного времени на таком высоком уровне IRQL, то возникнет противоречивая с точки зрения ядра ситуация, при которой поток должен ждать, но планировщик не может вытеснить его с процессора, так как сам при таких высоких IRQL маскируется. Инициирование перераспределения процессорного времени может быть как явное, так и неявное. Явное, например, когда драйвер сам вызовет KeWaitForSingleObject при высоком IRQL. Неявное – это попытка обращения к виртуальной странице, не спроецированной на физическую (например, в нерезидентном пуле). Как только поток сгенерирует ошибку страницы, это повлечет за собой операцию ввода-вывода для подкачки страницы с диска, а это в свою очередь повлечет к блокированию потока и вытеснению его с процессора (поток попадает в очередь ждущих), при этом происходит переключение контекста на другой поток. Во всех случаях вызов диспетчера на высоких IRQL повлечет за собой STOP-ошибку: IRQL_NOT_LESS_OR_EQUAL. В случае если эта проверка активирована, то все подкачиваемые данные ядра принудительно откачиваются на диск (выводятся из системного рабочего набора), таким образом, если драйвер содержит в себе неуправляемый указатель, который при условиях, что откачиваемые страницы еще спроецированы на физические, в результате обращения к ним не вызовет нарушения доступа, то с включенной проверкой сразу же произойдет крах системы с кодом, указывающим на сбойный драйвер.
DEP
Не секрет, что самомодификация в NT является простым делом. Достаточно поменять атрибуты страницы на PAGE_WRITE, как код сразу же можно править. Хотя в макросах, предназначенных для защиты страниц, и предусмотрены специальные атрибуты типа PAGE_EXECUTE, но все зависит от аппаратной платформы и тех атрибутов защиты страниц, которые она предоставляет. В x386 в PTE существует один бит, предназначенный для контроля вида доступа. Сброс или установка этого бита не влияет на то, будет выполняться код на странице или нет.
С Windows XP SP2 и Windows Server 2003 SP1 Microsoft стала продвигать технологию Data Execution Prevention (DEP), которая, разумеется, базируется на аппаратной поддержке. Intel и AMD ввели для своих процессоров новые биты защиты страниц. Для AMD функция называется no-execute page-protection (NX), а для Intel – Execute Disable Bit (XD). Попытка выполнить код на странице с таким атрибутом приведет к генерации исключения процессором. NT также применяет эту защиту к стекам потоков, что блокирует действия многих червей и Shell-кода, которые получают управление через стек.
DEP работает по-разному на разных машинах. Это зависит не только от платформы, но и от версии NT (32-или 64-разрядная). При этом следует учитывать некоторые особенности. При включенном DEP для 32-разрядной Windows XP последняя будет работать в PAE-режиме (т.е. параметр /PAE в boot.ini будет установлен). Соответственно, будет использована PAE-версия ядра и процессор также будет работать в PAE-режиме. В 64-разрядной версии (с соответствующей поддержкой со стороны процессора) DEP применяется ко всем 64-разрядным программам и драйверам, а также к страницам стеков потоков. Однако в 32-разрядной версии защита применяется только к страницам с данными в пользовательском режиме (включая стеки потоков). При активации DEP в boot.ini заносится параметр /NOEXECUTE. Таким образом, когда ntldr передаст управление ядру, последняя будет знать, что DEP включена.
Защитой можно управлять и из самой Windows. Для этого нужно перейти: «Пуск -> Панель управления -> Система -> Дополнительно -> Параметры быстродействия -> Предотвращение выполнения данных». При этом вы увидите окно как на рис. 8.
Рисунок 8. Диалоговое окно, управляющее некоторыми настройками DEP
Эти два параметра влияют на поведение DEP для 32-разрядных программ. Верхний параметр говорит о том, что DEP будет применяться только к программам самой Windows. Нижний параметр говорит о включении DEP для всех 32-разрядных программ, кроме тех, что добавлены в список. Обратитесь к документации по своему процессору, чтобы определить, поддерживает он DEP или нет.
Кроме редактирования DEP через Панель управления, вы также можете вручную отредактировать файл boot.ini, задав необходимые значения в форме /noexecute=value, где value может принимать значения указанные в таблице.
Значение value в файле boot.ini
Значение
|
Описание
|
OptIn
|
Используется по умолчанию. Включает DEP для системных программ на компьютерах с аппаратной поддержкой DEP
|
OptOut
|
DEP включена для всех процессов, кроме тех, что указаны в списке (см. рис. 8)
|
AlwaysOn
|
DEP включена для всей системы (в том числе и для всех процессов)
|
AlwaysOff
|
DEP отключена для всей системы, независимо от аппаратной поддержки
|
Заключение
Ограничение существующих методов очевидно в том, что, как только в режиме ядра запускается вредоносный код, он также по сути становится ядром, и следы его деятельности никак не проверяются. Возможно даже, что сам руткит пройдется по PTE, которые адресуют фреймы страниц кода ntoskrnl, и установит у них бит Write, отключая таким образом защиту системного кода от записи.
Структуры данных, создающиеся и уничтожающиеся в пулах, вообще нельзя контролировать на запись, чтение, так как пул управляется не страницами, а из него возможно выделение данных произвольного размера. Хотя и здесь ядру можно привить некоторую интеллектуальность. Если бы ядро изначально было построено как защищенное и контролировало бы доступ к своим структурам данных, то ничего не мешало бы системе выделять себе структуры данных на отдельной странице пула и контролировать обращения к ней. Контроль обращения в нерезидентном пуле можно организовать, специально выводя из системного рабочего набора фреймы страниц со структурами. Затем, когда происходит #PF на странице, то сравнить содержимое в стеке, сохраненного регистра EIP, на диапазон принадлежности ntoskrnl; если он входит в диапазон, то само ядро обращается к структурам данных.
Несмотря на все эти методы защиты, здесь скорее нужен другой принципиальный подход. Например, запуск только проверенного (подписанного) кода режима ядра, как это сделано в Vista.
Facebook
Мой мир
Вконтакте
Одноклассники
Google+
|