
Exploit de prueba de concepto para CVE-2022-0778, que demuestra un bucle infinito en BN_mod_sqrt de OpenSSL mediante un certificado X.509 manipulado con parámetros de curva no primos, lo que permite ataques de denegación de servicio.
La vulnerabilidad descubierta provoca un bucle infinito en la función BN_mod_sqrt() de OpenSSL al analizar una clave de curva elíptica. Esto significa que un certificado X.509 manipulado de forma maliciosa puede provocar un DoS en cualquier servidor sin parchear.
El núcleo de la vulnerabilidad está en el análisis de claves EC con puntos en formato comprimido: al analizar este tipo de claves, OpenSSL intentará expandir el punto comprimido, tratando de calcular una raíz cuadrada módulo el primo p sobre el que está definida la curva. Sin embargo, la primalidad de p no se comprueba en ningún sitio, ni siquiera en BN_mod_sqrt(), para la cual es un requisito; por lo tanto, un error en la implementación provocará un bucle infinito debido a que p no es primo como se esperaba.
La explicación en chino está disponible en mi blog OpenSSL CVE-2022-0778: reproducción del problema y construcción de certificados no válidos
BN_mod_sqrt()El requisito previo es tener instalados gcc y una versión vulnerable de OpenSSL.
Para el error en BN_mod_sqrt(): compila con gcc -o my_bad_sqrt my_bad_sqrt.c -lcrypto, ejecuta ./my_bad_sqrt y observa cómo se cuelga para siempre. :D
Con un certificado: ejecuta openssl x509 -in certs/cert.der.new -inform DER -text -noout en la línea de comandos; esto también se cuelga.
La función BN_mod_sqrt() implementa el algoritmo de Tonelli-Shanks para encontrar una raíz cuadrada modular, es decir, dado un entero a y un número primo p, devuelve un valor r tal que r^2 == a (mod p).
Al analizar el commit que parchea la vulnerabilidad, vemos que el culpable es el bucle que encuentra el índice más pequeño i para el cual b^(2^i)==1 (mod p), donde b se define antes en el algoritmo.
El bucle se cambia 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;
}
a
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;
}
En el segundo caso, hay un bucle for limitado por la variable e; en el código original, sin embargo, solo hay una comprobación del caso i==e dentro de un bucle while.
Dado que estos bucles están dentro de un bucle mayor que en cada iteración establece el nuevo valor de e al valor actual de i, probamos la siguiente estrategia de ataque:
i=1 al final; para ello, necesitamos que b^2=1 (mod p).e=1, y si podemos entrar en el bucle interno, la comprobación i==e siempre fallarát != 1 (mod p), entonces permaneceremos en el bucle para siempreObserva que los dos primeros pasos pueden ocurrir en una ejecución "normal", es decir, con un primo p. Sin embargo, si p es compuesto, ¡también podemos satisfacer el tercer paso!
El algoritmo de Tonelli-Shanks funciona escribiendo p - 1 = 2^e * q, con q impar. Este es también el orden del grupo multiplicativo de los enteros módulo p, y los valores e y q se usarán muchas veces durante la ejecución; sin embargo, si p no es primo, el orden del grupo multiplicativo no será p-1, y esto nos ayudará a entrar en el bucle infinito.
En particular, b se inicializa como b = a^q (mod p), lo que significa que si p fuera primo, entonces b tendría orden una potencia de 2, que luego encontraremos usando el bucle.
Pero si establecemos p = r * s, el orden del grupo multiplicativo es (r-1)*(s-1) = r*s - r - s + 1 en lugar de r*s-1. El algoritmo usa el valor de q para obtener un elemento y de orden exactamente 2^e; sin embargo, cuando p no es primo, el valor de q no tendrá un significado especial para el orden, por lo que el elemento y no tendrá orden una potencia de dos módulo p=r*s.
Dado que al final del primer bucle externo b se establece como b = b*y^(e-i) (mod p), en su segunda iteración el bucle interno intentará encontrar un valor i para el cual b^(2^i) == 1 (mod p), pero fallará dado que y ya no está garantizado que tenga orden una potencia de dos.
Los números en el exploit son muy simples: tomamos r=17,s=41, lo que da p=r*s=697. Esto significa que los valores calculados de e y q serán p-1 = 2^3 * 87.
Luego elegimos a=696, lo que significa que a == -1 (mod p) y también b == -1 (mod p) cuando se inicializa. Esto satisfará el paso 1 estableciendo e=1 para el siguiente bucle externo.
Entonces b se establecerá como un elemento con orden que no es una potencia de 2, y el bucle interno se quedará atascado tratando de encontrar un i para el cual b^(2^i)==1 (mod p).
Bien, ahora vamos a construir un certificado peligroso que contenga parámetros de curva explícitos no válidos con un punto base codificado en formato comprimido.
Los archivos de esta sección están en la carpeta certs.
Primero, necesitamos crear una clave privada EC. Como queremos un certificado objetivo con parámetros de curva explícitos, la clave también debe contener parámetros de curva explícitos.
$ openssl ecparam -out ec.key -name prime256v1 -genkey -noout -param_enc explicit -conv_form compressed
Luego, por conveniencia, firmamos automáticamente un certificado y lo generamos en formato DER.
$ openssl req -new -x509 -key ec.key -out cert.der -outform DER -days 360 -subj "/CN=TEST/"
Veamos la información del certificado. Contiene parámetros de curva explícitos como se esperaba.
$ 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
...
Ahora necesitamos modificar los valores de estos parámetros: Prime, A, B, Generator. Satisfacen la ecuación siguiente