Comment un débordement de tas a été introduit dans curl
(daniel.haxx.se)- Révélée avec la sortie de curl 8.4.0, CVE-2023-38545 est un débordement de tampon sur le tas survenant lors du traitement d’un proxy SOCKS5, une vulnérabilité de gravité HIGH rare parmi les problèmes de sécurité de curl
- Le problème a été introduit en 2020 lors du passage du code de connexion SOCKS5 d’appels bloquants à une machine à états non bloquante, et affecte curl à partir de la version 7.69.0
- Dans le mode de résolution de noms à distance, l’ancienne logique qui basculait les noms d’hôte de plus de 255 octets vers une résolution locale s’est combinée aux réinvocations de la machine à états, permettant la copie de noms d’hôte trop longs dans un petit tampon
- Pour qu’une attaque fonctionne, le client libcurl doit utiliser le proxy-resolver-mode SOCKS5 et les redirections automatiques, et un serveur HTTPS contrôlé par l’attaquant doit renvoyer un
Location:HTTP 30x contenant un nom d’hôte de plus de 16 KB et d’au plus 64 KB - À partir de curl 8.4.0, les noms d’hôte trop longs ne font plus basculer de la résolution distante vers la résolution locale et provoquent désormais une erreur ; un test empêchant ce même scénario a aussi été ajouté
CVE-2023-38545 et curl 8.4.0
- Avec la publication de curl 8.4.0, l’avis de sécurité et les détails de CVE-2023-38545 ont été rendus publics
- Ce problème a été considéré comme l’un des problèmes de sécurité les plus graves apparus dans curl depuis longtemps, avec un niveau de sévérité HIGH
- Le défaut central est un débordement de tampon sur le tas qui se produit, sous certaines conditions, lors du traitement d’une connexion via un proxy SOCKS5
SOCKS5 et les modes de résolution de noms
- curl prend en charge SOCKS5 depuis août 2002
- SOCKS5 est un protocole proxy qui établit des communications réseau via un serveur intermédiaire
- Il peut être utilisé pour établir des communications via Tor
- Il peut aussi être utilisé pour accéder à Internet au sein d’une organisation ou d’une entreprise
- SOCKS5 propose deux modes de résolution des noms d’hôte
- Le client résout localement le nom d’hôte puis transmet l’adresse résolue au proxy
- Le client transmet le nom d’hôte complet au proxy, qui le résout à distance
Le changement de 2020 qui a introduit la vulnérabilité
- Début 2020, la fonction de connexion à un proxy SOCKS5 est passée d’appels bloquants à une machine à états non bloquante
- Ce changement a été intégré dans la branche master le 14 février 2020 et inclus dans curl 7.69.0
- curl 7.69.0 a été la première version à inclure cette amélioration et, par conséquent, la première version vulnérable à CVE-2023-38545
- L’objectif était d’apporter une amélioration plus visible quand de nombreux transferts parallèles passaient tous par SOCKS5
Le mode de résolution cassé dans la machine à états
- La fonction de la machine à états est rappelée à chaque arrivée de nouvelles données réseau jusqu’à l’établissement de la connexion
- La variable locale
socks5_resolve_localen haut de la fonction indique si curl doit résoudre lui-même le nom d’hôte ou transmettre le nom au proxy - Cette variable est réinitialisée au début de chaque appel de la fonction en fonction du mode du proxy
- La condition de l’état INIT a créé le problème
- Le champ de nom d’hôte de SOCKS5 n’autorise qu’un maximum de 255 octets
- Si le nom d’hôte dépasse 255 octets, le proxy SOCKS5 ne peut pas le résoudre
- Dans l’ancien code de curl, en mode de résolution distante, lorsqu’un nom d’hôte trop long était rencontré,
socks5_resolve_localpassait àTRUE, ce qui basculait vers le mode de résolution locale
- Si l’utilisateur avait demandé une résolution distante, curl aurait dû échouer au lieu de changer de mode, mais ce comportement de bascule ajouté il y a longtemps a été conservé
Le déroulement qui mène au débordement sur le tas
- Si le serveur SOCKS5 n’est pas assez rapide et que la machine à états ne reçoit pas plus de données réseau pour avancer, la fonction se termine
- Quand des données arrivent ensuite, la même fonction de machine à états est rappelée
- Lors de ce nouvel appel,
socks5_resolve_localest de nouveau initialisée en haut de la fonction selon le mode du proxy- La valeur passée à
TRUElors de l’appel précédent à cause d’un nom d’hôte trop long n’est pas conservée - La valeur redevient donc l’état où le proxy doit résoudre le nom à distance
- La valeur passée à
- curl construit dans un tampon mémoire la trame du protocole à envoyer au proxy et y copie les informations de destination
- À cause de cette valeur d’état incorrecte, si curl tente de transmettre tel quel un nom d’hôte trop long, il peut écrire au-delà du tampon alloué et écraser la mémoire du tas adjacente
Taille du tampon et contraintes sur les noms d’hôte
- curl réutilise le tampon de téléchargement standard lorsqu’il construit la trame du protocole à envoyer au proxy
- Les conditions de taille du tampon de téléchargement sont les suivantes
- La valeur par défaut est de 16 KB
- L’outil curl règle la taille du tampon à 100 KB
- D’autres tailles peuvent être utilisées selon les demandes de l’application
- La taille minimale autorisée est de 1024 octets
- Si la taille du tampon est inférieure à 65541 octets, ce débordement est possible
- Plus le tampon est petit, plus la taille potentielle du débordement est grande
- Le nom d’hôte d’une URL n’a pas de limite pratique stricte, mais l’analyseur d’URL de libcurl refuse les noms de plus de 65535 octets
- Le DNS n’autorise des noms d’hôte que jusqu’à 253 octets
- Les noms valides de plus de 253 octets sont rares, et des noms réels dépassant 1024 octets sont pratiquement inexistants ; l’attaque nécessite donc intentionnellement un nom d’hôte très long
- Le champ de nom d’hôte d’une URL n’accepte que certains octets, et les valeurs d’octets invalides sont rejetées par l’analyseur d’URL
- Si libcurl a été compilé avec une bibliothèque IDN, cette bibliothèque peut également rejeter les noms d’hôte invalides
Conditions nécessaires à l’attaque
- L’attaquant doit contrôler un serveur HTTPS auquel un client utilisant libcurl accède via un proxy SOCKS5 en proxy-resolver-mode
- Le serveur malveillant doit pouvoir renvoyer une redirection HTTP 30x forgée
- L’en-tête
Location:de cette redirection peut contenir un nom d’hôte très long sous une forme comme celle-ciLocation: https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/- La longueur du nom d’hôte doit être supérieure à 16 KB et inférieure ou égale à 64 KB
- Si le suivi automatique des redirections est activé dans le client libcurl et que le proxy SOCKS5 est « suffisamment lent » pour déclencher ce problème de variable locale, le nom d’hôte forgé est copié dans un tampon trop petit
- Si ces conditions sont réunies, une écriture dans la mémoire du tas adjacente se produit, ce qui constitue un débordement de tampon sur le tas
Correctif et bug bounty
- Depuis curl 8.4.0, les noms d’hôte trop longs ne font plus basculer le mode de résolution distante vers la résolution locale et provoquent à la place une erreur
- Un cas de test dédié couvrant ce même scénario a également été ajouté
- Le problème a été signalé, analysé et corrigé par Jay Satiro
- Cette vulnérabilité a donné lieu au bug bounty le plus élevé jamais versé jusqu’à présent par curl
- 4 660 dollars au rapporteur
- 1 165 dollars au projet curl conformément à la politique IBB
C et les langages à sécurité mémoire
- Si curl avait été écrit non pas en C mais dans un langage à sécurité mémoire, cette catégorie de défauts ne se serait probablement pas produite
- Cependant, porter curl vers un autre langage n’est pas à l’ordre du jour actuellement
- L’approche réaliste se réduit à deux pistes
- Autoriser, utiliser et prendre davantage en charge des dépendances écrites dans des langages à sécurité mémoire
- Remplacer progressivement certaines parties de curl, comme avec l’introduction de hyper
- Ces efforts avancent actuellement très lentement et leurs difficultés apparaissent clairement
- curl restera écrit en C dans un avenir proche
- En incluant les deux derniers CVE signalés dans curl 8.4.0, 41 % des vulnérabilités de sécurité découvertes jusqu’ici dans curl n’auraient peut-être pas existé avec l’usage d’un langage à sécurité mémoire
- Cela dit, au moment où environ 80 % des problèmes liés au C ont été introduits, Rust n’était pas encore une option pratique pour cet usage
Un défaut découvert 1315 jours plus tard
- Ce défaut est resté dans le code pendant 1315 jours
- Il aurait pu être découvert avec une meilleure suite de tests
- curl exécute de manière répétée plusieurs analyseurs statiques de code, mais aucun n’a détecté le problème dans cette fonction
- Avoir distribué un débordement sur le tas dans un code déployé dans plus de 20 milliards d’environnements n’était pas une expérience recommandable
- Le signalement et le traitement avant divulgation publique peuvent être consultés dans le rapport HackerOne
1 commentaires
Avis sur Hacker News
Il reste étonnant qu’autant d’appareils dépendent de fait d’une bibliothèque écrite en grande partie par une seule personne. La pression devait être énorme, et la phrase ci-dessous le montre bien
« En lisant le code aujourd’hui, il est impossible de ne pas voir le bug. Le fait que je n’aie pas remarqué cette erreur, et que cette faille soit restée 1 315 jours dans le code sans être découverte, est vraiment douloureux à accepter. Je m’excuse. Je ne suis qu’un humain. »
Si Daniel voit cela, je pense qu’il faut le remercier pour tout son travail et qu’il n’a absolument pas à s’excuser. Au final, les sources étaient publiques et tout le monde pouvait les lire et les relire
https://www.buzzfeed.com/chrisstokelwalker/the-internet-is-b...
« Ce rapport semble entièrement correct, et il me fait profondément mal. »
De même que la phrase du blog : « Je ne recommande pas l’expérience qui consiste à déployer un dépassement de tas dans du code présent sur plus de deux milliards d’installations. » C’est dur de devoir assumer une responsabilité aussi énorme pour si peu de reconnaissance
Personnellement, j’ai souvent réfléchi à tout ce que l’on peut manquer, même en essayant de rester concentré. Des activités comme la revue de code semblent être un bon moyen de travailler cela : entraîner son attention, ou développer une perception plus globale et contextuelle. Par exemple, il y avait un parterre de fleurs bien entretenu par quelqu’un dans un endroit où je passais souvent depuis des années ; après l’avoir remarqué pour la première fois, je m’arrêtais parfois pour le regarder pendant environ un an. Puis, il y a quelques mois, j’ai découvert pour la première fois qu’il y avait un autre parterre assorti, à seulement quelques pieds de là. Il est très probable qu’il ait toujours été là, mais comme j’ignorais jusqu’à son existence, je ne peux pas en être certain
Si l’on étend cette idée à l’ensemble du savoir humain, il semble parfois qu’une seule personne remarque quelque chose, puis que cette observation modifie les comportements et commence à se diffuser à nous tous. Parfois, il faut plusieurs tentatives et beaucoup de temps. Je me demande aussi combien de fruits faciles à cueillir personne n’a encore remarqués, et je pense que le simple fait d’intégrer des bonnes pratiques simples et fondamentales établies par des gens qui y ont déjà prêté attention peut relever le niveau minimal pour tout le monde
Cela rejoint aussi https://jvns.ca/blog/2023/10/06/new-talk--making-hard-things... de Julia Evans et https://danluu.com/p95-skill/ de Dan Luu. En fin de compte, j’éprouve beaucoup de gratitude envers Stenberg et curl, ainsi qu’envers les personnes qui ont créé tant de parties de l’infrastructure Internet que nous utilisons chaque jour comme une évidence
Parfois, le principe « si ce n’est pas cassé, n’y touche pas » est interprété comme « ne le soutiens pas tant que ça ne casse pas », ce qui réserve des surprises assez désagréables
Il y a sans doute un problème de durabilité, mais ce type de projet continuera probablement d’exister, et je rends hommage à toutes les personnes qui, comme Daniel, travaillent non seulement pour elles-mêmes mais aussi pour la communauté
La conclusion sur la sécurité mémoire et les langages à mémoire sûre est très raisonnable. En substance
Si curl avait été écrit dans un langage à mémoire sûre plutôt qu’en C, cette famille de failles aurait été impossible
L’approche qui semble réaliste et raisonnable dans cette direction consiste à autoriser, utiliser et soutenir davantage de dépendances écrites dans des langages à mémoire sûre, et à remplacer progressivement des morceaux de curl, élément par élément, comme avec l’adoption de hyper
Cela dit, ce développement avance actuellement à un rythme presque glaciaire, et met douloureusement en évidence les difficultés associées. curl restera en C dans un avenir prévisible. Ceux à qui cela ne plaît pas peuvent retrousser leurs manches et s’y mettre eux-mêmes
En incluant les deux dernières CVE signalées dans curl 8.4.0, 41 % du total cumulé des vulnérabilités de sécurité découvertes dans curl jusqu’à présent ne se seraient probablement pas produites avec un langage à mémoire sûre. Mais Rust n’était pas une option pratiquement utilisable pour cet usage à l’époque où environ les premiers 80 % des problèmes liés au C ont été introduits
Plusieurs analyseurs statiques de code ont été exécutés à répétition, mais aucun n’a détecté le moindre problème dans cette fonction
Excellente analyse. Cela dit, même après avoir lu la CVE, je ne sais toujours pas exactement dans quelles situations on est affecté. D’après ce que j’ai compris, on est concerné si les conditions suivantes sont réunies
on utilise un proxy SOCKS5, on résout le nom d’hôte via ce proxy, la taille du buffer a été modifiée depuis la valeur par défaut de 100 Ko pour passer sous 65 541 octets, et le proxy SOCKS5 est trop lent pour traiter la requête immédiatement
Correction : mon explication concernant le buffer n’est pas exacte. libcurl réutilise le buffer de téléchargement, dont la valeur par défaut est de 16 Ko, mais curl lui-même le règle manuellement à 100 Ko sauf si
--limit-rateest utilisé. Et la condition liée au délai était également fausse. D’après la CVE, un simple délai serveur ordinaire a de bonnes chances d’être suffisamment « lent » pour déclencher ce bug, et l’attaquant n’a pas besoin d’influer par un déni de service ni de contrôler le serveur SOCKSLe chemin d’attaque me semble donc très étroit, et je me demande si je passe à côté de quelque chose
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, qui permet de créer un tunnel SOCKS5 via un conteneur Docker exécutant un VPN openconnect. C’est utile quand on gère différents VPN de plusieurs clients et que chacun exige que tout le trafic de la machine passe par luihttps://github.com/carlosonunez/docker-proxy
Comme il est conçu pour que la résolution DNS se fasse à distance, cette CVE s’applique directement ici
C’est probablement la meilleure analyse de CVE que j’aie lue jusqu’ici. Et on ne peut qu’admirer l’humilité qui transparaît dans tout le texte, alors même que l’auteur a créé l’un des logiciels essentiels de notre époque. Vraiment respectable
if(!socks5_resolve_local && hostname_len > 255) {socks5_resolve_local = TRUE;}C’est vraiment une très mauvaise idée. Pour les personnes qui protègent leur vie privée avec des outils de contournement de la censure, cela peut révéler leur identité via le DNS
L’auteur pense la même chose
https://hackerone.com/reports/2187833
On dit que « si curl avait été écrit dans un langage à sûreté mémoire plutôt qu’en C, ce type de défaut aurait été impossible », mais disons plutôt que cela aurait peut-être été impossible. On ne sait jamais vraiment s’il existe un moyen de sortir de la barrière de la machine virtuelle du langage. Cela dit, l’auteur a clairement ignoré la limite des noms d’hôte DNS définie dans la RFC1123, et cette limite est aussi codée en dur dans des bibliothèques Java
https://www.rfc-editor.org/rfc/rfc1123
« Il n’existe pas vraiment de limite pratique à la taille d’un nom d’hôte dans une URL, mais l’analyseur d’URL de libcurl rejette les noms de plus de 65 535 octets. DNS n’autorise que des noms d’hôte d’au plus 253 octets. Les noms légitimes de plus de 253 octets sont donc rares. Dans la pratique, on n’a quasiment jamais entendu parler de noms réels de plus de 1 024 octets. »
DNS est utilisé dans 99,9 % des cas aujourd’hui, mais ce n’est pas le seul mécanisme de résolution d’un nom d’hôte en adresse
Je n’arrive pas à trouver où la RFC fixe une limite supérieure à la taille des noms d’hôte
Cela peut vouloir dire que le compilateur tente de prouver que la mémoire n’est pas accédée hors limites, qu’elle n’est pas accédée sans propriété clairement définie et qu’elle ne l’est que pendant la durée de vie de l’objet sous-jacent. Rust fonctionne comme ça, par exemple
Bien sûr, le compilateur pourrait aussi ajouter des vérifications internes et des protections aux endroits importants. Rust ne le fait pas, mais ce serait utile dans des systèmes où il faut se soucier de retournements de bits dans les registres, par exemple dans des environnements à forte radiation comme les scanners à rayons X, où la mémoire ECC ne suffit pas forcément
Pour une faille dont l’exploitation exige des conditions très spécifiques, il y a pas mal d’exagération et de dramaturgie
La mise à jour de sécurité de Windows 10 publiée hier n’incluait pas la nouvelle version de curl. Le curl inclus dans Windows est encore en 8.0.1
C’est un article long et intéressant, mais on peut le résumer ainsi : « les bibliothèques système devraient donc être écrites dans un langage sûr ou être prouvées correctes »
Si l’un des meilleurs programmeurs C de notre époque, et l’un des plus bienveillants et transparents, écrit ce genre de texte, il faut y prêter attention