Compartir a través de


Control de búferes

Uno de los errores más comunes dentro de cualquier controlador se relaciona con el control del búfer, donde los búferes no son válidos o demasiado pequeños. Estos errores pueden permitir desbordamientos de búfer o provocar bloqueos del sistema, lo que puede poner en peligro la seguridad del sistema. En este artículo se describen algunos de los problemas comunes con el control del búfer y cómo evitarlos. También identifica el código de ejemplo de WDK que muestra técnicas de control de búfer adecuadas.

Tipos de búfer y direcciones no válidas

Desde la perspectiva de un conductor, los búferes vienen en una de estas dos variedades:

  • Búferes paginados, que podrían estar o no residentes en la memoria.

  • Búferes no paginados, que deben residir en la memoria.

Una dirección de memoria no válida no está paginada ni no paginada. A medida que el sistema operativo funciona para resolver un error de página causado por un control incorrecto del búfer, se realizan los pasos siguientes:

  • Aísla la dirección no válida en uno de los intervalos de direcciones "estándar" (direcciones de kernel paginadas, direcciones de kernel no paginadas o direcciones de usuario).

  • Genera el tipo de error adecuado. El sistema siempre controla los errores de búfer mediante una comprobación de errores como PAGE_FAULT_IN_NONPAGED_AREA o por una excepción como STATUS_ACCESS_VIOLATION. Si el error es una comprobación de errores, el sistema detendrá la operación. En el caso de una excepción, el sistema invoca controladores de excepciones basados en pila. Si ninguno de los controladores de excepciones controla la excepción, el sistema invoca una comprobación de errores.

Independientemente de la ruta de acceso a la que un programa de aplicación pueda llamar que haga que el controlador provoque una comprobación de errores es una infracción de seguridad dentro del controlador. Esta infracción permite que una aplicación cause ataques por denegación de servicio a todo el sistema.

Suposiciones y errores comunes

Uno de los problemas más comunes en esta área es que los escritores de controladores asumen demasiado sobre el entorno operativo. Algunas suposiciones y errores comunes son:

  • Un controlador simplemente comprueba si el bit alto está establecido en la dirección. Confiar en un patrón de bits fijo para determinar el tipo de dirección no funciona en todos los sistemas o escenarios. Por ejemplo, esta comprobación no funciona en equipos basados en x86 cuando el sistema usa el ajuste de cuatro gigabytes (4GT). Cuando se usa 4GT, las direcciones en modo de usuario establecen el bit alto para el tercer gigabyte del espacio de direcciones.

  • Un controlador que usa únicamente ProbeForRead y ProbeForWrite para validar la dirección. Estas llamadas garantizan que la dirección sea una dirección válida en modo de usuario en el momento del sondeo. Sin embargo, no hay ninguna garantía de que esta dirección permanecerá válida después de la operación de sondeo. Por lo tanto, esta técnica presenta una condición sutil de carrera que puede conducir a bloqueos irreproducibles periódicos.

    Las llamadas ProbeForRead y ProbeForWrite siguen siendo necesarias. Si un controlador omite el sondeo, los usuarios pueden pasar direcciones en modo kernel válidas que un __try bloque y __except (control de excepciones estructurados) no detectarán y, por tanto, abrirán un gran agujero de seguridad.

    La conclusión es que se necesitan tanto el sondeo como el control estructurado de excepciones:

    • El sondeo valida que la dirección es una dirección en modo de usuario y que la longitud del búfer está dentro del intervalo de direcciones de usuario.

    • Un __try/__except bloque protege contra el acceso.

    Tenga en cuenta que ProbeForRead solo valida que la dirección y la longitud se encuentran dentro del posible intervalo de direcciones en modo de usuario (ligeramente inferior a 2 GB para un sistema sin 4GT, por ejemplo), no si la dirección de memoria es válida. En cambio, ProbeForWrite intenta acceder al primer byte de cada página de la longitud especificada para comprobar que estos bytes son direcciones de memoria válidas.

  • Un controlador que se basa en funciones del administrador de memoria como MmIsAddressValid para asegurarse de que la dirección es válida. Como se describe para las funciones de sondeo, esta situación presenta una condición de carrera que puede provocar bloqueos irreproducibles.

  • Un controlador no puede usar el control de excepciones estructurado. Las __try/except funciones del compilador usan compatibilidad de nivel de sistema operativo para el control de excepciones. Las excepciones de nivel de kernel se devuelven al sistema a través de una llamada a ExRaiseStatus o a una de las funciones relacionadas. Un controlador que no puede usar el control estructurado de excepciones en torno a cualquier llamada que pueda generar una excepción provocará una comprobación de errores (normalmente KMODE_EXCEPTION_NOT_HANDLED).

    Es un error usar el control estructurado de excepciones en torno al código que no se espera que genere errores. Este uso solo enmascarará los errores reales que, de lo contrario, se encontrarían. Colocar un __try/__except contenedor en el nivel de distribución superior de la rutina no es la solución correcta a este problema, aunque a veces es la solución reflex probada por los escritores de controladores.

  • Un controlador suponiendo que el contenido de la memoria del usuario permanecerá estable. Por ejemplo, supongamos que un controlador escribió un valor en una ubicación de memoria en modo de usuario y, después, en la misma rutina, se hace referencia a esa ubicación de memoria. Una aplicación malintencionada podría modificar activamente esa memoria después de la escritura y, como resultado, provocar que el controlador se bloquee.

En el caso de los sistemas de archivos, estos problemas son graves porque los sistemas de archivos suelen depender de acceder directamente a los búferes de usuario (el método de transferencia de METHOD_NEITHER). Estos controladores manipulan directamente los búferes de usuario y, por tanto, deben incorporar métodos de precaución para el control del búfer para evitar bloqueos de nivel de sistema operativo. La E/S rápida siempre pasa punteros de memoria sin procesar, por lo que los controladores deben protegerse frente a problemas similares si se admite la E/S rápida.

Código de ejemplo para el control del búfer

El WDK contiene numerosos ejemplos de validación del búfer en el código de ejemplo del controlador del sistema de archivos fastfat y CDFS, entre los que se incluyen:

  • La función FatLockUserBuffer de fastfat\deviosup.c usa MmProbeAndLockPages para bloquear las páginas físicas detrás del búfer de usuario y MmGetSystemAddressForMdlSafe en FatMapUserBuffer para crear una asignación virtual para las páginas bloqueadas.

  • La función FatGetVolumeBitmap en fastfat\fsctl.c usa ProbeForRead y ProbeForWrite para validar los búferes de usuario en la API de desfragmentación.

  • La función CdCommonRead de cdfs\read.c usa __try y __except alrededor del código a cero búferes de usuario. El código de ejemplo de CdCommonRead aparece para usar las try palabras clave y except . En el entorno de WDK, estas palabras clave en C se definen en términos de las extensiones __try del compilador y __except. Cualquier persona que use código de C++ debe usar los tipos de compilador nativos para controlar las excepciones correctamente, como __try es una palabra clave de C++, pero no una palabra clave de C, y proporcionará una forma de control de excepciones de C++ que no es válido para los controladores de kernel.