
dtls-fuzzer é um fuzzer de estado de protocolo para implementações de servidor DTLS.
dtls-fuzzer é uma ferramenta Java que realiza fuzzing de estado de protocolo de servidores DTLS. Mais concretamente, suporta as seguintes funcionalidades:
dtls-fuzzer utiliza TLS-Attacker para gerar/analisar mensagens DTLS, bem como para manter o estado. Para esse fim, TLS-Attacker foi estendido com suporte para DTLS. dtls-fuzzer depende da versão 3.0b do TLS-Attacker, uma versão que implementa a melhoria DTLS.
O artefato contém:
As pastas mais importantes no diretório raiz do dtls-fuzzer são:
'experiments/results' contém resultados experimentais, que são o principal resultado do trabalho. Em particular
As pastas de saída são nomeadas com base na configuração da experiência, ou seja:
Como exemplo, o nome da pasta 'jsse-12_rsa_cert_none_rwalk_incl' indica uma experiência na implementação JSSE 12 do DTLS, usando um alfabeto que inclui entradas para realizar handshakes RSA, a autenticação de certificado do cliente está desativada, o algoritmo de teste é random walk e as retransmissões estão incluídas.
Uma pasta de saída contém:
O avaliador pode verificar (por exemplo) que os resultados experimentais em 'included' correspondem aos apresentados na Tabela 4, ou que as configurações testadas na Tabela 2 também aparecem em 'all_ciphers'. Note que os modelos que aparecem no artigo foram o resultado de uma poda/trimagem significativa, enquanto que os modelos que aparecem nas pastas de saída não foram alterados.
Para efeitos de avaliação do dtls-fuzzer é necessário realizar os seguintes passos:
Esta secção de avaliação é seguida por um guia sobre como usar o dtls-fuzzer que introduz os seus principais casos de uso.
dtls-fuzzer foi testado em distribuições Linux Ubuntu 18.04 e Debian 9. Deve funcionar em qualquer distribuição Linux recente. O suporte para outras plataformas não foi testado. Este guia assume que é utilizada uma distribuição baseada em Debian (que tem 'apt-get').
É necessária uma Máquina Virtual (VM) Java 8 JDK (Java Development Kit). A versão usada para executar experiências é a 1.8.0_222, embora versões posteriores do Java 8 também devam funcionar. Note que a ferramenta não compila em Java 9 ou posterior. Também dependemos do maven (o utilitário 'mvn') para gestão/implantação de dependências.
Recomendamos usar uma máquina suficientemente potente, caso contrário parâmetros de temporização sensíveis, como o tempo de espera de resposta, podem tornar-se demasiado baixos, causando saídas diferentes das obtidas no artigo. Pior ainda, podem fazer com que as experiências de aprendizagem falhem. As experiências originais foram executadas num servidor com muitos núcleos, no entanto, esperamos (embora não tenhamos testado exaustivamente) que a aprendizagem seja possível num desktop com processador i7. A aprendizagem também é possível em sistemas mais fracos se os parâmetros de temporização forem ajustados em conformidade. Finalmente, visualizar modelos .dot exportando-os para .pdf requer instalar a biblioteca graphviz. Assume-se que o utilitário 'dot' fornecido pelo graphviz está localizado no PATH do sistema.
Em suma, os pré-requisitos aconselhados são:
dtls-fuzzer requer Java 8 JDK (Java Development Kit). Se o Java não estiver instalado, instalamos a implementação OpenJDK (via 'apt-get' no Ubuntu), e podemos saltar o resto desta subsecção.
> sudo apt-get install openjdk-8-jdk
Se uma versão do java estiver instalada, podemos verificar qual a versão executando:
> java -version
O código de versão deve começar com 1.8 (por exemplo, 1.8.0_242), e a Máquina Virtual deve ser "Server VM" (indicando que o JDK completo está instalado, e não apenas o ambiente de execução). Se for o caso, terminámos com o Java. Se não for, podemos verificar se o Java 8 JDK está instalado na nossa plataforma mas não está selecionado atualmente, listando as VMs Java instaladas através de:
> update-java-alternatives --list
Se o Java 8 JDK aparecer, podemos usar o mesmo comando para configurar o Java 8 JDK como implementação Java padrão.
> sudo update-java-alternatives --set java-1.8.0-openjdk-amd64
Caso contrário, precisamos de realizar a instalação completa como mostrado no início. Infelizmente, 'update-java-alternatives' às vezes não é bem-sucedido, indicado por mensagens "error". Se tal caso surgir, podemos usar 'update-alternives' para configurar interativamente qual VM Java é selecionada por 'java' (interpretador) e 'javac' (compilador).
> sudo update-alternatives --config java
> sudo update-alternatives --config javac
Com o Java 8 configurado, procedemos à instalação das outras dependências, maven, graphviz mais algumas dependências comuns da SUT. Em seguida, clonamos o repositório do dtls-fuzzer para uma pasta à escolha, fazendo checkout do branch artifact. Para terminar, tornamos essa pasta o nosso diretório atual.
> sudo apt-get install maven graphviz autotools-dev automake libtool
> git clone -b usenix20-artifact https://github.com/assist-project/dtls-fuzzer.git ~/dtls-fuzzer
> cd ~/dtls-fuzzer
Primeiro executamos o script 'prepare.sh' que instala bibliotecas das quais dtls-fuzzer depende, nomeadamente dois .jars locais e TLS-Attacker 3.0b. Em seguida, instalamos a própria ferramenta. Os comandos resultantes num sistema POSIX serão:
> bash prepare.sh
> mvn clean install
Após estes passos, um diretório chamado 'target' deve ter sido construído contendo 'dtls-fuzzer.jar'. Esta é a nossa biblioteca executável. A partir deste ponto assume-se que os comandos são executados a partir do diretório raiz do dtls-fuzzer.
Suponha que queremos gerar um modelo para OpenSSL 1.1.1b usando apenas PSK (Pre-Shared Keys). Uma execução rápida do dtls-fuzzer é feita da seguinte forma.
Primeiro configuramos a SUT, o que é feito automaticamente por um script 'setup_sut.sh'.
> bash setup_sut.sh openssl-1.1.1b
Em seguida, selecionamos um ficheiro de argumentos da pasta 'args/openssl-1.1.1b'. Notamos que existem vários ficheiros de argumentos para escolher, nomeadamente:
learn_openssl-1.1.1b_all_cert_none_rwalk_incl
learn_openssl-1.1.1b_all_cert_nreq_rwalk_incl
learn_openssl-1.1.1b_all_cert_req_rwalk_incl
learn_openssl-1.1.1b_psk_rwalk_incl
O ficheiro de argumentos de interesse é 'learn_openssl-1.1.1b_psk_rwalk_incl', pois o seu nome indica PSK. Assim, selecionamo-lo e executamos o fuzzer nele. Adicionalmente, limitamos o número de testes a 200, para encurtar o tempo de aprendizagem. Finalmente, para o OpenSSL, LD_LIBRARY_PATH tem de ser definido para o diretório da implementação ('suts/openssl-1.1.1b/'). Antes de executar a aprendizagem, podemos querer executar um teste simples para verificar se a nossa configuração está a funcionar. Um bom teste é simplesmente completar um handshake. Fornecemos o ficheiro de argumentos, juntamente com um teste correspondente de 'examples/tests' como parâmetro. Obtemos:
> LD_LIBRARY_PATH=suts/openssl-1.1.1b/ java -jar target/dtls-fuzzer.jar @args/openssl-1.1.1b/learn_openssl-1.1.1b_psk_rwalk_incl -test examples/tests/psk
Se tudo correr bem, o servidor deve ter imprimido "This is a hello message", uma mensagem que enviamos após completar o handshake. Sabendo que a nossa configuração funciona, podemos agora iniciar a aprendizagem executando:
> LD_LIBRARY_PATH=suts/openssl-1.1.1b/ java -jar target/dtls-fuzzer.jar @args/openssl-1.1.1b/learn_openssl-1.1.1b_psk_rwalk_incl -queries 200
Notamos que foi criado um diretório de saída, 'output/openssl-1.1.1b_psk_rwalk_incl/' para a experiência. Podemos fazer 'ls' a este diretório para verificar o estado atual da experiência (o número de hipóteses geradas...).
> ls output/openssl-1.1.1b_psk_rwalk_incl/
Se tudo correr bem, após 20-30 minutos, o diretório de saída deve conter um ficheiro 'learnedModel.dot'. Podemos visualizar o ficheiro usando o utilitário 'dot' do graphviz, exportando para .pdf e abrindo o .pdf com o nosso visualizador de .pdf favorito.
> dot -Tpdf output/openssl-1.1.1b_psk_rwalk_incl/learnedModel.dot > output/openssl-1.1.1b_psk_rwalk_incl/learnedModel.pdf
> evince output/openssl-1.1.1b_psk_rwalk_incl/learnedModel.pdf
Finalmente, podemos usar 'trim_model.sh' para gerar uma versão melhor/mais simplificada do modelo. Isto pode ser feito da seguinte forma:
> bash trim_model.sh --output output/openssl-1.1.1b_psk_rwalk_incl/nicerLearnedModel.dot output/openssl-1.1.1b_psk_rwalk_incl/learnedModel.dot
> dot -Tpdf output/openssl-1.1.1b_psk_rwalk_incl/nicerLearnedModel.dot > output/openssl-1.1.1b_psk_rwalk_incl/nicerLearnedModel.pdf
> evince output/openssl-1.1.1b_psk_rwalk_incl/nicerLearnedModel.pdf
Podemos agora determinar a conformidade do sistema verificando o modelo em relação à especificação...
Ao fazer 'ls' do diretório de saída podemos encontrar 'error.msg'. Isso é um sinal de que a experiência falhou e a aprendizagem terminou abruptamente. Nesses casos, exibir o conteúdo revela a razão por detrás da falha.
> cat output/openssl-1.1.1b_psk_rwalk_incl/error.msg
Note que a verificação de conformidade ainda pode ser realizada na última hipótese gerada, desde que as descobertas potenciais sejam validadas contra o sistema (como devem ser de qualquer forma).
Fornecemos um script para configurar a SUT. Este script descarrega os ficheiros de origem, instala algumas dependências (jvm) e compila a SUT. Para ver as SUTs para as quais é fornecida configuração automática, execute:
> bash setup_sut.sh
Para configurar, por exemplo, a implementação tinydtls do Contiki-NG, execute:
> bash setup_sut.sh ctinydtls
O script irá gerar duas pastas no diretório raiz do dtls-fuzzer.
Infelizmente, automatizar a configuração da SUT é um processo complicado, por isso tomamos os seguintes atalhos. Para SUTs Java (JSSE, Scandium) não compilamos as implementações, em vez disso usamos os .jars compilados do diretório 'experiments/suts'. Note que o código-fonte destas SUTs Java (aplicações servidoras) está disponível publicamente online, veja Scandium e JSSE, o que também é o caso do PionDTLS. A instalação automática de dependências pode solicitar acesso 'sudo'. Isto acontece para o GnuTLS que depende de bibliotecas externas como nettle, e para o TinyDTLS da Eclipse, que depende de autoconf. Finalmente, não fornecemos configuração automática/ficheiros de argumentos para NSS e PionDTLS devido à complexidade da configuração destes sistemas.
Se algo no processo de configuração parar de funcionar, eliminar a pasta 'suts' (ou a pasta 'suts/SUT' específica da SUT) e reexecutar o script de configuração pode resolver o problema. Além disso, em caso de falha na compilação, o código-fonte da implementação ainda deve ser descarregado para o diretório 'suts'. Uma solução alternativa é compilar a implementação manualmente. Desde que a implementação seja compilada, a nossa configuração deve funcionar.
Fornecemos aqui uma árvore incompleta de dependências que as várias SUTs têm. As que estão em itálico são dependências que 'setup_sut.sh' tenta instalar usando acesso 'sudo'.
Estamos agora prontos para aprender uma configuração de SUT. Ficheiros de argumentos para várias configurações de SUT são fornecidos no diretório 'args' localizado no diretório base do dtls-fuzzer. Cada nome de ficheiro de argumentos descreve a configuração da experiência (SUT, alfabeto, autenticação) conforme descrito pelos nomes das pastas de saída em 'experiments/results/'. Para iniciar a aprendizagem para uma SUT usando um ficheiro de argumentos, execute:
> java -jar target/dtls-fuzzer.jar @args/sut_name/arg_file
A pasta de saída será armazenada num diretório 'output' gerado.
Comparado com as experiências no artigo, aumentámos o timeout de resposta para várias SUTs como adaptação a hardware menos potente. Para encurtar o tempo de aprendizagem, sugerimos diminuir o limite de testes do algoritmo random walk de 20000 para 5000. Isto pode ser feito com:
> java -jar target/dtls-fuzzer.jar @args/sut_name/arg_file -queries 5000
Isto irá sobrescrever a definição de limite no ficheiro de argumentos. Exceto para GnuTLS, PionDTLS e JSSE, esperamos que a aprendizagem produza os mesmos modelos para este limite mais baixo.
A temporização pode tornar-se um problema, causando não determinismo, seguido de terminação abrupta com um ficheiro 'error.msg' informativo. Nesses casos, existem dois parâmetros que podem ser ajustados:
Estes parâmetros podem ser ajustados sobrescrevendo (provavelmente com um valor mais alto) as definições correspondentes no ficheiro de argumentos:
> java -jar target/dtls-fuzzer.jar @args/sut_name/arg_file -timeout new_response_timeout -runWait new_start_timeout
Para evitar problemas relacionados com temporização, sugerimos executar experiências numa máquina suficientemente potente. A principal causa de não determinismo é a SUT demorar muito tempo a iniciar ou a gerar uma resposta. Esta probabilidade diminui à medida que mais poder computacional é fornecido.
Podemos desejar terminar automaticamente as experiências após um certo período, particularmente experiências que não se espera que terminem. Definir este período é possível através do parâmetro time limit ao qual é atribuída a duração máxima que a experiência pode decorrer. Esta duração é fornecida no formato ISO 8601. Para limitar o tempo de execução de uma experiência a 60 minutos, executaríamos:
> java -jar target/dtls-fuzzer.jar @args/sut_name/arg_file -timeLimit "PT60M"
É possível executar múltiplas experiências ao mesmo tempo desde que os servidores estejam configurados para ouvir em portas diferentes. Podemos escolher lançar cada experiência num terminal separado. Alternativamente, podemos lançar experiências num único terminal usando o utilitário 'disown':
> java -jar target/dtls-fuzzer.jar @args/ctinydtls/learn_ctinydtls_psk_rwalk 1>/dev/null 2>&1 & disown
No entanto, executar mais do que algumas (>2) coloca uma carga adicional na máquina. Também aumenta a probabilidade de falha na aprendizagem devido a colisão acidental de portas. Na maioria das configurações, os servidores estão configurados para ouvir numa porta hard-coded através de localhost, as configurações fornecidas em 'args' usam portas hard-coded distintas. Para configurações JSSE e Scandium, a configuração é diferente. Em cada teste, a SUT lança um servidor a ouvir numa porta escolhida dinamicamente e comunica a porta através de sockets TCP para o dtls-fuzzer. Isto tem a vantagem de notificar o dtls-fuzzer quando o servidor está pronto para receber pacotes (sem isso, o dtls-fuzzer teria de esperar cegamente um período arbitrário de tempo para o servidor iniciar). A desvantagem é que a porta alocada pode ser a mesma que alguma porta hard-coded de uma experiência diferente, onde uma thread de servidor foi recentemente parada e uma nova thread ainda não foi iniciada (significando que a porta hard-coded poderia ser usada na alocação dinâmica). Para evitar esta forma de colisão, aconselhamos executar experiências Scandium e JSSE separadamente de todas as outras.
Sugerimos as seguintes configurações para as quais a compilação automática é fiável, a aprendizagem é mais rápida ou foram encontrados bugs interessantes. Certifique-se de que configura a SUT antes de executar o comando fornecido. Notará que nos concentramos em configurações PSK sempre que possível. Isso deve-se ao facto de PSK com palavras-passe pequenas exigir significativamente menos tempo de processamento do que qualquer outro mecanismo de encriptação.### OpenSSL 1.1.1b Qualquer configuração do openssl-1.1.1b (por exemplo 'args/openssl-1.1.1b/learn_openssl-1.1.1b_all_cert_req_rwalk_incl') pode ser experimentada. Os experimentos terminam rapidamente (menos de um dia), exercitando todos os algoritmos de troca de chaves. Comando para configuração que exige certificado do cliente usando todos os algoritmos de troca de chaves (PSK, RSA, ECDH, DH):
> LD_LIBRARY_PATH=suts/openssl-1.1.1b/ java -jar target/dtls-fuzzer.jar @args/openssl-1.1.1b/learn_openssl-1.1.1b_all_cert_req_rwalk_incl -queries 5000
Nota: ao aprender o OpenSSL, é necessário apontar a variável LD_LIBRARY_PATH para o diretório de instalação.
Qualquer configuração do mbedtls-2.16.1 pode ser usada pelas mesmas razões que o OpenSSL. Os experimentos levam mais tempo para concluir, pois o SUT é mais lento. Comando para configuração com autenticação de certificado do cliente desativada usando todos os algoritmos de troca de chaves:
> java -jar target/dtls-fuzzer.jar @args/mbedtls-2.16.1/learn_mbedtls_all_cert_none_rwalk_incl -queries 5000
Uma versão editada do modelo obtido para esta configuração aparece no apêndice. Podemos usar um limite baixo de testes de 2000, já que o alfabeto de entrada é pequeno, facilitando os testes.
> java -jar target/dtls-fuzzer.jar @args/ctinydtls/learn_ctinydtls_psk_rwalk -queries 2000
Para o WolfSSL, fornecemos uma configuração PSK para a qual o aprendizado deve terminar relativamente rápido.
> java -jar target/dtls-fuzzer.jar @args/wolfssl-4.0.0/learn_wolfssl-4.0.0_psk_rwalk -queries 2000
A versão mais recente do GnuTLS que analisamos produziu modelos compactos e agradáveis. Infelizmente, ativar a autenticação do cliente levou a um aumento acentuado no número de testes necessários. Sugerimos uma configuração que a desative para reduzir o tempo de aprendizado:
> java -jar target/dtls-fuzzer.jar @args/gnutls-3.6.7/learn_gnutls-3.6.7_all_cert_none_rwalk_incl -queries 2000
Uma versão editada do modelo obtido para esta configuração aparece no artigo. O modelo expõe bugs importantes, infelizmente, o experimento é demorado. O experimento não deve ser executado em paralelo com experimentos que não envolvam Scandium ou JSSE. Comando:
> java -jar target/dtls-fuzzer.jar @args/scandium-2.0.0/learn_scandium-2.0.0_psk_rwalk -queries 2000
Uma versão editada do modelo obtido para esta configuração aparece no artigo. O modelo expõe bugs importantes. O experimento não deve ser executado em paralelo com experimentos que não envolvam Scandium ou JSSE. Note que o aprendizado para JSSE não termina/converge, construindo hipóteses com cada vez mais estados. Portanto, configuramos os experimentos JSSE para terminar automaticamente após um dia (dois dias no artigo). Comando para troca de chaves RSA:
> java -jar target/dtls-fuzzer.jar @args/jsse-12/learn_jsse-12_rsa_cert_req_rwalk_incl
Em vez de um aprendizado árduo, podemos querer simplesmente testar se um handshake pode ser concluído neste cenário sem enviar nenhuma mensagem de certificado. Isso pode ser feito executando:
> java -jar target/dtls-fuzzer.jar @args/jsse-12/learn_jsse-12_rsa_cert_req_rwalk_incl -test examples/tests/rsa
Uma vez que o aprendizado é concluído, as coisas a analisar no diretório de saída são:
O modelo aprendido em .dot pode ser visualizado usando a biblioteca graphviz, convertendo para .pdf:
> dot -Tpdf learnedModel.dot > learnedModel.pdf
Infelizmente, à medida que os modelos crescem em tamanho, os .pdfs gerados por este método tornam-se cada vez mais difíceis de ler. Por isso, desenvolvemos/usamos/importamos scripts de poda que são acessados por 'trim_model.sh'. O script fornece informações de uso executando:
> bash trim_model.sh
Recomendamos usar o script em sua forma mais básica, que é:
> bash trim_model.sh learnedModel.dot
O script:
(5) requer a instalação da biblioteca mypydot Python 3 personalizada encontrada em 'experiments\scripts'. Todos os outros passos usam 'sed' simples mais a biblioteca Java dot-trimmer. Um .jar para esta biblioteca está incluído em 'experiments\scripts'.
Execute:
> java -jar target/dtls-fuzzer.jar -help
O número de opções pode ser assustador. Para aprender uma implementação de servidor DTLS, você só precisa especificar algumas opções, a saber: "-connect ip_address:port" que é o endereço onde o servidor DTLS em execução está escutando. Todas as outras opções são definidas com valores padrão, incluindo o alfabeto.
Para lançar uma execução de aprendizado para uma implementação de servidor local existente, digamos, escutando na porta 20000, execute:
> java -jar target/dtls-fuzzer.jar -connect localhost:20000
Provavelmente haverá problemas com este tipo de aprendizado. O aprendizado exige que seja possível reiniciar o servidor após cada teste. Alguns servidores carregarão algum estado de um teste para outro. Isso pode levar a não-determinismo durante o aprendizado, portanto, uma abordagem melhor é lançar uma nova thread do servidor em cada teste usando um comando fornecido. A thread do servidor é encerrada assim que o teste é executado, garantindo a reinicialização adequada. Exemplo para OpenSSL:
> java -jar target/dtls-fuzzer.jar -connect localhost:20000 -cmd "openssl s_server -accept 20000 -dtls1_2"
Com tantos parâmetros, os comandos podem se tornar muito longos. dtls-fuzzer usa JCommander para analisar argumentos, que também pode ler parâmetros de um arquivo. Vá para 'experiments/args' para exemplos de argumentos. Para fornecer um arquivo de argumentos para dtls-fuzzer, forneça-o como parâmetro precedido por "@". Você também pode adicionar outros argumentos explícitos aos comandos (que sobrescreverão os do arquivo de argumentos)
> java -jar target/dtls-fuzzer.jar @arg_file ...parâmetros sobrescritos...
Para lançar um lote de execuções de aprendizado, pode-se usar o script 'launcher.py' em 'experiments/scripts'. Fornecido um diretório com arquivos de argumentos, a ferramenta lançará um processo de aprendizado para cada arquivo de argumento.
> python3 experiments/scripts/launcher.py --jar target/dtls-fuzzer.jar --args args_folder
Antes de executar experimentos de aprendizado, é útil verificar se os argumentos estão configurados corretamente, especialmente os parâmetros de temporização. Para isso, dtls-fuzzerr pode executar um conjunto de testes personalizado (coleção de testes) no SUT e fornecer um resumo das saídas. Essa funcionalidade também pode ser usada ao diagnosticar experimentos de aprendizado com falha, ou seja, descobrir o que deu errado.
Para executar o conjunto de testes em um servidor usando o alfabeto padrão, execute:
> java -jar target/dtls-fuzzer.jar -connect localhost:20000 -test test_file
Para exemplos de arquivos de teste, vá para 'examples/tests'. Um arquivo de teste consiste em uma lista de entradas separadas por nova linha. Os testes são separados por linhas novas vazias. O final de cada teste é o final do arquivo ou uma linha nova vazia. "#" é usado para comentar uma linha.
Se você tem um modelo/especificação, também pode executar o conjunto de testes e comparar a saída com a da especificação.
> java -jar target/dtls-fuzzer.jar -connect localhost:20000 -test test_file -specification model
O número de vezes que os testes são executados é configurável pelo parâmetro '-times', que padrão é 1. Definir um número alto ajuda a detectar não-determinismo nas configurações de aprendizado, comparando a saída de cada teste.
> java -jar target/dtls-fuzzer.jar -connect localhost:20000 -test test_file -times 10
Finalmente, se você tiver o arquivo de argumentos para um experimento de aprendizado, pode usá-los para executar testes no SUT envolvido apenas adicionando os argumentos de teste necessários:
> java -jar target/dtls-fuzzer.jar @learning_arg_file -test test_file