O problema
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).
O teste
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...
As ferramentas
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
Os resultados são apresentados da melhor performance para a pior.
Mudflap performance (8/8 tests)
| memory |
overflow |
overflow |
underflow |
underflow |
|
read |
write |
read |
write |
| static |
* |
* |
* |
* |
| dynamic |
* |
* |
* |
* |
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)
| memory |
overflow |
overflow |
underflow |
underflow |
|
read |
write |
read |
write |
| 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)
| memory |
overflow |
overflow |
underflow |
underflow |
|
read |
write |
read |
write |
| static |
! |
! |
! |
! |
| 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.
Environment
Linux 2.6.17, g++ 4.1.2, mudflap 4.1.1, DUMA 2.4.27
Conclusão
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.
Código fonte