
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
...