Como surgiu um heap overflow no curl
(daniel.haxx.se)- O CVE-2023-38545, revelado junto com o lançamento do curl 8.4.0, é um heap buffer overflow que ocorre durante o processamento de proxy SOCKS5, uma vulnerabilidade rara de gravidade HIGH entre os problemas de segurança do curl
- O problema foi introduzido em 2020 durante a mudança do código de conexão SOCKS5 de chamadas bloqueantes para uma máquina de estados não bloqueante, afetando o curl desde a versão 7.69.0
- A lógica existente que mudava nomes de host com mais de 255 bytes do modo de resolução remota para resolução local passou a interagir com a reinvocação da máquina de estados, permitindo que nomes de host longos demais fossem copiados para um buffer pequeno
- Para que o ataque funcione, o cliente libcurl precisa usar o proxy-resolver-mode do SOCKS5 e redirecionamento automático, enquanto um servidor HTTPS controlado pelo atacante deve retornar um
Location:de HTTP 30x com um nome de host entre mais de 16KB e até 64KB - A partir do curl 8.4.0, nomes de host longos demais não fazem mais a troca de resolução remota para local; em vez disso, é retornado um erro, e foram adicionados testes para impedir esse mesmo cenário
CVE-2023-38545 e curl 8.4.0
- Junto com o lançamento do curl 8.4.0, foram divulgados o aviso de segurança e os detalhes do CVE-2023-38545
- O problema foi considerado o mais sério problema de segurança do curl em muito tempo, com gravidade classificada como HIGH
- A falha central é um heap buffer overflow que ocorre sob certas condições durante o processamento de conexões via proxy SOCKS5
SOCKS5 e formas de resolução de nomes
- O curl oferece suporte a SOCKS5 desde agosto de 2002
- SOCKS5 é um protocolo de proxy que estabelece comunicação de rede por meio de um servidor intermediário
- Pode ser usado para estabelecer comunicação via Tor
- Também pode ser usado para acessar a internet em ambientes internos de organizações ou empresas
- No SOCKS5, há duas formas de resolver nomes de host
- O cliente resolve localmente o nome de host e depois envia o endereço resolvido ao proxy
- O cliente envia o nome de host completo ao proxy, e o proxy faz a resolução remota
A mudança de 2020 que introduziu a vulnerabilidade
- No início de 2020, a função que se conecta ao proxy SOCKS5 foi alterada de chamada bloqueante para uma máquina de estados não bloqueante
- Essa mudança entrou no branch master em 14 de fevereiro de 2020 e foi incluída no curl 7.69.0
- O curl 7.69.0 foi a primeira versão com essa melhoria e, como resultado, também a primeira versão vulnerável ao CVE-2023-38545
- O objetivo da mudança era oferecer uma melhoria mais perceptível quando muitas transferências paralelas passassem todas por SOCKS5
O modo de resolução quebrado na máquina de estados
- A função da máquina de estados é chamada repetidamente sempre que chegam mais dados de rede, até que a conexão seja estabelecida
- A variável local
socks5_resolve_local, no topo da função, indica se o curl deve resolver diretamente o nome de host ou passar o nome ao proxy - Essa variável é redefinida no início de cada execução da máquina de estados, com base no modo do proxy
- A condição do estado INIT criou o problema
- O campo de nome de host do SOCKS5 permite no máximo 255 bytes
- Se o nome de host ultrapassar 255 bytes, o proxy SOCKS5 não consegue resolvê-lo
- O código antigo do curl, ao encontrar um nome de host longo demais em modo de resolução remota, mudava
socks5_resolve_localparaTRUEe passava para o modo de resolução local
- Se o usuário havia pedido resolução remota, o curl deveria falhar em vez de mudar de modo, mas esse comportamento de troca, adicionado há muito tempo, continuou existindo
Como o heap overflow acontece
- Se o servidor SOCKS5 não for rápido o suficiente e a máquina de estados não receber dados de rede para avançar, a função retorna
- Quando mais dados chegam depois, a mesma função da máquina de estados é chamada novamente
- Na reinvocação,
socks5_resolve_localé redefinida no topo da função conforme o modo do proxy- O valor alterado para
TRUEna chamada anterior por causa do nome de host longo demais não é preservado - O valor volta ao estado em que o proxy deve resolver remotamente o nome
- O valor alterado para
- O curl monta em um buffer de memória o frame do protocolo que será enviado ao proxy e copia as informações de destino
- Por causa desse estado incorreto, ao tentar enviar o nome de host longo demais como está, ele pode sobrescrever a memória heap adjacente além do buffer de destino alocado
Tamanho do buffer e restrições de nome de host
- Ao montar o frame do protocolo a ser enviado ao proxy, o curl reutiliza o buffer de download comum
- As condições de tamanho desse buffer são as seguintes
- O valor padrão é 16KB
- A ferramenta curl configura o tamanho do buffer como 100KB
- Tamanhos diferentes podem ser usados conforme a solicitação da aplicação
- O menor tamanho permitido é 1024 bytes
- Se o buffer tiver menos de 65541 bytes, esse overflow é possível
- Quanto menor o buffer, maior pode ser o overflow possível
- O nome de host em uma URL não tem, na prática, um limite rígido de tamanho, mas o parser de URL do libcurl rejeita nomes com mais de 65535 bytes
- O DNS só permite nomes de host com até 253 bytes
- Nomes válidos com mais de 253 bytes são raros, e nomes reais com mais de 1024 bytes são praticamente inexistentes, então o ataque exige intencionalmente um nome de host muito longo
- O campo de nome de host da URL só pode conter alguns octetos, e valores de byte inválidos são rejeitados pelo parser de URL
- Se o libcurl tiver sido compilado para usar uma biblioteca IDN, essa biblioteca também pode rejeitar nomes de host inválidos
Condições para que o ataque funcione
- O atacante precisa controlar um servidor HTTPS acessado por um cliente que use libcurl passando por um proxy SOCKS5 em proxy-resolver-mode
- O servidor malicioso deve ser capaz de retornar um redirecionamento manipulado em uma resposta HTTP 30x
- O cabeçalho
Location:do redirecionamento pode conter um nome de host muito longo no formato a seguirLocation: https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/- O nome de host deve ter entre mais de 16KB e até 64KB
- Se o cliente libcurl estiver com o seguimento automático de redirecionamentos ativado e o proxy SOCKS5 for “lento o suficiente” para disparar esse problema de variável local, o nome de host manipulado será copiado para um buffer pequeno demais
- Se essas condições forem atendidas, ocorre escrita na memória heap adjacente, caracterizando um heap buffer overflow
Correção e bug bounty
- A partir do curl 8.4.0, nomes de host longos demais não fazem mais a troca de resolução remota para local; em vez disso, é retornado um erro
- Também foi adicionado um caso de teste dedicado para esse mesmo cenário
- O problema foi reportado, analisado e corrigido por Jay Satiro
- Essa vulnerabilidade recebeu o maior bug bounty já pago até agora pelo curl
- US$ 4.660 para quem reportou
- US$ 1.165 para o projeto curl, conforme a IBB policy
C e linguagens com segurança de memória
- Se o curl tivesse sido escrito em uma linguagem com segurança de memória em vez de C, esse tipo de falha não teria ocorrido
- No entanto, portar o curl para outra linguagem não está em pauta neste momento
- A abordagem realista se reduz a duas opções
- Permitir, usar e oferecer mais suporte a dependências escritas em linguagens com segurança de memória
- Substituir gradualmente partes do curl, como na adoção do hyper
- Esse desenvolvimento avança muito lentamente no momento, e as dificuldades envolvidas também estão ficando claras
- O curl continuará escrito em C no futuro próximo
- Incluindo os dois CVEs mais recentes reportados no curl 8.4.0, 41% das vulnerabilidades de segurança já encontradas no curl talvez não tivessem ocorrido se tivesse sido usada uma linguagem com segurança de memória
- Ainda assim, em cerca de 80% do período inicial em que os problemas relacionados a C foram introduzidos, o Rust ainda não era uma opção prática para esse objetivo
Uma falha descoberta 1315 dias depois
- Essa falha permaneceu no código por 1315 dias
- Com um conjunto de testes melhor, ela poderia ter sido descoberta antes
- O curl executa repetidamente vários analisadores estáticos de código, mas nenhum deles conseguiu encontrar o problema nessa função
- Ter distribuído um heap overflow em código presente em mais de 20 bilhões de instalações não foi exatamente uma experiência recomendável
- O processo de reporte e tratamento antes da divulgação pública pode ser visto no HackerOne report
1 comentários
Opiniões no Hacker News
Ainda me surpreende que tantos dispositivos dependam, na prática, de uma biblioteca escrita em grande parte por uma única pessoa. O peso deve ter sido enorme, e isso aparece também no trecho abaixo:
“Ao ler o código agora, é impossível não ver o bug. Dói muito aceitar que eu não percebi esse erro e que essa falha passou 1315 dias sem ser descoberta no código. Peço desculpas. Sou apenas humano.”
Se Daniel vir isso, acho que deve saber que agradecemos pelo trabalho duro e que não há absolutamente nenhuma necessidade de pedir desculpas. No fim das contas, o código-fonte estava aberto para qualquer pessoa ler e revisar.
https://www.buzzfeed.com/chrisstokelwalker/the-internet-is-b...
“Este relatório parece estar totalmente correto, e dói profundamente.”
E também a frase do blog: “não recomendo a experiência de distribuir um heap overflow em código incluído em mais de 2 bilhões de instalações”. É cruel ter de carregar uma responsabilidade tão grande por uma recompensa tão pequena.
Pessoalmente, tenho pensado com frequência em quantas coisas deixo passar, por mais que eu tente me concentrar. Atividades como revisão de código parecem ser uma boa forma de treinar esse aspecto, isto é, a atenção e a percepção geral/contextual. Por exemplo, havia um canteiro de flores bem cuidado em um lugar por onde eu passava com frequência havia anos; depois que o notei pela primeira vez, por cerca de um ano eu costumava parar para observá-lo. Só alguns meses atrás percebi pela primeira vez que havia outro canteiro, formando uma espécie de par, a poucos pés dali. É bem provável que esse canteiro sempre tenha estado lá, mas, como eu nem sabia que ele existia, não tenho como ter certeza.
Expandindo isso para o conhecimento da humanidade como um todo, às vezes parece que uma única pessoa nota algo, essa observação muda uma ação, e então começa a se espalhar para todos nós. Às vezes são necessárias várias tentativas e muito tempo. Também fico imaginando quantos frutos baixos ainda existem que ninguém percebeu, e acredito que apenas integrar práticas exemplares simples e fundamentais estabelecidas por pessoas que já vêm prestando atenção poderia elevar o piso mínimo para todos.
A palestra de Julia Evans em https://jvns.ca/blog/2023/10/06/new-talk--making-hard-things... e o texto de Dan Luu em https://danluu.com/p95-skill/ também se conectam a isso. Em conclusão, sinto uma grande gratidão por Stenberg e pelo curl, e pelas pessoas que criaram muitas das partes da infraestrutura da internet que usamos como algo óbvio todos os dias.
Às vezes, o princípio de “se não está quebrado, não conserte” acaba sendo interpretado como “não dê suporte até quebrar”, o que gera algumas surpresas bem desagradáveis.
Pode haver problemas de sustentabilidade, mas acho que projetos assim continuarão existindo, e presto homenagem a todos que, como Daniel, trabalham não só por si mesmos, mas pela comunidade.
A conclusão sobre segurança de memória e linguagens com segurança de memória é muito razoável. O ponto principal é este:
Se o curl tivesse sido escrito em uma linguagem com segurança de memória em vez de C, essa classe de falhas teria sido impossível.
A abordagem que parece viável e razoável nessa direção é permitir, usar e apoiar mais dependências escritas em linguagens com segurança de memória, e substituir partes do curl gradualmente, peça por peça, como na adoção do hyper.
No entanto, esse desenvolvimento hoje avança quase em ritmo glacial e expõe, de forma dolorosamente nítida, os desafios envolvidos. O curl continuará sendo C no futuro previsível. Quem não gostar pode arregaçar as mangas e trabalhar nisso.
Incluindo os dois CVEs mais recentes reportados no curl 8.4.0, 41% do total acumulado de vulnerabilidades de segurança já encontradas no curl provavelmente não teriam ocorrido se uma linguagem com segurança de memória tivesse sido usada. Mas Rust não era uma opção prática para esse uso na época em que cerca dos primeiros 80% dos problemas relacionados a C foram introduzidos.
Vários analisadores estáticos de código foram executados repetidamente, mas nenhum deles encontrou qualquer problema nessa função.
É uma análise excelente. Mas, mesmo depois de ler o CVE, ainda não está claro em quais situações alguém é afetado. Pelo que entendi, a pessoa é afetada quando as condições abaixo são atendidas:
usa um proxy SOCKS5, resolve o nome do host por esse proxy, o tamanho do buffer foi alterado do padrão de 100 KB para menos de 65541 bytes, e o proxy SOCKS5 é lento demais para processar a requisição imediatamente.
Correção: a explicação sobre o buffer não está correta. A libcurl reutiliza o buffer de download e o padrão é 16 KB, mas dizem que o próprio curl o configura manualmente para 100 KB, a menos que se use
--limit-rate. A condição de atraso também estava errada. Segundo o CVE, é muito provável que a latência normal de um servidor já seja “lenta” o bastante para acionar esse bug, e o atacante não precisa influenciar isso com negação de serviço nem controlando o servidor SOCKS.Então o caminho de ataque parece muito estreito; fico curioso se estou deixando algo passar.
root@1aac5e228e16:/build/curl-7.74.0# curl -vvv -x socks5h://host.docker.internal:9050 $(python3 -c "print(('A'10000), end='')")Trying 192.168.65.254:9050...* SOCKS5: server resolving disabled for hostnames of length > 255 [actual len=10000]* SOCKS5 connect to AAAAA...* Send failure: Bad file descriptor* Failed to send SOCKS5 connect request.Segmentation faulthttps://gist.github.com/xen0bit/0dccb11605abbeb6021963e2b1a8...
docker-proxy, que cria um túnel SOCKS5 para um contêiner Docker que executa uma VPN openconnect. É útil ao lidar com VPNs diferentes de vários clientes, quando cada VPN exige enviar todo o tráfego da máquina para o lado dela.https://github.com/carlosonunez/docker-proxy
Como ele foi projetado para que a resolução de DNS aconteça remotamente, este CVE se aplica diretamente aqui.
Este talvez seja o melhor texto de análise de CVE que li até agora. Também não dá para deixar de elogiar a postura humilde ao longo de todo o texto, mesmo sendo alguém que criou um dos softwares centrais da nossa era. É realmente admirável.
if(!socks5_resolve_local && hostname_len > 255) {socks5_resolve_local = TRUE;}Isso é uma péssima ideia. Para pessoas que protegem a própria privacidade usando ferramentas para contornar censura, isso pode vazar a identidade via DNS.
O autor pensa da mesma forma.
https://hackerone.com/reports/2187833
Dizem que “se o curl tivesse sido escrito em uma linguagem com segurança de memória em vez de C, essa classe de falha teria sido impossível”, mas no máximo seria algo que talvez fosse impossível. Nunca dá para saber se existe alguma forma de escapar da barreira da máquina virtual da linguagem. Dito isso, o autor claramente ignorou o limite de nomes de host DNS especificado na RFC1123, e esse limite também é hardcoded em bibliotecas Java.
https://www.rfc-editor.org/rfc/rfc1123
“Não há um limite real de tamanho para o nome de host em uma URL, mas o parser de URL da libcurl rejeita nomes com mais de 65535 bytes. O DNS permite apenas nomes de host de até 253 bytes. Portanto, nomes legítimos com mais de 253 bytes são raros. Nomes reais com mais de 1024 bytes são praticamente inéditos.”
O DNS é usado em 99,9% dos casos hoje, mas não é o único mecanismo para resolver nomes de host para endereços.
Não consegui encontrar onde a RFC define um limite superior para o tamanho de nomes de host.
Também pode significar que o compilador tenta provar que a memória não é acessada fora dos limites, que não é acessada sem propriedade clara e que só é acessada durante a vida útil do objeto subjacente. Rust faz isso, por exemplo.
Claro, o compilador também poderia adicionar verificações internas de falha e proteções em pontos importantes. Rust não faz isso, mas poderia ser bom em sistemas nos quais é preciso se preocupar com bit flips dentro de registradores. Por exemplo, em ambientes de alta radiação, como scanners de raios X, quando isso não é capturado por memória ECC.
Para algo que exige condições muito específicas para ser explorado, há bastante exagero e drama
A atualização de segurança do Windows 10 lançada ontem não incluiu a nova versão do curl. O curl incluído no Windows ainda é o 8.0.1
É um texto longo e interessante, mas pode ser resumido como: “bibliotecas de sistema deveriam ser escritas em uma linguagem segura ou ter sua correção comprovada”
Se um dos melhores, mais gentis e transparentes programadores C da nossa era está escrevendo algo assim, deveríamos prestar atenção