INF1018 - Software Básico

Invasão da Pilha de Execução

Neste laboratório você vai usar seus conhecimentos sobre a pilha de execução para entender o que são ataques do tipo buffer overflow. Este tipo de ataque se aproveita da ausência de verificação de "final de array" em C para sobrescrever a pilha de execução, alterando o endereço de retorno lá presente, e possivelmente outras informações. Ainda que com muitas simplificações, este laboratório mostra como é importante escrever código seguro...

Ao longo do tempo, os sistemas foram ficando mais protegidos contra esse tipo de ataque, por isso vamos precisar desabilitar algumas dessas proteções. Uma delas é feita pelo compilador gcc: a inserção de um código que verifica a integridade da pilha após uma chamada de função. Para desabilitar essa proteção, executamos o gcc com a opção -fno-stack-protector.

Outro mecanismo de segurança presente nos sistemas atuais é impedir que um programa execute código que "resida" na área de dados, ou na pilha. Para desabilitar essa proteção, passamos ao gcc a opção -Wa,--execstack.

A última proteção que precisamos desabilitar é a randomização de endereços, que impede que os endereços dos dados de um programa (e de sua pilha) sejam sempre os mesmos, em qualquer execução desse programa. Para realizar esse laboratório sem essa proteção, execute na linha de comando

setarch x86_64 -R /bin/bash
para entrar em um ambiente de execução sem randomização de endereços, e execute todos os comandos do laboratório dentro desse ambiente.



Vamos utilizar o programa bufbomb.c para testar alguns exemplos. Para isso, busque aqui o arquivo bufferbomb.tar e execute:
tar xvf bufferbomb.tar
Isso deve criar uma pasta bufferbomb com os arquivos: Para criar o programa executável bufbomb, escreva:
make bufbomb
O utilitário make é uma ferramenta que constrói programas e bibliotecas a partir de regras definidas em um arquivo denominado Makefile (ou makefile).

O programa bufbomb é bastante simples: sua main basicamente chama a função getbuf, definida no arquivo buf.c. Observe a função getbuf: repare que ela declara um array local e chama a função Gets para preencher esse array. Gets, definida em bufbomb.c, lê caracteres da entrada padrão e os armazena no array recebido como parâmetro.

Execute ./bufbomb escrevendo um texto qualquer quando o programa ficar a espera de entrada, e veja o que acontece.

Nosso objetivo é interferir no funcionamento de bufbomb, fornecendo no arquivo de entrada uma sequência de valores "apropriados" para um ataque à pilha de execução...

Para obtermos essa entrada sem precisar gerar diretamente um arquivo binário, usaremos o programa auxiliar hex2raw. Esse programa lê, de um arquivo texto, uma sequência de valores em formato hexadecimal e gera um arquivo binário com esses valores.

Por exemplo, suponha um arquivo texto, chamado s1, cujo conteúdo é

00 01 02 03 
Se executarmos na linha de comando
./hex2raw < s1 > s1.raw
o programa hex2raw vai ler o arquivo de entrada s1 e gerar o arquivo de saída s1.raw. Este arquivo de saída, ao invés dos caracteres '0','0', etc., conterá uma sequência de bytes com os valores 0, 1, 2 e 3.

Dessa forma, se executarmos depois

./bufbomb < s1.raw
o programa irá receber como entrada a sequência de bytes equivalente ao conteúdo do arquivo.

Essa é a base do ataque de buffer overflow: sobrescrever a pilha de execução, alterando os endereços de retorno que estão empilhados para apontarem para um outro código que o intruso deseja que seja executado, ou para forçar a execução de código inserido "maliciosamente" na pilha.


Exercícios

  1. Uma forma de ataque que se tornou bastante comum é desviar o controle para alguma função já existente no próprio código "atacado", mas que não seria chamada normalmente. Examine o código de bufbomb.c. Veja que existe uma função danger, normalmente chamada pela função protect, que determina se o usuário tem as credenciais apropriadas para executar a chamada. Você irá criar uma string de bytes que desviará o controle para danger sem passar por protect. Para isto, a string dada como entrada para bufbomb deve sobrescrever a pilha de execução, colocando o endereço de danger no lugar do endereço de retorno de getbuf.

    Você vai ter que criar uma sequência de bytes com tamanho suficiente para ocupar desde o início do espaço ocupado pelo array local buf até o endereço de retorno de getbuf. (os valores nas posições que não correspondem ao endereço de retorno de getbuf podem ser preenchidos com qualquer valor)

    Desenhe a pilha a partir da chamada a getbuf, na main. O código de getbuf está em buf.c. Para descobrir o endereço de danger vamos usar o utilitário objdump, que permite a inspeção de arquivos objeto e executáveis. A opção -d deste utilitário produz como saída um desassembly do código de máquina armazenado no arquivo. Com o comando

    objdump -d bufbomb > bufbomb.d
    
    redirecionamos a saída do objdump para um arquivo chamado bufbomb.d. Inspecione esse arquivo: o endereço de danger estará ao lado do label danger. Lembre-se que esse endereço deve aparecer na pilha em little-endian!

    Verifique também no "desassembly" da função getbuf onde começa, na pilha de execução, o array buf. Como o endereço do array é fornecido como parâmetro para Gets, verifique como é calculado o valor desse parâmetro (isto é, veja qual é o offset em relação ao %rbp correspondente ao endereço de buf).

    Crie um arquivo stringinvasora contendo uma sequência de valores hexa, por exemplo (quebras de linha não fazem diferença):

    00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00
    ....
    86 04 80 00 00 00 00 00
    
    Lembre-se que a sequência tem que ter um tamanho adequado para que o endereço de danger sobrescreva o endereço de retorno de getbuf!

    Utilize agora o programa hex2raw para gerar sua string e depois forneça o arquivo resultante a bufbomb:

    ./hex2raw < stringinvasora > stringinvasora.raw
    ./bufbomb < stringinvasora.raw
    
    Você verá se seu programa fez o que você queria pelas mensagens exibidas: se você conseguiu desviar o controle para danger, a mensagem exibida por essa função deverá estar no terminal.

    obs: Caso sua sequência de bytes esteja bem construída, seu programa irá chamar danger e depois deve gerar uma segmentation fault. Iremos mudar isso agora.

  2. No item anterior, o programa gera um segmentation fault ao tentar retornar da função danger, pois não há um endereço de retorno no lugar adequado na pilha. Corrija isto, criando um novo arquivo stringinvasora2. Nesse arquivo, estenda a sequência de bytes de maneira a preencher a pilha com um endereço de retorno para danger. Use o endereço da função smoke, fazendo com que o controle vá para ela depois da execução de danger. (Como smoke chama exit(), o programa agora deve terminar elegantemente.)

  3. Vamos experimentar agora ativar uma função passando um argumento para ela. Nosso novo objetivo é fazer getbuf "retornar" para a função fizz, passando como argumento um inteiro com valor 0x01020304. Veja o código de fizz; se você conseguir invocá-la com esse argumento, ela imprimirá a linha: fizz! You called fizz....

    Neste exercício, vamos precisar do endereço do array buf. Repare que a função getbuf imprime este endereço antes de ler sua entrada. (Esse endereço deve ser sempre o mesmo, em todas as execuções. Se não for, provavelmente você se esqueceu de desabilitar a randomização de endereços...)

    Crie um arquivo codigo.s com as instruções

    movl $0x01020304, %edi
    ret
    
    e gere o arquivo objeto correspondente com o comando
    gcc -c codigo.s
    
    Se você executar, agora,
    objdump -d codigo.o
    
    você poderá obter o código de máquina correspondente a essas instruções (repare que elas preparam um argumento em %edi).

    Prepare agora um arquivo stringinvasora3 que sobrescreva a pilha da seguinte forma:

    • as primeiras posições do array buf devem ser preenchidas com um código "malicioso" (as instruções de codigo.s);
    • o endereço de retorno de getbuf deve ser sobrescrito com o endereço de buf, para forçar a execução desse código;
    • a posição apropriada da pilha deve ser preenchida com o endereço de fizz, para que a instrução ret do código inserido na pilha force a invocação dessa função.