Una introducción sencilla al oscuro arte de las vulnerabilidades de C/C++
Aviad Hahami
15 de abril de 2022
0 minutos de lectura¡Lumos!
Como Snyk anuncia su compatibilidad con dependencias no administradas (principalmente bibliotecas de C/C++), pensamos que sería útil presentar a nuestra comunidad ajena a C algunos riesgos comunes y de alto impacto que acechan en el mundo de C (¿entiendes?). Considera esto una «guía para principiantes» sobre las vulnerabilidades de C y C++, cómo se manifiestan, qué problemas pueden causar y cómo corregirlas.
Vulnerabilidades magníficas y dónde encontrarlas
C y C++ (en el resto de esta publicación usaremos «C» para referirnos a ambos) se consideran lenguajes de programación de bajo nivel. Al comparar los lenguajes de bajo nivel con los de alto nivel (JS, Python, etc.), vemos que la principal diferencia está en cómo se administra la memoria de la máquina. Mientras que los lenguajes de alto nivel administran la asignación, el uso y la liberación de memoria, en C esa responsabilidad recae en el desarrollador. Esto permite que los desarrolladores optimicen con precisión el rendimiento y la implementación de sus rutinas, pero también puede introducir diversos problemas propios de este ámbito de lenguajes.
Desbordamientos de búfer (CWE-121) y escritura fuera de límites (CWE-787)
Los desbordamientos de búfer probablemente sean la vulnerabilidad relacionada con la memoria más conocida. Aunque explotarlos puede ser complicado, la vulnerabilidad en sí es simple: desbordas el búfer que te asignaron. En general, «desbordamientos de búfer» suele referirse a la explotación propiamente dicha de la vulnerabilidad, pero «desbordamiento de búfer basado en pila» y «escritura fuera de límites» son esencialmente la misma debilidad (por eso hablaremos de ambas a la vez).
La explotación puede ser difícil porque no siempre puedes simplemente «escribir código» en la pila (o el heap), aunque la desbordes. Con el tiempo, se han hecho intentos para reforzar y mitigar este problema, y se han introducido algunos mecanismos de defensa. Estos mecanismos dependen de muchos factores y pueden tener en cuenta tu sistema operativo, las funciones del kernel, el compilador y otros elementos para proteger tu código. Algunos ejemplos son ASLR (aleatorización del espacio de direcciones), los canarios de pila y DEP (prevención de ejecución de datos). Todos buscan prevenir errores de corrupción de memoria, como los desbordamientos de búfer. En tiempo de ejecución, si falla alguno de estos mecanismos, el sistema operativo detendrá la ejecución y generará un SEGFAULT, lo que hace que el proceso de explotación sea menos sencillo.
Veamos un ejemplo de este tipo de vulnerabilidad y cómo explotarla. Considera el siguiente programa en C:
Ejemplo cortesía de 0xRick, como se ve en su blog.
Al revisar el código (incluso sin saber C), podemos ver que buffer se asigna como un búfer de 64 caracteres y que modified es un entero. Genial.
También vemos en la línea 9 que modified se establece en 0 y, en la línea 12, verificamos si su valor es 0. Si todavía no lo adivinaste, nuestro objetivo es cambiarlo. En la línea 10, usamos la función gets para leer de stdin (=entrada del usuario) en la variable buffer. Si no conoces gets: lee de stdin en un búfer determinado hasta que lee un carácter de nueva línea (\n).
Como sabemos (porque viste el video de YouTube enlazado arriba) que la pila crece hacia una dirección menor y, debido a la estructura de datos de la pila, modified está «debajo» de buffer en la memoria, si escribimos más de 64 caracteres en buffer, ¡empezaremos a sobrescribir el valor de modified! Y ese es el mágico desbordamiento de búfer.
Este ejemplo en particular no es muy dañino (ya que es una demostración). Pero imagina qué podría ocurrir si sobrescribieras el valor de una variable de contraseña o la URL a la que se dirige la máquina. La mitigación es sencilla: se recomienda usar la función fgets, que también verifica la longitud de la entrada y no solo la presencia del «carácter de fin de secuencia».
Uso después de liberar (CWE-416)
Las vulnerabilidades de uso después de liberar se explican por sí solas: ocurren cuando usas una referencia a una variable después de liberarla. Esta vulnerabilidad se debe a un error en la administración de memoria relacionado con el flujo del software: la variable se usa después de borrarse, lo que provoca una acción inesperada o un efecto residual imprevisto en la aplicación.
Para ver esta vulnerabilidad y cómo explotarla en la práctica, te recomiendo el video de LiveOverflow sobre cómo explota una vulnerabilidad UAF en un desafío.
Desbordamiento/subdesbordamiento de enteros (CWE-190 y CWE-191)
Los desbordamientos y subdesbordamientos de enteros son dos tipos similares de errores (y, posteriormente, vulnerabilidades) que se producen por la forma en que se representan los números en las computadoras.
Aunque no profundizaré en los tipos de variables ni en cómo se representan exactamente los números en las computadoras, mencionaré que hay dos métodos principales para representar números: con signo y sin signo. Las variables con signo pueden representar números negativos y positivos, mientras que las variables sin signo almacenan solo números positivos.
Un desbordamiento de enteros significa que el valor que le pedimos a la máquina que almacene es mayor que el valor máximo que puede almacenar. Un subdesbordamiento de enteros significa que le pedimos a la máquina que almacene un valor menor que el valor mínimo (por ejemplo, pedirle a un entero sin signo que almacene un número negativo).
En ambos casos, el resultado será similar. El valor «dará la vuelta» (es decir, volverá al principio o al final del rango almacenable), por lo que cambiará. En un desbordamiento, el valor que da la vuelta comenzará desde 0; en un subdesbordamiento, desde el valor máximo almacenable (es decir, el entero sin signo de 8 bits dará la vuelta hasta 256 [decimal]).
Este diagrama muestra los tipos de variables y los valores que pueden contener:

En OpenSSH v3.3 se detectó un ejemplo de explotación de un desbordamiento de enteros que provocó un desbordamiento de búfer (CVE-2002-0639).
Considera el siguiente fragmento de código:
Supongamos que nresp es 1073741824 y que sizeof(char*) es 4 (el tamaño de puntero habitual). El resultado de nresp*sizeof(char*) se desborda (porque dará la vuelta y el valor resultante será 0). Por lo tanto, xmalloc() recibe y asigna un búfer de 0 bytes. El siguiente bucle provoca un desbordamiento de búfer en el heap al escribir en una ubicación de memoria no asignada, que, a su vez, un atacante podría usar para ejecutar código arbitrario.
Desreferencia de puntero nulo (CWE-467)
Desreferenciar consiste en realizar una acción sobre un valor en una dirección. Para explicar mejor esta vulnerabilidad, veamos un ejemplo:
Según el estándar de C, ejecutar el código anterior puede provocar un «comportamiento indefinido». Sin embargo, la mayoría de las implementaciones fallarán con SEGFAULT, lo que significa que el software intentó acceder a un área restringida de la memoria (y, por lo tanto, infringió las reglas de acceso a memoria). Esto hace que el sistema operativo cierre el software.
Lectura fuera de límites (CWE-125)
Una lectura fuera de límites ocurre cuando «lees fuera de la ubicación o el búfer designados». El resultado de esta vulnerabilidad puede ser una falla del sistema (en el mejor de los casos) o la divulgación de información de tu aplicación (por ejemplo, las contraseñas de otros usuarios), lo cual no es bueno.
Como ejemplo de esta vulnerabilidad, aquí tienes un fragmento de la aplicación PureFTPd. Considera la línea 17. Si la longitud de s1 es mayor que la de s2, entonces, como la línea 8 recorre la longitud de s1, la información a la que accederemos en la línea 10 excederá los límites de s2. Esto provocará una lectura fuera de límites.
Este error recibió el identificador CVE-2020-9365, y puedes leer el informe.
Conclusión y próximos pasos
A estas alturas, esperamos que tengas una idea general de cómo son las vulnerabilidades de C/C++, dónde suelen aparecer y qué formas pueden adoptar. Aunque algunas de estas explotaciones quizá no parezcan sencillas al principio, comprenderlas mejor te ayudará a entender los aspectos internos del software y puede ayudarte a evitar y prevenir errores críticos.
Como se muestra en la explicación anterior sobre el desbordamiento de enteros, estas vulnerabilidades pueden encadenarse entre sí y formar una cadena débil susceptible de explotación maliciosa.
Ahora que lanzamos la compatibilidad con C/C++ en Snyk Open Source, compartiremos más contenido sobre cómo encontrar, explotar y corregir vulnerabilidades de C y C++.
Protege tus dependencias de código abierto
Las herramientas de Snyk, diseñadas para desarrolladores, generan pull requests de corrección con un clic para dependencias vulnerables de código abierto y sus dependencias transitivas.



