Поделиться через


Обработка буфера

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

С точки зрения драйвера буферы бывают одной из двух разновидностей:

  • Буферы страничных буферов, которые могут находиться или не находиться в памяти.

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

Конечно, недопустимый адрес не является ни страничной, ни не разгружается, но по мере того как операционная система начинает работать над устранением ошибки страницы, вызванной буфером, она изолирует недопустимый адрес в одном из "стандартных" диапазонов адресов (адреса ядра с разбивкой на страницы, адреса ядра без страниц или адреса пользователей) и вызывает ошибку соответствующего типа. Ошибки буфера всегда обрабатываются проверка ошибок (например, PAGE_FAULT_IN_NONPAGED_AREA) или исключением (например, STATUS_ACCESS_VIOLATION). В случае ошибки проверка система остановит работу. В случае исключения будут вызваны обработчики исключений на основе стека, и если ни один из них не обработает исключение, будет вызван проверка ошибки.

Независимо от того, любой путь доступа, который может быть вызван программой приложения, которая приводит к ошибке драйвера проверка, является нарушением безопасности в драйвере. Это позволяет приложению вызывать атаки типа "отказ в обслуживании" на всю систему.

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

  • Проверка того, что в адресе задан высокий бит. Это не работает на компьютерах с архитектурой x86, где система использует настройку четырехбайтов (4GT), задав параметр /3 ГБ в файле Boot.ini. В этом случае адреса пользовательского режима задают высокий бит для третьего гигабайта (ГБ) адресного пространства.

  • Использование ProbeForRead и ProbeForWrite для проверки адреса. Хотя это гарантирует, что адрес является допустимым адресом в пользовательском режиме во время пробы, ничто не требует, чтобы он оставался действительным после операции пробы. Таким образом, этот метод вводит тонкое состояние гонки, которое может привести к периодическим невоспродуцируемым авариям. Вызовы ProbeForRead и ProbeForWrite необходимы по другой причине: для проверки того, является ли адрес адресом в пользовательском режиме и что длина буфера находится в пределах диапазона адресов пользователя. Если проба опущена, пользователи могут передавать допустимые адреса в режиме ядра, которые не будут перехвачены __try и __except блоком (структурированная обработка исключений) и откроет большое отверстие в безопасности. Поэтому вызовы ProbeForRead и ProbeForWrite необходимы для обеспечения выравнивания и того, что адрес в пользовательском режиме и длина находятся в пределах диапазона адресов пользователя. Однако для защиты от доступа требуется __try и блок __except.

    Обратите внимание, что ProbeForRead только проверяет, что адрес и длина входят в возможный диапазон адресов в пользовательском режиме (например, немного ниже 2 ГБ для системы без 4GT), а не является ли адрес памяти допустимым. Напротив, ProbeForWrite попытается получить доступ к первому байту на каждой странице указанной длины, чтобы убедиться, что это допустимые адреса памяти.

  • Использование функций диспетчера памяти (например, MmIsAddressValid) для обеспечения допустимости адреса. Как и в случае с функциями пробы, это приводит к состоянию гонки, которое может привести к неспродуцируемым сбоям.

  • Не удается использовать структурированную обработку исключений. Функции __try и __except в компиляторе используют поддержку на уровне операционной системы для обработки исключений. Исключения на уровне ядра вызываются вызовом ExRaiseStatus или одной из связанных функций. Если драйвер не сможет использовать структурированную обработку исключений для любого вызова, который может вызвать исключение, проверка ошибок (обычно KMODE_EXCEPTION_NOT_HANDLED).

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

  • Полагаться на содержимое пользовательской памяти, остающееся стабильным. Например, предположим, что драйвер должен был записать значение в расположение памяти в пользовательском режиме, а затем в той же процедуре ссылаться на это расположение памяти. Вредоносное приложение может активно изменять память и в результате приводить к аварийному завершению работы драйвера.

Для файловых систем эти проблемы особенно серьезны, так как обычно они используют прямой доступ к буферам пользователей (метод передачи METHOD_NEITHER). Такие драйверы напрямую управляют пользовательскими буферами и поэтому должны включать методы предосторожности для обработки буфера, чтобы избежать сбоев на уровне операционной системы. Быстрый ввод-вывод всегда передает необработанные указатели памяти, поэтому драйверы должны защищаться от аналогичных проблем, если поддерживается быстрый ввод-вывод.

WDK содержит множество примеров проверки буфера в примере кода файловой системы FASTFAT и CDFS, в том числе:

  • Функция FatLockUserBuffer в fastfat\deviosup.c использует MmProbeAndLockPages для блокировки физических страниц за буфером пользователя и MmGetSystemAddressForMdlSafe в FatMapUserBuffer для создания виртуального сопоставления заблокированных страниц.

  • Функция FatGetVolumeBitmap в fastfat\fsctl.c использует ProbeForRead и ProbeForWrite для проверки буферов пользователей в API дефрагментации.

  • Функция CdCommonRead в cdfs\read.c использует __try и __except вокруг кода с нулевыми пользовательскими буферами. Обратите внимание, что в примере кода в CdCommonRead используются ключевые слова try и except. В среде WDK эти ключевые слова в C определяются с точки зрения расширений компилятора __try и __except. Любой пользователь, использующий код C++, должен использовать собственные типы компилятора для правильной обработки исключений, так как __try является ключевое слово C++, но не ключевое слово C, и предоставляет форму обработки исключений C++, которая не является допустимой для драйверов ядра.