Blaze Blog /

Fuzzing de protocolos proprietários com Scapy, radamsa e alguns arquivos PCAP

Security
Jun 10, 2017
7min read
Ilustração de um orc observando um mago fazendo magia

Introdução

Como consultores de segurança, atuamos como especialistas contratados por nossos clientes para realizar testes de segurança black-box em aplicações. Frequentemente, precisamos avaliar a segurança de aplicações que utilizam seus próprios esquemas proprietários de comunicação, em vez de depender de protocolos convencionais como o HTTP.

Recentemente, nos deparamos com um projeto de curto prazo que envolvia testar a segurança de uma aplicação personalizada, a qual possuía seu próprio protocolo de comunicação encapsulado em um túnel SSL/TLS.

Este post tem como objetivo descrever o processo de como contornamos as limitações técnicas e as restrições de tempo impostas por este projeto, conseguindo testar com sucesso a aplicação e encontrar falhas por meio de fuzzing baseado em mutação.

O que são protocolos proprietários?

Em termos gerais, um protocolo proprietário é aquele que não é definido por um padrão aberto ou controlado por um indivíduo ou empresa. Em muitos casos, não existe documentação oficial sobre seu funcionamento interno, o que torna difícil a comunicação com aplicações que utilizam protocolos fechados.

Escusado será dizer que os protocolos variam em grau de complexidade e alguns são mais complicados que outros: por exemplo, um determinado protocolo pode ser bastante direto — baseado em caracteres ASCII, fácil de entender e com campos adicionais que podem ser facilmente correlacionados a outros elementos da mensagem.

Por outro lado, existem protocolos mais complexos. Estes podem envolver diferentes representações de dados, dados binários ou serializados, checksums e campos de valores que não são óbvios à primeira vista.

Apesar de serem proprietários, alguns protocolos têm suas especificações documentadas, seja por documentação oficial fornecida pelo fornecedor (como o Financial eXchange – FIX, amplamente utilizado para negociação de ações) ou por terem sido submetidos à engenharia reversa no passado por entusiastas e defensores de código aberto — um excelente exemplo disso é o protocolo OSCAR da AOL para mensagens instantâneas.

Embora a engenharia reversa de protocolos de rede seja possível, é uma tarefa trabalhosa que exige altas doses de paciência, habilidade e, acima de tudo, tempo. Ter todos esses elementos à disposição nem sempre é viável, por isso precisávamos de uma alternativa para resolver o problema que tínhamos em mãos.

Testes automatizados de segurança de software com fuzzing

O fuzzing é uma área que tem ganhado muita atenção nos últimos anos e várias abordagens mais avançadas surgiram tanto em círculos acadêmicos quanto na indústria.

As técnicas de fuzzing iniciais podem ser divididas, a grosso modo, em baseadas em mutação e em geração, embora existam abordagens mais modernas e avançadas, como o fuzzing evolutivo, que utiliza o feedback da cobertura de código e gera entradas melhores com o auxílio de algoritmos genéticos, além de outras técnicas que dependem de resolvedores de restrições, execução concolica e simbólica, etc.

O fuzzing baseado em mutação é frequentemente chamado de "dumb fuzzing" (fuzzing burro), pois o que ele faz é realizar mutações aleatórias na entrada e gerar dados corrompidos como resultado. No entanto, não se deixe enganar pelo nome: o "dumb fuzzing" pode ser muito eficaz e é responsável por encontrar inúmeras falhas em softwares populares.

Outra estratégia popular é conhecida como fuzzing baseado em geração. Esta técnica envolve conhecimento prévio do protocolo ou formato de arquivo que está sendo testado e gera casos de teste que, em sua maioria, estão em conformidade com o protocolo, mas com alguns campos contendo dados conhecidos por causar comportamento inesperado, como strings grandes, entradas maliciosas contendo metacaracteres de shell, números negativos, muito longos ou subnormais, entre outros.

Benefícios e limitações

O fuzzing baseado em mutação tem a vantagem de ser rápido de configurar e pode oferecer bons resultados; no entanto, pode não ser excelente para alcançar uma alta cobertura de código (suas amostras de teste desempenharão um papel fundamental aqui), ou em situações onde as somas de verificação (checksums) precisam ser ajustadas antes de serem enviadas para processamento pela aplicação — a aplicação provavelmente rejeitará o restante da entrada, pois as somas de verificação falharão, não alcançando áreas potencialmente vulneráveis do código.

O fuzzing baseado em geração tem maior probabilidade de obter uma cobertura de código superior, ao custo de ser mais demorado para criar. Além disso, a criatividade do oráculo de fuzzing e a inteligência das heurísticas de mutação e manipulação de dados determinarão seu sucesso na descoberta de bugs.

Nosso caso e a abordagem que adotamos

Tivemos sorte que o protocolo com o qual precisávamos lidar, embora proprietário, era baseado em ASCII e não parecia ser excessivamente complicado. No entanto, dedicar tempo para entendê-lo e criar um fuzzer baseado em geração para produzir casos de teste estava fora de questão devido às limitações de tempo. Além disso, por que gastar um tempo precioso escrevendo nosso próprio motor de mutação quando existem ótimos disponíveis?

Também relacionado ao protocolo em si: como não havia necessidade de calcular somas de verificação ou cumprir outras estruturas rígidas de protocolo, optamos por realizar o fuzzing usando uma abordagem baseada em mutação.

Para o nosso trabalho, tudo o que tínhamos em mãos eram capturas de pacotes (PCAPs) de tráfego em texto simples entre o cliente e a aplicação que queríamos testar.

Tivemos o cuidado de garantir que obteríamos várias amostras de PCAPs para diferentes funcionalidades e usos da aplicação; desta forma, podemos garantir que nosso fuzzer irá estressar, com casos de teste incomuns, diferentes áreas da superfície de ataque disponível da aplicação, aumentando a cobertura de código e, consequentemente, nossas chances de encontrar bugs.

Como neste caso todo o tráfego entre o cliente e a aplicação estava envolvido em TLS/SSL, foi muito importante solicitar capturas de tráfego não criptografado, pois para testar a aplicação precisávamos apenas da comunicação em nível de aplicação, não de tráfego de transporte ou de camadas superiores.

Além disso, o protocolo que precisávamos testar funcionava como uma espécie de máquina de estados: após a mensagem de handshake (que também poderia ser testada), ele esperava algumas outras mensagens, em uma determinada ordem, para processamento posterior.

Isso significa que, se enviássemos o handshake e, posteriormente, enviássemos uma mensagem malformada quando ele esperava, por exemplo, uma mensagem de login, não conseguiríamos testar nada após o login, pois ela era rejeitada anteriormente.

Para contornar isso, adicionamos um pouco mais de aleatoriedade ao nosso fuzzer: apenas uma certa porcentagem das cargas úteis (payloads) dentro do PCAP seria mutada. Isso nos daria mais permutações e a capacidade de enviar handshakes limpos e mensagens N-1, mas com apenas a última (mensagem N) testada; ou handshake testado e mensagens limpas depois, ou handshake limpo e login malformado, e mensagens subsequentes limpas, etc.

Ferramentas do ofício

Para montar um script em Python que pudesse realizar a tarefa descrita acima, utilizamos:

  • Scapy: uma biblioteca que serve como um canivete suíço para tudo relacionado a redes e que pode analisar, ler, escrever e reproduzir dados de PCAPs.
  • radamsa: uma ferramenta popular de fuzzing baseada em mutação e a arma de escolha de muitos pesquisadores de segurança.

Detalhes técnicos: montando tudo

Após delinearmos a ideia geral do que pretendíamos fazer, qual estratégia de fuzzing melhor se adequava ao nosso caso e as ferramentas necessárias para a tarefa, chegou a hora de detalhar os passos que seguimos para realizar um fuzzing simples (dumb fuzzing) na aplicação:

  • Passo 0: Definir um fator de fuzzing — por exemplo, apenas 20% dos pacotes serão mutados
  • Passo 1: Analisar o PCAP buscando apenas a comunicação do cliente para o servidor
  • Passo 2: Dentre essas capturas, encontrar as que contêm um payload real, já que PCAPs às vezes contêm comunicações irrelevantes para nossos propósitos, como handshakes TCP de 3 vias e outras sinalizações
  • Passo 3: Entrar em um loop infinito para:
    • Extrair o payload e decidir aleatoriamente se devemos aplicar o fuzzing com base no fator definido; se sim, enviá-lo para o motor de mutação
    • Obter os dados mutados
    • Enviar o payload modificado (ou o original, dependendo se foi aplicado o fuzzing ou não) para a aplicação alvo
    • Repetir o processo. Deixar rodando durante a noite.
  • Passo 4: Monitorar falhas e realizar a triagem de erros, se aplicável

Na verdade, o passo 4 não estava ao nosso alcance: como o trabalho foi realizado em uma perspectiva de caixa-preta, não tínhamos instrumentação nem observação de falhas da nossa parte. O cliente gentilmente verificou as falhas por conta própria, e nosso script de fuzzing verificava a conectividade com o alvo após o envio de cada caso de teste modificado. Embora lento, isso nos deu um certo grau de visibilidade sobre se a aplicação alvo travava ou não.

Mais tarde, durante o projeto, fomos informados pelo cliente que o fuzzer causou quatro falhas; três delas eram únicas. Não tivemos tempo suficiente para realizar qualquer tipo de análise de falha, mas os casos de teste geraram exceções não tratadas e poderiam impactar severamente a disponibilidade de um serviço que deveria estar sempre online.

Uma implementação de código aberto dos passos descritos acima pode ser encontrada em nosso Github. É importante ressaltar que ela serve apenas para ilustrar as ideias discutidas neste post. O código de exemplo dificilmente pode ser usado diretamente contra aplicações reais, mas, com algumas modificações, pode atender a diferentes necessidades.

Conclusão

Mesmo em sua forma mais simples, o fuzzing pode ser uma ferramenta muito útil para descobrir vulnerabilidades e deve estar no repertório de todo engenheiro de segurança da informação.

Para testar aplicações que utilizam protocolos proprietários ou não documentados, amostras de PCAPs de diferentes formas de uso da aplicação alvo são muito úteis para testar uma superfície de ataque maior e aumentar as chances de encontrar bugs. Um bom motor de fuzzing, com heurísticas criativas e não convencionais, também é necessário para mutar casos de teste que possam acionar bugs sutis e alcançar casos extremos escondidos profundamente na lógica do código.

Referências

Do you have questions? Let's talk.

Get in touch with our cybersecurity experts

Read More