Usando o event loop do Node.js para ataques de temporização
16 de fevereiro de 2016
0 minutos de leituraHá pouco mais de 3 anos, eu, alguns amigos e eu criamos um grupo chamado pasten para participar da competição do Chaos Computer Club Capture The Flag (CTF). É um CTF no estilo Jeopardy, em que as equipes participantes precisam resolver desafios relacionados à segurança em várias categorias, como exploração, engenharia reversa, web, perícia forense e criptografia. CTFs são divertidos e educativos, e recomendo muito participar. Fica ainda melhor quando você vence, como nós vencemos este ano!
A competição deste ano incluiu, entre outros desafios, um aplicativo Node.js vulnerável a um ataque de temporização interessante, que explorava o event loop específico do Node.js. Neste post, vamos analisar o desafio, explicar o risco e mostrar como um invasor pode descobrir e explorar uma vulnerabilidade desse tipo. Espero que isso também ajude você a evitar vulnerabilidades semelhantes no seu próprio código.
Ataques de temporização
Antes de mergulharmos no desafio, vamos explicar o que é um ataque de temporização.
Imagine um serviço que, quando você informa a senha errada para um e-mail existente, responde: “A quinta letra da sua senha está errada. Tente novamente.” Parece absurdo, não é? Dar esse tipo de informação permite que um invasor descubra a senha por força bruta, um caractere de cada vez, tornando a invasão trivial. No entanto, é exatamente isso que acontece quando usamos uma comparação ingênua de strings para verificar senhas ou tokens de autenticação.
A comparação nativa de strings, incluindo o operador == do JavaScript, normalmente percorre duas strings (de mesmo tamanho), comparando um caractere por vez e parando quando encontra uma diferença. Portanto, ao comparar foo com bar, o loop seria executado uma vez; já ao comparar foo com fox, compararia 3 caracteres, levando mais tempo.
Exemplo de função de autenticação vulnerável:
A comparação de um único caractere é muito rápida, mas ainda demora o suficiente. Pesquisas mostram que um invasor consegue medir eventos com precisão de 15 a 100 µs pela internet e de 100 ns em uma rede local. Com essas técnicas, os invasores podem aproveitar esses pequenos atrasos, que acabam equivalendo a informar ao usuário qual caractere está errado.
Esse tipo de ataque é chamado de ataque de temporização e pode ser realizado sempre que a entrada afeta o tempo de processamento. Para evitá-lo, precisamos fazer com que a comparação das strings leve o mesmo tempo, independentemente da senha informada. Uma opção é aplicar xoring às duas senhas e verificar se o resultado é zero.
Imune a ataques de temporização:
O desafio: Sequence Hunt
Com essas informações em mãos, vamos analisar o desafio. Tudo começou com um link para uma página com o seguinte conteúdo:
Sequence HuntBem-vindo à minha caça às sequências de inteiros. Você precisa calcular cinco números entre 0 e 100 implementando os algoritmos abaixo. Quando achar que tem a solução correta, envie-os como parâmetros GET aqui.Você também pode obter algumas informações sobre você aqui.Espero muita carga, então criei tudo em torno de IOLoops assíncronos.Divirta-se!
[EDIT]Alguém me enviou isto. Como cada verificação leva bastante tempo, agora o servidor espera até que tenham se passado três segundos desde a chegada da solicitação, para evitar esses ataques.[/EDIT]
AlgoritmosTODO publicar algoritmos
O texto aqui aponta para /check, onde precisamos enviar os parâmetros corretos para obter a flag, e IOLoops leva a /info, que mostra o seguinte:
você está em 37.120.106.140contagem de solicitações no seu IOLoop: 1
O que sabemos até agora?
Há um algoritmo desconhecido que recebe 5 números entre 0 e 100
Para obter a flag, precisamos informar os números corretos.
O tempo de execução do algoritmo provavelmente depende da entrada, o que pode permitir um ataque de temporização.
O servidor sempre espera pelo menos 3 segundos antes de enviar a resposta, para dificultar ataques de temporização.
Agora precisamos descobrir como capturar a flag. Como o desafio se chamava Sequence Hunt, vou usar sequencehunt.com como domínio fictício nos exemplos, mas não faça ataques a esse domínio (que não tem relação com o desafio)! Você pode executar o aplicativo de exemplo vulnerável que criei. Ele deve se comportar de forma parecida com o do CTF.
Explorando
Uma verificação rápida confirma que uma solicitação check realmente leva cerca de 3 segundos para retornar, independentemente dos valores informados. Isso significa que não é possível testar todas as combinações por força bruta. Seria necessário testar 100^5 (10 bilhões) de combinações, o que levaria milhares de anos…
Ao explorar a página /info, notamos que cada solicitação /check aumenta a contagem do IOLoop — e a própria solicitação /info também.
Enviar solicitações /check com vários números aleatórios não produz diferenças de tempo estatisticamente significativas. Pelo visto, o algoritmo leva menos de 3 segundos para ser executado, então todas as solicitações simplesmente retornam após o mínimo especificado de 3 segundos.
No entanto, percebemos que uma solicitação para /info demora muito mais para responder quando há uma solicitação /check em andamento — um indício relacionado ao tempo que podemos investigar. Para entender o próximo passo, vamos falar um pouco sobre o event loop do Node.js.
Event loop do Node.js
Projetado para oferecer escalabilidade, o Node.js (e o JavaScript em geral) é um framework assíncrono e orientado a eventos. Quando um trecho de código precisa executar uma ação potencialmente bloqueante, como abrir um arquivo ou gravar na rede, ele registra uma função de callback, inicia a ação correspondente e termina. Quando a ação é concluída (por exemplo, quando o arquivo é aberto), o event loop chama o evento de callback.
O modelo orientado a eventos é extremamente escalável, pois as threads nunca ficam paradas “esperando”. Porém, ele traz um problema quando uma função demora para ser concluída. Como o servidor Node.js executa apenas uma thread por núcleo (por padrão), uma função demorada ocupa o núcleo inteiro enquanto as demais aguardam na fila. No navegador, é por isso que você pode encontrar a mensagem de erro “o script está demorando muito para ser executado”. No servidor, as solicitações simplesmente ficam travadas.
Para entender melhor o event loop do Node.js, veja a ótima palestra que Philip Roberts apresentou na JSConf ou leia um destes artigos.
Resolvendo o enigma
No nosso caso, o event loop nos forneceu as informações de tempo de que precisávamos. Enquanto /check estava em execução, o event loop não retomava o controle, fazendo a solicitação /info ficar travada. Em contrapartida, a função setTimeout simplesmente registra um evento agendado e cede o controle, permitindo que as solicitações /info sejam processadas rapidamente. Publiquei no GitHub o código de um servidor simples que demonstra esse efeito, caso você queira testá-lo localmente.
Agora que tínhamos uma forma de obter informações de tempo (também conhecida como canal lateral), só precisávamos enviar uma solicitação /check e, sem esperar pela resposta, enviar uma solicitação /info, medindo quanto tempo ela levava para retornar.

Para automatizar a busca e compensar as variações da rede, o próximo passo foi escrever um script em Python que, dado um conjunto de entradas:
Abre
nthreadsEnvia uma solicitação
checke mede quanto tempo uma solicitação/infoparalela leva para retornarCalcula a média dos tempos e retorna o resultado
Usei n=5, e foi o suficiente. Esse número pode ser maior, dependendo da qualidade da conexão com a internet e das características de tempo do algoritmo. Estes são os resultados da primeira execução — observe a diferença significativa quando o valor é 4
Como a diferença de tempo só aparecia quando 4 estava na primeira posição, o próximo passo foi percorrer as demais posições dos parâmetros. O segundo dígito encontrado foi 12
E assim por diante, até encontrarmos todos os parâmetros corretos: [4, 12, 77, 98, 35]
Flag capturada.
Resumo
Ataques de temporização são uma ameaça real e afetam tanto empresas pequenas quanto grandes (como o Google). Mesmo pequenas diferenças no tempo de execução podem ser detectadas com uma amostra relativamente pequena (em escala de internet) e usadas por invasores para obter informações e aprimorar ataques de força bruta. No Node.js, especificamente, o event loop pode servir como outro sinal em ataques de temporização.
Algumas dicas para proteger seu aplicativo contra esse tipo de falha:
Ao implementar verificações de autenticação, faça com que o processo leve sempre o mesmo tempo. Você pode usar secure-compare ou outros pacotes para fazer comparações em tempo constante.
Não deixe de verificar se eles têm vulnerabilidades conhecidas
Teste se há vulnerabilidades de temporização com ferramentas.
Por fim, se quiser mostrar suas habilidades e encarar esses desafios, tente participar do Capture The Flag do CCC no próximo ano!
Comece a resolver desafios de capture the flag
Aprenda a resolver desafios de capture the flag assistindo sob demanda ao nosso workshop virtual introdutório.