Com o início de 2019, é bom lembrar o passado e pensar no futuro. Vamos relembrar 30 anos e refletir sobre os primeiros artigos científicos sobre fuzzing:
"Um estudo empírico da confiabilidade dos utilitários UNIX" e o trabalho subsequente de 1995
"Revisão de fuzzing", do mesmo autor
Barton Miller .
Neste artigo, tentaremos encontrar bugs nas versões modernas do Ubuntu Linux usando
as mesmas ferramentas dos trabalhos de difusão originais. Você deve ler os documentos originais, não apenas pelo contexto, mas também pelo entendimento. Eles se mostraram muito proféticos em relação a vulnerabilidades e façanhas nas próximas décadas. Os leitores atentos poderão notar a data de publicação do artigo original: 1990. Ainda mais atento notará os direitos autorais nos comentários da fonte: 1989.
Breve revisão
Para quem não leu os documentos (embora isso realmente deva ser feito), esta seção contém um breve resumo e algumas citações selecionadas.
O programa de difusão gera fluxos aleatórios de caracteres, com a capacidade de gerar apenas caracteres imprimíveis ou não imprimíveis. Ele usa um certo valor inicial (semente), garantindo resultados reproduzíveis, dos quais os difusores modernos geralmente não têm. Um conjunto de scripts é executado nos programas testados e verifica a presença de despejos básicos. Trava são detectados manualmente. Os adaptadores fornecem entrada aleatória para programas interativos (artigo de 1990), serviços de rede (1995) e aplicativos gráficos X (1995).
Um artigo de 1990 testou quatro arquiteturas de processador (i386, CVAX, Sparc, 68020) e cinco sistemas operacionais (4,3 BSD, SunOS, AIX, Xenix, Dynix). Em um artigo de 1995, uma escolha similar de plataformas. No primeiro artigo, 25-33% dos utilitários falham, dependendo da plataforma. Em um artigo subsequente, esses números variam de 9% a 33%, com GNU (no SunOS) e Linux com a menor taxa de falhas.
Um artigo de 1990 concluiu que 1) os programadores não verificam os limites da matriz ou os códigos de erro, 2) as macros dificultam a leitura e o código de depuração e 3) C é muito inseguro. A função
gets
extremamente insegura e o sistema de tipos C. foi especialmente mencionado. Durante o teste, os autores encontraram vulnerabilidades no Format String anos antes de sua exploração em massa. O artigo conclui com uma pesquisa de usuários sobre a frequência com que eles corrigem bugs ou os relatam. Aconteceu que relatar bugs era difícil e havia pouco interesse em corrigi-los.
Um artigo de 1995 menciona o software de código aberto e discute por que ele tem menos erros. Citação:
Quando investigamos as causas das falhas, um fenômeno perturbador apareceu: muitos dos bugs (cerca de 40%) relatados em 1990 ainda estão presentes em sua forma exata em 1995. ...
Os métodos usados aqui são simples e principalmente automatizados. É difícil entender por que os desenvolvedores não usam essa fonte fácil e gratuita para aumentar a confiabilidade.
Somente em 15 a 20 anos a técnica de difusão se tornará uma prática padrão para grandes fornecedores.
Parece-me também que esta declaração de 1990 prevê eventos futuros:
Freqüentemente, o estilo lacônico de programação C é levado ao extremo, a forma prevalece sobre a função correta. A possibilidade de um estouro no buffer de entrada é uma falha de segurança em potencial, como mostrou o recente worm da Internet .
Metodologia de teste
Felizmente, 30 anos depois, o Dr. Barton ainda fornece o
código-fonte completo, scripts e dados para reproduzir suas descobertas : um exemplo louvável que outros pesquisadores devem seguir. Os scripts funcionam sem problemas, e a ferramenta de difusão requer apenas pequenas alterações para compilar e executar.
Para esses testes, usamos
scripts e entradas do repositório fuzz-1995-basic , porque há a lista mais recente de
aplicativos testados . De acordo com o
README , aqui estão as mesmas entradas aleatórias do estudo original. Os resultados abaixo para o Linux moderno são obtidos
exatamente no mesmo código de difusão e dados de entrada que nos artigos originais. Somente a lista de utilitários para teste foi alterada.
Alterações de utilidade ao longo de 30 anos
Obviamente, houve algumas mudanças nos pacotes de software Linux nos últimos 30 anos, embora algumas utilidades comprovadas continuem com seu pedigree por décadas. Sempre que possível, utilizamos versões modernas dos mesmos programas em um artigo de 1995. Alguns programas não estão mais disponíveis, nós os substituímos. Justificação para todas as substituições:
cfe
⇨ cc1
: Equivalente ao pré-processador C do artigo de 1995.dbx
⇨ gdb
: Equivalente ao depurador de 1995.ditroff
⇨ groff
: o ditroff
não ditroff
mais disponível.dtbl
⇨ gtbl
: Equivalente ao GNU Troff do antigo utilitário dtbl
.lisp
⇨ clisp
: A implementação padrão do lisp.more
less
: menos é mais!prolog
swipl
: Existem duas opções para o prólogo: SWI Prolog e GNU Prolog. O SWI Prolog é preferível porque é uma implementação mais antiga e completa.awk
⇨ gawk
: versão GNU do awk
.cc
⇨ gcc
: O compilador C padrão.compress
⇨ gzip
: GZip é o descendente conceitual do antigo utilitário compress
Unix.lint
: lint
reescrito sob a GPL./bin/mail
⇨ /usr/bin/mail
: Utilitário equivalente de uma maneira diferente.f77
⇨ fort77
: Existem duas variações do compilador Fortan77: GNU Fortran e Fort77. O primeiro é recomendado para o Fortran 90 e o segundo para o suporte do Fortran77. O programa f2c
apoiado ativamente; sua lista de alterações é mantida desde 1989.
Resultados
A técnica de difusão de 1989 ainda encontra erros em 2018. Mas há algum progresso.
Para medir o progresso, você precisa de alguma base. Felizmente, essa estrutura existe para utilitários Linux. Embora o Linux não existisse na época do artigo original em 1990, um segundo teste em 1995 lançou o mesmo código difuso nos utilitários da distribuição Slackware 2.1.0 de 1995. Os resultados correspondentes são apresentados na
tabela 3 do artigo de 1995 (p. 7-9) . Comparado aos concorrentes comerciais, o GNU / Linux parece muito bom:
A porcentagem de falhas no utilitário na versão Linux gratuita do UNIX foi a segunda mais alta: 9%.
Então, vamos comparar os utilitários Linux de 1995 e 2018 com as ferramentas de difusão de 1989:
| Ubuntu 18.10 (2018) | Ubuntu 18.04 (2018) | Ubuntu 16.04 (2016) | Ubuntu 14.04 (2014) | Slackware 2.1.0 (1995) |
---|
Crashes | 1 (f77) | 1 (f77) | 2 (f77, ul) | 2 (swipl, f77) | 4 (ul, flex, travessão, gdb) |
Congela | 1 (feitiço) | 1 (feitiço) | 1 (feitiço) | 2 (feitiço, unidades) | 1 (ctags) |
Total testado | 81 | 81 | 81 | 81 | 55 |
Falhas / congelamentos,% | 2% | 2% | 4% | 5% | 9% |
Surpreendentemente, o número de travamentos e congelamentos do Linux ainda é maior que zero, mesmo na versão mais recente do Ubuntu. Portanto, o
f77
chama o programa
f2c
com um erro de segmentação e o programa
spell
fica
f2c
em duas versões da entrada de teste.
Quais erros?
Consegui descobrir manualmente a causa raiz de alguns erros. Alguns resultados, como um erro na glibc, foram inesperados, enquanto outros, como sprintf com um tamanho de buffer fixo, eram previsíveis.
Ul falha
O erro no
ul é realmente um erro no glibc. Em particular, foi relatado
aqui e
aqui (outra pessoa encontrou em
ul
) em 2016. De acordo com o rastreador de erros, o erro ainda não foi corrigido. Como o bug não pode ser reproduzido no Ubuntu 18.04 e posterior, ele é corrigido no nível de distribuição. A julgar pelos comentários no rastreador de erros, o principal problema pode ser muito sério.
Crash f77
O programa
f77
vem no pacote fort77, que é um script de shell em torno de
f2c
, o tradutor de origem do Fortran77 para C. A depuração do
f2c
mostra que ocorre uma falha quando a função
errstr
imprime uma mensagem de erro muito longa. O
código-fonte f2c mostra que a função sprintf é usada para gravar uma cadeia de comprimento variável em um buffer de tamanho fixo:
errstr(const char *s, const char *t) #endif { char buff[100]; sprintf(buff, s, t); err(buff); }
Parece que esse código foi preservado desde a criação do
f2c
. O programa tem um
histórico de mudanças desde pelo menos 1989. Em 1995, quando re-difundindo, o compilador Fortran77 não foi testado, caso contrário, o problema teria sido encontrado anteriormente.
Congelar feitiço
Um ótimo exemplo de impasse clássico.
spell
delega a
ispell
spell
através de um cano.
spell
lê o texto linha por linha e produz um registro de bloqueio do tamanho da linha em
ispell
. No entanto, o
ispell
lê no máximo
BUFSIZ/2
bytes por vez (4096 bytes no meu sistema) e emite um registro de bloqueio para garantir que o cliente tenha recebido dados de validação que foram processados até o momento. Duas entradas de teste diferentes forçaram a
spell
a escrever uma sequência de mais de 4096 caracteres para
ispell
, o que resultou em um impasse: a
spell
aguarda que a
ispell
leia a sequência inteira, enquanto a
ispell
aguarda a
spell
confirmar que leu as correções ortográficas originais.
Pendurar unidades
À primeira vista, parece que há uma condição de loop infinito. O travar parece estar em
libreadline
e não em
units
, embora as versões mais recentes das
units
não sofram esse erro. O log de alterações indica que foi adicionada a filtragem de entrada que pode corrigir acidentalmente esse problema. No entanto, uma investigação completa dos motivos está além do escopo deste blog. Talvez a maneira de travar a
libreadline
ainda
libreadline
lá.
Swipl crash
Por uma questão de completude, quero mencionar a falha
swipl
, embora não a tenha estudado com cuidado, pois o bug foi corrigido há muito tempo e parece ter uma qualidade bastante alta. Falha é, na verdade, uma declaração (isto é, que nunca deve acontecer) que é chamada ao converter caracteres:
[Thread 1] pl-fli.c:2495: codeToAtom: Assertion failed: chrcode >= 0
C-stack trace labeled "crash":
[0] __assert_fail+0x41
[1] PL_put_term+0x18e
[2] PL_unify_text+0x1c4
…
O travamento é sempre ruim, mas pelo menos aqui o programa pode relatar um erro, travando cedo e em voz alta.
Conclusão
Nos últimos 30 anos, a difusão permaneceu uma maneira simples e confiável de encontrar bugs. Embora
a pesquisa ativa esteja em andamento
nessa área , mesmo o fuzzer de 30 anos atrás encontra erros nos utilitários modernos do Linux.
O autor dos artigos originais previu os problemas de segurança que C causaria nas próximas décadas. Ele argumenta de forma convincente que código inseguro é muito fácil de escrever em C e deve ser evitado, se possível. Em particular, os artigos demonstram que os erros aparecem mesmo com as fases mais simples e esses testes devem ser incluídos na prática padrão de desenvolvimento de software. Infelizmente, esse conselho não é seguido há décadas.
Espero que você tenha gostado desta retrospectiva de 30 anos. Aguarde o próximo artigo “Fuzzing in 2000”, onde examinaremos o quão robustos os aplicativos Windows 10 são comparados
aos seus equivalentes Windows NT / 2000 quando testados com fuzzer . Eu acho que a resposta é previsível.