¿Por qué los programas escritos en C y C ++ son tan vulnerables a los ataques de desbordamiento?

131

Cuando observo las vulnerabilidades de los últimos años relacionadas con las implementaciones, veo que muchas de ellas son de C o C ++, y muchas de ellas son ataques de desbordamiento.

  • Heartbleed fue un desbordamiento de búfer en OpenSSL;
  • Recientemente, se encontró un error en glibc que permitía desbordamientos de búfer durante la resolución de DNS;

esos son los que puedo pensar ahora mismo, pero dudo que estos fueran los únicos que A) son para el software escrito en C o C ++ y B) se basan en un desbordamiento de búfer.

Especialmente en lo que respecta al error de glibc, leí un comentario que dice que si esto ocurriera en JavaScript en lugar de en C, no habría habido ningún problema. Incluso si el código fue simplemente compilado para Javascript, no habría sido un problema.

¿Por qué C y C ++ son tan vulnerables a los ataques de desbordamiento?

    
pregunta Nzall 23.02.2016 - 15:37
fuente

8 respuestas

169

C y C ++, contrariamente a la mayoría de los otros idiomas, tradicionalmente no comprueba los desbordamientos. Si el código fuente dice que se deben colocar 120 bytes en un búfer de 85 bytes, la CPU con mucho gusto lo hará. Esto se relaciona con el hecho de que mientras C y C ++ tienen una noción de array , esta noción es solo de compilación. En el momento de la ejecución, solo hay punteros, por lo que no hay un método de tiempo de ejecución para verificar el acceso a una matriz con respecto a la longitud conceptual de esa matriz.

Por el contrario, la mayoría de los otros idiomas tienen una noción de matriz que sobrevive en tiempo de ejecución, por lo que el sistema de tiempo de ejecución puede verificar sistemáticamente todos los accesos de matriz. Esto no elimina los desbordamientos: si el código fuente solicita algo sin sentido, como escribir 120 bytes en una matriz de longitud 85, todavía no tiene sentido. Sin embargo, esto activa automáticamente una condición de error interno (a menudo una "excepción", por ejemplo, un ArrayIndexOutOfBoundException en Java) que interrumpe la ejecución normal y no permite que el código continúe. Esto interrumpe la ejecución y, a menudo, implica un cese del procesamiento completo (el subproceso muere), pero normalmente evita la explotación más allá de una simple denegación de servicio.

Básicamente, las explotaciones de desbordamiento de búfer requieren que el código haga el desbordamiento (leer o escribir más allá de los límites del búfer al que se accede) y para seguir haciendo cosas más allá de ese desbordamiento. La mayoría de los idiomas modernos, al contrario de C y C ++ (y algunos otros, como Forth o Assembly), no permiten que realmente se produzca el desbordamiento y, en cambio, disparan al ofensor. Desde el punto de vista de la seguridad, esto es mucho mejor.

    
respondido por el Thomas Pornin 23.02.2016 - 15:48
fuente
56

Tenga en cuenta que existe una cierta cantidad de razonamiento circular: los problemas de seguridad están frecuentemente vinculados a C y C ++. Pero, ¿cuánto de eso se debe a las debilidades inherentes de estos idiomas, y cuánto de eso se debe a que son simplemente los lenguajes en los que la mayoría de la infraestructura de la computadora está escrita en?

C está destinado a ser "un paso adelante del ensamblador". No hay límites de verificación aparte de lo que usted mismo implementó, para exprimir el último ciclo de reloj de su sistema.

C ++ ofrece varias mejoras sobre C, la más relevante para la seguridad son sus clases de contenedor (por ejemplo, <vector> y <string> ) que le permiten manejar datos sin tener que manejar manualmente la memoria también. Sin embargo, debido a que es una evolución de C en lugar de un lenguaje completamente nuevo, también proporciona la mecánica de administración de memoria manual de C, por lo que si insiste en dispararse a sí mismo. el pie, C ++ no hace nada para evitarte.

Entonces, ¿por qué cosas como SSL, bind o kernels del sistema operativo todavía están escritas en estos idiomas?

Debido a que pueden pueden modificar la memoria directamente, lo que los hace especialmente adecuados para cierto tipo de aplicaciones de alto nivel y alto rendimiento (como cifrado, búsquedas de tablas DNS, controladores de hardware ... o Java). VMs, para el caso ;-)).

Por lo tanto, si se infringe un software relevante para la seguridad, la posibilidad de que se escriba en C o C ++ es alta, simplemente porque la mayoría del software relevante para la seguridad está escrito en C o C ++, generalmente por razones históricas y / o de rendimiento. Y si está escrito en C / C ++, el vector de ataque principal es la saturación del búfer.

Si fuera un idioma diferente, sería un vector de ataque diferente, pero estoy seguro de que también habría violaciones de seguridad.

Explotar el software C / C ++ es más fácil que explotar, por ejemplo, el software Java. La misma forma en que explotar un sistema Windows es más fácil que explotar un sistema Linux: el primero es ubicuo, bien entendido (es decir, vectores de ataque bien conocidos, cómo encontrarlos y cómo explotarlos), y un muchas personas están buscando en busca de vulnerabilidades donde la relación recompensa / esfuerzo es alta.

Eso no significa que este último sea inherentemente seguro (seguro er , quizás, pero no seguro ). Significa que, siendo el objetivo más difícil con menores beneficios, los Bad Boys no están perdiendo tanto tiempo en él todavía.

    
respondido por el DevSolar 23.02.2016 - 17:12
fuente
37

En realidad, "heartbleed" no era realmente un desbordamiento de búfer. Para hacer las cosas más "eficientes", ponen muchos búferes más pequeños en un búfer grande. El búfer grande contenía datos de varios clientes. El error leía bytes que no debía leer, pero en realidad no leía datos fuera de ese gran búfer. Un lenguaje que verificaba los desbordamientos de búfer no lo habría evitado, porque alguien se desvió de su camino o impidió que tales comprobaciones encontraran el problema.

    
respondido por el gnasher729 23.02.2016 - 16:46
fuente
25

Primero, como han mencionado otros, C / C ++ a veces se caracteriza como un macro ensamblador glorificado: está destinado a ser "cercano al hierro", como un lenguaje para la programación a nivel de sistema.

Por ejemplo, el lenguaje me permite declarar una matriz de longitud cero como marcador de posición cuando, de hecho, puede representar una sección de longitud variable en un paquete de datos o el comienzo de una región de longitud variable en la memoria que se utiliza para comunicarse con una pieza de hardware.

Desafortunadamente, también significa que C / C ++ es peligroso en las manos equivocadas; Si un programador declara una matriz de 10 elementos y luego escribe en el elemento 101, el compilador lo compilará, el código se ejecutará felizmente, eliminando cualquier cosa que se encuentre en esa ubicación de memoria (código, datos, pila, quién sabe).

Segundo, C / C ++ es idiosincrásico. Un buen ejemplo son las cadenas, que son básicamente matrices de caracteres. Pero cada constante de cadena lleva un carácter de terminación extra, invisible. Esta ha sido la causa de innumerables errores, ya que (especialmente, pero no exclusivamente), los programadores novatos a menudo no asignan ese byte extra necesario para la terminación nula.

Tercero, C / C ++ es en realidad bastante antiguo. El lenguaje surgió en un momento en que los ataques externos a un sistema de software eran básicamente inexistentes. Se esperaba que los usuarios fueran confiables y cooperativos, no hostiles, ya que su objetivo era hacer que el programa funcionara, no bloquearlo.

Es por eso que la biblioteca estándar de C / C ++ contiene muchas funciones que son inherentemente inseguras. Tome strcpy (), por ejemplo. Felizmente copiará cualquier cosa hasta un carácter nulo de terminación. Si no encuentra un carácter nulo de terminación, seguirá copiando hasta que el infierno se congele, o más probablemente, hasta que sobrescriba algo vital y el programa se bloquee. Esto no fue un problema en los viejos tiempos, cuando no se esperaba que un usuario ingresara en un campo reservado para, digamos, un código postal, 16000 caracteres de basura seguidos de un conjunto de bytes especialmente diseñados para ser ejecutados después de que la pila fue destruida y el procesador reanudó la ejecución en la dirección incorrecta.

Sólo para estar seguros, C / C ++ no es el único lenguaje idiosincrásico que existe. Otros sistemas tienen diferentes comportamientos idiosincrásicos, pero pueden ser igual de malos. Tome lenguajes de programación back-end como PHP, y lo fácil que es escribir código que permita la inyección de SQL.

Al final, si les damos a los programadores las herramientas poderosas que necesitan para hacer su trabajo, pero sin la capacitación y el conocimiento adecuados sobre el entorno de seguridad, sucederán cosas malas sin importar qué lenguaje de programación se use.

    
respondido por el Viktor Toth 24.02.2016 - 02:49
fuente
4

Probablemente tocaré algunas cosas que ya respondieron algunas de las otras respuestas ... pero ... me parece que la pregunta es errónea y "vulnerable".

Según lo planteado, la pregunta es asumir mucho sin comprender los problemas subyacentes. C / C ++ no son "más vulnerables" que otros idiomas. Más bien, ponen la mayor parte del poder de los dispositivos informáticos y la responsabilidad de usar ese poder, directamente en las manos del programador. Entonces, la realidad de la situación es que muchos programadores escriben código que es vulnerable a la explotación, y como C / C ++ no hace todo lo posible para proteger al programador de sí mismo como lo hacen algunos lenguajes, su código es más vulnerable. Esto no es un problema de C / C ++, ya que los programas escritos en lenguaje ensamblador tendrían los mismos problemas, por ejemplo.

La razón por la que tal programación de bajo nivel puede ser tan vulnerable es porque hacer cosas como la verificación de los límites de la matriz / búfer puede ser computacionalmente costoso, y muy a menudo es innecesario cuando se programa defensivamente. Imagine, por ejemplo, que está escribiendo código para algún motor de búsqueda importante, que tiene que procesar billones de registros de base de datos en un abrir y cerrar de ojos, para que el usuario final no se aburra ni se frustre mientras "carga de página ..." se visualiza. No desea que su código continúe revisando los límites de matriz / búfer cada vez que pasa por el bucle; Si bien puede tomar nanosegundos realizar una comprobación de este tipo, lo cual es trivial si solo procesa diez registros, puede agregar muchos segundos o minutos cuando recorre miles de millones o billones de registros.

Entonces, en cambio, confía en que la fuente de datos (por ejemplo, el "bot web" que escanea los sitios web y coloca los datos en la base de datos) ya verificó los datos. Esto no debe ser una suposición irrazonable; Para un programa típico, desea verificar los datos en input , de modo que el código que procesa los datos pueden operar a la máxima velocidad. Muchas bibliotecas de código también toman este enfoque. Algunos incluso documentan que esperan que el programador ya haya comprobado los datos antes de llamar a las funciones de la biblioteca para actuar sobre los datos.

Sin embargo, desafortunadamente, muchos programadores no programan de manera defensiva, y solo asumen que los datos deben ser válidos y estar dentro de límites / parámetros seguros. Y esto es lo que los atacantes explotan.

Algunos lenguajes de programación están diseñados de tal manera que tratan de proteger al programador de prácticas de programación deficientes al insertar automáticamente verificaciones adicionales en el programa generado, que el programador no escribió explícitamente en su código. Nuevamente, esto está bien cuando solo vas a recorrer el código unos cientos de veces o menos. Pero cuando está pasando por miles de millones o billones de iteraciones, se acumulan largos retrasos en el procesamiento de datos, lo que puede volverse inaceptable. Por lo tanto, es una compensación cuando se elige qué idioma usar para un fragmento de código en particular, y con qué frecuencia y dónde se verifican las condiciones potencialmente peligrosas / explotables dentro de los datos.

    
respondido por el C. M. 26.02.2016 - 00:53
fuente
2

Básicamente los programadores son personas perezosas (incluyéndome a mí). Hacen cosas como usar gets () en lugar de fgets () y la definición de buffers de i / o en la pila y no buscan las formas en que la memoria se puede sobrescribir involuntariamente (bien involuntariamente para el programador, intencionalmente para el hacker). p>     

respondido por el Bing Bang 23.02.2016 - 18:37
fuente
1

Existe una gran cantidad de código C existente que realiza la escritura sin marcar en los búferes. Algo de esto está en las bibliotecas. Este código es potencialmente inseguro si cualquier estado externo puede cambiar la longitud escrita, y de lo contrario es muy inseguro.

Hay una mayor cantidad de código C existente que hace escritura limitada a los búferes. Si el usuario de dicho código comete un error matemático y deja que se escriba más de lo que debería, esto es tan explotable como el anterior. No hay garantía de tiempo de compilación de que las matemáticas se hagan bien.

También hay una gran cantidad de código C existente que realiza lecturas basadas en compensaciones en la memoria. Si el desplazamiento no se comprueba como válido, esto puede filtrar información.

El código C ++ se usa a menudo como un lenguaje de alto nivel para interoperar con C, por lo que se siguen muchos conceptos de C, y los errores de comunicación con las API de C son comunes.

Los estilos de programación en C ++ que evitan que existan tales excesos, pero solo toma 1 error para permitir que ocurran.

Además, el problema de los punteros colgantes, donde los recursos de memoria se reciclan y el puntero ahora apunta a la memoria con una vida / estructura diferente a la que tenía originalmente, permite algunos tipos de ataques y filtraciones de información.

Este tipo de errores: los errores de "cerco", los errores de "puntero colgante" - son tan comunes y tan difíciles de eliminar por completo, que muchos idiomas se desarrollaron con sistemas diseñados explícitamente para prevenir que pasen.

No es sorprendente que en los idiomas diseñados para eliminar estos errores, estos errores no se produzcan con la misma frecuencia. A veces todavía ocurren: o el motor que ejecuta el idioma tiene el problema, o se configura una situación manual que coincide con el entorno del caso C / C ++ (reutilizando objetos en un grupo, usando un búfer común común subdividido por el consumidor, etc.) ). Pero como esos usos son menos frecuentes, el problema ocurre con menos frecuencia.

Cada asignación dinámica, cada uso de búfer, en C / C ++ ejecuta estos riesgos. Y ser perfecto no es posible.

    
respondido por el Yakk 26.02.2016 - 22:41
fuente
0

Los lenguajes más utilizados (Java y Ruby, por ejemplo) se compilan en el código que se ejecuta en una máquina virtual. La máquina virtual está diseñada para segregar el código de máquina, los datos y, por lo general, la pila. Esto significa que las operaciones de lenguaje regulares no pueden cambiar el código ni redirigir el flujo de control (a veces hay API especiales que pueden hacer esto, por ejemplo, para la depuración).

Por lo general,

C y C ++ se compilan directamente en el lenguaje de máquina nativo de la CPU; esto brinda beneficios de rendimiento y flexibilidad, pero significa que un código erróneo puede sobrescribir la memoria del programa o la pila y, por lo tanto, ejecutar instrucciones que no están en el programa original.

Esto suele ocurrir cuando un búfer se sobrepasa (tal vez deliberadamente) en C ++. En Java o Ruby, por el contrario, una saturación del búfer causará inmediatamente una excepción y no podrá (excepto los errores de VM) sobrescribir el código o cambiar el flujo de control.

    
respondido por el Rich 27.02.2016 - 00:07
fuente

Lea otras preguntas en las etiquetas