Detectando buffer over/underflows em C e C++ com ferramentas OpenSource
Desenvolvimento - C/C++
Quem programou em C ou C++ já esbarrou nesta categoria de erro: buffer overflows. Muito utilizados em ataques de seguranca, este tipo de erro acontece quando se acessa um ponto além (over) ou abaixo (under) do segmento de memória alocado para um dado buffer/vetor.
Este tipo de erro lógico pode ser ainda mais patológico se ocorre overflow por somente 1 a 3 bytes, pois o programa pode continuar executando normalmente sem sinais de problema. Acrescente a combinação de arquiteturas, compiladores, opções de otimização e o resultado é um erro difícil de ser descoberto.
Mas existe esperança, pois hoje existem ferramentas que auxiliam no processo de debugação e detecção deste tipo de erro (além de outros, como uso de ponteiros não inicializados, memory leaks, etc).
Recentemente eu buscava uma ferramenta/biblioteca que pudesse me ajudar a achar este tipo de erro. Então escrevi um pequeno programa para testar as condições:
Overflow 1 byte em leitura
Overflow 1 byte em escrita
Underflow 1 byte em leitura
Underflow 1 byte em escrita
O código de teste, resumido é algo como:
void buff_reader(char *v, int size) { char tmp; for (int i = 0; i < size; ++i) tmp = v[i]; } void buff_writer(char *v, int size) { for (int i = 0; i < size; ++i) v[i] = 0; } void corruption(char *v, int size) { /* Overflows reading/writing */ v++; buff_reader(v, size); buff_writer(v, size); /* Underflows reading/writing */ --v; --v; buff_reader(v, size); buff_writer(v, size); }
O programa principal chama a função corruption para testar o comportamento em memória alocada dinamicamente (heap) e também estática (stack).
Como são 4 condições de teste com 2 tipos de memória (estática e dinâmica) teremos um total de 8 testes no programa.
Antes dos flames, não estou dizendo que esta é a melhor maneira de testar o problema ou que este fragmento de código é perfeito. Somente queria algo simples para poder tirar minhas próprias conclusões sobre este problema bem definido de overflows. Críticas e sugestões são bem vindos desde que você tenha lido este parágrafo antes...
O critério de seleção das ferramentas tinha alguns requisitos extras. Era necessário que a ferramenta fosse gratuíta ou tivesse uma licença livre, pois nem sempre o(s) projeto(s) tem a possibilidade de adquirir ferramentas pagas. A ferramenta deveria ser não intrusiva, dispensando modificações no código fonte para os testes. Finalmente, eu tinha interesse que a ferramenta fosse capaz de funcionar com C++, além de linguagem C.
Seguindo estes critérios, foram testadas 3 ferramentas contra este programa
gcc stack smashing protector: implementado como patch no gcc 3.x, faz parte oficial do gcc 4.1. A técnica básica é modificar o stack de forma a incluir um valor especial (stack canary) junto aos buffers para monitorar overflows. A vantagem desta técnica é um overhead desprezível na execução dos aplicativos. Já se esperava um resultado ruim para memória alocada dinamicamente, uma vez que a ferramenta foi concebida somente para stack.
Dependência: gcc
DUMA (Detecting Unintended Memory Access): é um fork do electric fence de Bruce Perens, trata-se de um malloc/new e free/delete sobrecarregados. Exige a linkagem com a biblioteca e gera um overhead considerável para executar o aplicativo (portanto, somente válido para testes). Por trabalhar com memória dinâmica, não se esperava sucesso para detecção de falhas na memória estática (http://duma.sourceforge.net/).
Dependência: libduma
mudflap: pelo que li do artigo do autor desta extensão do gcc (http://gcc.fyxm.net/summit/2003/mudflap.pdf), ele analisa as estruturas (árvores) geradas pelo primeiro estágio de compilação do gcc à procura de padrões de operações com ponteiros potencialmente perigosas. Quando encontradas, insere expressões que retornem o mesmo valor, porém com referências a biblioteca libmudflap para detectar operações inválidas. Tem considerável impacto na execução, porém serviria para detectar falhas no heap e no stack.
Dependência: libmudflap, gcc > 4.0
Portanto, para ser capaz de compilar o programa utilizando o makefile incluído, deve-se ter instaladas no sistema as dependências (duh!).
Antes que comentem, conheço o Valgrind e acho ele bem legal. Em fato, acredito que ele merece um artigo próprio, por isso não inclui ele aqui neste comparativo. Testei ele com o programa e com as opções default ele pegou 4/8 erros (os problemas com memória estática não foram detectados). Acredito que talvez ele tenha maiores opções para melhorar este índice e pretendo investigar melhor a questão no futuro. Se alguém souber o que poderia/deveria ser feito, fico no aguardo de sugestões.
Sobre a disponibilidade das ferramentas, a DUMA oferece versão para para os compiladores gcc (linux), devcpp (windows), Microsoft Visual C++ (6.0, .net, 2005). A "stack smashing protector" está vinculado ao gcc 4.1, disponível nos *nix e com os primeiros "ports" para windows (mas especificamente, relacionado ao mingw). A biblioteca mudflap não parece ter versão disponível para windows no presente momento.
Os resultados são apresentados da melhor performance para a pior.
Mudflap performance (8/8 tests)
memory overflow overflow underflow underflow
static * * * *
Comentários: o mudflap conseguiu pegar todos os erros no programa, resultado acima das minhas expectativas. O programa de teste gerado flap deve imprimir informações de violação de acesso à memória no terminal. Opções de compilação: -lmudflap -fmudflap.
DUMA performance (3/8 tests)
static ! ! ! !
dynamic * * ! *
Comentários: como esperado, DUMA somente funcionou para memória dinâmica. Porém no teste de underflow de leitura ele não apontou o erro (ver comentários extras nas conclusões). O arquivo de core gerado pode ser utilizado pelo gdb para gerar um backtrace onde se localiza precisamente o ponto onde ocorre violação de memória (dica: após compilar e rodar o programa de teste target duma, no terminal faça "$gdb duma core"). Opções de compilação: -lduma.
GCC (-fstack-protector-all) performance (0/8 tests)
dynamic ! ! ! !
Comentários: esta opção de compilação falhou para apontar todas as falhas de acesso a memória estática com overflow de 1 byte, somente funcionando quando o overflow superava 3 bytes (programa gerado stacker. Esta é uma prova de como overflows/underflows por 1 byte podem aparecer em programas aparentemente corretos, sem qualquer sinal de erro. Opções de compilação: -fstack-protector-all.
Linux 2.6.17, g++ 4.1.2, mudflap 4.1.1, DUMA 2.4.27
Das alternativas testadas, a de melhor desempenho foi o mudflap, obtendo êxito para detectar falhas em todos os testes. Trata-se de um projeto novo e somente suporta versões mais recentes do gcc (este pode ser um fator negativo dependendo do projeto).
dica: pode-se obter uma lista de opções do mudflap ao se executar um programa linkado à libmudflap. Tente rodar "$MUDFLAP_OPTIONS=-help ./flap"
Entretanto, o mudflap possui alguns problemas, segundo a documentação, pode apresentar falhas em alguns códigos C++ muito complexos.
A DUMA obteve desempenho bom e o fato de não apontar o underflow de leitura pode ser considerado um feature, pois nem sempre é um erro no programa fazer um acesso de leitura em 1 posição abaixo do começo do buffer (e.g. no livro Numerical Recipes in C, para acesso a vetores/matrizes com base em índice 1 i.e. v[1], usa-se passar o endereço de memória 1 posição abaixo da memória alocada).
A opção de proteção de stack do gcc foi criada para ser leve e apontar falhas mais graves de acesso à memória, impedindo ataques por overflow. Por estes motivos, pode ser relevada sua baixa performance nos testes, porém é curioso saber que na implementação testada do gcc somente apareceram falhas usando-se overflows superiores a 3 bytes.
Acredito que o uso destas ferramentas pode aumentar sensivelmente a qualidade do software gerado, sendo uma técnica muito útil para desenvolvedores.