Red de conocimiento informático - Consumibles informáticos - (Imagine) Cómo mejorar el modelo de programación y la arquitectura de la CPU para evitar el desbordamiento del búfer. No se requiere una respuesta estándar, siempre que la imaginación tenga sentido.

(Imagine) Cómo mejorar el modelo de programación y la arquitectura de la CPU para evitar el desbordamiento del búfer. No se requiere una respuesta estándar, siempre que la imaginación tenga sentido.

Desbordamiento de buffer. Este artículo comienza explicando qué son los desbordamientos de búfer y por qué son tan comunes y peligrosos. Luego analiza los nuevos métodos de Linux y UNIX que se utilizan ampliamente para resolver desbordamientos de búfer y por qué no son suficientes. Luego mostrará varias formas de evitar desbordamientos de búfer en programas C/C++, incluidos métodos de cambio de tamaño estáticos (como la biblioteca C estándar y las soluciones OpenBSD/strlcpy) y soluciones de cambio de tamaño dinámico, así como algunas que le ayudarán. . Finalmente, este artículo finaliza con algunas predicciones sobre el desarrollo futuro de los defectos de desbordamiento del búfer.

Si desea que sus programas sean seguros, necesita saber qué son los desbordamientos de búfer, cómo prevenirlos, cuáles son las últimas herramientas automatizadas disponibles para prevenirlos (y por qué no son suficientes) y Hay formas de prevenirlos en sus propios programas.

¿Qué es un desbordamiento de buffer?

Un búfer podría haberse definido anteriormente como "un bloque contiguo de memoria de computadora que contiene instancias del mismo tipo de datos". En C y C++, los buffers generalmente se implementan usando matrices y rutinas de asignación de memoria como malloc() y new. Un tipo de buffer extremadamente común es una matriz de caracteres simple. Un desbordamiento es cuando se agregan datos fuera del bloque de memoria asignado al búfer.

Si un atacante puede provocar un desbordamiento del búfer, entonces podrá controlar otros valores en el programa. Si bien hay muchas formas de aprovechar los desbordamientos del búfer, la más común es el ataque "destructor de pila". Los ataques de destrucción de pilas se explican en un artículo clásico, "Smashing the Stack for Fun and Profit" de Elias Levy (también conocido como Aleph One), ex moderador de la lista de correo de Bugtraq (consulte Recursos para obtener un enlace).

Listado 1. Un programa simple

void function1(int a, int b, int c) {

char buffer1[5];

gets(buffer1); /* NO HAGAS ESTO */

}

void main() {

función(1,2, 3);

}

Supongamos que gcc se utiliza para compilar el programa simple del Listado 1, que se ejecuta en Linux en X86 e inmediatamente después de una llamada a gets() Abortar. ¿Cómo se ve el contenido de la memoria en este momento? La respuesta es que se parece a la Figura 1, que muestra un diseño de memoria ordenado desde las direcciones inferiores a la izquierda hasta las direcciones superiores a la derecha.

Figura 1. Vista de pila

Parte inferior de la memoria Parte superior de la memoria

buffer1 sfp ret a b c

<---Crecimiento-- - [ ] [ ] [ ] [ ] [ ] [ ] ...

¿Por qué son tan comunes los desbordamientos del buffer?

En casi todos los lenguajes informáticos, nuevos o antiguos, cualquier intento de desbordar un búfer suele ser detectado y bloqueado automáticamente por el propio lenguaje (por ejemplo, generando una excepción o, según sea necesario, añadiendo más espacio al búfer). . Pero hay dos lenguajes en los que esto no es así: los lenguajes C y C++. Los lenguajes C y C++ a menudo simplemente permiten que se escriban datos adicionales en todas partes del resto de la memoria, y esto puede explotarse con resultados horribles.

Peor aún, escribir código correcto para manejar los desbordamientos del búfer de manera consistente en C y C++ es aún más difícil; es fácil provocar un desbordamiento del búfer accidentalmente; Estos pueden ser hechos irrelevantes excepto que C y C++ se usan ampliamente; por ejemplo, el 86% de las líneas de código en Red Hat Linux 7.1 están escritas en C o C++; Por lo tanto, una gran cantidad de código es vulnerable a este problema porque el lenguaje de implementación no puede proteger el código de este problema.

En los propios lenguajes C y C++, este problema no es fácil de resolver. La pregunta se basa en decisiones de diseño fundamentales del lenguaje C (específicamente la forma en que se manejan los punteros y las matrices en C). Dado que C++ es el superconjunto más compatible del lenguaje C, tiene el mismo problema. Hay algunas versiones compatibles con C/C++ que evitan este problema, pero sufren problemas de rendimiento extremadamente graves. Y una vez que cambia C para evitar este problema, ya no es C. Muchos lenguajes, como Java y C#, son sintácticamente similares a C, pero en realidad son lenguajes diferentes, y cambiar un programa C o C++ existente para usar esos lenguajes es una tarea difícil.

Sin embargo, los usuarios de otros idiomas no deben caer en la complacencia. Algunos lenguajes tienen cláusulas de "escape" que permiten que se produzcan desbordamientos del búfer. Ada generalmente detecta y previene desbordamientos del búfer (es decir, genera una excepción en respuesta a tales intentos), pero diferentes programas pueden desactivar esta característica. C# generalmente detecta y previene los desbordamientos del búfer, pero permite a los programadores definir ciertas rutinas como "inseguras" y dicho código puede causar desbordamientos del búfer. Entonces, si usa esos mecanismos de escape, necesita usar el mismo tipo de mecanismos de protección que los programas C/C++ deben usar. Muchos lenguajes se implementan (al menos en parte) en C, y todos los programas escritos en cualquier lenguaje dependen esencialmente de bibliotecas escritas en C o C++. Por lo tanto, todos los programas heredan esos problemas, por lo que es importante comprenderlos.

Errores comunes de C y C++ que provocan desbordamientos del búfer

Básicamente, cada vez que un programa lee o copia datos en un búfer, necesita comprobar si hay suficiente espacio. Es poco probable que se produzcan excepciones fácilmente visibles, pero los programas suelen cambiar con el tiempo, haciendo posible lo imposible.

Desafortunadamente, una gran cantidad de funciones peligrosas (o bibliotecas de uso común) incluidas con C y C++ no pueden hacer ni siquiera esto (en referencia a verificar el espacio). Cualquier uso de estas funciones por parte de un programa es una señal de advertencia porque, a menos que se utilicen con cuidado, pueden convertirse en errores del programa. No es necesario que memorice la lista de estas funciones; mi verdadero propósito es ilustrar cuán común es este problema. Estas funciones incluyen strcpy(3), strcat(3), sprintf(3) (y su prima vsprintf(3)) y gets(3). El conjunto de funciones scanf() (scanf(3), fscanf(3), sscanf(3), vscanf(3), vsscanf(3) y vfscanf(3)) puede causar problemas al utilizar un formato que no define un la longitud máxima es fácil (usar el formato "%s" siempre es un error al leer entradas que no son de confianza).

Otras funciones peligrosas incluyen realpath(3), getopt(3), getpass(3), streadd(3), strecpy(3) y strtrns(3). En teoría, snprintf() debería ser relativamente seguro, y lo es en los sistemas GNU/Linux modernos. Pero los sistemas UNIX y Linux muy antiguos no implementan el mecanismo de protección que debería implementar snprintf().

Hay otras funciones en las bibliotecas de Microsoft que causan el mismo problema en las plataformas correspondientes (estas funciones incluyen wcscpy(), _tcscpy(), _mbscpy(), wcscat(), _tcscat(), _mbscat() y CopiarMemoria()). Tenga en cuenta que existe un error común y peligroso si utiliza la función MultiByteToWideChar() de Microsoft: la función requiere un tamaño máximo como número de caracteres, pero los programadores a menudo especifican el tamaño en bytes (la necesidad más común), lo que resulta en una falla de desbordamiento del búfer. .

Otro problema es que C y C++ tienen una verificación de tipos muy débil para números enteros y generalmente no detectan problemas con la manipulación de estos números enteros. Dado que requieren que el programador realice toda la detección del problema manualmente, es fácil manipular esos números enteros incorrectamente de alguna manera explotable. En particular, este suele ser el caso cuando necesita realizar un seguimiento de la longitud del búfer o leer la longitud de algo. Pero, ¿qué sucede si se utiliza un valor con signo para almacenar este valor de longitud? ¿Puede un atacante convertirlo en "negativo" e interpretar los datos como un valor positivo realmente grande? ¿Podría un atacante aprovechar esta operación cuando se convierten valores numéricos entre diferentes tamaños? ¿Se puede explotar el desbordamiento numérico? A veces, la forma en que se manejan los números enteros puede provocar fallas en el programa.

Nueva tecnología para evitar desbordamientos de buffer

Por supuesto, es difícil lograr que los programadores eviten cometer errores comunes y lograr que el programa (y el programador) utilice otro lenguaje. Generalmente más difícil. Entonces, ¿por qué no dejar que el sistema subyacente se proteja de estos problemas? Como mínimo, es bueno evitar los ataques de destrucción de pilas porque los ataques de destrucción de pilas son extremadamente fáciles de realizar.

En general, es una excelente idea cambiar el sistema subyacente para evitar problemas de seguridad comunes, un tema que también abordaremos más adelante en este artículo. Resulta que hay muchas defensas disponibles, y algunas de las más populares se pueden agrupar en las siguientes categorías:

Defensas basadas en Canarias. Esto incluye StackGuard (usado por Immunix), ProPolice (usado por OpenBSD) y la opción /GS de Microsoft.

Defensa de pila de no ejecución. Esto incluye el parche no ejecutivo de Solar Designer (usado por OpenWall) y el escudo ejecutivo (usado por Red Hat/Fedora).

Otros métodos. Esto incluye libsafe (usado por Mandrake) y métodos de división de pila.

Desafortunadamente, todos los métodos vistos hasta ahora tienen debilidades, por lo que no son una panacea, pero proporcionarán algo de ayuda.

Defensa basada en sondas

El investigador Crispen Cowan creó un enfoque interesante llamado StackGuard. Stackguard modifica el compilador de C (gcc) para que se inserte un valor de "sonda" delante de la dirección de retorno. Un "detector" es como un detector en una mina de carbón: avisa cuando algo va mal en alguna parte. Antes de que cualquier función regrese, realiza una verificación para garantizar que el valor de la sonda no haya cambiado. Si un atacante reescribe la dirección del remitente (como parte de un ataque de destrucción de pila), el valor de la sonda puede cambiar y el sistema se detendrá en consecuencia. Este es un método útil, pero tenga en cuenta que no evita que los desbordamientos del búfer sobrescriban otros valores (un atacante aún puede usar estos valores para atacar el sistema). La gente también ha ampliado este enfoque para proteger otros valores (como los del montón). Immunix utiliza Stackguard (entre otras defensas).

El protector de destrucción de pilas de IBM (ssp, originalmente llamado ProPolice) es una variación del enfoque de StackGuard.

Al igual que StackGuard, ssp utiliza un compilador modificado para insertar una sonda en las llamadas a funciones para detectar desbordamientos de pila. Sin embargo, añade algunos giros interesantes a esta idea básica. Reordena dónde se almacenan las variables locales y copia los punteros en los argumentos de la función para que también precedan a cualquier matriz. Esto aumenta las capacidades de protección de ssp; significa que un desbordamiento del búfer no modificará el valor del puntero (de lo contrario, un atacante con control del puntero podría usarlo para controlar dónde guarda los datos el programa). Por defecto no detecta todas las funciones, sino sólo aquellas que realmente necesitan protección (principalmente funciones que utilizan matrices de caracteres). En teoría, esto reduce ligeramente la protección, pero este comportamiento predeterminado mejora el rendimiento y al mismo tiempo previene la mayoría de los problemas. Por razones prácticas, implementan sus métodos usando gcc de manera independiente de la arquitectura, lo que los hace más fáciles de implementar. A partir de la versión de mayo de 2003, el aclamado OpenBSD (que se centra principalmente en la seguridad) utiliza ssp (también conocido como ProPolice) en toda su suite de distribución.

Microsoft se basó en el trabajo de StackGuard y agregó un indicador de compilador (/GS) para implementar sondas en su compilador de C.

Defensa de la pila sin ejecución

Otro enfoque hace imposible ejecutar código en la pila en primer lugar. Desafortunadamente, los mecanismos de protección de la memoria de los procesadores x86 (los procesadores más comunes) no pueden soportar esto fácilmente. Generalmente, si una página de memoria es legible, es ejecutable; A un desarrollador llamado Solar Designer se le ocurrió una combinación inteligente de mecanismos de kernel y procesador para crear un "parche de pila no ejecutable" para el kernel de Linux. Con este parche, los programas en la pila ya no pueden funcionar como se ejecuta en x86 como de costumbre; Resulta que hay situaciones en las que el ejecutable debe estar en la pila, esto incluye el manejo de señales y el manejo de trampolines; Los trampolines son construcciones sofisticadas que a veces generan compiladores (como el compilador GNAT Ada) para admitir construcciones como subrutinas anidadas. Solar Designer también aborda cómo protegerse contra ataques sin afectar estos casos especiales.

Algún tiempo después, a la gente se le ocurrió una nueva idea para evitar este problema: mover todo el código ejecutable a un área de memoria llamada área de "armadura ASCII". Para entender cómo funciona esto, es importante tener en cuenta el hecho de que un atacante normalmente no puede insertar el carácter ASCII NUL (0) mediante un ataque de desbordamiento de búfer normal. Esto significa que a un atacante le resultará difícil hacer que un programa devuelva una dirección que contenga 0. Debido a este hecho, mover todo el código ejecutable a direcciones que contienen 0 hace que sea mucho más difícil atacar el programa.

El rango de memoria contiguo más grande con esta propiedad es un conjunto de direcciones de memoria de 0 a 0x01010100, por lo que se denominan áreas protegidas ASCII (hay otras direcciones con esta propiedad, pero están dispersas). Combinado con una pila no ejecutable, este enfoque es bastante valioso: una pila no ejecutable impide que un atacante envíe código ejecutable, y la memoria protegida por ASCII dificulta que un atacante eluda el código no ejecutable explotando la pila existente. . Esto protegerá el código de su programa contra desbordamientos de pila, búfer y puntero de función, todo sin necesidad de recompilación.

Sin embargo, la memoria protegida con ASCII no funciona para todos los programas; es posible que los programas grandes no quepan en el área de memoria protegida con ASCII (por lo que la protección es imperfecta) y, a veces, un atacante puede insertar ceros en el destino. DIRECCIÓN . Además, algunas implementaciones no admiten código de trampolín, por lo que es posible que sea necesario desactivar esta función para los programas que requieren esta protección. Ingo Molnar de Red Hat implementó esta idea en su parche "exec-shield" utilizado por el núcleo de Fedora (cuya versión gratuita está disponible en Red Hat).

Las últimas versiones de OpenWall GNU/Linux (OWL) utilizan la implementación de este enfoque proporcionada por Solar Designer (consulte Recursos para obtener enlaces a estas versiones).

Otros métodos

Existen muchos otros métodos. Una forma de hacerlo es hacer que la biblioteca estándar sea más resistente a los ataques. Lucent Technologies desarrolló Libsafe, que es un contenedor de varias funciones estándar de la biblioteca C, concretamente funciones como strcpy() que se sabe que son vulnerables a ataques de destrucción de pilas. Libsafe es un software de código abierto con licencia LGPL. Las versiones libsafe de esas funciones realizan comprobaciones para garantizar que las sobrescrituras de matrices no excedan el marco de la pila. Sin embargo, este enfoque solo protege esas funciones específicas en lugar de prevenir fallas de desbordamiento de la pila en general, y solo protege la pila, no las variables locales en la pila. Su implementación original utilizaba LD_PRELOAD, que puede entrar en conflicto con otros programas. La distribución Mandrake para Linux (a partir de la versión 7.1) incluye libsafe.

Otro método se llama "dividir la pila de control y datos". La idea básica es dividir la pila en dos pilas, una para almacenar información de control (como la dirección de "retorno") y la otra. Se utiliza para controlar todos los demás datos. Xu et al. implementaron este enfoque en gcc y StackShield lo implementaron en ensamblador. Esto hace que la manipulación de la dirección de retorno sea mucho más difícil, pero no evita los ataques de desbordamiento del búfer que alteran los datos de la función que llama.

De hecho, existen otros métodos, incluida la aleatorización de la ubicación del programa ejecutable; "PointGuard" de Crispen extiende esta idea del detector al montón, etc. Cómo proteger las computadoras actuales es ahora una tarea de investigación activa.

La protección general no es suficiente

¿Qué significan tantos enfoques diferentes? Lo bueno para los usuarios es que a largo plazo se están probando muchos enfoques innovadores; esta "competencia" hará que sea más fácil ver cuál es el mejor. Además, esta diversidad también hace que a los atacantes les resulte más difícil evadir todos estos métodos. Sin embargo, esta diversidad también significa que los desarrolladores deben evitar escribir código que interfiera con cualquiera de estos métodos. Esto es fácil en la práctica; simplemente no escriba código que realice operaciones de bajo nivel en marcos de pila o haga suposiciones sobre el diseño de la pila. Incluso si estos métodos no existen, este sigue siendo un buen consejo.

Es bastante obvio que los proveedores de sistemas operativos deben involucrarse: elija al menos un enfoque y utilícelo. Los desbordamientos de búfer son el problema número uno y el mejor de estos métodos suele mitigar los efectos de casi la mitad de los fallos conocidos en las distribuciones. Se puede argumentar que si un enfoque basado en sondas es mejor o un enfoque basado en pilas no ejecutables es mejor, cada uno tiene sus propios méritos. Es posible combinarlos, pero algunos métodos no admiten este uso porque la penalización adicional en el rendimiento hace que no valga la pena. No me refiero a nada más, al menos en lo que respecta a los métodos en sí; tanto libsafe como el método de dividir las pilas de control y datos tienen limitaciones en la protección que brindan. Por supuesto, la peor solución es no proporcionar ninguna protección para este defecto número uno. Los proveedores de software que aún no hayan implementado un método deben planear hacerlo de inmediato. A partir de 2004, los usuarios deberían empezar a evitar los sistemas operativos que no proporcionaban al menos cierta protección automática contra desbordamientos del búfer.

Sin embargo, no existe ningún método que permita a los desarrolladores ignorar los desbordamientos del búfer. Todos estos métodos pueden verse comprometidos por un atacante. Un atacante podría aprovechar un desbordamiento del búfer cambiando el valor de otros datos en la función; no hay forma de evitarlo. Los atacantes pueden eludir muchos de estos métodos si pueden insertar ciertos valores difíciles de crear (como caracteres NUL) a medida que los datos multimedia y comprimidos se vuelvan más comunes, será más fácil para los atacantes eludir estos métodos. Básicamente, todos estos métodos mitigan el daño causado por los ataques de desbordamiento del búfer, desde ataques de apropiación de programas hasta ataques de denegación de servicio.

Lamentablemente, como los sistemas informáticos se utilizan en situaciones más críticas, incluso la denegación de servicio suele ser inaceptable. Por lo tanto, aunque los kits de distribución deben incluir al menos un método de defensa apropiado, y los desarrolladores deben trabajar con (en lugar de contra) esos métodos, los desarrolladores aún necesitan escribir software libre de errores en primer lugar.

Solución C/C++

Una solución sencilla para los desbordamientos del búfer es cambiar a un lenguaje que proteja contra los desbordamientos del búfer. Después de todo, casi todos los lenguajes de alto nivel, excepto C y C++, tienen mecanismos integrados para prevenir eficazmente los desbordamientos del búfer. Pero muchos desarrolladores todavía optan por utilizar C y C++ por varias razones. Entonces, ¿qué puedes hacer?

Resulta que existen muchas técnicas diferentes para prevenir desbordamientos de búfer, pero todas se dividen en dos categorías: búferes asignados estáticamente y búferes asignados dinámicamente. Primero, describiremos cuáles son estos dos métodos. Luego discutiremos dos ejemplos de métodos estáticos (strncpy/strncat estándar de C y strlcpy/strlcat de OpenBSD), seguidos de dos ejemplos de métodos dinámicos (SafeStr y std::string de C++).

Opciones importantes: buffers asignados estática y dinámicamente

Los buffers tienen espacio limitado. Por lo tanto, en realidad hay dos formas posibles de lidiar con un espacio de búfer insuficiente.

El enfoque del "búfer asignado estáticamente": es decir, cuando el búfer se agota, usted se queja y se niega a agregar más espacio al búfer.

El método del "búfer asignado dinámicamente": es decir, cuando el búfer se agota, el tamaño del búfer se ajusta dinámicamente a un tamaño mayor hasta que se agota toda la memoria.

Los métodos estáticos tienen algunas desventajas. De hecho, los métodos estáticos a veces pueden traer diferentes inconvenientes. Los métodos estáticos básicamente descartan los datos "sobrantes". Si el programa consume los datos resultantes de todos modos, el atacante intentará llenar el búfer con lo que quiera cuando los datos se trunquen. Si utiliza métodos estáticos, debe asegurarse de que lo peor que puede hacer un atacante no invalide las suposiciones preexistentes, y también es una buena idea comprobar el resultado final.

Los métodos dinámicos tienen muchas ventajas: escalan hacia problemas más grandes (en lugar de imponer limitaciones arbitrarias) y no sufren el truncamiento de la matriz de caracteres, lo que causa problemas de seguridad. Pero también tienen sus propios problemas: al aceptar datos de cualquier tamaño, es posible que se quede sin memoria, lo que puede no ocurrir en el momento de la entrada. Cualquier asignación de memoria puede fallar, y escribir un programa en C o C++ que maneje este problema realmente bien es difícil. Esto puede hacer que la computadora esté demasiado ocupada y quede inutilizable incluso antes de que se quede sin memoria. En resumen, los métodos dinámicos a menudo facilitan que los atacantes lancen ataques de denegación de servicio. Por lo tanto, todavía es necesario limitar las entradas. Además, los programas deben diseñarse cuidadosamente para manejar el agotamiento de la memoria en cualquier ubicación, lo cual no es una tarea fácil.

Método de biblioteca estándar de C

Uno de los métodos más sencillos es simplemente usar aquellas funciones de biblioteca estándar de C que están diseñadas para evitar desbordamientos de búfer (incluso cuando se usa C++, esto también es posible). , especialmente strncpy(3) y strncat(3). Estas funciones estándar de la biblioteca C generalmente admiten un enfoque de asignación estática, que descarta datos cuando no caben en el búfer. La gran ventaja de este enfoque es que puede estar seguro de que estas funciones estarán disponibles en cualquier máquina y que cualquier desarrollador de C/C++ las conocerá. Hay toneladas de programas escritos de esta manera y realmente funciona.

Desafortunadamente, hacer esto correctamente es sorprendentemente difícil. Estos son algunos de los problemas:

Strncpy(3) y strncat(3) requieren que proporciones el espacio restante, no el tamaño total del buffer. La razón por la que esto se convierte en un problema es que, si bien el tamaño del búfer no cambia una vez asignado, la cantidad de espacio restante en el búfer cambia cada vez que se agregan o eliminan datos. Esto significa que el programador siempre debe realizar un seguimiento o recalcular el espacio restante. Este seguimiento o recálculo es propenso a errores, y cualquier error podría abrir la puerta a ataques de buffer.

Ninguna función proporciona un informe sencillo cuando se produce un desbordamiento (y pérdida de datos), por lo que el programador debe trabajar más si se quiere detectar un desbordamiento del búfer.

La función strncpy(3) tampoco usa NUL para terminar la cadena si la cadena de origen es al menos tan larga como la de destino; esto puede causar estragos más adelante; Por lo tanto, después de ejecutar strncpy(3), normalmente necesitará volver a terminar la cadena de destino.

La función strncpy(3) también se puede utilizar para copiar solo una parte de la cadena de origen al destino. Al realizar esta operación, la cantidad de caracteres a copiar generalmente se calcula en función de la información sobre la cadena de origen. El peligro es que si olvida tener en cuenta el espacio de búfer disponible, puede quedar expuesto a ataques de búfer incluso cuando utilice strncpy(3). Esta función tampoco copia caracteres NUL, lo que también puede ser un problema.

Es posible utilizar sprintf() de una manera que evite los desbordamientos del búfer, pero es muy fácil quedar accidentalmente vulnerable a un ataque de desbordamiento del búfer. La función sprintf() utiliza una cadena de control para especificar el formato de salida, que generalmente incluye " %s " (salida de cadena). Si especifica un especificador preciso para la salida de cadena (como %.10s ), puede evitar desbordamientos del búfer especificando la longitud máxima de la salida. Incluso puede utilizar " * " como especificador exacto (como " %.*s "), de modo que pueda pasar un valor de longitud máxima en lugar de incrustar el valor de longitud máxima en la cadena de control. El problema con esto es que es fácil usar sprintf() incorrectamente. Un "ancho de campo" (como " %10s ") solo especifica una longitud mínima, no una longitud máxima. El especificador "ancho de campo" deja un riesgo de desbordamiento del búfer, mientras que los especificadores de ancho de campo y ancho exacto parecen casi idénticos; la única diferencia es que la versión segura tiene un punto. Otro problema es que el campo exacto solo especifica la longitud máxima de un argumento, pero el tamaño del búfer debe ajustarse al tamaño máximo de los datos combinados.

La serie de funciones scanf() tiene un valor de ancho máximo. Al menos el estándar IEEE 1003-2001 estipula claramente que estas funciones no deben leer datos que excedan el ancho máximo. Desafortunadamente, no todas las especificaciones especifican esto claramente, y no está claro si todas las implementaciones implementan estas restricciones correctamente (lo que no funciona correctamente en los sistemas GNU/Linux actuales). Si confía en él, sería aconsejable ejecutar una pequeña prueba durante la instalación o inicialización para asegurarse de que funciona correctamente.

strncpy(3) también tiene un molesto problema de rendimiento. En teoría, strncpy(3) es un reemplazo seguro de strcpy(3), pero strncpy(3) también llena todo el espacio de destino con NUL al final de la cadena de origen. Esto es extraño porque realmente no hay una buena razón para hacer esto, pero ha sido así desde el principio y algunos programas todavía dependen de esta característica. Esto significa que cambiar de strcpy(3) a strncpy(3) reducirá el rendimiento, lo que generalmente no es un problema grave en las computadoras actuales, pero aún así puede ser dañino.

Entonces, ¿puedo usar rutinas de la biblioteca C estándar para evitar desbordamientos del búfer? Sí, pero no es fácil. Si planea seguir esta ruta, debe comprender todos los puntos mencionados anteriormente. Alternativamente, puede utilizar un método alternativo que se describe en las siguientes secciones.

Strlcpy/strlcat para OpenBSD

Los desarrolladores de OpenBSD han desarrollado un enfoque estático diferente basado en las nuevas funciones que desarrollaron, strlcpy(3) y strlcat(3). Estas funciones realizan copia y concatenación de cadenas, pero son menos propensas a errores.

Los prototipos de estas funciones son los siguientes:

size_t strlcpy (char *dst, const char *src, size_t size);

size_t strlcat (char *dst, const char *src); , size_t size );

La función strlcpy() copia una cadena terminada en NUL de " src " a " dst " (hasta caracteres de tamaño 1). La función strlcat() agrega la cadena terminada en NUL src al final de dst (pero la cantidad de caracteres en el destino no excederá el tamaño-1).

A primera vista, sus prototipos no son muy diferentes de las funciones estándar de la biblioteca C. Pero, de hecho, existen algunas diferencias significativas entre ellos. Todas estas funciones aceptan el tamaño total del objetivo (no el espacio restante) como argumento. Esto significa que no es necesario volver a calcular continuamente el tamaño del espacio, lo cual es una tarea propensa a errores. Además, ambas funciones garantizan que el destino terminará en NUL siempre que el tamaño del destino sea al menos 1 (no se puede colocar nada en un búfer de longitud cero). Si no se produce ningún desbordamiento del búfer, el valor de retorno es siempre la longitud de la cadena combinada, lo que hace que detectar desbordamientos del búfer sea realmente fácil.

Desafortunadamente, strlcpy(3) y strlcat(3) generalmente no están disponibles en la biblioteca estándar en sistemas tipo UNIX. OpenBSD y Solaris los tienen integrados en , pero los sistemas GNU/Linux no. Esto no es tan difícil; incluso puedes incluir pequeñas funciones directamente en el código fuente de tu propio programa cuando el sistema subyacente no las proporciona.

SafeStr

Messier y Viega desarrollaron la biblioteca "SafeStr", un método dinámico para C que cambia automáticamente el tamaño de las cadenas según sea necesario. La cadena Safestr se convierte fácilmente en una cadena C " char * " normal usando el mismo truco utilizado por la implementación malloc(): safestr almacena información importante en la dirección "antes" de pasar el puntero. La ventaja de esta técnica es que será fácil utilizar SafeStr en programas existentes. SafeStr también admite cadenas de "solo lectura" y "confiables", lo que también puede resultar útil. Un problema con este enfoque es que requiere XXL (una biblioteca que agrega soporte para manejo de excepciones y administración de recursos a C), por lo que básicamente estás incorporando una biblioteca importante solo para manejar cadenas. Safestr se publica bajo una licencia estilo BSD de código abierto.

C++ std::string

Otra solución para los usuarios de C++ es la clase estándar std::string, que es un enfoque dinámico (el búfer cambia según sea necesario). Es casi una obviedad porque el lenguaje C++ admite esta clase directamente, por lo que no se requiere ningún trabajo especial para usarla, y otras bibliotecas probablemente también la usarán. Por sí solo, std::string generalmente previene los desbordamientos del búfer, pero si extraes una cadena C simple a través de él (por ejemplo, usando data() o c_str() ), todos los problemas discutidos anteriormente reaparecen. Recuerde también que data() no siempre devuelve una cadena terminada en NUL.

Por diversas razones históricas, muchas bibliotecas de C++ y programas preexistentes han creado sus propias clases de cadenas. Esto puede hacer que std::string sea más difícil de usar e ineficiente al usar esas bibliotecas o modificar esos programas, porque los diferentes tipos de cadenas tendrán que convertirse continuamente. No todas esas otras clases de cadenas protegen contra desbordamientos de búfer, y los defectos de desbordamiento de búfer pueden introducirse fácilmente en esas clases si realizan conversiones automáticas a tipos char* desprotegidos de C.