
Análise técnica aprofundada e anti-POC para CVE-2022-3602, um estouro de buffer em punycode no OpenSSL 3.0.x, com scripts de reprodução, análise de pilha e avaliação de mitigação do compilador.
Este documento e repositório é um relatório detalhado da CVE-2022-3602, um problema de estouro de buffer punycode no OpenSSL. É um "anti-POC" (o problema não parece ser explorável) destinado a pessoas que mantêm suas próprias compilações do OpenSSL e para mantenedores de compiladores.
Há uma CVE separada na mesma versão, a CVE-2022-3786, que também leva a estouros de buffer, mas um atacante não pode controlar o conteúdo nesse caso. Não há reprodução para esse problema aqui, mas esse problema pode levar a uma Negação de Serviço devido a falha.
Estouros e transbordos de buffer nunca são bons e se você estiver usando OpenSSL 3.0.x, é prudente atualizar o mais rápido possível.
Sinta-se à vontade para relatar quaisquer erros ou omissões através de issues do GitHub ou pull-requests.
Há um problema de off-by-one em como ossl_punycode_decode lida com a decodificação punycode que resulta em um estouro de 4 bytes. Esse problema é razoável apenas quando o OpenSSL processa uma cadeia de certificados e requer duas condições. Primeiramente, um certificado CA ou Intermediário em uma cadeia deve conter um campo de restrição de nome (name-constraint) que use punycode.
nameConstraints = permitted;email:xn-maccrthaigh-n7a.com
Em segundo lugar, o certificado folha deve conter um campo SubjectAlternateName (SAN) otherName que especifique uma string SmtpUTF8Mailbox.
otherName = 1.3.6.1.5.5.7.8.9;UTF8:[email protected]
quando acionado, o punycode no campo nameConstraints, mas não o punycode no campo otherName, será tratado pela análise punycode vulnerável do OpenSSL.
David Benjamin e Matt Caswell determinaram que a verificação de nameConstraint ocorre após a validação comum da cadeia de certificados e verificação de assinatura. Para a maioria das aplicações, isso significa que o problema não pode ser acionado com um certificado auto-assinado ou cadeia inválida.
Note que as aplicações s_client e s_server do openssl são destinadas para depuração e não param o processamento quando uma cadeia é inválida.
Uma CA ou Intermediário confiável terá que conter a carga maliciosa, e também terá que ter assinado o certificado folha que aciona o problema.
Pode haver alguns ambientes onde partes não confiáveis são as CAs ou Intermediários, por exemplo, um serviço de hospedagem que suporta CAs privadas fornecidas pelo cliente, mas isso não é comum.
A resposta para muitas aplicações será "não" por causa de como o compilador organizou a pilha e devido à presença de outras proteções como canários de pilha / cookies de pilha, preenchimento, PIE, FORTIFY_SOURCE.
O problema leva a um estouro de 32 bits na pilha. Isso não é suficiente para executar diretamente shellcode, mas pode ser suficiente para alterar o fluxo de controle de uma aplicação. Por exemplo, saltar para shellcode que foi embutido em uma cadeia de certificados X509 pode ser possível se esses dados também forem copiados para a pilha em um local executável.
Em todas as plataformas Linux que testei, o estouro ocorre no preenchimento e é inofensivo. Em teoria, um compilador pode dispor as variáveis de modo que o estouro ocorra em uma das outras variáveis na função ossl_a2ulabel.
Dependendo de inlining, a lista completa de variáveis presentes é:
outptr, inptr, size, result, tmpptr, delta, seed, utfsize
e nenhuma me parece fornecer um caminho óbvio para escalada de privilégio ou controle interessante.
Anexei um tarball com ferramentas que podem ser usadas para criar reproduções e estouros com o máximo controle possível sobre todos os quatro bytes. A string de reprodução de referência (xn--ww90271...aaaa) estoura os quatro bytes com os valores 0xFF 0x0F 0x0F 0x0F. Se isso não falhar uma aplicação, é possível (provável?) que essa aplicação não seja vulnerável.
O script shell run-poc pode ser usado para gerar uma cadeia de certificados maliciosa. Um certificado CA malicioso é gerado a partir de ca.cnf e um certificado folha acionador é gerado a partir de leaf.cnf.
O certificado CA usa a seguinte carga de referência:
xn--ww902716aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Um script Python pode ser usado para gerar outras strings punycode para diferentes cargas.
Quando executado, run-poc executará um cliente e servidor openssl e tentará explorar o problema dez vezes.
Um OpenSSL vulnerável provavelmente falhará. Isso não significa que a versão do OpenSSL seja vulnerável a um RCE, já que canários de pilha e proteções de cookie de pilha também tipicamente causam uma falha (mais segura) da aplicação. Observe também que isso não altera a gravidade da outra CVE na mesma versão.
É surpreendentemente sutil obter controle quase total dos quatro bytes de estouro e requer explorar o decodificador punycode do OpenSSL com punycode não padrão / inválido. O tarball anexado contém um script que pode construir uma string que lida com a sutileza. O que se segue é uma explicação de como funciona.
O problema de segurança está em ossl_punycode_decode()
int ossl_punycode_decode(const char *pEncoded, const size_t enc_len,
unsigned int *pDecoded, unsigned int *pout_length)
ossl_punycode_decode é invocada a partir de ossl_a2ulabel. O buffer pEncoded é um buffer de tamanho mais ou menos arbitrário que vem de uma cadeia de certificados X509. É a parte que vem após qualquer "xn--" em um campo nameConstraint. Veja a [reprodução] para como reproduzir tal cadeia de certificados.
pDecoded é um array de tamanho LABEL_BUF_SIZE de unsigned ints. LABEL_BUF_SIZE é 512, e na maioria das plataformas um unsigned int terá 4 bytes de largura. Então na maioria das plataformas pDecoded tem 2048 bytes de comprimento.
Dentro de ossl_punycode_decode(), o cerne do problema é esta verificação de comprimento incorreta:
if (written_out > max_out)
max_out corresponde a *pout_length que é sempre 512. E written_out mantém controle de quantos unsigned ints foram escritos em pDecoded. Como written_out é incrementado depois apenas após a escrita, esta verificação defeituosa permite que 513 unsigned ints sejam escritos em pDecoded. O resultado final se parece algo assim ...
pDecoded = [ ... , 'X , 'Y' , 'Z' ] 'P'
// Índices 509 510 511
Aqui, por convenção do C, os índices são baseados em zero, então o slot número 511 é o 512º elemento do array. 'P' é uma carga de quatro bytes que foi colocada fora dos limites, além do espaço alocado na pilha para o buffer buf em ossl_a2ulabel(), que é o que pDecoded aponta.
Quatro bytes é um pequeno estouro, e não é suficiente para carregar um nop-sled ou executar diretamente shellcode, mas é suficiente para alterar o fluxo de controle de uma aplicação. Por exemplo, saltar para shellcode que foi embutido em uma cadeia de certificados x509 pode ser possível, dependendo de como esses dados (ou fragmentos copiados desses dados) são armazenados e se essa memória é executável. No entanto, ainda há mais dificuldade para um possível atacante.
Primeiro, o preenchimento e alinhamento da pilha pelo compilador, ou defesas como canários de pilha, podem tornar qualquer exploração completamente impossível.