quinta-feira, 17 de junho de 2010

Ataques por buffer overflow – e além deles

Este artigo é um resumo e revisão do artigo “Beyond Stack Smashing”, cuja referência está ao final do texto.

Uma das principais formas de ataque aos sistemas é a injeção de código. O hacker identifica brechas de segurança no sistema e injeta um conjunto de dados em memória que corresponde às instruções que deseja executar. A partir daí basta (como se fosse simples!) forçar o redirecionamento do contador de programa para aquela posição de memória e pronto, o código injetado passa a ser executado, realizando a alteração de comportamento que o atacante projetou, que pode ser simplesmente um “denial of service” (DoS) ou algo mais sofisticado como executar um programa ou mudar uma senha através da chamada ao comando apropriado. O ping da morte é um exemplo clássico de buffer overflow (ou buffer overrun) que provocava uma queda no sistema atacado.

Só para relembrarmos, temos duas estruturas básicas na organização de memórias de um processo: o stack e o heap. O stack é a região de memória organizada numa fila lifo (last-in, first-out) que suporta as chamadas de rotinas, armazenando variáveis locais, parâmetros de chamada e endereços de retorno. A organização varia entre os sistemas operacionais, mas para cada um deles possui uma estrutura relativamente bem conhecida. Já o heap é a estrutura responsável por manter as estruturas criadas por alocação dinâmica.
Uma das formas de se realizar um ataque de buffer overflow é através do chamado “stack smashing” (“esmagamento” de pilha), onde o atacante se utiliza de um software com falhas de projeto que permitam o envio de dados além de uma quantidade esperada por um buffer. Com isso, os dados enviados sobrescrevem o espaço de memória reservado para o ponteiro-de-retorno e, com o conhecimento adequado, pode-se colocar a informação desejada naquele ponto.

Para ficar mais claro, vamos a um exemplo com objetivo puramente didático. Considere portanto o programa em C a seguir e a figura que ilustra a organização da stack.

void funcaoErrada(char* str) {
int i = 10;
char buffer[10];
strcpy(buffer,str);
}

int main() {
char *s = “Texto com mais de dez caracteres”;
funcaoErrada(s);
}

Fica evidente que este programa irá se comportar de forma imprevisível, corrompendo a pilha à medida em que a função strcpy, na “funcaoErrada”, ultrapassa os limites do buffer e escreve por sobre o ponteiro de retorno e outras estruturas.

Agora, imagine que o hacker tem conhecimento suficiente sobre a organização de memória de um certo sistema operacional e sobre um determinado programa em questão, de modo que consiga, através de um buffer overflow, sobrescrever o ponteiro-de-retorno com um conjunto de bytes que aponte exatamente para a posição buffer[0], onde ele colocou um conjunto de bytes que corresponde às instruções desejadas? Pronto. Tomou posse do comportamento do sistema através do código injetado.

Esta é a base do “stack smashing”, que possui variantes mais sofisticadas.

O "Trampolining" é uma variação de “stack smashing”. No modelo básico, o atacante precisa saber o endereço absoluto de buffer[0], para poder registrar esta informação no ponteiro-de-retorno. Isto nem sempre é factível. No entanto, se sabe-se que um registrador R possui um endereço relativo ao do buffer, então pode-se de forma indireta usar o valor de R para apontar para buffer[0]. O conjunto de instruções que permite esta transferência indireta de controle é denominada “trampolim”.

Outra forma é utilizar o que podemos chamar de “stack smashing em 2 passos”. A técnica é útil quando o buffer é muito pequeno. Neste caso o hacker utiliza alguma operação anterior para carregar as instruções do código injetado para uma posição específica e em seguida realiza o buffer overflow para carregar o ponteiro-de-retorno com o endereço daquela posição.

"Arc Injection" é outra técnica de exploração que exige que o programa atacado utilize os dados passados como parâmetro para outro processo inicializado por ele. Para isto é necessário saber antecipadamente que a função hackeada realiza este tipo de chamada, ou então usar o stack buffer overrun para direcionar a execução a uma chamada de uma função de criação de um novo processo (execl, system), por exemplo. Outra possibilidade com esta técnica é executar uma chamada a uma função com endereço já conhecido na estrutura de memória do sistema operacional. Então, na realidade não estamos injetando código, mas injetando uma chamada para um outro processo (daí o nome).

Finalmente, “Pointer Subterfuge"consiste na técnica de alterar a organização de ponteiros dentro de um programa, colocando a informação apropriada para permitir o redirecionamento. Esta técnica se divide em outras categorias:
  • function-pointer clobbering: consiste na modificação do código de uma função quando a mesma está sendo acionada por ponteiros (void (*function) () = ….)
  • data-pointer modification: consiste em utilizar um buffer oveflow para escrever sobre um ponteiro dentro da função.
  • exception-handler hijacking: no windows ponteiros para os manipuladores de exceção são armazenados na pilha. Se sobrescrevemos estas posições por meio de um buffer overrun e em seguida disparamos a exceção, não é ela que será executada mas sim o código armazenado no endereço escrito no ponteiro.
  • virtual pointer smashing (VPTR Smashing): A “virtual function table” (VTBL) é um array para funções virtuais disponibilizadas para uma classe e é, normalmente, armazenado no cabeçalho do objeto. A VTBL é acessada por um VPTR (virtual pointer). A substituição do VPTR por um valor próprio, apontando para uma estrutura VTBL forjada, construida intencionalmente para apontar para funções injetadas no sistema pelo atacante (na pilha, por exemplo, por stack smashing).
Além do stack – heap smashing
Até algum tempo atrás imaginava-se que ataques baseados no heap não eram possíveis devido a sua estrutura imprevisível. Como sabemos, o heap é a área onde acontece a alocação dinâmica de memória mediante solicitação do programa e é o fluxo do programa que determina como o heap estará estruturado.

Mais uma vez, para explorar qualquer fragilidade no heap o atacante tem que conhecer algum invariante na estrutura e nos processos de sua manipulação. Obviamente, o caminho inicial deve ser encontrar um comportamento padrão e fragilidades e na arquitetura no processo que realiza a alocação dinâmica, no entanto esta técnica é bem mais difícil que aquelas baseadas em stack por que os processos de alocação e liberação, bem como os processos de compressão de espaços livres de memória, dificultam qualquer previsão sobre onde estão os espaços alocados, quais os tamanhos alocados e quais informações eles contém. No caso de aplicações multi-threaded a coisa piora ainda mais. Difícil, mas não impossível. Existem pesquisadores (principalmente de grupos Hacker) trabalhando nas possibilidades de uso do heap para abrigar código injetado que possa ser executado posteriormente por uma mudança no contador de programa.

A coisa não é tão simples assim
Fica claro que não é trivial realizar este tipo de ataque. É necessário conhecer a organização de memória dos processos no sistema operacional, conhecer o código-fonte do programa atacado e, certamente, não é o tipo de ataque que ocorre com apenas uma tentativa. De qualquer forma, é um tipo de ataque que ainda ocorre com freqüência.

Para quem quer mais informações, basta dar uma olhada nas referências.

Referências