Una introducción sencilla a las artes oscuras de las vulnerabilidades en C y C++

Aviad Hahami
15 de abril de 2022
0 minutos de lectura¡Lumos!
Como Snyk anunció compatibilidad con dependencias no administradas (en su mayoría bibliotecas de C y C++), pensamos que sería útil presentarle a nuestra comunidad ajena a C algunos peligros comunes y de alto riesgo que acechan el mundo del lenguaje C. Considera este artículo como una “guía para principiantes” sobre las vulnerabilidades en C y C++, cómo son, qué problemas podrían causar y cómo corregirlas.
Grandes vulnerabilidades y dónde encontrarlas
C y C++ se consideran lenguajes de programación de bajo nivel (durante todo el artículo usaremos el término “C” para referirnos a ambos). Si comparamos los lenguajes de bajo nivel con los lenguajes de alto nivel (como JS y Python), podemos ver que la principal diferencia es cómo se administra la memoria de la máquina. Mientras que los lenguajes de alto nivel administran la asignación, el consumo y la liberación de memoria, en C la responsabilidad sobre la administración de la memoria recae en el desarrollador. De esta forma, el desarrollador puede actuar con precisión para optimizar las implementaciones y el rendimiento de las rutinas, pero esto podría introducir diversos problemas que son específicos de estos lenguajes.
Desbordamiento del búfer (CWE-121) y escritura fuera de límites (CWE-787)
El desbordamiento del búfer es quizá la vulnerabilidad relativa a la memoria más conocida que existe. Aunque explotar desbordamientos de búferes puede ser complicado, la vulnerabilidad es muy sencilla: se desborda el búfer que se había asignado. En la definición de “desbordamiento del búfer”, suele hacerse referencia a la explotación concreta de la vulnerabilidad, pero “desbordamiento del búfer de pila” y “escritura fuera de límites” son, en esencia, la misma vulnerabilidad. Por este motivo, las mencionaremos juntas.
Explotar esta vulnerabilidad puede ser difícil porque no siempre se puede “escribir código” en la pila (o el montón), aunque se desborde. A lo largo de la historia, se intentó resolver y mitigar este problema. Sin embargo, se implementaron muy pocos mecanismos de defensa, que, para defender el código, dependían de demasiados factores y podían tener en cuenta el SO, las características del núcleo, el compilador y muchas otras cuestiones. A modo de ejemplo, existen mecanismos como ASLR (aleatoriedad en la disposición del espacio de direcciones), valores controlados de pilas y DEP (prevención de ejecución de datos). Todos estos tienen como objetivo evitar errores de corrupción de la memoria, como el desbordamiento del búfer. Durante el tiempo de ejecución, si alguno de estos mecanismos falla, el SO detendrá la ejecución y mostrará un error SEGFAULT, lo que afectará el proceso de explotación.
Me gustaría brindar un ejemplo de esta vulnerabilidad y cómo se explota. Veamos este programa de C:
1#include <stdlib.h>
2#include <unistd.h>
3#include <stdio.h>
4
5int main(int argc, char **argv)
6 volatile int modified;
7 char buffer[64];
8
9 modified = 0;
10 gets(buffer);
11
12 if(modified != 0) {
13 printf("you have changed the 'modified' variable\n");
14 } else {
15 printf("Try again?\n");
16 }
17}
Ejemplo cortesía de 0xRick,tal como se ve en su blog.
Si observamos el código (aunque no sepamos C), podemos notar que buffer
se estima como un búfer de 64 caracteres de largo y que modified
es un entero. Hasta ahora, todo bien.
También podemos notar que en la línea 9, modified
recibe el valor 0 y, en la línea 12, se verifica si el valor es 0 o no. Si aún no te percataste, el objetivo es lograr que no sea cero. En la línea 10, se usa la función gets
para leer desde stdin (es decir, la entrada del usuario) a la variable buffer
. Para quienes no conocen la función gets
, esta lee desde stdin a un determinado búfer hasta que se lea el carácter de una nueva línea (\\n
).
Sabemos (porque ya viste el video de YouTube que aparece más arriba) que la pila crece de modo descendente y que, debido a la estructura de datos de la pila, modified
está “debajo” de buffer
en memoria. Entonces, si se escriben más de 64 caracteres en buffer
, se sobrescribirá el valor de modified
. Esa es la magia del desbordamiento del búfer.
Este ejemplo no causa demasiado daño; después de todo, se trata de una demostración. Pero imagina qué podría suceder si sobrescribieras el valor de la variable de una contraseña o de la dirección URL que indica la máquina. Mitigar esta vulnerabilidad es sencillo: se recomienda usar la función fgets
, que también verifica la longitud de la entrada y no solo la existencia de un “carácter de fin de secuencia”.
Uso después de liberar (CWE-416)
Las vulnerabilidades “uso después de liberar” son justamente eso: ocurren cuando usas la referencia a una variable luego de que se liberó. Esta vulnerabilidad surge de un error en la administración de la memoria relativo al flujo del software y cuando la variable se usa después de que se liberó. Por este motivo, la aplicación ejecuta una acción imprevista o tiene un efecto residual inesperado.
Para ver la vulnerabilidad y cómo se explota en acción, recomendamos el video de LiveOverflow donde se explota una vulnerabilidad UAF (uso después de liberar) en un desafío.
Desbordamiento o subdesbordamiento de enteros (CWE-190 y CWE-191)
El desbordamiento y el subdesbordamiento de enteros son dos tipos de errores similares (que luego se convertirán en vulnerabilidades) y ocurren por la representación numérica en las computadoras.
Aunque no nos adentraremos en los tipos de variables que existen y cómo se representan exactamente los números en las computadoras, sí mencionaremos que existen dos métodos principales de representación de números: con signo y sin signo. Las variables con signo pueden almacenar números negativos y positivos, pero las variables sin signo solo pueden almacenar números positivos.
En un desbordamiento de enteros, el valor que se solicita que almacene la máquina es mayor que el valor máximo que puede almacenar. Por otro lado, en un subdesbordamiento de enteros, se solicita a la máquina que almacene un valor menor que el valor mínimo; por ejemplo, se intenta almacenar un número negativo en una variable de entero sin signo.
En ambos casos, el resultado será similar. El valor se ajustará (es decir, empezará desde el comienzo o el final del rango posible de almacenamiento), por lo que se modificará su valor. En el caso de un desbordamiento, el valor ajustado comenzará en 0, mientras que en un subdesbordamiento comenzará en el valor máximo almacenable; es decir, el entero sin signo de 8 bits se ajustará a 256 (decimal).
En el siguiente diagrama, se observan los tipos de variables y qué valores pueden almacenar.

En OpenSSH v3.3, se observó un ejemplo de cómo se explotó un desbordamiento de enteros que provocó un desbordamiento del búfer (CVE-2002-0639).
Veamos este fragmento de código:
1nresp = packet_get_int();
2if (nresp > 0) {
3 response = xmalloc(nresp*sizeof(char*));
4 for (i = 0; i < nresp; i++)
5 response[i] = packet_get_string(NULL);
6}
Imaginemos que nresp es 1073741824
y sizeof(char\*)
es 4 (que suele ser el tamaño del puntero); nresp*sizeof(char*)
provocará un desbordamiento porque se ajustará y el valor resultante será 0. Por lo tanto, xmalloc()
recibirá y asignará un búfer de 0 bytes. El siguiente bucle provocará un desbordamiento de búfer de montón cuando se escriba en una ubicación de memoria no asignada, la cual, a su vez, podría ser usada por cualquier atacante para ejecutar el código que desee.
Desreferenciamiento de puntero nulo (CWE-467)
Al desreferenciar, se realiza una acción en un valor de una dirección. Para explicar esta vulnerabilidad con mayor claridad, observemos un ejemplo:
1#include <stddef.h>
2
3void main(){
4 int *x = NULL;
5 *x = 1;
6}
Según el estándar de C, ejecutar el código anterior podría provocar un “comportamiento indefinido”. Sin embargo, en la mayoría de las implementaciones, esto generará un error SEGFAULT, lo que significa que el software intentó acceder a un área de memoria restringida y, por lo tanto, cometió una infracción de acceso a memoria. En consecuencia, el sistema operativo finaliza la ejecución del software.
Lectura fuera de límites (CWE-125)
Una lectura fuera de límites ocurre cuando se lee “por fuera del búfer o la ubicación designada”. Como resultado, esta vulnerabilidad podría, en el mejor de los casos, bloquear el sistema o, en el peor, revelar información de la aplicación (como las contraseñas de otros usuarios).
Para mostrar un ejemplo de esta vulnerabilidad, aquí presentamos un fragmento de código de la aplicación PureFTPd. Observa la línea 17. Si la longitud de s1
es mayor que la longitud de s2
, entonces, dado que la línea 8 se itera en toda la longitud de s1
-, la información a la que se accede en la línea 10 superará los límites de s2
. En consecuencia, esta acción leerá fuera de los límites.
1int pure_memcmp(const void *const b1_, const void *const b2_, size_t len)
2 {
3 const unsigned char *b1 = (const unsigned char *) b1_;
4 const unsigned char *b2 = (const unsigned char *) b2_;
5 size_t i;
6 unsigned char d = (unsigned char) 0 U;
7 for (i = 0 U; i < len; i++)
8 {
9 d |= b1[i] ^ b2[i];
10 }
11 return (int)((1 &((d - 1) >> 8)) - 1);
12}
13
14int pure_strcmp(const char *const s1, const char *const s2)
15{
16 return pure_memcmp(s1, s2, strlen(s1) + 1 U);
17}
El error se identificó como CVE-2020-9365. Puedes leer el informe aquí.
Conclusión y próximos pasos
Esperamos que ahora tengas una idea general de cómo lucen las vulnerabilidades en C y C++, y dónde suelen aparecer. Aunque, al principio, algunas de estas explotaciones podrían parecer complejas, si las comprendes en más detalle, aumentará tu entendimiento general de las características fundamentales del software y podrías evitar errores críticos.
Como vimos en la explicación de desbordamiento de enteros más arriba, estas vulnerabilidades pueden encadenarse, lo que creará una cadena débil vulnerable ante una explotación maliciosa.
Con la publicación de Snyk Open Source para C y C++, compartiremos más contenido sobre temas que te indicarán cómo encontrar, explotar y corregir vulnerabilidades en C y C++.
Protege las dependencias de código abierto
La herramienta de Snyk pensada para desarrolladores permite corregir dependencias de código abierto vulnerables y sus dependencias transitivas con un solo clic desde una solicitud de cambios.