
Variante sintética de desbordamiento de búfer de pila CWE-120 de CVE-2020-8597 (pppd EAP) como objetivo de análisis estático de CodeQL
Un programa en C deliberadamente vulnerable, de ~170 líneas, usado como objetivo de análisis estático de CodeQL. Reproduce la clase de bug de CVE-2020-8597 — el desbordamiento de búfer de pila en rhostname de EAP en pppd (CWE-120) — en un programa que no comparte ninguno de los nombres de funciones, la profundidad de llamadas ni la estructura de despacho de pppd.
El objetivo es una prueba de generalidad: una consulta de CodeQL escrita para detectar el bug de pppd también debe dispararse en este programa, sin ediciones. Si lo hace, la consulta expresa la clase de bug en lugar de la forma del código original.
Este programa es intencionalmente inseguro y existe solo para su análisis. No lo despliegues. El bug que replica es público (CVE-2020-8597, divulgado en 2020).
Una longitud derivada del atacante se copia en un búfer de tamaño fijo, sin ninguna guarda que relacione esa longitud con el tamaño del búfer.
En todos los casos hay presente una comprobación de límites — simplemente no logra relacionar las dos cantidades que importan. Hay exactamente dos formas de equivocarse en eso, y el programa contiene una de cada una:
sizeof(dest). Evita una sobrelectura, no hace nada
respecto a la sobreescritura. (handle_hello)sizeof(dest) — se ve exactamente
como un límite de búfer — pero restringe una variable distinta de la usada como
longitud de copia. (handle_stat)La segunda es la más difícil, y es lo que es la comprobación muerta de pppd
vallen >= len + sizeof(rhostname): una comparación que menciona el
tamaño del destino mientras restringe algo que no es la longitud de copia. Una
consulta que solo pregunta "¿alguna comparación aquí menciona sizeof(dest)?" queda
silenciada por ella.
| pppd / CVE-2020-8597 | este proyecto | |
|---|---|---|
| Origen | read() sobre el fd PPP | recvfrom() sobre un socket UDP |
| Despacho | struct protent *protocols[] global, coincidencia lineal por n.º de protocolo | const struct frame_op ops[] local al archivo, coincidencia lineal por etiqueta de 1 byte |
| Profundidad hasta el sink | get_input → (*input) → eap_input → eap_request | dispatch_frame → (*handle) → handle_hello |
| Destino | char rhostname[256] | char name[64] |
| Guarda incorrecta | vallen acotado por el len del paquete | vlen acotado por el plen de la trama |
Ambos conservan la única propiedad que hace que esto sea flujo de datos, y no grep: una llamada indirecta a través de una tabla de punteros a función entre el origen y el sink.
| Manejador | Línea | Comprobación presente | Veredicto |
|---|---|---|---|
handle_hello() | sink en :80 | vlen > plen - 2 — valor correcto, límite incorrecto | debe dispararse |
handle_echo() | copia en :106 | vlen >= sizeof(buf) — ambos correctos | debe permanecer silencioso — control negativo |
handle_stat() | sink en :145 | hlen >= sizeof(report) — límite correcto, valor incorrecto | debe dispararse |
handle_stat es el caso discriminante. Su comprobación nombra sizeof(report), así que una
consulta que acepta cualquier comparación que mencione el tamaño del destino lo trata como
protegido y pasa por alto el bug. Detectarlo requiere comparar el valor que se
comprueba contra el valor usado como longitud de copia — numeración global de valores.
Elimina eso de la consulta y este manejador se convierte en un falso negativo mientras
todos los demás sitios conservan su veredicto.
Un datagrama UDP = una trama:
[ type : 1 ] [ length : 2, big-endian ] [ value : length bytes ]
type 0x01 → hello, 0x02 → echo, 0x03 → stat. Una trama hello con una longitud
declarada entre 65 y ~2045 desborda name[64]. Una trama stat lleva en su lugar dos
longitudes de un byte — una longitud de cabecera y una longitud de cuerpo — y cualquier longitud
de cuerpo superior a 32 desborda report[32], diga lo que diga la longitud de cabecera.
make # gcc -Wall -Wextra -O0 -g -o tlv_server tlv_server.c
Linux/POSIX (sockets BSD). Compila limpio sin advertencias.
CodeQL rastrea una compilación real, así que compila desde limpio:
make clean
codeql database create db --language=cpp --command="make"
# o, sin el paso de limpieza:
codeql database create db --language=cpp --command="make -B"
Luego ejecuta la consulta de la Parte 3 contra db; debería reportar el memcpy en
handle_hello y el de handle_stat, y permanecer silenciosa en handle_echo.
La consulta y sus instrucciones de ejecución están en codeql/.
El código fuente cambia cada vez que se añade un manejador, así que reconstruye la base de datos —
CodeQL captura una instantánea del código en el momento de database create y un db/ existente
no verá el código nuevo.