
Prova de conceito de exploit para CVE-2022-0778, demonstrando um loop infinito no BN_mod_sqrt do OpenSSL por meio de um certificado X.509 malicioso com parâmetros de curva não primos, permitindo ataques de negação de serviço.
A vulnerabilidade descoberta desencadeia um loop infinito na função BN_mod_sqrt() do OpenSSL durante a análise de uma chave de curva elíptica. Isso significa que um certificado X.509 maliciosamente criado pode causar DoS em qualquer servidor não corrigido.
O cerne da vulnerabilidade está na análise de chaves EC com pontos em formato comprimido: ao analisar esse tipo de chave, o OpenSSL tentará expandir o ponto comprimido, tentando calcular uma raiz quadrada módulo o primo p sobre o qual a curva é definida. No entanto, a primalidade de p não é verificada em nenhum lugar, nem mesmo em BN_mod_sqrt(), para a qual é um requisito; assim, um bug na implementação causará um loop infinito devido a p não ser primo como esperado.
A explicação em chinês pode ser encontrada no meu blog Reprodução do OpenSSL CVE-2022-0778 e construção de certificado inválido
BN_mod_sqrt()O pré-requisito é ter instalado gcc e uma versão vulnerável do OpenSSL.
Para o bug em BN_mod_sqrt(): compile com gcc -o my_bad_sqrt my_bad_sqrt.c -lcrypto, execute ./my_bad_sqrt e veja-o travar para sempre! :D
Com um certificado: execute openssl x509 -in certs/cert.der.new -inform DER -text -noout na linha de comando; isso também trava.
A função BN_mod_sqrt() implementa o algoritmo Tonelli-Shanks para encontrar uma raiz quadrada modular, ou seja, dado um inteiro a e um número primo p, retorna um valor r tal que r^2 == a (mod p).
Analisando o commit que corrige a vulnerabilidade, vemos que o culpado é o loop que encontra o menor índice i para o qual b^(2^i)==1 (mod p), onde b é definido anteriormente no algoritmo.
O loop foi alterado 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;
}
para
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;
}
No segundo caso, há um loop for limitado pela variável e; no código original, no entanto, há apenas uma verificação para o caso i==e dentro de um loop while.
Como esses loops estão dentro de um loop maior que, a cada iteração, define o novo valor de e como o valor atual de i, tentamos a seguinte estratégia de ataque:
i=1 no final; para isso, precisamos que b^2=1 (mod p).e=1, e se conseguirmos entrar no loop interno, a verificação i==e sempre falharát != 1 (mod p), ficaremos presos no loop para sempreObserve que as duas primeiras etapas podem realmente acontecer em uma execução "normal", ou seja, com um primo p. No entanto, se p for composto, também podemos satisfazer a terceira etapa!
O algoritmo Tonelli-Shanks funciona escrevendo p - 1 = 2^e * q, com um q ímpar. Esta é também a ordem do grupo multiplicativo de inteiros módulo p, e os valores e e q serão usados muitas vezes durante a execução; no entanto, se p não for primo, a ordem do grupo multiplicativo não será p-1, e isso nos ajudará a entrar no loop infinito.
Em particular, b é inicializado como b = a^q (mod p), o que significa que se p fosse primo, então b teria ordem alguma potência de 2, que então encontraríamos usando o loop.
Mas se definirmos p = r * s, a ordem do grupo multiplicativo é (r-1)*(s-1) = r*s - r - s + 1 em vez de r*s-1. O algoritmo usa o valor q para obter um elemento y de ordem exatamente 2^e; no entanto, quando p não é primo, o valor q não terá um significado especial para a ordem, então o elemento y não terá ordem uma potência de dois módulo p=r*s.
Como no final do primeiro loop externo b é definido como b = b*y^(e-i) (mod p), em sua segunda iteração o loop interno tentará encontrar um valor i para o qual b^(2^i) == 1 (mod p), mas falhará, dado que y não está mais garantido de ter ordem uma potência de dois.
Os números no exploit são muito simples: pegamos r=17,s=41, o que dá p=r*s=697. Isso significa que os valores calculados de e e q serão p-1 = 2^3 * 87.
Em seguida, escolhemos a=696, o que significa que a == -1 (mod p) e também b == -1 (mod p) quando inicializado. Isso satisfará a etapa 1 definindo e=1 para o loop externo seguinte.
Então b será definido como um elemento com ordem não sendo uma potência de 2, e o loop interno ficará preso tentando encontrar um i para o qual b^(2^i)==1 (mod p).
OK, agora vamos criar um certificado perigoso que contém parâmetros de curva explícitos inválidos com um ponto base codificado em formato comprimido.
Os arquivos desta seção estão na pasta certs.
Primeiro, precisamos criar uma chave privada EC. Como queremos um certificado alvo com parâmetros de curva explícitos, a chave também deve conter parâmetros de curva explícitos.
$ openssl ecparam -out ec.key -name prime256v1 -genkey -noout -param_enc explicit -conv_form compressed
Então, por conveniência, auto-assinamos um certificado e o emitimos no formato DER.
$ openssl req -new -x509 -key ec.key -out cert.der -outform DER -days 360 -subj "/CN=TEST/"
Vamos verificar as informações do certificado. Ele contém parâmetros de curva explícitos como esperado.
$ 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
...
Agora precisamos modificar os valores desses parâmetros: Primo, A, B, Gerador. Eles satisfazem a equação abaixo
onde p é o Primo, a é o parâmetro A e b é o parâmetro B. p, a, b juntos determinam uma curva. Descomprimir um ponto significa calcular a coordenada Y a partir da coordenada X correspondente.
Obviamente, a operação de raiz quadrada modular deve ser usada, que chamará BN_mod_sqrt().