Cómo se produjo un desbordamiento de heap en curl
(daniel.haxx.se)- CVE-2023-38545, revelada junto con el lanzamiento de curl 8.4.0, es un desbordamiento de búfer de heap que ocurre durante el manejo de proxies SOCKS5, una vulnerabilidad de gravedad HIGH poco común entre los problemas de seguridad de curl
- El problema se introdujo en 2020 durante el cambio del código de conexión SOCKS5 de una llamada bloqueante a una máquina de estados no bloqueante, y afecta desde curl 7.69.0
- La lógica existente que cambiaba a resolución local cuando, en modo de resolución remota de nombres, se encontraba un hostname de más de 255 bytes, terminó interactuando con la reinvocación de la máquina de estados, lo que permitía copiar un hostname demasiado largo a un búfer pequeño
- Para que el ataque sea viable, el cliente libcurl debe usar SOCKS5 con proxy-resolver-mode y redirección automática, y un servidor HTTPS controlado por el atacante debe devolver un
Location:HTTP 30x con un hostname de más de 16KB y hasta 64KB - Desde curl 8.4.0, si el hostname es demasiado largo ya no se cambia de resolución remota a local, sino que se devuelve un error, y además se añadió una prueba para bloquear este mismo escenario
CVE-2023-38545 y curl 8.4.0
- Junto con el lanzamiento de curl 8.4.0 se publicaron el aviso de seguridad y los detalles de CVE-2023-38545
- Este problema fue considerado el incidente de seguridad más grave en curl en mucho tiempo, y se le asignó severidad HIGH
- El fallo central es un desbordamiento de búfer de heap que ocurre bajo ciertas condiciones durante el manejo de conexiones a través de un proxy SOCKS5
SOCKS5 y los modos de resolución de nombres
- curl soporta SOCKS5 desde agosto de 2002
- SOCKS5 es un protocolo de proxy que establece comunicaciones de red a través de un servidor intermedio
- Puede usarse para establecer comunicaciones a través de Tor
- También puede usarse para acceder a internet desde dentro de una organización o empresa
- SOCKS5 tiene dos formas de resolver hostnames
- El cliente resuelve localmente el hostname y luego envía la dirección resuelta al proxy
- El cliente entrega el hostname completo al proxy y este lo resuelve de forma remota
El cambio de 2020 que introdujo la vulnerabilidad
- A comienzos de 2020, la función que conecta a un proxy SOCKS5 pasó de una llamada bloqueante a una máquina de estados no bloqueante
- Este cambio entró en master el 14 de febrero de 2020 y se incluyó en curl 7.69.0
- curl 7.69.0 fue la primera versión con esta mejora y, como resultado, también la primera vulnerable a CVE-2023-38545
- El objetivo del cambio era ofrecer una mejora más visible cuando muchas transferencias en paralelo pasaban todas por SOCKS5
El modo de resolución que se rompió en la máquina de estados
- La función de la máquina de estados se invoca repetidamente cada vez que entra más tráfico de red hasta que se establece la conexión
- La variable local
socks5_resolve_localal inicio de la función indica si curl resolverá el hostname por sí mismo o si le pasará el nombre al proxy - Esa variable se vuelve a configurar al inicio de cada llamada a la función según el modo del proxy
- La condición en el estado INIT fue la que creó el problema
- El campo de hostname de SOCKS5 solo permite un máximo de 255 bytes
- Si el hostname supera los 255 bytes, el proxy SOCKS5 no puede resolverlo
- El código anterior de curl, al encontrarse con un hostname demasiado largo en modo de resolución remota, cambiaba
socks5_resolve_localaTRUEpara pasar al modo de resolución local
- Si el usuario había solicitado resolución remota, curl no debía cambiar de modo sino fallar, pero ese comportamiento de cambio heredado siguió existiendo
Cómo ocurre el desbordamiento de heap
- Si el servidor SOCKS5 no responde lo bastante rápido y la máquina de estados no recibe más datos de red para avanzar, la función retorna
- Cuando luego llegan datos, se vuelve a llamar a la misma función de la máquina de estados
- En esa nueva llamada,
socks5_resolve_localse vuelve a configurar al inicio de la función según el modo del proxy- El valor que había sido cambiado a
TRUEen la llamada anterior debido al hostname demasiado largo no se conserva - El valor vuelve al estado en que el proxy debe resolver remotamente el nombre
- El valor que había sido cambiado a
- curl construye en un búfer de memoria la trama del protocolo que enviará al proxy y copia la información del destino
- Debido a ese valor de estado incorrecto, al intentar pasar el hostname demasiado largo tal cual, puede sobrescribirse memoria de heap vecina más allá del búfer de destino asignado
Tamaño del búfer y restricciones del hostname
- curl reutiliza el búfer de descarga general al construir la trama del protocolo que enviará al proxy
- Las condiciones del tamaño del búfer de descarga son las siguientes
- El valor predeterminado es 16KB
- La herramienta curl configura el búfer en 100KB
- Las aplicaciones pueden usar otros tamaños según lo solicitado
- El tamaño mínimo permitido es 1024 bytes
- Si el tamaño del búfer es menor que 65541 bytes, este desbordamiento es posible
- Cuanto más pequeño sea el búfer, mayor puede ser el tamaño del desbordamiento
- El hostname de una URL no tiene en la práctica un límite estricto de tamaño, pero el parser de URL de libcurl rechaza nombres de más de 65535 bytes
- DNS solo permite hostnames de hasta 253 bytes
- Los nombres válidos de más de 253 bytes son raros, y los de más de 1024 bytes son prácticamente inexistentes en el mundo real, así que el ataque requiere deliberadamente un hostname muy largo
- En el campo hostname de la URL solo pueden aparecer ciertos octetos, y el parser de URL rechaza valores de bytes inválidos
- Si libcurl fue compilado para usar una biblioteca IDN, esa biblioteca también puede rechazar hostnames inválidos
Condiciones para que el ataque funcione
- El atacante debe controlar un servidor HTTPS al que un cliente que usa libcurl acceda a través de un proxy SOCKS5 en proxy-resolver-mode
- El servidor malicioso debe poder devolver una redirección manipulada en una respuesta HTTP 30x
- El encabezado
Location:de esa redirección puede contener un hostname extremadamente largo con una forma como la siguienteLocation: https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/- La longitud del hostname debe ser de más de 16KB y hasta 64KB
- Si en el cliente libcurl está activado el seguimiento automático de redirecciones y el proxy SOCKS5 es lo bastante “lento” como para disparar este problema de variable local, el hostname manipulado se copia a un búfer demasiado pequeño
- Si se cumplen esas condiciones, se produce una escritura sobre memoria de heap adyacente y se concreta el desbordamiento de búfer de heap
Corrección y bug bounty
- Desde curl 8.4.0, si el hostname es demasiado largo ya no se cambia de resolución remota a local, sino que se devuelve un error
- También se añadió un caso de prueba dedicado para este mismo escenario
- Este problema fue reportado, analizado y corregido por Jay Satiro
- Esta vulnerabilidad recibió hasta ahora la mayor bug bounty pagada por curl
- 4,660 dólares para la persona que la reportó
- 1,165 dólares para el proyecto curl según la IBB policy
C y los lenguajes con seguridad de memoria
- Si curl hubiera estado escrito en un lenguaje con seguridad de memoria en lugar de C, este tipo de fallo no habría ocurrido
- Sin embargo, portar curl a otro lenguaje no está actualmente sobre la mesa
- El enfoque realista se reduce a dos caminos
- Permitir, usar y dar soporte a más dependencias escritas en lenguajes con seguridad de memoria
- Reemplazar gradualmente partes de curl, como en la adopción de hyper
- Ese desarrollo avanza hoy muy lentamente, y sus dificultades también quedan claras
- curl seguirá escrito en C en el futuro cercano
- Si se incluyen los dos CVE más recientes reportados en curl 8.4.0, es posible que 41% de las vulnerabilidades de seguridad encontradas hasta ahora en curl no se hubieran producido si se hubiera usado un lenguaje con seguridad de memoria
- Aun así, cuando se introdujo aproximadamente el 80% inicial de los problemas relacionados con C, Rust todavía no era una opción práctica para ese propósito
Un fallo descubierto 1315 días después
- El fallo permaneció en el código durante 1315 días
- Con un mejor conjunto de pruebas podría haberse detectado antes
- curl ejecuta repetidamente varios analizadores estáticos de código, pero ninguno logró encontrar el problema en esta función
- Haber distribuido un desbordamiento de heap en código presente en más de 20 mil millones de instalaciones no fue precisamente una experiencia recomendable
- El reporte y el proceso de manejo antes de la divulgación pública pueden revisarse en el HackerOne report
1 comentarios
Opiniones de Hacker News
Todavía me sorprende que tantos dispositivos dependan, en la práctica, de una biblioteca escrita en su mayor parte por una sola persona. La carga debe haber sido enorme, y eso también se nota en la frase de abajo:
“Al leer el código ahora, es imposible no ver el bug. Duele muchísimo aceptar que no me di cuenta de este error y que esa falla pasó 1315 días dentro del código sin ser descubierta. Lo siento. Soy solo un ser humano.”
Si Daniel llega a ver esto, creo que hay que agradecerle por su arduo trabajo y que no tiene absolutamente nada de qué disculparse. Al final, el código fuente estaba publicado para que cualquiera pudiera leerlo y revisarlo.
https://www.buzzfeed.com/chrisstokelwalker/the-internet-is-b...
“Este reporte parece completamente correcto, y duele profundamente.”
Y también en lo del blog: “No recomendaría la experiencia de distribuir un heap overflow en código que terminó en más de 2000 millones de instalaciones”. Es cruel tener que cargar con una responsabilidad tan grande a cambio de una compensación tan pequeña.
Personalmente, he pensado muchas veces en cuántas cosas se me escapan por más que intente concentrarme. Actividades como la revisión de código parecen una buena forma de entrenar esa atención o de desarrollar una conciencia más global y contextual. Por ejemplo, durante años pasé seguido por un lugar donde había un cantero muy bien cuidado por alguien; después de notarlo por primera vez, durante más o menos un año solía detenerme a mirarlo, pero hace unos meses me di cuenta por primera vez de que a apenas unos pies había otro cantero, como haciendo juego. Lo más probable es que ese cantero siempre haya estado ahí, pero como ni siquiera sabía que existía, no puedo estar seguro.
Si ampliamos esta idea al conocimiento de toda la humanidad, a veces parece que una sola persona nota algo y esa observación cambia una conducta, y entonces empieza a propagarse a todos nosotros. A veces hacen falta varios intentos y mucho tiempo. También me pregunto cuánta fruta al alcance de la mano habrá que todavía nadie notó, y creo que solo con integrar prácticas ejemplares simples y fundamentales establecidas por personas que ya venían prestando atención podríamos elevar el piso mínimo de todos.
Esto también conecta con https://jvns.ca/blog/2023/10/06/new-talk--making-hard-things... de Julia Evans y https://danluu.com/p95-skill/ de Dan Luu. En resumen, siento un enorme agradecimiento por Stenberg y curl, y por las personas que construyeron muchas partes de la infraestructura de Internet que usamos todos los días como si fuera algo obvio.
A veces el principio de “si no está roto, no lo arregles” se interpreta como “no le des soporte hasta que se rompa”, lo que termina generando sorpresas bastante desagradables.
Aunque exista el problema de la sostenibilidad, creo que este tipo de proyectos seguirá existiendo, y respeto a Daniel y a todos los que trabajan no solo para sí mismos, sino también para la comunidad.
La conclusión sobre la seguridad de memoria y los lenguajes con seguridad de memoria es muy razonable. La idea central es esta:
si curl hubiera sido escrito en un lenguaje con seguridad de memoria en vez de C, esta familia de fallas habría sido imposible.
El enfoque que parece viable y razonable en esa dirección es permitir, usar y dar más soporte a dependencias escritas en lenguajes con seguridad de memoria, e ir reemplazando partes de curl de forma gradual, pieza por pieza, como con la adopción de hyper.
Dicho eso, este desarrollo actualmente avanza casi a ritmo glacial, y muestra con una claridad dolorosa las dificultades relacionadas. curl seguirá siendo C en el futuro previsible. A quien no le guste, puede arremangarse y ponerse a trabajar.
Incluyendo los dos CVE más recientes reportados en curl 8.4.0, el 41% del total acumulado de vulnerabilidades de seguridad descubiertas hasta ahora en curl probablemente no habría ocurrido si se hubiera usado un lenguaje con seguridad de memoria. Pero Rust no era una opción práctica para este uso en la época en que se introdujo alrededor del primer 80% de los problemas relacionados con C.
Se ejecutaron repetidamente varios analizadores estáticos de código, pero ninguno encontró ningún problema en esta función.
Excelente análisis. Aun así, incluso después de leer el CVE, todavía no queda claro en qué situaciones se ve afectado. Según lo que entendí, afecta cuando se cumplen las siguientes condiciones:
se usa un proxy SOCKS5, se resuelve el nombre de host mediante ese proxy, el tamaño del búfer se cambió del valor predeterminado de 100 KB a menos de 65541 bytes, y el proxy SOCKS5 es demasiado lento para procesar la solicitud de inmediato.
Corrección: la explicación sobre el búfer no es precisa. libcurl reutiliza el búfer de descarga y su valor predeterminado es 16 KB, pero se dice que curl en sí lo configura manualmente en 100 KB salvo que se use
--limit-rate. Además, la condición de demora también era incorrecta. Según el CVE, es muy probable que una demora normal del servidor sea lo bastante “lenta” como para activar este bug, y no hace falta que un atacante influya mediante una denegación de servicio o controlando el servidor SOCKS.Así que la ruta de ataque parece muy estrecha, y me pregunto si se me está escapando algo.
root@1aac5e228e16:/build/curl-7.74.0# curl -vvv -x socks5h://host.docker.internal:9050 $(python3 -c "print(('A'10000), end='')")Trying 192.168.65.254:9050...* SOCKS5: server resolving disabled for hostnames of length > 255 [actual len=10000]* SOCKS5 connect to AAAAA...* Send failure: Bad file descriptor* Failed to send SOCKS5 connect request.Segmentation faulthttps://gist.github.com/xen0bit/0dccb11605abbeb6021963e2b1a8...
docker-proxy, que crea un túnel SOCKS5 hacia un contenedor Docker que ejecuta una VPN con openconnect. Es útil cuando se manejan distintas VPN de varios clientes y cada VPN exige enviar todo el tráfico de la máquina hacia su lado.https://github.com/carlosonunez/docker-proxy
Como está diseñado para que la resolución DNS ocurra de forma remota, este CVE aplica directamente aquí.
Diría que es el mejor análisis de CVE que he leído hasta ahora. También hay que elogiar que, aun siendo la persona que creó una de las piezas de software clave de esta era, mantiene una actitud humilde durante todo el texto. De verdad inspira mucho respeto.
if(!socks5_resolve_local && hostname_len > 255) {socks5_resolve_local = TRUE;}Esto es una pésima idea. Para las personas que protegen su privacidad con herramientas para evadir censura, podría filtrar su identidad a través del DNS.
El autor piensa lo mismo.
https://hackerone.com/reports/2187833
Se dice que “si curl estuviera escrito en un lenguaje con seguridad de memoria en vez de C, esta clase de fallas habría sido imposible”, pero como mucho podría haber sido imposible. Nunca se sabe del todo si hay formas de salir de las barreras de la máquina virtual del lenguaje. Sin embargo, el autor claramente ignoró el límite de nombres de host DNS especificado en RFC1123, y ese límite también está hardcodeado en bibliotecas de Java.
https://www.rfc-editor.org/rfc/rfc1123
“Aunque no existe un límite real de tamaño para el nombre de host en una URL, el parser de URL de libcurl rechaza nombres de más de 65535 bytes. DNS solo permite nombres de host de hasta 253 bytes. Por lo tanto, los nombres legítimos de más de 253 bytes son raros. En la práctica, casi no se han visto nombres reales de más de 1024 bytes.”
Aunque DNS se usa hoy en el 99,9% de los casos, no es el único mecanismo para resolver nombres de host a direcciones.
No logro encontrar dónde el RFC fija un límite superior para el tamaño de los nombres de host.
También puede significar que el compilador intenta demostrar que no se accede a la memoria fuera de límites, que no se accede sin una propiedad clara y que solo se accede dentro de la vida útil del objeto base. Rust, por ejemplo, hace eso.
Por supuesto, el compilador también podría agregar verificaciones internas de fallos y protecciones en puntos importantes. Rust no lo hace, pero podría ser útil en sistemas donde hay que preocuparse por cosas como bit flips dentro de registros, por ejemplo en entornos de alta radiación como escáneres de rayos X, que no se detectan con memoria ECC.
Hay bastante exageración y drama para tratarse de algo que requiere condiciones muy específicas para explotarse
La actualización de seguridad de Windows 10 que salió ayer no incluía la nueva versión de curl. El curl incluido en Windows sigue siendo 8.0.1
Es un artículo largo e interesante, pero se puede resumir en: “por eso las bibliotecas del sistema deben escribirse en lenguajes seguros o demostrar formalmente su corrección”
Si uno de los mejores programadores en C de nuestra época, y de los más amables y transparentes, está escribiendo algo así, deberíamos prestar atención