¿Por qué es tan difícil hacer que C sea menos propenso a desbordamientos de búfer?


23

Estoy haciendo un curso en la universidad, donde uno de los laboratorios es realizar exploits de desbordamiento de búfer en el código que nos dan. Esto abarca desde exploits simples como cambiar la dirección de retorno de una función en una pila para volver a una función diferente, hasta el código que cambia el registro de un programa / estado de memoria pero luego regresa a la función que usted llamó, lo que significa que el La función que llamaste es completamente ajena al exploit.

Investigué un poco sobre esto, y este tipo de exploits se usan prácticamente en todas partes, incluso ahora, en cosas como ejecutar homebrew en Wii y el jailbreak sin ataduras para iOS 4.3.1

Mi pregunta es por qué este problema es tan difícil de solucionar. Es obvio que esta es una de las principales vulnerabilidades utilizadas para hackear cientos de cosas, pero parece que sería bastante fácil de solucionar simplemente truncando cualquier entrada más allá de la longitud permitida y simplemente desinfectando toda la entrada que tome.

EDITAR: Otra perspectiva que me gustaría tener en cuenta las respuestas: ¿por qué los creadores de C no solucionan estos problemas al reimplementar las bibliotecas?

Respuestas:


35

Ellos arreglaron las bibliotecas.

Cualquier biblioteca estándar moderno C contiene variantes más seguras de strcpy, strcat, sprintf, y así sucesivamente.

En los sistemas C99, que es la mayoría de los Unix, los encontrará con nombres como strncaty snprintf, la "n" indica que se necesita un argumento que tenga el tamaño de un búfer o un número máximo de elementos para copiar.

Estas funciones se pueden usar para manejar muchas operaciones de manera más segura, pero en retrospectiva, su usabilidad no es excelente. Por ejemplo, algunas snprintfimplementaciones no garantizan que el búfer esté terminado en nulo. strncattoma una serie de elementos para copiar, pero muchas personas pasan por error el tamaño del búfer de destino.

En Windows, es frecuente encontrar el strcat_s, sprintf_s, el sufijo "_s" que indica "seguro". Estos también han encontrado su camino en la biblioteca estándar de C en C11, y proporcionan más control sobre lo que sucede en caso de un desbordamiento (truncamiento vs. aserción, por ejemplo).

Muchos proveedores ofrecen aún más alternativas no estándar como asprintfen la libc de GNU, que asignará automáticamente un búfer del tamaño apropiado.

La idea de que puedes "simplemente arreglar C" es un malentendido. La fijación de C no es el problema, y ​​ya se ha hecho. El problema es arreglar décadas de código C escrito por programadores ignorantes, cansados ​​o apresurados, o código que ha sido portado desde contextos donde la seguridad no importaba a contextos donde la seguridad sí lo hace. Ningún cambio en la biblioteca estándar puede corregir este código, aunque la migración a compiladores más nuevos y bibliotecas estándar a menudo puede ayudar a identificar los problemas automáticamente.


11
+1 por apuntar el problema a los programadores, no al lenguaje.
Nicol Bolas

8
@Nicol: Decir "el problema [son] los programadores" es injustamente reduccionista. El problema es que durante años (décadas) C hizo que fuera más fácil escribir código inseguro que código seguro, particularmente porque nuestra definición de "seguro" evolucionó más rápido que cualquier estándar de lenguaje, y ese código todavía existe. Si quiere intentar reducir eso a un solo sustantivo, el problema es "1970-1999 libc", no "los programadores".

1
Todavía es responsabilidad de los programadores usar las herramientas que tienen ahora para solucionar estos problemas. Tómate medio día más o menos y revisa el código fuente de estas cosas.
Nicol Bolas

1
@Nicol: Aunque es trivial detectar un posible desbordamiento del búfer, a menudo no es trivial estar seguro de que es una amenaza real, y es menos trivial determinar qué debería suceder si el búfer alguna vez se desborda. El manejo de errores a menudo no fue considerado, no es posible implementar una mejora "rápidamente" ya que puede cambiar el comportamiento de un módulo de maneras inesperadas. Acabamos de hacer esto en una base de código heredada multimillonaria, y aunque valió la pena hacer ejercicio, costó mucho tiempo (y dinero).
mattnz

44
@NicolBolas: No está seguro de qué tipo de tienda que trabaja, pero el último lugar escribí C para uso en producción requiere la modificación del documento de diseño detallado, lo revise, cambiar el código, se modifica el plan de pruebas, la revisión del plan de pruebas, realizando una completa prueba del sistema, revisando los resultados de la prueba y luego volviendo a certificar el sistema en el sitio del cliente. Esto es para un sistema de telecomunicaciones en un continente diferente escrito para una compañía que ya no existe. Lo último que supe fue que la fuente estaba en un archivo RCS en una cinta QIC que aún debería ser legible, si puedes encontrar una unidad de cinta adecuada.
TMN

19

No es realmente inexacto decir que C es realmente "propenso a errores" por diseño . Aparte de algunos errores graves como gets, el lenguaje C no puede ser de otra manera sin perder la característica principal que atrae a las personas a C en primer lugar.

C fue diseñado como un lenguaje de sistemas para actuar como una especie de "ensamblaje portátil". Una característica importante del lenguaje C es que, a diferencia de los lenguajes de nivel superior, el código C a menudo se asigna muy de cerca al código de máquina real. En otras palabras,++i generalmente es solo una incinstrucción y, a menudo, puede obtener una idea general de lo que hará el procesador en tiempo de ejecución mirando el código C.

Pero la adición de la comprobación implícita de los límites agrega una sobrecarga adicional, una sobrecarga que el programador no solicitó y podría no querer. Esta sobrecarga va más allá del almacenamiento adicional requerido para almacenar la longitud de cada matriz, o las instrucciones adicionales para verificar los límites de la matriz en cada acceso a la matriz. ¿Qué pasa con la aritmética de puntero? ¿O qué pasa si tiene una función que toma un puntero? El entorno de tiempo de ejecución no tiene forma de saber si ese puntero cae dentro de los límites de un bloque de memoria asignado legítimamente. Para realizar un seguimiento de esto, necesitaría una arquitectura de tiempo de ejecución seria que pueda verificar cada puntero contra una tabla de bloques de memoria asignados actualmente, momento en el que ya estamos entrando en el territorio de tiempo de ejecución administrado estilo Java / C #.


12
Honestamente, cuando la gente pregunta por qué C no es "seguro", me pregunto si se quejarán de que el ensamblaje no es "seguro".
Ben Brocka

55
El lenguaje C es muy parecido al ensamblaje portátil en una máquina PDP-11 de Digital Equipment Corporation. Al mismo tiempo, las máquinas Burroughs tenían límites de matriz en la CPU, por lo que fueron realmente fáciles de
instalar

15

Creo que el problema real no es que este tipo de errores son difíciles de solucionar, pero que son tan fáciles de hacer: si utiliza strcpy, sprintfy amigos en el camino (aparentemente) más simple que el trabajo puede, entonces usted probablemente ha abrió la puerta para un desbordamiento de búfer. Y nadie lo notará hasta que alguien lo explote (a menos que tenga muy buenas revisiones de código). Ahora agregue el hecho de que hay muchos programadores mediocres y que están bajo presión de tiempo la mayor parte del tiempo, y tiene una receta para el código que está tan plagado de desbordamientos de búfer que será difícil solucionarlos todos simplemente porque hay muchos de ellos y se están escondiendo muy bien.


3
Realmente no necesita "muy buenas revisiones de código". Solo necesita prohibir sprintf, o redefinir sprintf a algo que use sizeof () y errores en el tamaño de un puntero, etc. Ni siquiera necesita revisiones de código, puede hacer este tipo de cosas con SCM commit ganchos y grep.

1
@JoeWreschnig: sizeof(ptr)es 4 u 8, en general. Esa es otra limitación de C: no hay forma de determinar la longitud de una matriz, dado solo el puntero a ella.
MSalters

@MSalters: Sí, una matriz de int [1] o char [4] o lo que sea puede ser un falso positivo, pero en la práctica nunca está manejando buffers de ese tamaño con esas funciones. (No estoy hablando teóricamente aquí - He trabajado en una gran base de código C durante cuatro años que utilizan este enfoque nunca golpeó la limitación de sprintfing en un char [4]..)

55
@BlackJack: la mayoría de los programadores no son estúpidos; si los obligas a pasar el tamaño, pasarán el correcto. Es solo que la mayoría tampoco pasará el tamaño a menos que se lo obligue. Puede escribir una macro que devolverá la longitud de una matriz si es estática o de tamaño automático, pero errores si se le da un puntero. Luego re # define sprintf para llamar a snprintf con esa macro dando el tamaño. Ahora tiene una versión de sprintf que solo funciona en matrices con tamaños conocidos y, de lo contrario, obliga al programador a llamar a snprintf con un tamaño especificado manualmente.

1
Un ejemplo simple de tal macro sería el #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / (sizeof(a) != sizeof(void *))que desencadenará una división de tiempo de compilación por cero. Otra inteligente que vi por primera vez en Chromium es #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / !(sizeof(a) % sizeof((a)[0]))que intercambia el puñado de falsos positivos por algunos falsos negativos, desafortunadamente es inútil para char []. Puede usar varias extensiones del compilador para hacerlo aún más confiable, por ejemplo, blogs.msdn.com/b/ce_base/archive/2007/05/08/… .

7

Es difícil reparar los desbordamientos del búfer porque C no proporciona prácticamente herramientas útiles para abordar el problema. Es una falla fundamental del lenguaje que los búferes nativos no brinden protección y es prácticamente, si no completamente, imposible reemplazarlos con un producto superior, como lo hizo C ++ con std::vectory std::array, y es difícil incluso en modo de depuración encontrar desbordamientos de búfer.


13
"Defecto del lenguaje" es una afirmación muy sesgada. Que las bibliotecas no proporcionaran verificación de límites era una falla; que el idioma no fue una elección consciente para evitar gastos generales. Esa elección es parte de lo que permite que las construcciones de nivel superior std::vectorse implementen de manera eficiente. Y vector::operator[]hace la misma elección de velocidad sobre seguridad. La seguridad vectorviene de hacer que sea más fácil cargar alrededor del tamaño, que es el mismo enfoque que toman las bibliotecas C modernas.

1
@Charles: "C simplemente no proporciona ningún tipo de búferes de expansión dinámica como parte de la biblioteca estándar". No, esto no tiene nada que ver con eso. Primero, C los proporciona a través de realloc(C99 también le permite dimensionar matrices de pila usando un tamaño determinado en tiempo de ejecución pero constante a través de cualquier variable automática, casi siempre preferible char buf[1024]). Segundo, el problema no tiene nada que ver con expandir los buffers, tiene que ver con si los buffers llevan o no el tamaño con ellos y verifican ese tamaño cuando accedes a ellos.

55
@ Joe: El problema no es tanto que las matrices nativas estén rotas. Es que son imposibles de reemplazar. Para empezar, vector::operator[]hace la verificación de límites en modo de depuración, algo que las matrices nativas no pueden hacer, y en segundo lugar, en C no hay forma de cambiar el tipo de matriz nativa por una que pueda hacer la verificación de límites, porque no hay plantillas ni operador sobrecarga En C ++, si quiere pasar de T[]a std::array, prácticamente puede cambiar un typedef. En C, no hay forma de lograr eso, y no hay forma de escribir una clase con una funcionalidad equivalente, y mucho menos la interfaz.
DeadMG

3
@ Joe: excepto que nunca puede tener un tamaño estático, y nunca puedes hacerlo genérico. Es imposible escribir cualquier biblioteca de C, que realiza la misma función que std::vector<T>y std::array<T, N>se puede hacer en C ++. No habría forma de diseñar y especificar ninguna biblioteca, ni siquiera estándar, que pudiera hacer esto.
DeadMG

1
No estoy seguro de lo que quieres decir con "nunca puede tener un tamaño estático". Como usaría ese término, std::vectortampoco puede tener un tamaño estático. En cuanto a genérico, puede hacerlo tan genérico como lo necesite C: una pequeña cantidad de operaciones fundamentales en void * (agregar, eliminar, cambiar el tamaño) y todo lo demás escrito específicamente. Si va a quejarse de que C no tiene genéricos de estilo C ++, eso está fuera del alcance del manejo seguro del búfer.

7

El problema no es con la C lenguaje .

En mi opinión, el obstáculo principal que hay que superar es que C simplemente se enseña mal . Se han institucionalizado décadas de malas prácticas e información incorrecta en manuales de referencia y notas de conferencias, envenenando las mentes de cada nueva generación de programadores desde el principio. Los estudiantes reciben una breve descripción de las funciones de E / S "fáciles", como gets1 o, scanfy luego se dejan en sus propios dispositivos. No se les dice dónde ni cómo pueden fallar esas herramientas, ni cómo prevenirlas. No se les dice sobre el uso fgetsystrtol/strtodporque se consideran herramientas "avanzadas". Luego se desatan en el mundo profesional para causar estragos. No es que muchos de los programadores más experimentados conozcan mejor, porque recibieron la misma educación sobre daño cerebral. Es enloquecedor. Veo muchas preguntas aquí y en Stack Overflow y en otros sitios donde está claro que la persona que hace la pregunta está siendo enseñada por alguien que simplemente no sabe de qué está hablando , y por supuesto, no se puede decir "tu profesor está equivocado", porque él es profesor y tú solo eres un tipo en Internet.

Y luego tienes la multitud que desdeña cualquier respuesta que comience con, "bueno, de acuerdo con el estándar del idioma ..." porque están trabajando en el mundo real y según ellos, el estándar no se aplica al mundo real . Puedo tratar con alguien que solo tiene una mala educación, pero cualquiera que insista en ser ignorante es solo una plaga para la industria.

No habría problemas de desbordamiento del búfer si el idioma se enseñara correctamente con énfasis en escribir código seguro. No es "difícil", no es "avanzado", solo es tener cuidado.

Sí, esto ha sido una queja.


1 Que, afortunadamente, finalmente se ha eliminado de la especificación del lenguaje, aunque estará al acecho en 40 años de código heredado para siempre.


1
Si bien estoy mayormente de acuerdo contigo, creo que sigues siendo un poco injusto. Lo que consideramos "seguro" también es una función del tiempo (y veo que ha sido un desarrollador de software profesional mucho más tiempo que yo, así que estoy seguro de que está familiarizado con esto). Dentro de diez años, alguien tendrá esta misma conversación sobre por qué demonios todos en 2012 usaron implementaciones de tablas hash compatibles con DoS, ¿no sabíamos nada sobre seguridad? Si hay un problema en la enseñanza, es un problema que nos enfocamos demasiado en enseñar la "mejor" práctica, y no que la mejor práctica en sí misma evolucione.

1
Y seamos honestos. Usted podría escribir código seguro con solo sprintf, pero eso no significa que el idioma no era la adecuada. C tenía fallas y tiene fallas, como cualquier lenguaje, y es importante que admitamos esas fallas para poder continuar reparándolas.

@JoeWreschnig: si bien estoy de acuerdo con el punto más amplio, creo que hay una diferencia cualitativa entre las implementaciones de tablas hash compatibles con DoS y los desbordamientos del búfer. Lo primero puede atribuirse a circunstancias que evolucionan a tu alrededor, pero lo segundo no tiene excusas; los desbordamientos del búfer son errores de codificación, punto. Sí, C no tiene protectores de cuchillas y te cortará si eres descuidado; podemos discutir si eso es un defecto en el idioma o no. Eso es ortogonal al hecho de que muy pocos estudiantes se les da ninguna instrucción de seguridad cuando están aprendiendo el idioma.
John Bode

5

El problema es tanto la miopía gerencial como la incompetencia del programador. Recuerde, una aplicación de 90,000 líneas necesita solo una operación insegura para ser completamente insegura. Está casi más allá de los límites de la posibilidad de que cualquier aplicación escrita sobre el manejo de cadenas fundamentalmente inseguras sea 100% perfecta, lo que significa que será insegura.

El problema es que los costos de la inseguridad no se cargan al destinatario correcto (la empresa que vende la aplicación casi nunca tendrá que reembolsar el precio de compra), o no son claramente visibles en el momento en que se toman las decisiones ("Tenemos que enviar en marzo pase lo que pase "). Estoy bastante seguro de que si considerara los costos a largo plazo y los costos para sus usuarios en lugar de las ganancias de su empresa, escribir en C o lenguajes relacionados sería mucho más costoso, probablemente tan costoso que claramente es la elección incorrecta en muchos campos donde hoy en día la sabiduría convencional dice que es una necesidad. Pero eso no cambiará a menos que se introduzca una responsabilidad de software mucho más estricta, lo que nadie en la industria quiere.


-1: Culpar a la administración como la raíz de todo mal no es particularmente constructivo. Ignorando la historia un poco menos. La respuesta está casi redimida por la última oración.
mattnz

Los usuarios interesados ​​en la seguridad y dispuestos a pagar por ella podrían presentar una responsabilidad de software más estricta. Podría decirse que podría introducirse con sanciones severas por infracciones de seguridad. Una solución basada en el mercado funcionaría si los usuarios estuvieran dispuestos a pagar por la seguridad, pero no lo hacen.
David Thornley

4

Uno de los grandes poderes del uso de C es que le permite manipular la memoria de la forma que mejor le parezca.

Una de las grandes debilidades del uso de C es que le permite manipular la memoria de la forma que mejor le parezca.

Hay versiones seguras de cualquier función insegura. Sin embargo, los programadores y el compilador no imponen estrictamente su uso.


2

¿Por qué los creadores de C no solucionan estos problemas reimplementando las bibliotecas?

Probablemente porque C ++ ya hizo esto y es compatible con el código C. Entonces, si desea un tipo de cadena seguro en su código C, simplemente use std :: string y escriba su código C usando un compilador C ++.

El subsistema de memoria subyacente puede ayudar a prevenir el desbordamiento del búfer mediante la introducción de bloques de protección y la verificación de validez de ellos, por lo que todas las asignaciones tienen 4 bytes de 'fefefefe' agregados, cuando estos bloques se escriben, el sistema puede arrojar un wobbler. No se garantiza que evite una escritura en la memoria, pero mostrará que algo salió mal y necesita ser reparado.

Creo que el problema es que las viejas rutinas strcpy, etc., todavía están presentes. Si se eliminaron a favor de strncpy, etc., eso ayudaría.


1
Eliminar Strcpy, etc. por completo haría que las rutas de actualización incrementales fueran aún más difíciles, lo que a su vez resultaría en que las personas no actualicen en absoluto. De la forma en que se hace ahora, puede cambiar a un compilador C11, luego comenzar a usar variantes _s, luego prohibir variantes que no sean _s, luego corregir el uso existente, durante cualquier período de tiempo que sea prácticamente viable.

-2

Es simple entender por qué el problema de desbordamiento no está solucionado. C tenía defectos en un par de áreas. En ese momento, esos defectos eran vistos como tolerables o incluso como una característica. Ahora, décadas después, esos defectos no se pueden solucionar.

Algunas partes de la comunidad de programación no quieren que se tapen esos agujeros. Solo mira todas las guerras de llamas que comienzan con cadenas, matrices, punteros, recolección de basura ...


55
LOL, respuesta terrible y equivocada.
Heath Hunnicutt

1
Para explicar por qué esta es una mala respuesta: C tiene muchos defectos, pero permitir desbordamientos de búfer, etc. tiene muy poco que ver con ellos, pero con los requisitos básicos del lenguaje. No sería posible diseñar un lenguaje para hacer el trabajo de C y no permitir desbordamientos de búfer. Partes de la comunidad no quieren renunciar a las capacidades que C les permite, a menudo con buenas razones. También hay desacuerdos sobre cómo evitar algunos de estos problemas, lo que demuestra que no tenemos una comprensión completa del diseño del lenguaje de programación, nada más.
David Thornley

1
@DavidThornley: Uno podría diseñar un lenguaje para hacer el trabajo de C pero hacerlo de manera que las formas idiomáticas normales de hacer las cosas al menos permitan que un compilador verifique los desbordamientos del búfer de manera razonablemente eficiente, si el compilador elige hacerlo. Hay una gran diferencia entre tener memcpy()disponible y que sea solo un medio estándar para copiar eficientemente un segmento de matriz.
supercat
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.