Il y a quelques jours, un groupe de hackers, THC, a mis à disposition un outil permettant de lancer un déni de service contre des serveurs web SSL. Comme indiqué dans la description de l’outil, le problème n’est pas réellement nouveau : une poignée de mains SSL implique de coûteux calculs cryptographiques.

L’attaque s’appuie sur deux aspects importants :

  • Une poignée de mains SSL nécessite des ressources de calcul plus importantes du côté du serveur que du côté du client. Il est ainsi indiqué qu’un serveur usera 15 fois plus de ressources qu’un client. Il est donc possible pour un ordinateur personnel de mettre à mal un serveur haut de gamme.
  • L’utilisation de la renégociation SSL permet, dans une seule connexion TCP, de demander un nombre important de poignées de mains, évitant ainsi de devoir rétablir la connexion après chaque poignée de mains. De fait, un client derrière une simple connexion ADSL peut bombarder le serveur de demandes de renégociation.

MISE À JOUR : Bien que le contenu de cet article soit toujours pertinent, il est important de comprendre qu’il a été rédigé en 2011 et qu’il ne prend pas en compte certains aspects contemporains, notamment la chute de RC4 en tant qu’algorithme approprié.

Solutions techniques possibles§

Il n’y a pas de solution toute faite à ce problème, mais il existe certaines mesures pour atténuer celui-ci. Comme l’outil développé par THC s’appuie sur la renégociation SSL, la mesure la plus immédiate est de désactiver celle-ci, mais nous allons explorer d’autres possibilités.

Désactiver la renégociation SSL§

La façon la plus simple de parer à l’attaque mise au point par THC est de désactiver la renégociation SSL. C’est un mécanisme qui est rarement utilisé en pratique. Un serveur peut demander une renégociation si le client désire accéder à une ressource nécessitant un certificat mais un client n’a habituellement aucune raison de demander une renégociation. En raison d’une vulnérabilité passée, les versions récentes d’Apache et nginx interdisent simplement la renégociation SSL, même si la version non vulnérable est disponible.

Pour déterminer si la renégociation SSL est autorisée, il est possible d’utiliser openssl s_client. Une rénégociation peut être demandée en envoyant R sur une ligne vide. Voici un exemple où la renégociation est désactivée (malgré le fait qu’elle soit indiquée comme supportée lors de la poignée de mains initiale) :

$ openssl s_client -connect www.luffy.cx:443 -tls1
[...]
New, TLSv1/SSLv3, Cipher is DHE-RSA-AES256-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: zlib compression
Expansion: zlib compression
[...]
R
RENEGOTIATING
140675659794088:error:1409E0E5:SSL routines:SSL3_WRITE_BYTES:ssl handshake failure:s3_pkt.c:591:

Désactiver la renégociation dans un programme n’est pas quelque chose de trivial avec OpenSSL. À titre d’exemple, j’ai écrit un patch désactivant celle-ci dans stud, un logiciel faisant office de terminaison SSL.

Limiter la fréquence des poignées de mains SSL§

Désactiver la renégociation SSL n’est pas toujours possible. Par exemple, votre serveur web peut être trop ancien pour proposer cette option. Comme les renégociations ne doivent avoir lieu que très rarement, voire jamais, une solution est d’en limiter fortement le nombre.

Lorsque l’outil de THC a été publié pour la première fois, F5 Networks a fourni une règle afin de mettre au point une telle limitation pour leurs répartiteurs de charge. Il est possible d’obtenir un résultat similaire avec Netfilter en repérant les paquets TCP commençant par un enregistrement TLS correspondant à une poignée de mains chiffrée. Il est possible de les trouver dans la poignée de mains initiale mais ils ne sont habituellement pas en début de paquet. Il n’y a toutefois aucun champ permettant d’indiquer si un enregistrement TLS est chiffré ou non (TLS utilise un mécanisme d’états pour le déterminer). Il est alors nécessaire d’utiliser quelques heuristiques. Par exemple, si le type de poignée de mains ne correspond pas à un type connu et que le paquet TCP a le drapeau « push », il est alors fortement probable qu’il s’agisse du paquet qui nous intéresse :

# Access to TCP payload (if not fragmented)
payload="0 >> 22 & 0x3C @ 12 >> 26 & 0x3C @"
iptables -A LIMIT_RENEGOCIATION \
    -p tcp --dport 443 \
    --tcp-flags SYN,FIN,RST,PSH PSH \
    -m u32 \
    --u32 "$payload 0 >> 8 = 0x160300:0x160303 && $payload 2 & 0xFF = 3:10,17:19,21:255" \
    -m hashlimit \
    --hashlimit-above 5/minute --hashlimit-burst 3 \
    --hashlimit-mode srcip --hashlimit-name ssl-reneg \
    -j DROP

L’utilisation de l’extension u32 peut sembler particulièrement difficile à lire. Il est conseillé de lire attentivement la page de manuel qui contient des exemples. $payload permet de trouver le contenu du paquet TCP (à condition qu’il n’y ait pas de fragmentation). On vérifie ensuite qu’il s’agit bien d’un enregistrement correspondant à une poignée de mains (0x16) puis que la version de TLS nous est bien connue (0x300, 0x0301, 0x0302 ou 0x0303). Enfin, il est vérifié que le type n’est pas connu.

Il y a un risque évident de faux positifs avec cette règle mais l’utilisation de hashlimit permet de diminuer très fortement les problèmes. Il ne s’agit pas d’une solution incontournable. Un attaquant peut utiliser diverses techniques de fragmentation pour éviter la détection.

Une solution quasiment équivalente consiste à utiliser CONNMARK pour suivre le déroulement de la première poignée de mains et d’interdire toutes celles qui pourraient survenir ensuite1.

Si les renégociations SSL sont désactivées, il est possible de simplement limiter le nombre de poignées de mains en limitant le nombre de connexions TCP en provenance d’une IP :

iptables -A LIMIT_SSL \
    -p tcp --dport 443 \
    --syn -m state --state NEW \
    -m hashlimit \
    --hashlimit-above 120/minute --hashlimit-burst 20 \
    --hashlimit-mode srcip --hashlimit-name ssl-conn \
    -j DROP

Les serveurs ainsi protégés sont toujours vulnérables à une attaque de plus grande envergure mais devraient pouvoir écarter tout attaquant solitaire2.

J’ai réuni ces solutions dans un simple fichier pour référence.

Augmenter les capacités de calcul côté serveur§

Il est relativement facile de faire évoluer les capacités côté serveur aussi bien horizontalement que verticalement. Verticalement, les performances de SSL augmentent linéairement avec le nombre de cœurs et il suffit donc d’ajouter des CPU supplémentaires ou d’opter pour des CPU avec plus de cœurs. Il est également possible d’acquérir des accélérateurs cryptos (assez chers). Horizontalement, il suffit d’ajouter des serveurs supplémentaires en prenant garde à bien cerner la problématique de la reprise des sessions SSL.

Augmenter le travail nécessaire côté client§

Dans la présentation de leur outil, THC indique notamment :

Établir une connexion SSL sécurisée nécessite 15 fois plus de puissance de calcul sur le serveur que sur le client.

J’ignore d’où provient ce chiffre. Pour le vérifier, j’ai construit un petit outil comparant le temps CPU pour 1000 poignées de mains. Les résultats sont présentés dans le graphique ci-dessous :

Graphique pour comparer la puissance nécessaire côté serveur et client

MISE À JOUR : Adam Langley a annoncé le support sur les sites HTTPS de Google de suites basées sur Diffie-Hellman et l’utilisation de ECDHE-RSA-RC4-SHA comme suite de chiffrement préférée grâce à une implémentation rapide et en temps constant des courbes elliptiques P-224, P-256 et P-521 dans OpenSSL. Les tests ci-dessus n’utilisent pas ces implémentations.

Ainsi, avec des clefs RSA de 2048 bits et une suite de chiffrement telles que AES256-SHA, un serveur effectue 6 fois plus de calculs qu’un client. Toutefois, avec une suite telle que DHE-RSA-AES256-SHA, la tendance s’inverse et le serveur nécessite 34% de puissance en moins. Elle s’accentue encore plus avec une suite telle que DHE-DSS-AES256-SHA où le serveur fait moitié moins de calculs que le client.

Toutefois, dans la réalité, il est difficile d’utiliser de telles suites de chiffrement :

  1. Certains navigateurs ne les supportent simplement pas. Ils sont limités aux suites classiques utilisant uniquement RSA3.
  2. Les utiliser conduirait à augmenter fortement la charge habituelle des serveurs qui risquent alors de s’écrouler sous le trafic légitime.
  3. Enfin, certains clients, notamment les mobiles, ne peuvent pas se permettre d’utiliser des suites aussi coûteuses qui sont trop lentes et trop gourmandes en énergie.

Étudions plus en avant pourquoi, dans le cadre d’une suite basée sur RSA comme AES256-SHA, le serveur doit effectuer plus de calculs que le client. Voici à quoi ressemble une poignée de mains SSL :

Poignée de mains SSL

En envoyant le message Client Key Exchange, le client va chiffrer4 la version de TLS ainsi que 46 octets aléatoires avec la clef publique du serveur, contenue dans le message Certificate qu’il vient de recevoir. Le serveur va ensuite devoir déchiffrer ce message à l’aide de sa clef privée. Il s’agit là des deux opérations les plus coûteuses d’une poignée de mains. Pour comprendre pourquoi le déchiffrement est plus coûteux que le chiffrement, il faut d’abord comprendre comme RSA fonctionne.

Tout d’abord, un serveur a besoin d’une clef privée et d’une clef publique. Voici les étapes principales pour générer celles-ci :

  1. Prendre au hasard deux entiers premiers distincts ·p· et ·q·, chacun étant approximativement de la même taille.
  2. Calculer ·n=pq·. Il s’agit du module de chiffrement.
  3. Calculer ·\varphi(n)=(p-1)(q-1)·.
  4. Choisir ·e· tel que ·1<e<\varphi(n)· et ·\gcd(\varphi(n),e) = 1· (c’est-à-dire que ·e· et ·\varphi(n)· sont premiers entre eux). Il s’agit de l’exposant de chiffrement.
  5. Calculer ·d=e^{-1}\mod\varphi(n)·. Il s’agit de l’exposant de déchiffrement.

La clef publique est ·(n,e)· et la clef privée est ·(n,d)·. Pour chiffrer un message, il faut d’abord le transformer en un entier ·m<n· (que l’on doit « bourrer »). Il est chiffré avec la clef publique pour obtenir le message ·c· et ne peut être en théorie déchiffré qu’avec la clef privée :

  • ·c=m^e\mod n·  (chiffrement)
  • ·m=c^d\mod n·  (déchiffrement)

Cela ne nous explique pas encore pourquoi le déchiffrement est plus coûteux que le chiffrement. La paire de clefs n’est en réalité pas construite comme expliqué ci-dessus. Habituellement, ·e· est fixé comme un petit entier premier dont la représentation binaire contient beaucoup de 0, comme 17 (0x11) ou 65537 (0x10001). Ensuite, ·p· et ·q· sont choisis de façon à ce que ·\varphi(n)· soit premier avec ·e·. Cela permet d’obtenir un chiffrement très rapide en utilisant l’exponentiation rapide. Cela implique alors que son inverse ·d· est un nombre très grand ne disposant pas de propriétés particulières et l’exponentiation est alors très lente.

Au lieu de déduire ·d· à partir de ·e·, il est possible de faire l’inverse. Ainsi, ·d· pourrait être petit et nous aurions ainsi un déchiffrement très rapide. Il y a toutefois deux problèmes importants empêchant cette solution :

Ainsi, le mieux que l’on puisse faire est de choisir l’exposant public ·e’=4294967291· (plus grand nombre premier de 32 bits ; il ne contient qu’un seul 0). Toutefois, en pratique, cela n’est pas suffisant comme on peut le constater sur notre graphique comparatif.

Ainsi, il n’y a pas de réelles solutions dans ce domaine : il est nécessaire d’autoriser les suites de chiffrement basées sur RSA et il n’est pas possible avec celles-ci d’inverser le rapport entre le client et le serveur.

Où les choses se corsent§

Peu après la publication de l’outil de déni de service par THC, Eric Rescorla5 a publié une très bonne analyse de l’impact d’un tel outil. Il s’interroge sur l’intérêt d’utiliser la renégociation pour une telle attaque :

Ce que vous devez vous demander à ce point est si une attaque par déni de service basée sur la renégociation est plus efficace qu’une attaque similaire utilisant de nombreuses connexions. La façon de mesurer ceci est de faire le rapport entre le travail nécessaire pour l’attaquant et celui nécessaire pour le serveur. Je n’ai jamais vu de véritables mesures ici (et THC n’en présente pas) mais quelques calculs rapides semblent indiquer que la différence est faible.

Avec l’ancienne méthode, utilisant les connexions multiples, les coûts à prendre en compte sont les suivants :

  1. Faire une poignée de mains TCP (3 paquets)
  2. Envoyer un ClientHello (1 paquet). Il peut s’agir d’un message préfabriqué.
  3. Envoyer les messages ClientKeyExchange, ChangeCipherSpec, Finished (1 paquet). Ils peuvent également être préfabriqués.

À noter que je n’ai pas besoin d’analyser une seule des réponses envoyées par le serveur et je n’ai pas besoin de faire un seul calcul cryptographique. Il suffit d’envoyer n’importe quoi au serveur. Par exemple, je peux envoyer les mêmes messages incorrects ClientKeyExchange et Finished à chaque fois. Le serveur ne peut pas savoir qu’ils sont incorrects avant d’avoir effectué les calculs les plus coûteux. En résumé, l’attaque consiste à envoyer une série de messages préfabriqués afin de forcer le serveur à effectuer un déchiffrement RSA.

J’ai rapidement écrit un outil effectuant les étapes ainsi décrites. Afin d’éviter les abus, celui-ci ne fonctionne que si le serveur accepte la suite de chiffrement NULL-MD5. Aucun serveur configuré sérieusement ne va l’accepter. Il vous faudra donc en modifier la configuration pour tester.

Alors qu’Eric indique qu’il n’a pas besoin d’interpréter les réponses du serveur, j’ai été confronté au fait que la connexion est abandonnée si le client envoie la clef avant que le serveur n’ait répondu aux premiers messages. Ainsi, j’interprète sommairement les réponses du serveur avant de continuer. De plus, Eric indique qu’un message Client Key Exchange erroné suffit car le serveur devra le déchiffrer avant de s’en rendre compte. J’ai choisi de construire un message valide, fabriqué lors de la première poignée de mains et rejoué ensuite, afin de ne pas permettre au serveur de se rendre compte que le message est invalide avant d’avoir terminé l’intégralité du calcul (par exemple, si sa longueur est incorrecte).

MISE À JOUR : Michał Trojnara a développé sslsqueeze, un outil similaire. Les principales différences sont l’utilisation de libevent2 qui permet d’obtenir de meilleures performances que l’utilisation des threads et le fait d’envoyer un message Client Key Exchange erroné mais tout de même de la bonne taille.

Avec un tel outil et un certificat RSA de 2048 bits, le serveur a besoin de 100 fois plus de puissance de calcul que le client. Cela signifie notamment que la plupart des solutions exposées auparavant sont ineffectives, à l’exception des limitations via Netfilter.


  1. Décoder l’état d’une connexion TLS dans Netfilter est complexe. La première solution repose globalement sur le fait qu’une rénégociation est empaquetée dans un segment TCP avec drapeau « push ». Ce n’est pas forcément le cas. Avec la seconde solution, on part du principe que le premier enregistrement TLS chiffré est dans le même segment TCP que le message Client Key Exchange. S’il est dans son propre segment TCP, il sera vu comme une renégociation. La machine à état doit être améliorée pour pouvoir détecter cet enregistrement à n’importe quelle position. 

  2. À noter toutefois que comme toute limitation assimilant un utilisateur à une IP, il y a un risque important de faux positifs comprenant notamment les serveurs mandataires légitimes, les réseaux derrière du NAT, les utilisateurs de mobiles partageant une même IP ou ceux qui se trouvent derrière un CGN

  3. Les suites de chiffrement supportées par tous les navigateurs sont RC4-MD5, RC4-SHA et 3DES-SHA. DHE-DSS-AES256-SHA nécessite normalement TLS 1.2 (présent dans aucun navigateur). 

  4. En Français, le vocabulaire approprié est chiffrement et déchiffrement. En Anglais, il s’agit de encrypt et decrypt. En Français, crypter ne veut rien dire. Décrypter signifie décoder le message sans en détenir la clef secrète. 

  5. Eric est l’auteur de nombreuses RFC liées à TLS. Il connaît donc bien le sujet.