Des geekeries, de la MAO, de tout et de rien…
Je suis
Charlie

Cloudbleed

Icône auteur nah, Icône canondrier 24 février 2017, Icône commentaire
Mots clés Icône catégorie Actualité, Internet, classé dans Icône catégorie Internet
Logo représentant un nuage aux couleurs de cloudflare, mais les gouttes de pluie sont remplacées par des gouttes de sang (référence à heartbleed, le cœur qui saigne). En dessous se trouve le texte #cloudbleed.

Cloudbleed ?

Cloudbleed est le nom donné à cette faille, nom plus facile à retenir que project-zero:1139.
https://bugs.chromium.org/p/project-zero/issues/detail?id=1139
https://blog.cloudflare.com/incident-report-on-memory-leak-caused-by-cloudflare-parser-bug/

Qu'est-ce que Cloudflare ?

Cloudflare est un service de reverse proxy (serveur cache + protection contre les attaques de type DDOS), un service de CDN (réplication de contenu partout dans le monde sur des serveurs plus proches physiquement des utilisateurs), un service d'optimisation des pages (réduction/réécriture).

L'infrastructure de Cloudflare est essentiellement basée sur Nginx (serveur web + reverse proxy, comme Apache ou lighttpd, mais en plus léger et rapide).

Est-ce lié à Heartbleed ?

Non. Même si l'importance de cette faille a plus ou moins le même impact que Heartbleed (fuite de données).

La faille, en détails

Provocation de la faille

Une page html, hébergée derrière un service proposé par cloudflare, pesant moins de 4 ko (4096 octets) et contenant des données mal formatées (balise fermante absente), peut contenir des données provenant de la mémoire (RAM) du serveur.

Ces données peuvent être des clefs de chiffrement, des cookies (d'authentification, par exemple), des mots de passe, des fragments de données de type POST (données envoyés depuis un formulaire web, que ça soit un panier sur un site marchand, un couple identifiant/mot de passe, un commentaire sur un blog…), voire même des requêtes https provenant d'autres sites utilisant également cloudflare.

Ces données ont été indexées par les moteurs de recherche (google, bing, yahoo, yandex…), et sont encore accessible dans leurs cache (le vidage des caches contenant ces données sont en cours).

Le problème provient d'un bout de code source, utilisé par email obfuscation (masquage d'adresse email), Server-side Excludes (Exclusions côté serveur) et automatic HTTPS rewrites (remplacement à la volée des adresses http:// en https:///.

En résumé, une page web est lue par le serveur nginx de cloudflare, ce serveur réécrit une partie de la page, par exemple, pour masquer une adresse email, puis envoie cette page au navigateur web.

Le problème est situé au moment où le contenu est réécrit.

Comment le contenu est-il réécrit ?

Depuis les débuts de cloudflare, les ingénieurs utilisaient Ragel, un langage de programmation basé sur des expressions régulières, qui génère du code en langage C (transcompilation, conversion d'un langage de programmation vers un autre langage de programmation. En général, on utilise cette technique de programmation pour pouvoir écrire le code en un langage relativement simple, ou contenant certaines fonctionnalités (langage dit de haut-niveau). Ce code est alors converti en un autre langage, plus "proche de la machine" (langage dit de bas niveau)).

Ragel : https://www.colm.net/open-source/ragel/

Les expressions régulières sont donc converties en C, jusque là, pas de problème. Sauf qu'avec le temps, le code écrit avec Ragel a augmenté en longueur et en complexité (de plus en plus de règles à évaluer), et devenait de plus en plus difficile à maintenir. Les ingénieurs décidèrent alors d'écrire un nouveau code (appelé cf-html) pour le remplacer. Il s'est avéré que cf-html était beaucoup plus rapide, fonctionnait correctement avec le HTML 5 et était facile à maintenir (du code facile à maintenir est du code facile et rapide à corriger et à améliorer).

La première fonctionnalité à utiliser cf-html est la fonctionnalité qui corrige les adresses http en https. Les autres fonctionnalités, sont migrées progressivement.

L'un des points à noter, c'est que le code généré par Ragel et cf-html est compilé en tant que modules, puis est chargé par ngnix.

Chaque module chargé par le serveur proxy analyse le contenu html dans des blocs de mémoire, effectue des modifications si nécessaire, et passe le contenu au module suivant. Lorsque tous les modules ont effectués leurs traitements, le serveur ngnix envoie la page au navigateur web.

Parmi ces traitements, cela peut être le déchiffrement du contenu https, l'analyse de données (vers qui est destiné cette requête), le contenu est-il encore à jour (cache expiré ou non)…

Une fois le bug isolé, il s'est avéré qu'il provenait de cf-html et non de Ragel. Cependant, le bug était également présent dans le code écrit en Ragel, mais il n'y avait aucun impact, parce que la mémoire était correctement traitée. Via cf-html, il s'est avéré que la mémoire était traitée de manière légèrement différente (de manière subtile) par rapport à Ragel, et c'est de là que provient la faille.

Désactivation des services concernés

Dès que cloudflare a déterminé que le problème venait de cf-html, la société a immédiatement désactivé les services dépendants de ce module, en attendant de comprendre exactement d'où venait le problème.

Origine du problème

Si vous lisez couramment l'anglais (technique), je conseille vivement la lecture de ce paragraphe (et celui d'après).
https://blog.cloudflare.com/incident-report-on-memory-leak-caused-by-cloudflare-parser-bug/#rootcauseofthebug

Il est nettement plus détaillé que mes explications plus bas. Mes explications sont plus génériques (tentative de vulgarisation des éléments techniques avec exemple d'un débordement).

Le code généré est du code en langage C. Ce langage, de bas niveau, permet la manipulation de « pointeurs », et d'accéder directement à la mémoire de la machine (bon, avec quelques restrictions, je simplifie parce que c'est plutôt compliqué en fait. Et il faudrait tout un livre pour expliquer les pointeurs).

le code posant problème est le suivant :

/* generated code */
if ( ++p == pe )
goto _test_eof;

p correspond au pointeur (où on se trouve actuellement).
pe : l'adresse mémoire de la fin de la zone à lire.

++p : on ajoute 1 à p (on se déplace d'un octet)
puis on compare p avec pe.
Si p est égal à pe alors on saute à l'étiquette _test_eof.
Sinon on continue la lecture.

À noter : EOF signifie End Of File (Fin de fichier).

Ok, et en quoi ce code pose problème ?

Déroulons l'exécution :

Le texte html est stocké entre les adresses 0 et 4096 (4 ko). On ne tient pas compte si c'est de l'utf-8 (pour l'exemple).
On initialise le pointeur p à zéro, et pe à 4096.

on définit une étiquette _begin

On lit l'octet à l'adresse stockée dans p (p étant un pointeur vers cette adresse).
On fait le traitement requis.
On incrémente p
p contient désormais 1
On compare p avec pe
p -> 1, pe -> 4096.
Les résultats sont différents, on continue le traitement.

On va à l'étiquette début.

On lit l'octet à l'adresse stockée dans p.
On fait le traitement requis.
On incrémente p
p contient désormais 2
On compare p avec pe
p -> 2, pe -> 4096.
Les résultats sont différents, on continue le traitement.

On va à l'étiquette début.

ainsi de suite…

on arrive au moment ou p contient 4095

On lit l'octet à l'adresse stockée dans p.
On fait le traitement requis.
On incrémente p
p contient désormais 4096
On compare p avec pe
p -> 4096, pe -> 4096.
Les résultats sont identiques, on va à l'étiquette _test_eof.

Ici, le code fonctionne sans problème.

Que se passerait-il si, par hasard, on ajoute 1 entretemps ?

Admettons que l'on soit dans la boucle, et p = 4094

On lit l'octet à l'adresse stockée dans p.
On fait le traitement requis.
On incrémente p
p contient désormais 2
On compare p avec pe
p -> 4095, pe -> 4096.
Les résultats sont différents, on continue le traitement.

pour une certaine raison, on incrémente p de 1.

p vaut alors 4096.

Vous voyez où est le problème ? Non ? On continue le déroulement.

p vaut 4096.

On lit l'octet à l'adresse stockée dans p.
On fait le traitement requis.
On incrémente p
p contient désormais 4097
On compare p avec pe
p -> 4097, pe -> 4096.
Les résultats sont différents, on continue le traitement.

On commence à lire les données situées dans les adresses mémoire au delà de 4096, comme défini plus haut.

Et là, c'est le drame. On vient de sortir de la zone mémoire dans laquelle on était censé lire les données (nom de ce bug : buffer overrun).

La solution pour éviter ce genre de problème est toute simple.

Le code effectuant le test est

if ( ++p == pe )
goto _test_eof;

Il suffit de remplacer le test de l'égalité == par est supérieur ou égal à

if ( ++p >= pe )
goto _test_eof;

Ainsi, lors de la lecture de la mémoire, si jamais le pointeur p est supérieur ou égal à l'adresse pe, on sortira systématiquement en allant à l'étiquette _test_eof.

Mais, le problème de cloudfront, c'est que ce code n'est pas écrit par un·e humain·e, ce code est du code généré via Ragel.

Le code écrit en Ragel est le suivant :

script_consume_attr := ((unquoted_attr_char)* :>> (space|'/'|'>'))
>{ ddctx("script consume_attr"); }
@{ fhold; fgoto script_tag_parse; }
$lerr{ dd("script consume_attr failed");
fgoto script_consume_attr; };

On essaie de lire un attribut dans une balise, par exemple %lt;script type="javascript">
Si l'attribut contient un espace, un slash ou un > alors on est à la balise de fin.
Si l'attribut est correctement formaté, alors on exécute le code situé dans @{}. Sinon, on exécute le code situé dans $lerr{}.

Admettons que dans la page, le contenu soit tronqué, et contienne uniquement <script type=
Dans ce cas, le code situé dans $lerr{} sera appelé. À l'intérieur, on trouve simplement "fgoto script_consume_attr", qui fait en sorte que le code saute à l'étiquette script_consume_attr, qui est définie juste au dessus. On entre dans une boucle (infernale), on sort de la zone mémoire attendue et on commence à lire le contenu de la mémoire qui ne nous est pas destiné (buffer overrun).

La solution ici, est de rajouter une instruction fhold dans le bloc $lerr{}.
fhold est l'équivalent en C de p-- (on décrémente p de 1). Du coup, p contient le caractère qui a provoqué l'erreur :)

Je pense que c'est assez compliqué comme ça, du coup, je vais arrêter là pour l'origine du problème.

Qui est impacté ?

La liste, encore incomplète, des sites potentiellement impactés, est disponible ici :
https://github.com/pirate/sites-using-cloudflare

Il est possible de télécharger le fichier complet (> 22 Mio) et de chercher à l'intérieur.

L'ironie de l'histoire

Cloudflare a bien un programme de chasse aux bugs, pour tout bug découvert, on peut gagner un t-shirt.

En comparaison, quand on voit les montants proposés par Google, ça laisse songeur…
https://sites.google.com/a/chromium.org/dev/Home/chromium-security/hall-of-fame