Desenvolvimento - C/C++

Detectando buffer over/underflows em C e C++ com ferramentas OpenSource

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.

por Adenilson Cavalcanti da Silva



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

  1. 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
  1. 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
  1. 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

Adenilson Cavalcanti da Silva

Adenilson Cavalcanti da Silva - Adenilson (a.k.a. Savago) desenvolve sistemas há 10 anos, utilizando diversas linguagens de programação e sistemas operacionais. Tendo se especializado em C++, está sempre a procura de novos desafios com características multidisciplinares. Mestre pela USP, possui interesse especial por visão artificial, *nix, programação baixo nível.